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,500 @@
|
|
|
1
|
+
"""`merge`, `promote` and `discard`: the three consolidator write verbs, each one transaction.
|
|
2
|
+
|
|
3
|
+
`architecture.md` §"Consolidator tool surface" and §"Row-level completion" are normative. Each verb
|
|
4
|
+
runs the ladder in `authorization`, then mutates, then dispositions the members it touched and
|
|
5
|
+
closes whatever that closure reaches. Nothing here re-implements a row rule: `records.memory`'s
|
|
6
|
+
`apply_*` mutators own the column writes, version bumps and receipts, and `indexing.writes` owns
|
|
7
|
+
the index.
|
|
8
|
+
|
|
9
|
+
**Completion is tracked per journal row, never per group.** A group with a mixed disposition —
|
|
10
|
+
merge A, promote B, discard C — takes three calls, and the group closes only when its last member is
|
|
11
|
+
dispositioned. That is what makes the never-lose guard structural rather than a protocol: a model
|
|
12
|
+
that returns nothing, stops halfway, crashes or lets its lease expire loses **no** rows, because the
|
|
13
|
+
undispositioned ones are still `tier='journal' AND active=1` and the next run plans them again. No
|
|
14
|
+
cooperation from the model is required for the guard to hold.
|
|
15
|
+
|
|
16
|
+
**Every response carries `remaining_uuids`**, in the success shape *and* in the conflict shape, so a
|
|
17
|
+
caller learns the same remaining state whether its call landed or raced. It is recomputed from the
|
|
18
|
+
member table at response time rather than tracked, which is what makes it shrink within one call.
|
|
19
|
+
|
|
20
|
+
**The chunking preflight runs before the transaction, and `promote` pays for one it may discard.**
|
|
21
|
+
The preflight is where `gist_max_tokens` is enforced — rung 1, before the store is touched — and
|
|
22
|
+
where the embedding happens, and holding SQLite's write lock across a model load is what the whole
|
|
23
|
+
outside-the-transaction rule exists to avoid. `promote`'s form, though, can only be decided from
|
|
24
|
+
authorized state *inside* the transaction, since it turns on whether the sole absorbed row's stored
|
|
25
|
+
prose is byte-identical to the arguments. So an in-place promotion embeds a vector it then does not
|
|
26
|
+
use: a few milliseconds on a manually-invoked, rare, unwaited call, in exchange for the form being
|
|
27
|
+
decided once, from state that has been version-checked. Deciding it from a pre-transaction read
|
|
28
|
+
would have been sound — a matching version implies unchanged prose — but it buys nothing and adds a
|
|
29
|
+
disagreement between two reads that would have to be handled somewhere.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from collections.abc import Sequence
|
|
33
|
+
|
|
34
|
+
import aiosqlite
|
|
35
|
+
|
|
36
|
+
from zikaron.core.consolidation import authorization, groups, runs
|
|
37
|
+
from zikaron.core.consolidation.authorization import Authorized
|
|
38
|
+
from zikaron.core.consolidation.context import ConsolidationCall
|
|
39
|
+
from zikaron.core.consolidation.groups import Disposition, GroupStatus
|
|
40
|
+
from zikaron.core.consolidation.payload import (
|
|
41
|
+
Discarded,
|
|
42
|
+
DiscardOutcome,
|
|
43
|
+
GroupConflict,
|
|
44
|
+
Merged,
|
|
45
|
+
MergeOutcome,
|
|
46
|
+
MergeTarget,
|
|
47
|
+
NamedRow,
|
|
48
|
+
NamedRows,
|
|
49
|
+
Promoted,
|
|
50
|
+
PromoteOutcome,
|
|
51
|
+
)
|
|
52
|
+
from zikaron.core.errors import ErrorCode, IndexStage, ZikaronError
|
|
53
|
+
from zikaron.core.events import (
|
|
54
|
+
AuthoredSize,
|
|
55
|
+
DiscardDetail,
|
|
56
|
+
GroupRef,
|
|
57
|
+
MergeDetail,
|
|
58
|
+
MergeRole,
|
|
59
|
+
PromoteDetail,
|
|
60
|
+
PromoteForm,
|
|
61
|
+
PromoteRole,
|
|
62
|
+
)
|
|
63
|
+
from zikaron.core.indexing import vectors, writes
|
|
64
|
+
from zikaron.core.indexing.chunking import ChunkPlan
|
|
65
|
+
from zikaron.core.indexing.writes import PreparedIndex
|
|
66
|
+
from zikaron.core.records import memory as records
|
|
67
|
+
from zikaron.core.records.memory import Memory, Rewrite, Tier, log_event
|
|
68
|
+
from zikaron.core.store import transactions
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _authored(plan: ChunkPlan) -> AuthoredSize:
|
|
72
|
+
"""The four size fields, from the preflight that actually cut the chunks being written."""
|
|
73
|
+
return AuthoredSize(
|
|
74
|
+
token_count=plan.content_tokens,
|
|
75
|
+
gist_tokens=plan.gist_tokens,
|
|
76
|
+
n_chunks=plan.n_chunks,
|
|
77
|
+
truncated=plan.truncated,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _stored_size(
|
|
82
|
+
db: aiosqlite.Connection, *, row: Memory, call: ConsolidationCall
|
|
83
|
+
) -> AuthoredSize:
|
|
84
|
+
"""The four size fields read off what the store already holds, for a call that authored nothing
|
|
85
|
+
new.
|
|
86
|
+
|
|
87
|
+
`promote`'s in-place form only: it changes no prose, so it rebuilds no index, and a preflight
|
|
88
|
+
run under a `chunk_max_tokens` that has moved since the row was written would report an
|
|
89
|
+
`n_chunks` the store does not contain. `token_count` is the row's own column, `n_chunks` and
|
|
90
|
+
`truncated` come from its chunk rows, and `gist_tokens` is counted over the stored gist with
|
|
91
|
+
the deployed tokenizer — which is a fresh measurement of stored prose rather than a number
|
|
92
|
+
carried from elsewhere, because no column records it.
|
|
93
|
+
"""
|
|
94
|
+
stored = await vectors.stored_chunks(db, memory_uuid=row.uuid)
|
|
95
|
+
return AuthoredSize(
|
|
96
|
+
token_count=row.token_count,
|
|
97
|
+
gist_tokens=call.index.encoder.count_tokens(row.gist),
|
|
98
|
+
n_chunks=stored.n_chunks,
|
|
99
|
+
truncated=stored.truncated,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def _disposition_and_close(
|
|
104
|
+
db: aiosqlite.Connection,
|
|
105
|
+
*,
|
|
106
|
+
authorized: Authorized,
|
|
107
|
+
disposition: Disposition,
|
|
108
|
+
call: ConsolidationCall,
|
|
109
|
+
at: str,
|
|
110
|
+
) -> tuple[tuple[str, ...], bool, int]:
|
|
111
|
+
"""Disposition the absorbed members, close what that closes, and refresh the lease.
|
|
112
|
+
|
|
113
|
+
The whole tail every verb shares, and it is shared rather than repeated because invariant 16's
|
|
114
|
+
closure property is stated over *committed* state: a group whose members are all dispositioned
|
|
115
|
+
is never left open, so the disposition and the group's transition — and, if that was the run's
|
|
116
|
+
last open group, the run's — must land in one transaction. Three copies of that sequence would
|
|
117
|
+
be three chances for one verb to leave a group uncloseable.
|
|
118
|
+
|
|
119
|
+
The lease is refreshed because this call made progress. A `{conflict: true}` response never
|
|
120
|
+
reaches here, which is deliberate: it mutates nothing, so extending the lease would let a
|
|
121
|
+
consolidator that is achieving nothing hold the store indefinitely.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
`(remaining_uuids, group_complete, dispositioned)` — the remaining set in group order,
|
|
125
|
+
whether this call closed the group, and how many member rows actually moved, which is what
|
|
126
|
+
`discard` reports as `retired` rather than the length of its own argument.
|
|
127
|
+
"""
|
|
128
|
+
moved = await groups.disposition_members(
|
|
129
|
+
db,
|
|
130
|
+
group_id=authorized.group.group_id,
|
|
131
|
+
memory_uuids=authorized.absorbed_uuids,
|
|
132
|
+
disposition=disposition,
|
|
133
|
+
at=at,
|
|
134
|
+
)
|
|
135
|
+
remaining = await groups.open_member_uuids(db, authorized.group.group_id)
|
|
136
|
+
complete = not remaining
|
|
137
|
+
if complete:
|
|
138
|
+
await groups.set_status(
|
|
139
|
+
db,
|
|
140
|
+
group_id=authorized.group.group_id,
|
|
141
|
+
status=GroupStatus.COMPLETE,
|
|
142
|
+
expected=authorized.group.status,
|
|
143
|
+
)
|
|
144
|
+
await runs.close_if_finished(db, run_id=authorized.run.run_id, ctx=call.ctx)
|
|
145
|
+
await runs.refresh_lease(
|
|
146
|
+
db,
|
|
147
|
+
run_id=authorized.run.run_id,
|
|
148
|
+
at=at,
|
|
149
|
+
lease_seconds=call.settings.run_lease_seconds,
|
|
150
|
+
)
|
|
151
|
+
return remaining, complete, moved
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def _retire_absorbed(
|
|
155
|
+
db: aiosqlite.Connection, *, row: Memory, superseded_by: str | None, call: ConsolidationCall
|
|
156
|
+
) -> Memory:
|
|
157
|
+
"""Retire one absorbed member, pointing it at the survivor where there is one.
|
|
158
|
+
|
|
159
|
+
`superseded_by` set writes D25's supersession edge, validated in this same transaction; `None`
|
|
160
|
+
is `discard`'s outright retirement, which names no replacement because there is none — the row
|
|
161
|
+
was judged not worth keeping, and D16 leaves its prose intact so that judgment is recoverable.
|
|
162
|
+
"""
|
|
163
|
+
return await records.apply_retirement(
|
|
164
|
+
db, current=row, superseded_by=superseded_by, ctx=call.ctx
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _failure_map(verb: str) -> transactions.FailureMap:
|
|
169
|
+
"""Contention is `store_busy`, everything else `index_failed` — a consolidator verb is a write.
|
|
170
|
+
|
|
171
|
+
Named per verb so `store_busy`'s payload says which call may be retried, and decided here rather
|
|
172
|
+
than shared because `architecture.md` §Errors makes it a property of the *kind* of call: a write
|
|
173
|
+
performs index maintenance and so has an `index_failed` to report, while a read does not.
|
|
174
|
+
"""
|
|
175
|
+
return lambda error: (
|
|
176
|
+
ZikaronError(ErrorCode.STORE_BUSY, verb=verb)
|
|
177
|
+
if transactions.is_contention(error)
|
|
178
|
+
else ZikaronError(ErrorCode.INDEX_FAILED, stage=IndexStage.INDEX_WRITE)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async def merge(
|
|
183
|
+
db: aiosqlite.Connection,
|
|
184
|
+
*,
|
|
185
|
+
group_id: str,
|
|
186
|
+
target: MergeTarget,
|
|
187
|
+
absorb: Sequence[NamedRow],
|
|
188
|
+
call: ConsolidationCall,
|
|
189
|
+
) -> MergeOutcome:
|
|
190
|
+
"""Rewrite an existing long-term record to absorb part or all of a group.
|
|
191
|
+
|
|
192
|
+
The target must be in this group's persisted authorization set — its anchor or one of its
|
|
193
|
+
candidates — and must still be an active long-term record at mutation time. Absorbed rows are
|
|
194
|
+
retired with `superseded_by = target.uuid`, which demotes rather than hides them (D25), so
|
|
195
|
+
historical-intent queries still reach them and their original `session_id` still records who
|
|
196
|
+
learned the lesson.
|
|
197
|
+
|
|
198
|
+
Emits one `merge` event per row it mutated: the target, carrying the preflight's size fields,
|
|
199
|
+
then one per absorbed row with those four fields null, because only the target authored prose.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
`Merged` with the target's new version, or `GroupConflict` when a named row's version has
|
|
203
|
+
moved — carrying the current record for every conflicting uuid and the remaining member set.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
ZikaronError: `BOUNDS` if the gist is empty or over `gist_max_tokens`, or a uuid repeats in
|
|
207
|
+
`absorb`; then the ladder's own rejections — `GROUP_UNKNOWN`, `GROUP_EXPIRED`,
|
|
208
|
+
`GROUP_COMPLETE`, `GROUP_DEFERRED`, `NOT_IN_GROUP`, `BAD_MERGE_TARGET`,
|
|
209
|
+
`NO_READ_RECEIPT`; or `INDEX_FAILED` / `STORE_BUSY`, either of which leaves every row
|
|
210
|
+
as it was.
|
|
211
|
+
"""
|
|
212
|
+
named = NamedRows(absorb=tuple(absorb), target=target.row)
|
|
213
|
+
prepared = await writes.prepare(target.rewrite, index=call.index)
|
|
214
|
+
|
|
215
|
+
async def work(connection: aiosqlite.Connection) -> MergeOutcome:
|
|
216
|
+
authorized = await authorization.authorize(
|
|
217
|
+
connection, group_id=group_id, named=named, call=call, verb="merge"
|
|
218
|
+
)
|
|
219
|
+
if isinstance(authorized, GroupConflict):
|
|
220
|
+
return authorized
|
|
221
|
+
return await _apply_merge(connection, authorized=authorized, prepared=prepared, call=call)
|
|
222
|
+
|
|
223
|
+
return await transactions.in_one_transaction(db, work, failure=_failure_map("merge"))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
async def _apply_merge(
|
|
227
|
+
db: aiosqlite.Connection,
|
|
228
|
+
*,
|
|
229
|
+
authorized: Authorized,
|
|
230
|
+
prepared: PreparedIndex,
|
|
231
|
+
call: ConsolidationCall,
|
|
232
|
+
) -> Merged:
|
|
233
|
+
"""Rung 7 for `merge`: rewrite the target, retire the absorbed rows, log, disposition, close."""
|
|
234
|
+
at = runs.now()
|
|
235
|
+
before = authorized.target
|
|
236
|
+
if before is None:
|
|
237
|
+
# `Authorized.target` is optional because `promote` and `discard` name none; `merge` always
|
|
238
|
+
# does, and rung 2 refused the call if it was not authorized. A real `raise` rather than a
|
|
239
|
+
# bare `assert`, for the reason `write.tools` gives at the same kind of site: an assertion
|
|
240
|
+
# can be compiled away, and this exists to catch the ladder changing what it returns.
|
|
241
|
+
raise ValueError("merge reached rung 7 with no authorized target")
|
|
242
|
+
after = await records.apply_rewrite(db, current=before, rewrite=prepared.rewrite, ctx=call.ctx)
|
|
243
|
+
await writes.reindex_rewrite(
|
|
244
|
+
db, before=before, after=after, prepared=prepared, index=call.index
|
|
245
|
+
)
|
|
246
|
+
reference = GroupRef(group_id=authorized.group.group_id, run_id=authorized.run.run_id)
|
|
247
|
+
n_absorbed = len(authorized.absorbed)
|
|
248
|
+
await log_event(
|
|
249
|
+
db,
|
|
250
|
+
ctx=call.ctx,
|
|
251
|
+
detail=MergeDetail(
|
|
252
|
+
group=reference,
|
|
253
|
+
role=MergeRole.TARGET,
|
|
254
|
+
from_version=before.version,
|
|
255
|
+
to_version=after.version,
|
|
256
|
+
n_absorbed=n_absorbed,
|
|
257
|
+
size=_authored(prepared.plan),
|
|
258
|
+
),
|
|
259
|
+
memory_uuid=after.uuid,
|
|
260
|
+
)
|
|
261
|
+
for row in authorized.absorbed:
|
|
262
|
+
retired = await _retire_absorbed(db, row=row, superseded_by=after.uuid, call=call)
|
|
263
|
+
await log_event(
|
|
264
|
+
db,
|
|
265
|
+
ctx=call.ctx,
|
|
266
|
+
detail=MergeDetail(
|
|
267
|
+
group=reference,
|
|
268
|
+
role=MergeRole.ABSORBED,
|
|
269
|
+
from_version=row.version,
|
|
270
|
+
to_version=retired.version,
|
|
271
|
+
n_absorbed=n_absorbed,
|
|
272
|
+
size=AuthoredSize.none_authored(),
|
|
273
|
+
),
|
|
274
|
+
memory_uuid=row.uuid,
|
|
275
|
+
)
|
|
276
|
+
remaining, complete, _ = await _disposition_and_close(
|
|
277
|
+
db, authorized=authorized, disposition=Disposition.MERGED, call=call, at=at
|
|
278
|
+
)
|
|
279
|
+
return Merged(
|
|
280
|
+
uuid=after.uuid,
|
|
281
|
+
version=after.version,
|
|
282
|
+
remaining_uuids=remaining,
|
|
283
|
+
group_complete=complete,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
async def promote(
|
|
288
|
+
db: aiosqlite.Connection,
|
|
289
|
+
*,
|
|
290
|
+
group_id: str,
|
|
291
|
+
rewrite: Rewrite,
|
|
292
|
+
absorb: Sequence[NamedRow],
|
|
293
|
+
call: ConsolidationCall,
|
|
294
|
+
) -> PromoteOutcome:
|
|
295
|
+
"""Create a long-term record from part or all of a group, or lift one member where it stands.
|
|
296
|
+
|
|
297
|
+
**Which form runs is determined by the payload, not guessed**: exactly one `absorb` row whose
|
|
298
|
+
stored gist and content are byte-identical to the arguments flips that row's tier in place, and
|
|
299
|
+
anything else inserts a new long-term record and retires every absorbed row with `superseded_by`
|
|
300
|
+
set to the new uuid. The version bumps in both forms — invariant 8 makes a tier flip a real
|
|
301
|
+
mutation, because a primary agent may be amending the row concurrently.
|
|
302
|
+
|
|
303
|
+
Event cardinality differs between the forms and is fixed rather than left to this
|
|
304
|
+
implementation. `new_row` emits one `created` event plus one `absorbed` event per absorbed row.
|
|
305
|
+
`in_place` emits **exactly one**, `flipped`, `n_absorbed: 1` — because the flipped row *is* the
|
|
306
|
+
absorbed member and a singular role cannot say both. One mutated row, one event.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
`Promoted` with the long-term record's uuid and version, or `GroupConflict`.
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
ZikaronError: as `merge`, minus `BAD_MERGE_TARGET` — this verb names no target.
|
|
313
|
+
"""
|
|
314
|
+
named = NamedRows(absorb=tuple(absorb))
|
|
315
|
+
prepared = await writes.prepare(rewrite, index=call.index)
|
|
316
|
+
|
|
317
|
+
async def work(connection: aiosqlite.Connection) -> PromoteOutcome:
|
|
318
|
+
authorized = await authorization.authorize(
|
|
319
|
+
connection, group_id=group_id, named=named, call=call, verb="promote"
|
|
320
|
+
)
|
|
321
|
+
if isinstance(authorized, GroupConflict):
|
|
322
|
+
return authorized
|
|
323
|
+
if _is_in_place(authorized, rewrite=rewrite):
|
|
324
|
+
return await _apply_in_place_promote(connection, authorized=authorized, call=call)
|
|
325
|
+
return await _apply_new_row_promote(
|
|
326
|
+
connection, authorized=authorized, prepared=prepared, call=call
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
return await transactions.in_one_transaction(db, work, failure=_failure_map("promote"))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _is_in_place(authorized: Authorized, *, rewrite: Rewrite) -> bool:
|
|
333
|
+
"""Whether this promotion lifts its sole member in place rather than authoring a new record.
|
|
334
|
+
|
|
335
|
+
Byte-identical prose is the whole test, and it is evaluated against the row the ladder loaded
|
|
336
|
+
and version-checked — so "identical to what is stored" means identical to the version the caller
|
|
337
|
+
presented, not to whatever the row held when the payload was assembled.
|
|
338
|
+
"""
|
|
339
|
+
if len(authorized.absorbed) != 1:
|
|
340
|
+
return False
|
|
341
|
+
only = authorized.absorbed[0]
|
|
342
|
+
return only.gist == rewrite.gist and only.content == rewrite.content
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
async def _apply_in_place_promote(
|
|
346
|
+
db: aiosqlite.Connection, *, authorized: Authorized, call: ConsolidationCall
|
|
347
|
+
) -> Promoted:
|
|
348
|
+
"""Rung 7 for `promote`'s in-place form: flip the tier, log one event, close.
|
|
349
|
+
|
|
350
|
+
No index work at all: the prose is byte-identical to what is stored, `memory_fts` indexes `gist`
|
|
351
|
+
and `content` only, and `tier` appears in neither index — so there is nothing to rebuild, and
|
|
352
|
+
rebuilding it anyway would rewrite chunks under a possibly-changed `chunk_max_tokens` for no
|
|
353
|
+
gain.
|
|
354
|
+
"""
|
|
355
|
+
at = runs.now()
|
|
356
|
+
before = authorized.absorbed[0]
|
|
357
|
+
after = await records.apply_tier(db, current=before, tier=Tier.LONG_TERM, ctx=call.ctx)
|
|
358
|
+
await log_event(
|
|
359
|
+
db,
|
|
360
|
+
ctx=call.ctx,
|
|
361
|
+
detail=PromoteDetail(
|
|
362
|
+
group=GroupRef(group_id=authorized.group.group_id, run_id=authorized.run.run_id),
|
|
363
|
+
role=PromoteRole.FLIPPED,
|
|
364
|
+
form=PromoteForm.IN_PLACE,
|
|
365
|
+
from_version=before.version,
|
|
366
|
+
to_version=after.version,
|
|
367
|
+
n_absorbed=1,
|
|
368
|
+
size=await _stored_size(db, row=after, call=call),
|
|
369
|
+
),
|
|
370
|
+
memory_uuid=after.uuid,
|
|
371
|
+
)
|
|
372
|
+
remaining, complete, _ = await _disposition_and_close(
|
|
373
|
+
db, authorized=authorized, disposition=Disposition.PROMOTED, call=call, at=at
|
|
374
|
+
)
|
|
375
|
+
return Promoted(
|
|
376
|
+
uuid=after.uuid,
|
|
377
|
+
version=after.version,
|
|
378
|
+
remaining_uuids=remaining,
|
|
379
|
+
group_complete=complete,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
async def _apply_new_row_promote(
|
|
384
|
+
db: aiosqlite.Connection,
|
|
385
|
+
*,
|
|
386
|
+
authorized: Authorized,
|
|
387
|
+
prepared: PreparedIndex,
|
|
388
|
+
call: ConsolidationCall,
|
|
389
|
+
) -> Promoted:
|
|
390
|
+
"""Rung 7 for `promote`'s new-row form: insert the record long-term, retire what it absorbed."""
|
|
391
|
+
at = runs.now()
|
|
392
|
+
created = await writes.insert_row_and_indexes(
|
|
393
|
+
db, prepared=prepared, call=call.indexed, tier=Tier.LONG_TERM
|
|
394
|
+
)
|
|
395
|
+
reference = GroupRef(group_id=authorized.group.group_id, run_id=authorized.run.run_id)
|
|
396
|
+
n_absorbed = len(authorized.absorbed)
|
|
397
|
+
await log_event(
|
|
398
|
+
db,
|
|
399
|
+
ctx=call.ctx,
|
|
400
|
+
detail=PromoteDetail(
|
|
401
|
+
group=reference,
|
|
402
|
+
role=PromoteRole.CREATED,
|
|
403
|
+
form=PromoteForm.NEW_ROW,
|
|
404
|
+
from_version=created.version,
|
|
405
|
+
to_version=created.version,
|
|
406
|
+
n_absorbed=n_absorbed,
|
|
407
|
+
size=_authored(prepared.plan),
|
|
408
|
+
),
|
|
409
|
+
memory_uuid=created.uuid,
|
|
410
|
+
)
|
|
411
|
+
for row in authorized.absorbed:
|
|
412
|
+
retired = await _retire_absorbed(db, row=row, superseded_by=created.uuid, call=call)
|
|
413
|
+
await log_event(
|
|
414
|
+
db,
|
|
415
|
+
ctx=call.ctx,
|
|
416
|
+
detail=PromoteDetail(
|
|
417
|
+
group=reference,
|
|
418
|
+
role=PromoteRole.ABSORBED,
|
|
419
|
+
form=PromoteForm.NEW_ROW,
|
|
420
|
+
from_version=row.version,
|
|
421
|
+
to_version=retired.version,
|
|
422
|
+
n_absorbed=n_absorbed,
|
|
423
|
+
size=AuthoredSize.none_authored(),
|
|
424
|
+
),
|
|
425
|
+
memory_uuid=row.uuid,
|
|
426
|
+
)
|
|
427
|
+
remaining, complete, _ = await _disposition_and_close(
|
|
428
|
+
db, authorized=authorized, disposition=Disposition.PROMOTED, call=call, at=at
|
|
429
|
+
)
|
|
430
|
+
return Promoted(
|
|
431
|
+
uuid=created.uuid,
|
|
432
|
+
version=created.version,
|
|
433
|
+
remaining_uuids=remaining,
|
|
434
|
+
group_complete=complete,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
async def discard(
|
|
439
|
+
db: aiosqlite.Connection,
|
|
440
|
+
*,
|
|
441
|
+
group_id: str,
|
|
442
|
+
absorb: Sequence[NamedRow],
|
|
443
|
+
reason: str,
|
|
444
|
+
call: ConsolidationCall,
|
|
445
|
+
) -> DiscardOutcome:
|
|
446
|
+
"""Retire journal rows judged not worth keeping: `active=0`, `superseded_by NULL` (D16).
|
|
447
|
+
|
|
448
|
+
`reason` is recorded in the **event log and nowhere else**, which is deliberate rather than an
|
|
449
|
+
omission: D16 leaves the row's own prose untouched, so "we decided this was noise" stays
|
|
450
|
+
recoverable if that judgment was wrong, without editing the row to say so.
|
|
451
|
+
|
|
452
|
+
Runs no chunking preflight and touches no index: a discard writes `active`, `version` and
|
|
453
|
+
`updated_at`, none of which is an indexed column, and D16 leaves the chunks exactly where they
|
|
454
|
+
are so a discarded row stays recoverable.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
`Discarded` with how many member rows actually moved, or `GroupConflict`.
|
|
458
|
+
|
|
459
|
+
Raises:
|
|
460
|
+
ZikaronError: the ladder's own rejections, or `STORE_BUSY` / `INDEX_FAILED`. No `BOUNDS`
|
|
461
|
+
from a gist, since this verb authors no prose — only from a repeated `absorb` uuid.
|
|
462
|
+
"""
|
|
463
|
+
named = NamedRows(absorb=tuple(absorb))
|
|
464
|
+
|
|
465
|
+
async def work(connection: aiosqlite.Connection) -> DiscardOutcome:
|
|
466
|
+
authorized = await authorization.authorize(
|
|
467
|
+
connection, group_id=group_id, named=named, call=call, verb="discard"
|
|
468
|
+
)
|
|
469
|
+
if isinstance(authorized, GroupConflict):
|
|
470
|
+
return authorized
|
|
471
|
+
return await _apply_discard(connection, authorized=authorized, reason=reason, call=call)
|
|
472
|
+
|
|
473
|
+
return await transactions.in_one_transaction(db, work, failure=_failure_map("discard"))
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
async def _apply_discard(
|
|
477
|
+
db: aiosqlite.Connection, *, authorized: Authorized, reason: str, call: ConsolidationCall
|
|
478
|
+
) -> Discarded:
|
|
479
|
+
"""Rung 7 for `discard`: retire each named member outright and record why."""
|
|
480
|
+
at = runs.now()
|
|
481
|
+
reference = GroupRef(group_id=authorized.group.group_id, run_id=authorized.run.run_id)
|
|
482
|
+
n_absorbed = len(authorized.absorbed)
|
|
483
|
+
for row in authorized.absorbed:
|
|
484
|
+
retired = await _retire_absorbed(db, row=row, superseded_by=None, call=call)
|
|
485
|
+
await log_event(
|
|
486
|
+
db,
|
|
487
|
+
ctx=call.ctx,
|
|
488
|
+
detail=DiscardDetail(
|
|
489
|
+
group=reference,
|
|
490
|
+
reason=reason,
|
|
491
|
+
from_version=row.version,
|
|
492
|
+
to_version=retired.version,
|
|
493
|
+
n_absorbed=n_absorbed,
|
|
494
|
+
),
|
|
495
|
+
memory_uuid=row.uuid,
|
|
496
|
+
)
|
|
497
|
+
remaining, complete, moved = await _disposition_and_close(
|
|
498
|
+
db, authorized=authorized, disposition=Disposition.DISCARDED, call=call, at=at
|
|
499
|
+
)
|
|
500
|
+
return Discarded(retired=moved, remaining_uuids=remaining, group_complete=complete)
|