zikaron 0.1.0__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.
- zikaron/__init__.py +1 -0
- zikaron/cli/__init__.py +1 -0
- zikaron/cli/main.py +114 -0
- zikaron/core/__init__.py +1 -0
- zikaron/core/clock.py +78 -0
- zikaron/core/config/__init__.py +1 -0
- zikaron/core/config/keys.py +395 -0
- zikaron/core/config/resolution.py +267 -0
- zikaron/core/consolidation/__init__.py +1 -0
- zikaron/core/consolidation/authorization.py +316 -0
- zikaron/core/consolidation/candidates.py +147 -0
- zikaron/core/consolidation/context.py +166 -0
- zikaron/core/consolidation/grouping.py +383 -0
- zikaron/core/consolidation/groups.py +490 -0
- zikaron/core/consolidation/payload.py +246 -0
- zikaron/core/consolidation/planning.py +192 -0
- zikaron/core/consolidation/rowstate.py +68 -0
- zikaron/core/consolidation/runs.py +306 -0
- zikaron/core/consolidation/serving.py +462 -0
- zikaron/core/consolidation/verbs.py +500 -0
- zikaron/core/errors.py +355 -0
- zikaron/core/events.py +748 -0
- zikaron/core/indexing/__init__.py +1 -0
- zikaron/core/indexing/acquisition.py +255 -0
- zikaron/core/indexing/chunking.py +368 -0
- zikaron/core/indexing/encoder.py +537 -0
- zikaron/core/indexing/lexical.py +86 -0
- zikaron/core/indexing/model_cache.py +93 -0
- zikaron/core/indexing/model_pin.py +89 -0
- zikaron/core/indexing/vectors.py +223 -0
- zikaron/core/indexing/writes.py +461 -0
- zikaron/core/knowledge/__init__.py +5 -0
- zikaron/core/knowledge/arms.py +104 -0
- zikaron/core/knowledge/builds.py +204 -0
- zikaron/core/knowledge/candidates.py +130 -0
- zikaron/core/knowledge/changes.py +175 -0
- zikaron/core/knowledge/chunking.py +376 -0
- zikaron/core/knowledge/counters.py +228 -0
- zikaron/core/knowledge/database.py +380 -0
- zikaron/core/knowledge/ddl.py +196 -0
- zikaron/core/knowledge/disposal.py +213 -0
- zikaron/core/knowledge/errors.py +166 -0
- zikaron/core/knowledge/files.py +202 -0
- zikaron/core/knowledge/git.py +385 -0
- zikaron/core/knowledge/groups.py +450 -0
- zikaron/core/knowledge/lexical.py +64 -0
- zikaron/core/knowledge/lifecycle.py +418 -0
- zikaron/core/knowledge/lock.py +277 -0
- zikaron/core/knowledge/meta.py +393 -0
- zikaron/core/knowledge/paths.py +55 -0
- zikaron/core/knowledge/pending.py +59 -0
- zikaron/core/knowledge/registry.py +264 -0
- zikaron/core/knowledge/repair.py +152 -0
- zikaron/core/knowledge/reporting.py +436 -0
- zikaron/core/knowledge/roots.py +91 -0
- zikaron/core/knowledge/scan.py +429 -0
- zikaron/core/knowledge/search.py +346 -0
- zikaron/core/knowledge/state.py +174 -0
- zikaron/core/knowledge/text.py +166 -0
- zikaron/core/knowledge/vectors.py +102 -0
- zikaron/core/knowledge/walk.py +264 -0
- zikaron/core/knowledge/writes.py +127 -0
- zikaron/core/records/__init__.py +1 -0
- zikaron/core/records/memory.py +961 -0
- zikaron/core/records/receipts.py +161 -0
- zikaron/core/records/supersession.py +221 -0
- zikaron/core/retrieval/__init__.py +1 -0
- zikaron/core/retrieval/arms.py +318 -0
- zikaron/core/retrieval/block.py +107 -0
- zikaron/core/retrieval/eligibility.py +164 -0
- zikaron/core/retrieval/query.py +327 -0
- zikaron/core/retrieval/ranking.py +260 -0
- zikaron/core/retrieval/reads.py +294 -0
- zikaron/core/retrieval/retrieve.py +158 -0
- zikaron/core/retrieval/similarity.py +87 -0
- zikaron/core/signals/__init__.py +34 -0
- zikaron/core/signals/contention.py +106 -0
- zikaron/core/signals/dedup.py +201 -0
- zikaron/core/signals/horizon.py +47 -0
- zikaron/core/signals/repair.py +161 -0
- zikaron/core/signals/retirement.py +83 -0
- zikaron/core/signals/sessions.py +105 -0
- zikaron/core/signals/writes.py +200 -0
- zikaron/core/store/__init__.py +1 -0
- zikaron/core/store/connection.py +202 -0
- zikaron/core/store/ddl.py +215 -0
- zikaron/core/store/embedder.py +45 -0
- zikaron/core/store/meta.py +152 -0
- zikaron/core/store/permissions.py +160 -0
- zikaron/core/store/store.py +408 -0
- zikaron/core/store/transactions.py +181 -0
- zikaron/core/write/__init__.py +33 -0
- zikaron/core/write/dedup.py +145 -0
- zikaron/core/write/tools.py +290 -0
- zikaron/doctor/__init__.py +1 -0
- zikaron/doctor/checks.py +220 -0
- zikaron/doctor/main.py +64 -0
- zikaron/harness/__init__.py +1 -0
- zikaron/harness/detect.py +92 -0
- zikaron/harness/spec.py +320 -0
- zikaron/hook/__init__.py +1 -0
- zikaron/hook/connect.py +379 -0
- zikaron/hook/envelope.py +106 -0
- zikaron/hook/failure.py +104 -0
- zikaron/hook/limits.py +61 -0
- zikaron/hook/main.py +118 -0
- zikaron/hook/push.py +183 -0
- zikaron/hook/rpc.py +85 -0
- zikaron/hook/spawn_warm.py +81 -0
- zikaron/hook/subagent_policy.py +57 -0
- zikaron/hook/tripwire.py +54 -0
- zikaron/hook/warm_helper.py +137 -0
- zikaron/hook/write_policy.py +319 -0
- zikaron/install/__init__.py +4 -0
- zikaron/install/__main__.py +18 -0
- zikaron/install/assets.py +394 -0
- zikaron/install/entries.py +370 -0
- zikaron/install/harness.py +185 -0
- zikaron/install/main.py +375 -0
- zikaron/install/targets.py +789 -0
- zikaron/install/writer.py +973 -0
- zikaron/knowledge/__init__.py +1 -0
- zikaron/knowledge/__main__.py +17 -0
- zikaron/knowledge/indexer/__init__.py +1 -0
- zikaron/knowledge/indexer/__main__.py +17 -0
- zikaron/knowledge/indexer/detach.py +83 -0
- zikaron/knowledge/indexer/main.py +187 -0
- zikaron/knowledge/main.py +466 -0
- zikaron/knowledge/scope.py +133 -0
- zikaron/mcp/__init__.py +6 -0
- zikaron/mcp/connection.py +583 -0
- zikaron/mcp/consolidator.py +316 -0
- zikaron/mcp/errors.py +73 -0
- zikaron/mcp/main.py +66 -0
- zikaron/mcp/primary.py +420 -0
- zikaron/mcp/server.py +96 -0
- zikaron/mcp/spill.py +328 -0
- zikaron/mcp/tool_names.py +67 -0
- zikaron/py.typed +0 -0
- zikaron/service/__init__.py +1 -0
- zikaron/service/asyncio_compat.py +126 -0
- zikaron/service/context.py +251 -0
- zikaron/service/dispatch.py +332 -0
- zikaron/service/dispatch_consolidation.py +397 -0
- zikaron/service/dispatch_knowledge.py +469 -0
- zikaron/service/envelope.py +166 -0
- zikaron/service/lifecycle.py +467 -0
- zikaron/service/log.py +96 -0
- zikaron/service/main.py +531 -0
- zikaron/service/params.py +168 -0
- zikaron/service/paths.py +181 -0
- zikaron/service/rpc.py +176 -0
- zikaron/service/security.py +156 -0
- zikaron/service/serialize.py +204 -0
- zikaron/service/serialize_knowledge.py +238 -0
- zikaron/service/server.py +416 -0
- zikaron-0.1.0.dist-info/METADATA +770 -0
- zikaron-0.1.0.dist-info/RECORD +162 -0
- zikaron-0.1.0.dist-info/WHEEL +5 -0
- zikaron-0.1.0.dist-info/entry_points.txt +4 -0
- zikaron-0.1.0.dist-info/licenses/LICENSE +21 -0
- zikaron-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""The group-level candidate query: the one external query the system builds from stored prose.
|
|
2
|
+
|
|
3
|
+
`consolidation.md` §"What `candidates` actually is" and `retrieval.md` §"Query construction" are
|
|
4
|
+
normative. Two rules meet here and neither is obvious from the other:
|
|
5
|
+
|
|
6
|
+
**Whole gists are dropped rather than one being cut in half.** A half-truncated gist is a garbled
|
|
7
|
+
query; a shorter well-formed concatenation is not. So the concatenation **stops before** the gist
|
|
8
|
+
that would exceed the embedder's input budget, and `n_gists_used` reports how many fitted.
|
|
9
|
+
|
|
10
|
+
**The budget is checked on the assembled string, never on the sum of the pieces' counts.** A
|
|
11
|
+
tokenizer re-tokenizes across every join, so neither count bounds the other — which is the same
|
|
12
|
+
rule the write path's chunking preflight and the read path's query preflight both follow, through
|
|
13
|
+
the same counter.
|
|
14
|
+
|
|
15
|
+
Separate from the serve loop because it is the only *retrieval* concern in the serve: it decides
|
|
16
|
+
which long-term records a group is shown against, and it would be equally correct if the lifecycle
|
|
17
|
+
around it were different.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Final
|
|
24
|
+
|
|
25
|
+
import aiosqlite
|
|
26
|
+
|
|
27
|
+
from zikaron.core.consolidation.context import ConsolidationCall
|
|
28
|
+
from zikaron.core.consolidation.payload import GroupRecord, RankedRecord
|
|
29
|
+
from zikaron.core.indexing import encoder as encoding
|
|
30
|
+
from zikaron.core.records import memory as records
|
|
31
|
+
from zikaron.core.retrieval import query as query_construction
|
|
32
|
+
from zikaron.core.retrieval.eligibility import Consumer, Scope
|
|
33
|
+
from zikaron.core.retrieval.query import PreparedQuery
|
|
34
|
+
from zikaron.core.retrieval.retrieve import retrieve
|
|
35
|
+
|
|
36
|
+
#: How many long-term records beyond the anchor one serve delivers. `consolidation.md`: "at most,
|
|
37
|
+
#: one anchor plus four candidates".
|
|
38
|
+
#:
|
|
39
|
+
#: **A fixed number rather than a config key, because `zikaron_memory_next_group`'s own tool
|
|
40
|
+
#: description commits to it** — "up to four further related long-term records" — so the model is
|
|
41
|
+
#: told this number and a configurable one would make the tool lie. *The comment previously cited
|
|
42
|
+
#: a promise "the shipped prompt makes"; the prompt states no number, saying only "a few further
|
|
43
|
+
#: long-term records". The justification for this being a constant rested on a sentence that does
|
|
44
|
+
#: not exist.*
|
|
45
|
+
MAX_CANDIDATES: Final = 4
|
|
46
|
+
|
|
47
|
+
#: What joins the served set's gists into the candidate query. The same single newline an internal
|
|
48
|
+
#: query's lexical side uses between `gist` and `content`, so there is one convention; it is part of
|
|
49
|
+
#: what the budget is counted over, which is the only reason the choice needs stating at all.
|
|
50
|
+
_GIST_JOIN: Final = "\n"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class GroupQuery:
|
|
55
|
+
"""The candidate query built from the served set's gists, and how many of them fitted.
|
|
56
|
+
|
|
57
|
+
`prepared` is `None` when **no** gist fitted, which is not a fallback but the answer: the
|
|
58
|
+
candidates are the persisted set a later `merge` is authorized to target, so a query assembled
|
|
59
|
+
from the configured prefix and nothing else would authorize whichever records happen to sit
|
|
60
|
+
nearest an empty query. `n_gists_used = 0` is what tells an operator that is what happened.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
prepared: PreparedQuery | None
|
|
64
|
+
n_gists_used: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def build_group_query(
|
|
68
|
+
served: Sequence[GroupRecord], *, call: ConsolidationCall
|
|
69
|
+
) -> GroupQuery:
|
|
70
|
+
"""Concatenate the served set's gists under the embedder's input budget, then embed the result.
|
|
71
|
+
|
|
72
|
+
Whole gists are dropped rather than one being cut in half, because a half-truncated gist is a
|
|
73
|
+
garbled query while a shorter well-formed concatenation is not. The concatenation **stops
|
|
74
|
+
before** the gist that would exceed the budget, and the budget is checked on the **assembled**
|
|
75
|
+
string — prefix, separators and all — never as the sum of the pieces' own counts, because a
|
|
76
|
+
tokenizer re-tokenizes across every join and the sum bounds neither direction.
|
|
77
|
+
|
|
78
|
+
This is the one place in the system where an *external* query is assembled from stored prose, so
|
|
79
|
+
both arms follow the external rules: the concatenation is embedded through the dense preflight
|
|
80
|
+
and the same concatenation goes through the lexical term constructor.
|
|
81
|
+
|
|
82
|
+
Deterministic, because group order is total and the served set is committed state — which is
|
|
83
|
+
what makes a re-serve's narrower query text a defined consequence rather than a surprise.
|
|
84
|
+
"""
|
|
85
|
+
encoder = call.index.encoder
|
|
86
|
+
prefix = call.retrieval.embed_prefix_query
|
|
87
|
+
kept: list[str] = []
|
|
88
|
+
for record in served:
|
|
89
|
+
candidate = _GIST_JOIN.join([*kept, record.gist])
|
|
90
|
+
assembled = encoding.assembled_tokens(prefix, candidate, encoder=encoder)
|
|
91
|
+
if assembled > encoder.max_sequence_tokens:
|
|
92
|
+
break
|
|
93
|
+
kept.append(record.gist)
|
|
94
|
+
if not kept:
|
|
95
|
+
return GroupQuery(prepared=None, n_gists_used=0)
|
|
96
|
+
external = await asyncio.to_thread(
|
|
97
|
+
query_construction.external_query,
|
|
98
|
+
_GIST_JOIN.join(kept),
|
|
99
|
+
encoder=encoder,
|
|
100
|
+
prefix=prefix,
|
|
101
|
+
max_terms=call.retrieval.fts_query_max_terms,
|
|
102
|
+
)
|
|
103
|
+
return GroupQuery(prepared=external.prepared, n_gists_used=len(kept))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def select(
|
|
107
|
+
db: aiosqlite.Connection,
|
|
108
|
+
*,
|
|
109
|
+
group_query: GroupQuery,
|
|
110
|
+
excluded: frozenset[str],
|
|
111
|
+
call: ConsolidationCall,
|
|
112
|
+
) -> tuple[RankedRecord, ...]:
|
|
113
|
+
"""Up to `MAX_CANDIDATES` further active long-term records, from **one** group-level query.
|
|
114
|
+
|
|
115
|
+
One query over the whole served set rather than the union of each member's own top five: that
|
|
116
|
+
union has no cap, no defined order, and would grow with group size.
|
|
117
|
+
|
|
118
|
+
`excluded` is the anchor plus **every** member of the group — the frozen plan-time universe, not
|
|
119
|
+
only the served set — because a member the consolidator has already dispositioned is not a
|
|
120
|
+
record to be shown back to it, and a member promoted in place is active long-term and would
|
|
121
|
+
otherwise surface here while not being in the authorization set anyway.
|
|
122
|
+
|
|
123
|
+
An empty result is normal rather than an error: early in a store's life the long-term tier is
|
|
124
|
+
empty, so every group is an orphan group with no anchor and no candidates, and the
|
|
125
|
+
consolidator's only available verbs are `promote` and `discard`.
|
|
126
|
+
"""
|
|
127
|
+
prepared = group_query.prepared
|
|
128
|
+
if prepared is None:
|
|
129
|
+
return ()
|
|
130
|
+
retrieved = await retrieve(
|
|
131
|
+
db, query=prepared, scope=Scope(Consumer.CONSOLIDATION), settings=call.retrieval
|
|
132
|
+
)
|
|
133
|
+
found: list[RankedRecord] = []
|
|
134
|
+
for ranked in retrieved.pool:
|
|
135
|
+
if ranked.row.uuid in excluded:
|
|
136
|
+
continue
|
|
137
|
+
row = await records.load(db, ranked.row.uuid)
|
|
138
|
+
if row is None:
|
|
139
|
+
# A pooled uuid with no row is a row that vanished between the arm query and this read,
|
|
140
|
+
# which one transaction makes impossible. Dropped rather than guessed at, for the same
|
|
141
|
+
# reason `ranking.rank` drops one: a candidate is a merge *authorization*, so inventing
|
|
142
|
+
# prose for it would authorize a rewrite of something the serve never showed.
|
|
143
|
+
continue
|
|
144
|
+
found.append(RankedRecord(record=GroupRecord.of(row), rank=len(found) + 1))
|
|
145
|
+
if len(found) == MAX_CANDIDATES:
|
|
146
|
+
break
|
|
147
|
+
return tuple(found)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""What one consolidation call holds constant: who owns the run, and the parameters D29 is tuned by.
|
|
2
|
+
|
|
3
|
+
Three value types, each self-validating at construction, because every one of them carries a fact a
|
|
4
|
+
later layer would otherwise have to keep re-deciding.
|
|
5
|
+
|
|
6
|
+
`RunOwner` is the **pair** `(session_id, pid)`, never the session alone. Since every client of one
|
|
7
|
+
kiro session shares a `session_id` *and* two consolidators of that session also share
|
|
8
|
+
`client_kind='consolidator'`, a session-only owner would make two concurrently launched
|
|
9
|
+
consolidators the same worker — which would silently defeat the one-worker guarantee `next_group`
|
|
10
|
+
establishes, and with it D29's claim that transitive merging emerges from processing groups
|
|
11
|
+
oldest-first against a store that updates as it goes.
|
|
12
|
+
|
|
13
|
+
`ConsolidationSettings` range-checks itself against `schema.md` §"Configuration keys" rather than
|
|
14
|
+
trusting that its one real caller resolved the config properly, for the reason `EffectiveConfig`
|
|
15
|
+
self-validates: "the only caller checks first" is a fact about today's call sites, not a property of
|
|
16
|
+
the type, and a cutoff outside `[0, 1]` silently turns a threshold into a pass-everything or a
|
|
17
|
+
reject-everything.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Final, Self
|
|
22
|
+
|
|
23
|
+
from zikaron.core.config.resolution import EffectiveConfig
|
|
24
|
+
from zikaron.core.events import ClientKind
|
|
25
|
+
from zikaron.core.indexing.writes import IndexedCall, IndexingContext
|
|
26
|
+
from zikaron.core.records.memory import CallParams
|
|
27
|
+
from zikaron.core.retrieval.retrieve import RetrievalSettings
|
|
28
|
+
|
|
29
|
+
#: `schema.md` §"Configuration keys"' own ranges for the six `[consolidation]` keys. Transcribed
|
|
30
|
+
#: here as bounds this type refuses to be built outside, not as defaults — the defaults live in
|
|
31
|
+
#: `config.keys`, which is the one declaration of the table, and a second copy of them here would be
|
|
32
|
+
#: a second thing to keep in step.
|
|
33
|
+
_CUTOFF_MIN: Final = 0.0
|
|
34
|
+
_CUTOFF_MAX: Final = 1.0
|
|
35
|
+
_MUTUAL_K_MIN: Final = 2
|
|
36
|
+
_MUTUAL_K_MAX: Final = 50
|
|
37
|
+
_GROUP_MAX_MIN: Final = 2
|
|
38
|
+
_GROUP_MAX_MAX: Final = 64
|
|
39
|
+
_MAX_GROUP_SERVES_MIN: Final = 1
|
|
40
|
+
_MAX_GROUP_SERVES_MAX: Final = 16
|
|
41
|
+
_RUN_LEASE_MIN: Final = 60
|
|
42
|
+
_RUN_LEASE_MAX: Final = 86400
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class RunOwner:
|
|
47
|
+
"""Who holds a consolidation run: the `(session_id, pid)` pair, both required.
|
|
48
|
+
|
|
49
|
+
Compared by value, which is what makes "is the caller this run's owner" a single `==` rather
|
|
50
|
+
than two comparisons one call site could get half right.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: an empty `session_id`, or a `pid` below 1 — `architecture.md` rejects a missing
|
|
54
|
+
envelope `pid` with `bounds` at the transport boundary, and this type is the reason a
|
|
55
|
+
layer inward may then assume it has one.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
session_id: str
|
|
59
|
+
pid: int
|
|
60
|
+
|
|
61
|
+
def __post_init__(self) -> None:
|
|
62
|
+
if self.session_id == "":
|
|
63
|
+
raise ValueError("a run owner's session_id cannot be empty")
|
|
64
|
+
if self.pid < 1:
|
|
65
|
+
raise ValueError(f"a run owner's pid must be >= 1, got {self.pid}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class ConsolidationSettings:
|
|
70
|
+
"""The six `[consolidation]` config keys, resolved once for the life of a store handle.
|
|
71
|
+
|
|
72
|
+
Bundled for the reason `RetrievalSettings` is: planning is a pure function of the store **plus
|
|
73
|
+
these**, so two calls of one run reading them from different snapshots would make the run
|
|
74
|
+
irreproducible, and `architecture.md` reads the effective config once at service startup
|
|
75
|
+
precisely so that cannot happen.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
ValueError: any key outside the range `schema.md` §"Configuration keys" states for it.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
anchor_cutoff: float
|
|
82
|
+
orphan_edge_cutoff: float
|
|
83
|
+
mutual_k: int
|
|
84
|
+
group_max: int
|
|
85
|
+
max_group_serves: int
|
|
86
|
+
run_lease_seconds: int
|
|
87
|
+
|
|
88
|
+
def __post_init__(self) -> None:
|
|
89
|
+
self._require_cutoff("anchor_cutoff", self.anchor_cutoff)
|
|
90
|
+
self._require_cutoff("orphan_edge_cutoff", self.orphan_edge_cutoff)
|
|
91
|
+
self._require_range("mutual_k", self.mutual_k, _MUTUAL_K_MIN, _MUTUAL_K_MAX)
|
|
92
|
+
self._require_range("group_max", self.group_max, _GROUP_MAX_MIN, _GROUP_MAX_MAX)
|
|
93
|
+
self._require_range(
|
|
94
|
+
"max_group_serves",
|
|
95
|
+
self.max_group_serves,
|
|
96
|
+
_MAX_GROUP_SERVES_MIN,
|
|
97
|
+
_MAX_GROUP_SERVES_MAX,
|
|
98
|
+
)
|
|
99
|
+
self._require_range("run_lease", self.run_lease_seconds, _RUN_LEASE_MIN, _RUN_LEASE_MAX)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _require_cutoff(name: str, value: float) -> None:
|
|
103
|
+
if not _CUTOFF_MIN <= value <= _CUTOFF_MAX:
|
|
104
|
+
raise ValueError(f"{name}={value} outside [{_CUTOFF_MIN}, {_CUTOFF_MAX}]")
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _require_range(name: str, value: int, low: int, high: int) -> None:
|
|
108
|
+
if not low <= value <= high:
|
|
109
|
+
raise ValueError(f"{name}={value} outside {low}-{high}")
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def from_config(cls, config: EffectiveConfig) -> Self:
|
|
113
|
+
"""Read the six `[consolidation]` keys D29 is parameterized by."""
|
|
114
|
+
return cls(
|
|
115
|
+
anchor_cutoff=config.get_float("anchor_cutoff"),
|
|
116
|
+
orphan_edge_cutoff=config.get_float("orphan_edge_cutoff"),
|
|
117
|
+
mutual_k=config.get_int("mutual_k"),
|
|
118
|
+
group_max=config.get_int("group_max"),
|
|
119
|
+
max_group_serves=config.get_int("max_group_serves"),
|
|
120
|
+
run_lease_seconds=config.get_int("run_lease"),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True, slots=True)
|
|
125
|
+
class ConsolidationCall:
|
|
126
|
+
"""Everything one consolidator call needs: identity, ownership, parameters and the index.
|
|
127
|
+
|
|
128
|
+
The same bundling `WriteCall` and `ReadCall` use, and for the same reasons — every entry point
|
|
129
|
+
in this package needs at least three of the five, none changes partway through a call, and
|
|
130
|
+
keeping them loose invites a call site that pairs one store's encoder with another's context.
|
|
131
|
+
|
|
132
|
+
`pid` is separate from `ctx` rather than added to `CallParams` because it is not a fact every
|
|
133
|
+
verb in the system needs: only a consolidation run has an owner, and `CallParams` is what every
|
|
134
|
+
`event` row and every receipt in the store is written from.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: `ctx.client_kind` is not `consolidator`. Checked here rather than assumed,
|
|
138
|
+
because `read_receipt`'s key includes `client_kind` and this layer both mints and spends
|
|
139
|
+
receipts: minted under `mcp`, a serve's receipts would license the **primary** agent to
|
|
140
|
+
amend rows it never fetched, which is the exact hole that column was added to close.
|
|
141
|
+
Also raised for an out-of-range setting or a malformed owner, by the two types above.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
ctx: CallParams
|
|
145
|
+
pid: int
|
|
146
|
+
settings: ConsolidationSettings
|
|
147
|
+
retrieval: RetrievalSettings
|
|
148
|
+
index: IndexingContext
|
|
149
|
+
|
|
150
|
+
def __post_init__(self) -> None:
|
|
151
|
+
if self.ctx.client_kind != ClientKind.CONSOLIDATOR:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"a consolidation call runs as client_kind={ClientKind.CONSOLIDATOR.value!r}, "
|
|
154
|
+
f"not {self.ctx.client_kind!r}: receipts are scoped by client kind, so minting "
|
|
155
|
+
f"this serve's under another kind would license a different client to write"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def owner(self) -> RunOwner:
|
|
160
|
+
"""This call's `(session_id, pid)` ownership pair."""
|
|
161
|
+
return RunOwner(session_id=self.ctx.session_id, pid=self.pid)
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def indexed(self) -> IndexedCall:
|
|
165
|
+
"""This call as the indexed write path takes it, for the two verbs that author prose."""
|
|
166
|
+
return IndexedCall(ctx=self.ctx, index=self.index)
|