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.
Files changed (162) hide show
  1. zikaron/__init__.py +1 -0
  2. zikaron/cli/__init__.py +1 -0
  3. zikaron/cli/main.py +114 -0
  4. zikaron/core/__init__.py +1 -0
  5. zikaron/core/clock.py +78 -0
  6. zikaron/core/config/__init__.py +1 -0
  7. zikaron/core/config/keys.py +395 -0
  8. zikaron/core/config/resolution.py +267 -0
  9. zikaron/core/consolidation/__init__.py +1 -0
  10. zikaron/core/consolidation/authorization.py +316 -0
  11. zikaron/core/consolidation/candidates.py +147 -0
  12. zikaron/core/consolidation/context.py +166 -0
  13. zikaron/core/consolidation/grouping.py +383 -0
  14. zikaron/core/consolidation/groups.py +490 -0
  15. zikaron/core/consolidation/payload.py +246 -0
  16. zikaron/core/consolidation/planning.py +192 -0
  17. zikaron/core/consolidation/rowstate.py +68 -0
  18. zikaron/core/consolidation/runs.py +306 -0
  19. zikaron/core/consolidation/serving.py +462 -0
  20. zikaron/core/consolidation/verbs.py +500 -0
  21. zikaron/core/errors.py +355 -0
  22. zikaron/core/events.py +748 -0
  23. zikaron/core/indexing/__init__.py +1 -0
  24. zikaron/core/indexing/acquisition.py +255 -0
  25. zikaron/core/indexing/chunking.py +368 -0
  26. zikaron/core/indexing/encoder.py +537 -0
  27. zikaron/core/indexing/lexical.py +86 -0
  28. zikaron/core/indexing/model_cache.py +93 -0
  29. zikaron/core/indexing/model_pin.py +89 -0
  30. zikaron/core/indexing/vectors.py +223 -0
  31. zikaron/core/indexing/writes.py +461 -0
  32. zikaron/core/knowledge/__init__.py +5 -0
  33. zikaron/core/knowledge/arms.py +104 -0
  34. zikaron/core/knowledge/builds.py +204 -0
  35. zikaron/core/knowledge/candidates.py +130 -0
  36. zikaron/core/knowledge/changes.py +175 -0
  37. zikaron/core/knowledge/chunking.py +376 -0
  38. zikaron/core/knowledge/counters.py +228 -0
  39. zikaron/core/knowledge/database.py +380 -0
  40. zikaron/core/knowledge/ddl.py +196 -0
  41. zikaron/core/knowledge/disposal.py +213 -0
  42. zikaron/core/knowledge/errors.py +166 -0
  43. zikaron/core/knowledge/files.py +202 -0
  44. zikaron/core/knowledge/git.py +385 -0
  45. zikaron/core/knowledge/groups.py +450 -0
  46. zikaron/core/knowledge/lexical.py +64 -0
  47. zikaron/core/knowledge/lifecycle.py +418 -0
  48. zikaron/core/knowledge/lock.py +277 -0
  49. zikaron/core/knowledge/meta.py +393 -0
  50. zikaron/core/knowledge/paths.py +55 -0
  51. zikaron/core/knowledge/pending.py +59 -0
  52. zikaron/core/knowledge/registry.py +264 -0
  53. zikaron/core/knowledge/repair.py +152 -0
  54. zikaron/core/knowledge/reporting.py +436 -0
  55. zikaron/core/knowledge/roots.py +91 -0
  56. zikaron/core/knowledge/scan.py +429 -0
  57. zikaron/core/knowledge/search.py +346 -0
  58. zikaron/core/knowledge/state.py +174 -0
  59. zikaron/core/knowledge/text.py +166 -0
  60. zikaron/core/knowledge/vectors.py +102 -0
  61. zikaron/core/knowledge/walk.py +264 -0
  62. zikaron/core/knowledge/writes.py +127 -0
  63. zikaron/core/records/__init__.py +1 -0
  64. zikaron/core/records/memory.py +961 -0
  65. zikaron/core/records/receipts.py +161 -0
  66. zikaron/core/records/supersession.py +221 -0
  67. zikaron/core/retrieval/__init__.py +1 -0
  68. zikaron/core/retrieval/arms.py +318 -0
  69. zikaron/core/retrieval/block.py +107 -0
  70. zikaron/core/retrieval/eligibility.py +164 -0
  71. zikaron/core/retrieval/query.py +327 -0
  72. zikaron/core/retrieval/ranking.py +260 -0
  73. zikaron/core/retrieval/reads.py +294 -0
  74. zikaron/core/retrieval/retrieve.py +158 -0
  75. zikaron/core/retrieval/similarity.py +87 -0
  76. zikaron/core/signals/__init__.py +34 -0
  77. zikaron/core/signals/contention.py +106 -0
  78. zikaron/core/signals/dedup.py +201 -0
  79. zikaron/core/signals/horizon.py +47 -0
  80. zikaron/core/signals/repair.py +161 -0
  81. zikaron/core/signals/retirement.py +83 -0
  82. zikaron/core/signals/sessions.py +105 -0
  83. zikaron/core/signals/writes.py +200 -0
  84. zikaron/core/store/__init__.py +1 -0
  85. zikaron/core/store/connection.py +202 -0
  86. zikaron/core/store/ddl.py +215 -0
  87. zikaron/core/store/embedder.py +45 -0
  88. zikaron/core/store/meta.py +152 -0
  89. zikaron/core/store/permissions.py +160 -0
  90. zikaron/core/store/store.py +408 -0
  91. zikaron/core/store/transactions.py +181 -0
  92. zikaron/core/write/__init__.py +33 -0
  93. zikaron/core/write/dedup.py +145 -0
  94. zikaron/core/write/tools.py +290 -0
  95. zikaron/doctor/__init__.py +1 -0
  96. zikaron/doctor/checks.py +220 -0
  97. zikaron/doctor/main.py +64 -0
  98. zikaron/harness/__init__.py +1 -0
  99. zikaron/harness/detect.py +92 -0
  100. zikaron/harness/spec.py +320 -0
  101. zikaron/hook/__init__.py +1 -0
  102. zikaron/hook/connect.py +379 -0
  103. zikaron/hook/envelope.py +106 -0
  104. zikaron/hook/failure.py +104 -0
  105. zikaron/hook/limits.py +61 -0
  106. zikaron/hook/main.py +118 -0
  107. zikaron/hook/push.py +183 -0
  108. zikaron/hook/rpc.py +85 -0
  109. zikaron/hook/spawn_warm.py +81 -0
  110. zikaron/hook/subagent_policy.py +57 -0
  111. zikaron/hook/tripwire.py +54 -0
  112. zikaron/hook/warm_helper.py +137 -0
  113. zikaron/hook/write_policy.py +319 -0
  114. zikaron/install/__init__.py +4 -0
  115. zikaron/install/__main__.py +18 -0
  116. zikaron/install/assets.py +394 -0
  117. zikaron/install/entries.py +370 -0
  118. zikaron/install/harness.py +185 -0
  119. zikaron/install/main.py +375 -0
  120. zikaron/install/targets.py +789 -0
  121. zikaron/install/writer.py +973 -0
  122. zikaron/knowledge/__init__.py +1 -0
  123. zikaron/knowledge/__main__.py +17 -0
  124. zikaron/knowledge/indexer/__init__.py +1 -0
  125. zikaron/knowledge/indexer/__main__.py +17 -0
  126. zikaron/knowledge/indexer/detach.py +83 -0
  127. zikaron/knowledge/indexer/main.py +187 -0
  128. zikaron/knowledge/main.py +466 -0
  129. zikaron/knowledge/scope.py +133 -0
  130. zikaron/mcp/__init__.py +6 -0
  131. zikaron/mcp/connection.py +583 -0
  132. zikaron/mcp/consolidator.py +316 -0
  133. zikaron/mcp/errors.py +73 -0
  134. zikaron/mcp/main.py +66 -0
  135. zikaron/mcp/primary.py +420 -0
  136. zikaron/mcp/server.py +96 -0
  137. zikaron/mcp/spill.py +328 -0
  138. zikaron/mcp/tool_names.py +67 -0
  139. zikaron/py.typed +0 -0
  140. zikaron/service/__init__.py +1 -0
  141. zikaron/service/asyncio_compat.py +126 -0
  142. zikaron/service/context.py +251 -0
  143. zikaron/service/dispatch.py +332 -0
  144. zikaron/service/dispatch_consolidation.py +397 -0
  145. zikaron/service/dispatch_knowledge.py +469 -0
  146. zikaron/service/envelope.py +166 -0
  147. zikaron/service/lifecycle.py +467 -0
  148. zikaron/service/log.py +96 -0
  149. zikaron/service/main.py +531 -0
  150. zikaron/service/params.py +168 -0
  151. zikaron/service/paths.py +181 -0
  152. zikaron/service/rpc.py +176 -0
  153. zikaron/service/security.py +156 -0
  154. zikaron/service/serialize.py +204 -0
  155. zikaron/service/serialize_knowledge.py +238 -0
  156. zikaron/service/server.py +416 -0
  157. zikaron-0.1.0.dist-info/METADATA +770 -0
  158. zikaron-0.1.0.dist-info/RECORD +162 -0
  159. zikaron-0.1.0.dist-info/WHEEL +5 -0
  160. zikaron-0.1.0.dist-info/entry_points.txt +4 -0
  161. zikaron-0.1.0.dist-info/licenses/LICENSE +21 -0
  162. 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)