Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17288afa18 | ||
|
|
9359761672 | ||
|
|
ef9568655e | ||
|
|
b48e132119 | ||
|
|
34099183b3 | ||
|
|
2bf39eef34 | ||
|
|
4006b14bf2 | ||
|
|
73edee5cda | ||
|
|
74804d8362 | ||
|
|
5ab7dcd8ae | ||
|
|
5d30c4207a | ||
|
|
2f48391533 | ||
|
|
66a0f0c03d | ||
|
|
2248274f8d | ||
|
|
98c8112209 | ||
|
|
4891f2e590 | ||
|
|
d54068598e | ||
|
|
d7074ab544 | ||
|
|
b6c2ca112b | ||
|
|
4f784d04d3 | ||
|
|
a71f38f381 | ||
|
|
bcfc0d54a1 | ||
|
|
38bc538110 | ||
|
|
45dfab4ea8 | ||
|
|
608b166a26 | ||
|
|
466628ac39 | ||
|
|
0631cbefb6 | ||
|
|
0d1e592b9a | ||
|
|
1c340799da | ||
|
|
ec704d645e | ||
|
|
998f97e84d | ||
|
|
1298dace8c | ||
|
|
e2967c4d27 | ||
|
|
a4f78c2587 | ||
|
|
703fdd0bf7 | ||
|
|
5f916b6974 | ||
|
|
4d1113ef0a | ||
|
|
0bc90b9c2b | ||
|
|
6090ae519b | ||
|
|
8473e75df2 | ||
|
|
d16685c9eb | ||
|
|
ee52408718 | ||
|
|
33571aa471 | ||
|
|
d74e36aa06 | ||
|
|
c68db64270 | ||
|
|
224d045d45 | ||
|
|
6dbd0c9d01 | ||
|
|
53965ad490 | ||
|
|
d198ea744e | ||
|
|
9701b8eedf | ||
|
|
b3ae787b7d | ||
|
|
104c0bf255 | ||
|
|
4f41706f3d | ||
|
|
9d3a3efd56 | ||
|
|
b4aab3f7c3 | ||
|
|
ed36236f72 | ||
|
|
ff8ba146f8 | ||
|
|
5743fcf4be | ||
|
|
2876a82799 | ||
|
|
9cfbdeb314 | ||
|
|
ca17451a26 | ||
|
|
d138bf73de | ||
|
|
3325f515c8 | ||
|
|
67947a29fa | ||
|
|
7cc18fcb3a | ||
|
|
44f89035a7 | ||
|
|
b1e919e6f6 | ||
|
|
add4f9d093 | ||
|
|
c6d24a44ea | ||
|
|
5cda0886bd | ||
|
|
01cf2d4482 | ||
|
|
9d63f0dc55 | ||
|
|
c1ae5cdf10 | ||
|
|
fb18395fe3 | ||
|
|
b2d4f16d67 | ||
|
|
00624c8f24 | ||
|
|
94df3a8fc7 | ||
|
|
1e11427425 | ||
|
|
d5a36c2a29 | ||
|
|
ded1a81c91 | ||
|
|
32fbea4797 | ||
|
|
b82c76dfd6 | ||
|
|
9699751f00 | ||
|
|
ceeffabf26 | ||
|
|
b693f9f3b3 | ||
|
|
542dc1848c | ||
|
|
e306964d04 | ||
|
|
ec0ff804bc | ||
|
|
76167f0a2a | ||
|
|
993600fd6b | ||
|
|
87d3420c31 | ||
|
|
3bd0826d48 | ||
|
|
32b638dcec | ||
|
|
b4b17fc895 | ||
|
|
dfbc5700bc | ||
|
|
78238ebabc | ||
|
|
aef0e1bf5a | ||
|
|
b3bd4a82e4 | ||
|
|
0b8d60a685 | ||
|
|
5a71c4bb3e | ||
|
|
955b7672c8 | ||
|
|
a65c6420f6 | ||
|
|
3f7d58d191 | ||
|
|
fcfbda3f94 | ||
|
|
fc7ad7457a | ||
|
|
c792075ec8 | ||
|
|
ca7e230cd8 | ||
|
|
dddf9ede53 | ||
|
|
13570e8458 | ||
|
|
4d6af4090f | ||
|
|
7cd1a38bc8 | ||
|
|
117f54b791 | ||
|
|
ecf88f5020 | ||
|
|
9d57a46079 | ||
|
|
d094a19986 | ||
|
|
79c5d38436 | ||
|
|
b359548241 | ||
|
|
97fac26a32 | ||
|
|
5670c76234 | ||
|
|
ed09cd919e | ||
|
|
f00062dbb1 | ||
|
|
9370aa0f60 | ||
|
|
2a31ae0ab8 | ||
|
|
d03a32dfc1 | ||
|
|
4b85dd2d0c | ||
|
|
103f885afa | ||
|
|
95ee551aa0 | ||
|
|
1bddd2acb3 | ||
|
|
fcf1ce6bdf | ||
|
|
602ffd9fcf | ||
|
|
4a654351e3 | ||
|
|
4cccb45c5e | ||
|
|
9b68d83cf8 | ||
|
|
9127da3c4c | ||
|
|
3ee78e05a4 | ||
|
|
80db5db6c0 | ||
|
|
b6f6e8023c | ||
|
|
eb55efe9c9 | ||
|
|
08c1c9d907 | ||
|
|
d17cc4415f | ||
|
|
7ab838f167 | ||
|
|
93463160b3 | ||
|
|
617c397d88 | ||
|
|
bf990d1869 | ||
|
|
c18e334ab7 | ||
|
|
4c091f57db | ||
|
|
1758ad5006 | ||
|
|
cda365419b | ||
|
|
06100d8e30 | ||
|
|
186dbc62d7 | ||
|
|
07f19d3635 | ||
|
|
8a598ebbed | ||
|
|
43a08baa3c | ||
|
|
592b79cabd | ||
|
|
a9f06c2ae5 | ||
|
|
ca275c7b35 | ||
|
|
beff45719a | ||
|
|
304832ad6c | ||
|
|
5fb4eb222c | ||
|
|
79b87eee66 | ||
|
|
a317d4cf26 | ||
|
|
187e01e7b3 | ||
|
|
b4f88d2980 | ||
|
|
0c3efe32fe | ||
|
|
74ba6aa9f6 | ||
|
|
ad18c385dc | ||
|
|
a1473c452b | ||
|
|
5f98228d15 | ||
|
|
cb6ef5d2c3 | ||
|
|
b5324face7 | ||
|
|
46460efc80 | ||
|
|
00991c1b03 | ||
|
|
589e71c321 | ||
|
|
df82992c60 | ||
|
|
8b52d02c79 | ||
|
|
2b76c2e758 | ||
|
|
f05686c137 | ||
|
|
0636176a0b | ||
|
|
0d3f6bd65b | ||
|
|
81be7a2f39 | ||
|
|
7132471870 | ||
|
|
b5d9d4fff3 | ||
|
|
b76c32a52b | ||
|
|
bc57980432 | ||
|
|
2b410e59d8 | ||
|
|
383de5b9ac | ||
|
|
616a2c8e74 | ||
|
|
68939b2c1f | ||
|
|
e27f7a8b93 | ||
|
|
4bdf410a33 | ||
|
|
1499d65275 | ||
|
|
377be331b9 | ||
|
|
1e57bfacaa | ||
|
|
cf9fb44513 | ||
|
|
90c1a5d28c | ||
|
|
b164fe06ab | ||
|
|
c58660f0a7 | ||
|
|
27e1dca1dc | ||
|
|
839f74daa8 | ||
|
|
8ea1a8f401 | ||
|
|
ed4ca58ca3 | ||
|
|
b2d2965c24 | ||
|
|
9f189871bc | ||
|
|
62b387ef95 | ||
|
|
e7b66b35d5 | ||
|
|
a41fe69447 | ||
|
|
fca516f54f | ||
|
|
24f8db777c | ||
|
|
1a9dec56c0 | ||
|
|
d064efb48b | ||
|
|
d0a317bd10 | ||
|
|
7758f0703c | ||
|
|
f962c0bc43 | ||
|
|
7450b33db4 | ||
|
|
e905dcd976 | ||
|
|
d426d50a49 | ||
|
|
1e4fa53393 | ||
|
|
cb611def86 | ||
|
|
200732e951 | ||
|
|
c02a1c3e87 | ||
|
|
fab6a59a32 | ||
|
|
42cb8d1eef | ||
|
|
1e66e77071 | ||
|
|
d03469d0fd | ||
|
|
8086d66fc3 | ||
|
|
2f9f140e98 | ||
|
|
b728d6854e | ||
|
|
a721b76372 | ||
|
|
b83c041670 | ||
|
|
dfe3757bfe | ||
|
|
8edb47e093 | ||
|
|
125d77a266 | ||
|
|
59b0c2af9f | ||
|
|
2bcbf2687d | ||
|
|
99e883968c | ||
|
|
5f1bea3913 | ||
|
|
34f9092c65 | ||
|
|
7374be5ece | ||
|
|
a1eedbac9a | ||
|
|
7a125d7838 | ||
|
|
895476b1b3 | ||
|
|
87ea81db2a | ||
|
|
c37cae8bc1 | ||
|
|
4177e186bb | ||
|
|
a01c684262 | ||
|
|
08588a6d3f | ||
|
|
24023265f4 | ||
|
|
dbb00c2bf9 | ||
|
|
ab9cad4462 | ||
|
|
2c3335dcaf | ||
|
|
7e5ea3e733 | ||
|
|
8eaa96c5d6 | ||
|
|
c2be631ec1 | ||
|
|
53e869c078 | ||
|
|
7189243fd9 | ||
|
|
10a70afcbd | ||
|
|
f5cd509a1e | ||
|
|
a46725a1c7 | ||
|
|
734fbd3b84 | ||
|
|
cabdd6c386 | ||
|
|
d75583c45e | ||
|
|
edb5c3a3c3 | ||
|
|
dd55f0f896 | ||
|
|
07529fb521 | ||
|
|
3421af57a4 | ||
|
|
ed7357a28b | ||
|
|
ce22c3b11f | ||
|
|
1ff87ab30d | ||
|
|
8e555e358f | ||
|
|
c873bef3b1 | ||
|
|
f532c3ad00 | ||
|
|
a6cf9383d8 | ||
|
|
3263640378 | ||
|
|
ce6bff1239 | ||
|
|
4d6dd54c41 | ||
|
|
b3e9b229e3 | ||
|
|
2ead42ed42 | ||
|
|
fdefb8898a | ||
|
|
87c14b5093 | ||
|
|
83611f9b71 | ||
|
|
189f108f76 | ||
|
|
3b991e4079 | ||
|
|
e60b924f73 | ||
|
|
ad89b816a8 | ||
|
|
a4356d1f12 | ||
|
|
9d51148517 | ||
|
|
718ce9c1e3 | ||
|
|
93b8fa8c15 | ||
|
|
952dcd9f65 | ||
|
|
4003258b8f | ||
|
|
6f5d660082 | ||
|
|
ce3f90df6c | ||
|
|
d9a196a017 | ||
|
|
e65fa9f4a7 | ||
|
|
65bf044f5c | ||
|
|
00522e6037 | ||
|
|
695af6c630 | ||
|
|
755bc60f5d | ||
|
|
8bb0719bc6 | ||
|
|
dbed11b3e9 | ||
|
|
3a8cd673e8 | ||
|
|
fe118e13c3 | ||
|
|
cec8a0db1c | ||
|
|
ffd91c20e9 | ||
|
|
61238cfdb2 | ||
|
|
2a435eb5ab | ||
|
|
4e0f3d3cd2 | ||
|
|
9abadfe787 | ||
|
|
84b4012aa5 | ||
|
|
a602fbdc2e | ||
|
|
115f875167 | ||
|
|
fed4b85780 | ||
|
|
a2bf15359e | ||
|
|
a6f73fc4cc | ||
|
|
24e2949e46 | ||
|
|
2e3489c4f5 | ||
|
|
16c7b4d760 | ||
|
|
0091e0e122 | ||
|
|
b987976b74 | ||
|
|
3140d3054a | ||
|
|
14e3c44f5c | ||
|
|
be611f2411 | ||
|
|
3a0c7798a6 | ||
|
|
f6a2ad7f84 | ||
|
|
47e0c413f1 | ||
|
|
6ccaf93704 | ||
|
|
92d1b8d6bd | ||
|
|
5a0479446e | ||
|
|
d15404c57b | ||
|
|
0a97eef8d8 | ||
|
|
1e1012de0c | ||
|
|
cd2177e834 | ||
|
|
e6fae8b076 | ||
|
|
196222d991 | ||
|
|
247271b4cc | ||
|
|
63d2991e63 | ||
|
|
5a9f6593c7 | ||
|
|
3ed1d5649f | ||
|
|
1fca52de4f | ||
|
|
8dbfcf6365 | ||
|
|
80afb67cd5 | ||
|
|
b55e96eaf9 | ||
|
|
bae9d338e4 | ||
|
|
b0a4978da2 |
3748 changed files with 26157 additions and 874796 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/schemas/cultnet/epiphany.pipeline.*.schema.json text eol=lf
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
|
|
@ -1,6 +0,0 @@
|
|||
[submodule "vendor/cultcache-rs"]
|
||||
path = vendor/cultcache-rs
|
||||
url = https://github.com/GameCult/cultcache-rs.git
|
||||
[submodule "vendor/cultnet-rs"]
|
||||
path = vendor/cultnet-rs
|
||||
url = https://github.com/GameCult/cultnet-rs.git
|
||||
14
AGENTS.md
14
AGENTS.md
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
## Project Purpose
|
||||
|
||||
This repo is the forge-temple of Epiphany: an opinionated Codex fork where
|
||||
This repo is the forge-temple of Epiphany: an opinionated agent harness, born
|
||||
as a Codex fork and now backend-agnostic. Codex, Claude, and OpenAI-compatible
|
||||
APIs speak to it through connectors that share one standardized
|
||||
Epiphany-facing CultNet API, the same one Ghostlight uses. It is the harness where
|
||||
external typed state, explicit mental maps, bounded scratch work, verifier
|
||||
evidence, and anti-churn discipline are wired into the blessed harness instead
|
||||
of taped onto the chat transcript like a purity seal over a cracked reactor.
|
||||
|
|
@ -102,7 +105,6 @@ machine-spirit awake.
|
|||
## Important Paths
|
||||
|
||||
- Project root: `F:\Projects\Epiphany`
|
||||
- Vendored Codex repo: `F:\Projects\Epiphany\vendor\codex`
|
||||
- Fork implementation plan: `F:\Projects\Epiphany\notes\epiphany-fork-implementation-plan.md`
|
||||
- Handoff summary: `F:\Projects\Epiphany\notes\fresh-workspace-handoff.md`
|
||||
- Epiphany algorithmic map: `F:\Projects\Epiphany\notes\epiphany-current-algorithmic-map.md`
|
||||
|
|
@ -123,14 +125,6 @@ $env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'
|
|||
cargo run --manifest-path .\Cargo.toml --bin epiphany-state -- add-evidence --type research --status ok --note '...'
|
||||
```
|
||||
|
||||
Useful Codex repo searches:
|
||||
|
||||
```powershell
|
||||
rg -n "pub enum ModeKind|TUI_VISIBLE_COLLABORATION_MODES" .\vendor\codex\codex-rs\protocol\src\config_types.rs
|
||||
rg -n "builtin_collaboration_mode_presets|fn plan_preset|fn default_preset" .\vendor\codex\codex-rs\models-manager\src\collaboration_mode_presets.rs
|
||||
rg -n "collaboration_mode_label|collaboration_mode_indicator|set_collaboration_mask" .\vendor\codex\codex-rs\tui\src\chatwidget.rs
|
||||
```
|
||||
|
||||
## Session Bootstrap And Re-entry Rite
|
||||
|
||||
On fresh awakening, perform this rite before wandering into implementation like
|
||||
|
|
|
|||
3
CLAUDE.md
Normal file
3
CLAUDE.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Repository doctrine for this project lives in AGENTS.md, shared with Codex.
|
||||
|
||||
@AGENTS.md
|
||||
5497
Cargo.lock
generated
5497
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
34
Cargo.toml
34
Cargo.toml
|
|
@ -2,15 +2,11 @@
|
|||
members = [
|
||||
"epiphany-core",
|
||||
"epiphany-model-adapter",
|
||||
"epiphany-openai-adapter",
|
||||
"epiphany-openai-codex-spine",
|
||||
"epiphany-openai-runtime",
|
||||
"epiphany-release-construction",
|
||||
"epiphany-state-model",
|
||||
"epiphany-pipeline",
|
||||
"epiphany-tool-adapter",
|
||||
"epiphany-tool-mcp-runtime",
|
||||
]
|
||||
exclude = ["vendor/codex", "vendor/cultcache-rs"]
|
||||
resolver = "3"
|
||||
|
||||
[package]
|
||||
|
|
@ -25,33 +21,26 @@ license-file = "LICENSE"
|
|||
anyhow = "1"
|
||||
chrono = "0.4.44"
|
||||
ctrlc = { version = "3.5.1", features = ["termination"] }
|
||||
cultcache-rs = { path = "vendor/cultcache-rs" }
|
||||
cultmesh-rs = { path = "vendor/cultmesh-rs" }
|
||||
cultnet-rs = { path = "vendor/cultnet-rs" }
|
||||
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
|
||||
cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultnet-rs" }
|
||||
epiphany-core = { path = "epiphany-core" }
|
||||
epiphany-model-adapter = { path = "epiphany-model-adapter" }
|
||||
epiphany-openai-adapter = { path = "epiphany-openai-adapter" }
|
||||
epiphany-openai-runtime = { path = "epiphany-openai-runtime", optional = true }
|
||||
epiphany-state-model = { path = "epiphany-state-model" }
|
||||
codex-connector = { git = "https://github.com/GameCult/CodexConnector", rev = "be1776ab455fd79bd74b393518ab07211dc9c1f0", default-features = false, optional = true }
|
||||
epiphany-tool-adapter = { path = "epiphany-tool-adapter" }
|
||||
epiphany-tool-mcp-runtime = { path = "epiphany-tool-mcp-runtime", optional = true }
|
||||
http = "1"
|
||||
ignore = "0.4.23"
|
||||
postgres = "0.19"
|
||||
reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "json", "rustls"] }
|
||||
rmp-serde = "1"
|
||||
rmcp = { version = "2.2.0", default-features = false, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
semver = { version = "1", features = ["serde"] }
|
||||
sha2 = "0.10"
|
||||
tokio = { version = "1", features = ["io-util", "macros", "process", "rt", "rt-multi-thread", "time"] }
|
||||
toml = "0.9"
|
||||
tracing = "0.1.44"
|
||||
ureq = { version = "3.4", default-features = false, features = ["rustls"], optional = true }
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
zeroize = { version = "1", optional = true }
|
||||
|
||||
[features]
|
||||
openai-runtime = ["dep:epiphany-openai-runtime"]
|
||||
openai-runtime = ["dep:codex-connector", "dep:epiphany-openai-runtime", "dep:ureq", "dep:zeroize"]
|
||||
tool-mcp-runtime = ["dep:epiphany-tool-mcp-runtime"]
|
||||
release-runtime = ["openai-runtime", "tool-mcp-runtime"]
|
||||
|
||||
|
|
@ -61,13 +50,6 @@ incremental = true
|
|||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[patch.crates-io]
|
||||
tokio-tungstenite = { git = "https://github.com/openai-oss-forks/tokio-tungstenite", rev = "132f5b39c862e3a970f731d709608b3e6276d5f6" }
|
||||
tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "9200079d3b54a1ff51072e24d81fd354f085156f" }
|
||||
|
||||
[patch."ssh://git@github.com/openai-oss-forks/tungstenite-rs.git"]
|
||||
tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "9200079d3b54a1ff51072e24d81fd354f085156f" }
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-release"
|
||||
path = "epiphany-core/src/bin/epiphany-release.rs"
|
||||
|
|
@ -99,7 +81,7 @@ path = "epiphany-core/src/bin/epiphany-mvp-coordinator.rs"
|
|||
|
||||
[[bin]]
|
||||
name = "epiphany-model-runtime"
|
||||
path = "epiphany-openai-runtime/src/bin/epiphany-model-runtime.rs"
|
||||
path = "epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs"
|
||||
required-features = ["openai-runtime"]
|
||||
|
||||
[[bin]]
|
||||
|
|
|
|||
35
LICENSE
35
LICENSE
|
|
@ -19,15 +19,10 @@ For this file:
|
|||
directory states different terms.
|
||||
- `Upstream Material` means material copied or adapted from third parties and
|
||||
distributed under their own licenses.
|
||||
- `Mixed Apache Files` means files under `vendor/codex/**` that contain
|
||||
project-authored changes on top of upstream Apache-licensed material.
|
||||
|
||||
2. Upstream And Third-Party Material
|
||||
------------------------------------
|
||||
|
||||
- All material under `vendor/codex/**` remains subject to its upstream licenses
|
||||
and notices, including Apache License 2.0 at `vendor/codex/LICENSE` and any
|
||||
applicable `NOTICE` or file-level notices.
|
||||
- Other third-party material distributed with the repository keeps its own
|
||||
license terms where provided.
|
||||
- You must continue to comply with those upstream terms when using,
|
||||
|
|
@ -36,8 +31,8 @@ For this file:
|
|||
3. Public License For Project-Authored Material
|
||||
-----------------------------------------------
|
||||
|
||||
Except where a file or directory says otherwise, Project-Authored Material
|
||||
outside `vendor/codex/**` is licensed to the public under:
|
||||
Except where a file or directory says otherwise, Project-Authored Material is
|
||||
licensed to the public under:
|
||||
|
||||
- PolyForm Noncommercial License 1.0.0
|
||||
|
||||
|
|
@ -66,27 +61,7 @@ entity is formed and designated in writing, any commercial license for
|
|||
Project-Authored Material may be offered directly by the individual copyright
|
||||
holder or holders.
|
||||
|
||||
5. Mixed Apache Files Under `vendor/codex/**`
|
||||
---------------------------------------------
|
||||
|
||||
Some files under `vendor/codex/**` contain project-authored changes layered on
|
||||
top of upstream Apache-licensed code.
|
||||
|
||||
For those files:
|
||||
|
||||
- Apache License 2.0 and the applicable upstream notices still apply.
|
||||
- You must not read this repository notice as converting those files into a
|
||||
PolyForm-only codebase.
|
||||
- To the extent the project owns copyright in new additions to those files, the
|
||||
project may also offer separate commercial terms for its own contributions.
|
||||
- This repository notice does not claim to erase or retract Apache-2.0
|
||||
permissions already granted for Apache-covered material.
|
||||
|
||||
The strongest commercial boundary in this repository therefore applies to
|
||||
clearly separable Project-Authored Material outside `vendor/codex/**`,
|
||||
including `epiphany-core/**` and other non-vendored project files.
|
||||
|
||||
6. Open Source Status
|
||||
5. Open Source Status
|
||||
---------------------
|
||||
|
||||
Because the public license path for Project-Authored Material is PolyForm
|
||||
|
|
@ -94,7 +69,7 @@ Noncommercial License 1.0.0 rather than an OSI-approved license, the
|
|||
repository as a whole is source-available and dual-licensed. It is not an OSI
|
||||
Open Source distribution as a whole.
|
||||
|
||||
7. Reference Structures
|
||||
6. Reference Structures
|
||||
-----------------------
|
||||
|
||||
This licensing structure intentionally borrows the general dual-license shape
|
||||
|
|
@ -107,7 +82,7 @@ This repository does not claim that Qt or MySQL use the same exact public
|
|||
license terms used here. Those projects are referenced only as structural
|
||||
examples of dual licensing, not as the operative terms of this repository.
|
||||
|
||||
8. Contributions
|
||||
7. Contributions
|
||||
----------------
|
||||
|
||||
External contributions are accepted only under the EpiphanyAgent Contributor
|
||||
|
|
|
|||
19
README.md
19
README.md
|
|
@ -23,6 +23,19 @@ enough to act honestly.
|
|||
This is not a faster autocomplete costume. It is the beginning of governed
|
||||
human/agent labor.
|
||||
|
||||
## Front Door
|
||||
|
||||
- **Value:** the shared typed Mind for agent work: governed project state, role lanes for research, modeling, implementation, verification, planning and Persona, and receipts for commands, edits and commits, so work stays coherent past one prompt, one agent or one session.
|
||||
- **Problem:** global coherence collapse. Agents keep producing plausible local progress after the project has lost a valid global model; Epiphany turns understanding into inspectable state instead of a longer prompt.
|
||||
- **Entry points:**
|
||||
- `AGENTS.md`: operating law and re-entry rites. Read it before touching the machine.
|
||||
- `epiphany-core/`: the typed domain organs (`src/runtime_spine.rs` job, worker, model and tool documents; `src/substrate_gate.rs`; `src/state_model.rs`; `src/mind_documents.rs`).
|
||||
- `cargo run --bin epiphany-state -- status`: the steward CLI for direct Mind inspection (other native binaries are declared in `Cargo.toml`).
|
||||
- `schemas/cultnet/`, `state/map.yaml` (canonical project map), `notes/epiphany-current-algorithmic-map.md` (live control flow).
|
||||
- `docs/positioning.md`: what is claimed and what is not yet earned.
|
||||
- **Live:** a supervised engineering alpha: durable typed state and prompt projection, update/proposal/promotion/review paths, runtime-spine documents, operator status and coordinator commands, heartbeat and Persona-state machinery, and the Hands, Eyes, Soul, Mind, Continuity and Substrate Gate receipt families.
|
||||
- **Planned:** sustained autonomous GameCult production, complete Bifrost attribution, and a longitudinal fleet of one Epiphany organism per project Body. See What Exists Now.
|
||||
|
||||
## The GameCult Bet
|
||||
|
||||
GameCult is building infrastructure for coordinated work between humans,
|
||||
|
|
@ -120,15 +133,15 @@ Epiphany is a native GameCult runtime that began as an opinionated Codex fork.
|
|||
|
||||
Her body is made of:
|
||||
|
||||
- typed Rust domain organs in `epiphany-core` and `epiphany-state-model`
|
||||
- typed Rust domain organs owned by `epiphany-core`
|
||||
- CultCache `.cc` stores for runtime, heartbeat, agent state, local Verse,
|
||||
memory graph, and thread state
|
||||
- CultMesh and CultNet contracts for local and distributed state
|
||||
- provider-neutral model and tool request/receipt documents
|
||||
- Mind, Substrate Gate, Eyes, Hands, Soul, Continuity, Persona, and heartbeat
|
||||
authority surfaces
|
||||
- Codex retained only for honest OpenAI subscription auth and model transport;
|
||||
Epiphany does not publish a Codex-owned project-state surface
|
||||
- a narrow standalone CodexConnector process for OpenAI subscription auth and
|
||||
model transport; Epiphany compiles no Codex crate and keeps cognition local
|
||||
|
||||
That last clause matters. Codex and other frontier agents can be excellent
|
||||
workers. They do not decide which organizational state is authoritative, who
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ The practical definition in this repository is:
|
|||
|
||||
\begin{itemize}
|
||||
\item the root checkout at \code{F:\Projects\Epiphany};
|
||||
\item Epiphany Rust crates such as \code{epiphany-core}, \code{epiphany-state-model}, model/tool adapters, and OpenAI transport crates;
|
||||
\item Epiphany Rust crates such as \code{epiphany-core}, model/tool adapters, and provider runtimes;
|
||||
\item vendored substrate in \code{vendor/codex}, \code{vendor/cultcache-rs}, \code{vendor/cultnet-rs}, and \code{vendor/cultmesh-rs};
|
||||
\item typed state stores under \code{state/};
|
||||
\item schemas under \code{schemas/cultnet/};
|
||||
|
|
@ -59,20 +59,19 @@ The practical definition in this repository is:
|
|||
|
||||
\section{Executive Map}
|
||||
|
||||
Epiphany's current Body is a hybrid machine in transition. Its center of gravity
|
||||
has moved into typed Rust/CultCache organs, but compatibility arteries still
|
||||
run through vendored Codex. The live target is not a cleaner pile of Codex
|
||||
routes. The target is an Epiphany-native CultCache/CultMesh/CultNet body with a
|
||||
relatively vanilla Codex-derived reliquary retained for OpenAI subscription
|
||||
auth and model transport.
|
||||
Epiphany's current Body is a typed Rust/CultCache organism with provider
|
||||
transport outside its cognition boundary. Codex requests cross a lean client
|
||||
ABI into the independent CodexConnector daemon, which owns subscription
|
||||
credential refresh, transport, and replay. Epiphany owns prompts, tools,
|
||||
reasoning context, decisions, and Mind admission; it compiles no Codex crate.
|
||||
|
||||
\begin{longtable}{>{\raggedright\arraybackslash}p{0.23\linewidth}>{\raggedright\arraybackslash}p{0.32\linewidth}>{\raggedright\arraybackslash}p{0.35\linewidth}}
|
||||
\textbf{Body Region} & \textbf{Current Owner} & \textbf{Invariant} \\
|
||||
\hline
|
||||
Durable Mind state & keyed CultCache documents assembled by \code{epiphany-core} & Prompt rendering consumes sealed typed projections; no persisted thread aggregate or global authority revision exists. \\
|
||||
Domain policy & \code{epiphany-core} & Verdicts, launch policy, acceptance rules, runtime lifecycle, and mutation law live here, not in bridge DTOs. \\
|
||||
Vendored Codex & \code{vendor/codex} & Retain host/app-server/auth/model compatibility where useful; do not let Codex own Epiphany prompts, scheduler, or state. \\
|
||||
Runtime spine & \code{epiphany-core/src/runtime_spine.rs} and \code{state/runtime-spine.cc} & Runtime identity, jobs, events, receipts, tool/model documents are typed CultCache records. \\
|
||||
Codex transport & independent \code{GameCult/CodexConnector} daemon & Own subscription credentials, provider transport, and exact replay only; never own Epiphany prompts, scheduler, decisions, or state. \\
|
||||
Runtime spine & \code{epiphany-core/src/runtime_spine.rs} and \code{state/runtime-spine.cc} & Runtime identity, jobs, receipts, and tool/model documents are typed CultCache records. \\
|
||||
Persona memory & keyed Mind Persona-memory documents & Persona decisions admit bounded memories by semantic identity; no parallel role-memory aggregate or generation head exists. \\
|
||||
Resident Self physiology & Resident Self CultCache documents via \code{epiphany-swarm} & Typed pressure, exact grant creation, launch exclusion, leases, terminal receipts, retry, and bounded retention have one owner. No scheduler singleton or acknowledgement loop exists. \\
|
||||
Local Verse & CultMesh over \code{state/local-verse.cc} & Local Verse context is compact prompt/operator context; private state does not become public by flipping a flag. \\
|
||||
|
|
@ -82,27 +81,21 @@ Operator artifacts & \code{.epiphany-*}, \code{tools/epiphany_local_run.ps1} & A
|
|||
|
||||
\section{Repository Body}
|
||||
|
||||
The checkout has nine primary Epiphany Rust crates:
|
||||
The checkout has eight primary Epiphany Rust packages:
|
||||
|
||||
\begin{itemize}
|
||||
\item \code{epiphany-core}: the main domain organ. It owns state-update
|
||||
validation, launch documents, runtime spine, Resident Self, Persona
|
||||
projection, Substrate Gate, Mind, Eyes, Hands, Soul, Continuity, CultMesh
|
||||
integration, surface derivation, smokes, and local operator binaries.
|
||||
\item \code{epiphany-state-model}: the durable \code{EpiphanyThreadState}
|
||||
type plus \code{render_epiphany_state}. This is the typed Mind surface that
|
||||
prompt fragments and views read.
|
||||
\item \code{epiphany-core}: the main domain organ. It owns semantic state
|
||||
contracts, keyed Mind admission, launch documents, runtime spine, Resident
|
||||
Self, Persona projection, Substrate Gate, Eyes, Hands, Soul, Continuity,
|
||||
CultMesh integration, and surface derivation.
|
||||
\item \code{epiphany-model-adapter}: provider-neutral model request, stream
|
||||
event, receipt, and status documents.
|
||||
\item \code{epiphany-openai-adapter}: OpenAI-shaped request/event/receipt and
|
||||
status documents.
|
||||
\item \code{epiphany-openai-auth-spine}: Codex-derived auth boundary over
|
||||
\code{codex-login}. It preserves honest Codex-compatible credential and token
|
||||
behavior rather than impersonating it from scratch.
|
||||
\item \code{epiphany-openai-codex-spine}: model transport over vendored Codex
|
||||
client machinery.
|
||||
\item \code{epiphany-openai-runtime}: executable model runtime, including
|
||||
\code{epiphany-model-runtime}.
|
||||
\item \code{epiphany-release-construction}: exact package construction and
|
||||
release-witness verification.
|
||||
\item \code{epiphany-tool-adapter}: typed tool capability, invocation intent,
|
||||
and invocation receipt documents.
|
||||
\item \code{epiphany-tool-mcp-runtime}: first-party tool executor. Native
|
||||
|
|
@ -114,8 +107,8 @@ The checkout has nine primary Epiphany Rust crates:
|
|||
The vendored region contains:
|
||||
|
||||
\begin{itemize}
|
||||
\item \code{vendor/codex}: the Codex-derived host and compatibility substrate.
|
||||
It is tracked directly, not as a submodule.
|
||||
\item \code{vendor/codex}: retained Codex-fork source history. It is not in
|
||||
Epiphany's compiled runtime graph.
|
||||
\item \code{vendor/cultcache-rs}, \code{vendor/cultnet-rs}, and
|
||||
\code{vendor/cultmesh-rs}: repo-contained Rust Cult substrate.
|
||||
\end{itemize}
|
||||
|
|
@ -170,7 +163,7 @@ publication, sealed forensic artifacts, or named quarantine experiments.
|
|||
the Body. It imports and registers:
|
||||
|
||||
\begin{itemize}
|
||||
\item runtime identity, session, job, job-result, and event records;
|
||||
\item runtime identity, session, job, and job-result records;
|
||||
\item worker launch requests and role/reorient worker result documents;
|
||||
\item model adapter, OpenAI adapter, and tool adapter documents;
|
||||
\item sealed reasoning bases, decision contexts, typed pass failures, and
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ conflicts, or a consequential judgment cannot be derived honestly.
|
|||
|
||||
The current native Epiphany body includes:
|
||||
|
||||
- typed Rust domain organs in `epiphany-core` and `epiphany-state-model`
|
||||
- typed Rust domain organs owned by `epiphany-core`
|
||||
- keyed Mind state with typed projection and promotion paths
|
||||
- RepoModel and Model Atlas surfaces for source-grounded architectural belief
|
||||
- CultCache `.cc` state and CultMesh/CultNet contracts
|
||||
|
|
@ -86,7 +86,7 @@ The current native Epiphany body includes:
|
|||
- runtime-spine job and worker-result documents
|
||||
- Mind, Modeling, Substrate Gate, Eyes, Hands, Soul, Continuity, Persona, and
|
||||
heartbeat authority surfaces
|
||||
- local operator status, coordinator, smoke, and Verse-context commands
|
||||
- local operator status, coordinator, and Verse-context commands
|
||||
|
||||
Codex is retained only for OpenAI authentication and model transport. Epiphany
|
||||
does not publish Codex app-server state as its project Mind.
|
||||
|
|
|
|||
|
|
@ -14,26 +14,23 @@ path = "src/lib.rs"
|
|||
anyhow = "1"
|
||||
chrono = "0.4.44"
|
||||
ctrlc = { version = "3.5.1", features = ["termination"] }
|
||||
cultcache-rs = { path = "../vendor/cultcache-rs" }
|
||||
cultmesh-rs = { path = "../vendor/cultmesh-rs" }
|
||||
cultnet-rs = { path = "../vendor/cultnet-rs" }
|
||||
epiphany-openai-adapter = { path = "../epiphany-openai-adapter" }
|
||||
epiphany-release-construction = { path = "../epiphany-release-construction" }
|
||||
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
|
||||
cultmesh-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultmesh-rs" }
|
||||
cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultnet-rs" }
|
||||
epiphany-model-adapter = { path = "../epiphany-model-adapter" }
|
||||
epiphany-tool-adapter = { path = "../epiphany-tool-adapter" }
|
||||
epiphany-state-model = { path = "../epiphany-state-model" }
|
||||
ghostlight-persona-projection = { git = "https://github.com/GameCult/Ghostlight.git", rev = "22281891cbf9258db80c15a9fd465e6fa251e7dd", package = "ghostlight-persona-projection" }
|
||||
fs2 = "0.4"
|
||||
ghostlight-persona-projection = { git = "https://github.com/GameCult/Ghostlight.git", rev = "22937b71f056d2000dd7ea1f6af0c45825de495f", package = "ghostlight-persona-projection" }
|
||||
ed25519-dalek = "2"
|
||||
ignore = "0.4.23"
|
||||
rmp-serde = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde ={ version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
semver = { version = "1", features = ["serde"] }
|
||||
sha2 = "0.10"
|
||||
uuid = { version = "1", features = ["serde", "v4", "v5"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.1"
|
||||
tempfile = "3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ pub fn validate_request(request: &AdmittedModelDirectionConsiderationRequest) ->
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn request_is_superseded(
|
||||
pub(crate) fn request_is_superseded(
|
||||
cache: &cultcache_rs::CultCache,
|
||||
request: &AdmittedModelDirectionConsiderationRequest,
|
||||
) -> Result<bool> {
|
||||
|
|
@ -240,7 +240,7 @@ pub fn result_id_for_launch(request_id: &str, job_id: &str) -> String {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn render_prompt(request: &AdmittedModelDirectionConsiderationRequest) -> String {
|
||||
pub(crate) fn render_prompt(request: &AdmittedModelDirectionConsiderationRequest) -> String {
|
||||
format!(
|
||||
"Act as Epiphany Imagination for one proposal-only direction consideration. Inspect the exact current keyed Modeling map bound by request {} at projection digest {}. Suggest options or hold. Do not adopt, edit, execute, release, or deploy.",
|
||||
request.request_id, request.model_projection_digest
|
||||
|
|
|
|||
|
|
@ -13,35 +13,6 @@ pub const EPIPHANY_VERIFICATION_OWNER_ROLE: &str = "epiphany-verifier";
|
|||
pub const EPIPHANY_REORIENT_LAUNCH_BINDING_ID: &str = "reorient-worker";
|
||||
pub const EPIPHANY_REORIENT_OWNER_ROLE: &str = "epiphany-reorient";
|
||||
|
||||
pub fn epiphany_role_binding_id(role_id: EpiphanyRoleResultRoleId) -> Result<&'static str, String> {
|
||||
match role_id {
|
||||
EpiphanyRoleResultRoleId::Imagination => Ok(EPIPHANY_IMAGINATION_ROLE_BINDING_ID),
|
||||
EpiphanyRoleResultRoleId::Research => Ok(EPIPHANY_RESEARCH_ROLE_BINDING_ID),
|
||||
EpiphanyRoleResultRoleId::Modeling => Ok(EPIPHANY_MODELING_ROLE_BINDING_ID),
|
||||
EpiphanyRoleResultRoleId::Verification => Ok(EPIPHANY_VERIFICATION_ROLE_BINDING_ID),
|
||||
EpiphanyRoleResultRoleId::Implementation => Err(
|
||||
"implementation is owned by the main coding agent; no role specialist launch template exists"
|
||||
.to_string(),
|
||||
),
|
||||
EpiphanyRoleResultRoleId::Reorientation => Err(
|
||||
"reorientation uses epiphany.coordinator.reorient.launch and epiphany.coordinator.reorient.result.read"
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn epiphany_role_owner(role_id: EpiphanyRoleResultRoleId) -> Result<&'static str, String> {
|
||||
match role_id {
|
||||
EpiphanyRoleResultRoleId::Imagination => Ok(EPIPHANY_IMAGINATION_OWNER_ROLE),
|
||||
EpiphanyRoleResultRoleId::Research => Ok(EPIPHANY_RESEARCH_OWNER_ROLE),
|
||||
EpiphanyRoleResultRoleId::Modeling => Ok(EPIPHANY_MODELING_OWNER_ROLE),
|
||||
EpiphanyRoleResultRoleId::Verification => Ok(EPIPHANY_VERIFICATION_OWNER_ROLE),
|
||||
EpiphanyRoleResultRoleId::Implementation | EpiphanyRoleResultRoleId::Reorientation => {
|
||||
Err(epiphany_role_binding_id(role_id).unwrap_err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn epiphany_role_label(role_id: EpiphanyRoleResultRoleId) -> &'static str {
|
||||
match role_id {
|
||||
EpiphanyRoleResultRoleId::Implementation => "implementation",
|
||||
|
|
@ -760,17 +731,3 @@ pub fn epiphany_reorient_launch_output_schema() -> serde_json::Value {
|
|||
"additionalProperties": true
|
||||
})
|
||||
}
|
||||
|
||||
pub fn unique_strings(values: impl IntoIterator<Item = String>) -> Vec<String> {
|
||||
let mut unique = Vec::new();
|
||||
extend_unique_strings(&mut unique, values);
|
||||
unique
|
||||
}
|
||||
|
||||
fn extend_unique_strings(target: &mut Vec<String>, values: impl IntoIterator<Item = String>) {
|
||||
for value in values {
|
||||
if !target.iter().any(|existing| existing == &value) {
|
||||
target.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ impl AtlasCultCacheStore {
|
|||
cache.register_entry_type::<AtlasEntanglementProjection>()?;
|
||||
cache.register_entry_type::<AtlasImpactDedupeRecord>()?;
|
||||
cache.register_entry_type::<AtlasImpactStateRecord>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&self.path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&self.path))?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
Ok(cache)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,10 +2,9 @@ use anyhow::{Result, bail};
|
|||
use chrono::Utc;
|
||||
use epiphany_core::{
|
||||
EpiphanyMemoryAnchor, EpiphanyMemoryDomain, EpiphanyMemoryLifecycle, EpiphanyMemoryNode,
|
||||
EpiphanyMemoryNodeKind, EpiphanyRepoModelSeed,
|
||||
EpiphanyRepoModelSeedDocuments, ObserveOutcome, RuntimeSpineInitOptions,
|
||||
admit_repository_body_observation, bind_repository_body, bind_runtime_to_swarm,
|
||||
initialize_keyed_repo_model, initialize_runtime_spine,
|
||||
EpiphanyMemoryNodeKind, EpiphanyRepoModelSeed, EpiphanyRepoModelSeedDocuments, ObserveOutcome,
|
||||
RuntimeSpineInitOptions, admit_repository_body_observation, bind_repository_body,
|
||||
bind_runtime_to_swarm, initialize_keyed_repo_model, initialize_runtime_spine,
|
||||
load_current_runtime_repository_body_basis, load_repository_body_status,
|
||||
observe_repository_body,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,19 +10,17 @@ use epiphany_core::{
|
|||
derive_resident_cognition_readiness, enqueue_resident_self_pressure_idempotent,
|
||||
import_bifrost_persona_feedback_deliveries, ingest_resident_self_current_work_pressure,
|
||||
issue_resident_self_grant, live_resident_self_typed_request_ids,
|
||||
load_epiphany_cultmesh_swarm_brake,
|
||||
load_resident_self_state, materialize_resident_self_domain_obligations,
|
||||
observe_process_instance, prepare_resident_self_launch,
|
||||
publish_resident_provider_readiness, reap_exited_child_process,
|
||||
load_epiphany_cultmesh_swarm_brake, load_resident_self_state,
|
||||
materialize_resident_self_domain_obligations, observe_process_instance,
|
||||
prepare_resident_self_launch, publish_resident_provider_readiness, reap_exited_child_process,
|
||||
recover_dead_runtime_worker_attempts, resident_cognitive_runtime_id,
|
||||
resident_prepared_launch_thread_id, resident_self_child_claim,
|
||||
resident_self_local_provider_status, retain_completed_runtime_sessions,
|
||||
retain_failed_runtime_worker_attempts,
|
||||
retain_fulfilled_runtime_worker_attempts, retain_resident_self_lifecycles,
|
||||
runtime_worker_process_claims, settle_resident_self_exited_coordinator,
|
||||
settle_resident_self_receipt_free_dead_coordinator, terminate_process_instance,
|
||||
validate_persona_feedback_store_separation, validate_resident_self_coordinator_receipt_binding,
|
||||
validate_resident_self_store_separation,
|
||||
retain_failed_runtime_worker_attempts, retain_fulfilled_runtime_worker_attempts,
|
||||
retain_resident_self_lifecycles, runtime_worker_process_claims,
|
||||
settle_resident_self_exited_coordinator, settle_resident_self_receipt_free_dead_coordinator,
|
||||
terminate_process_instance, validate_persona_feedback_store_separation,
|
||||
validate_resident_self_coordinator_receipt_binding, validate_resident_self_store_separation,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
|
@ -39,6 +37,9 @@ use std::time::Duration;
|
|||
const RESIDENT_MAINTENANCE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
fn main() -> Result<()> {
|
||||
if env::args().nth(1).as_deref() == Some("initialize-and-release-brake") {
|
||||
return initialize_and_release_brake();
|
||||
}
|
||||
let mut args = Args::parse()?;
|
||||
authenticate_resident_self_policy(&mut args.policy)?;
|
||||
args.policy.validate()?;
|
||||
|
|
@ -119,6 +120,46 @@ fn main() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn initialize_and_release_brake() -> Result<()> {
|
||||
let mut values = BTreeMap::new();
|
||||
let mut args = env::args().skip(2);
|
||||
while let Some(flag) = args.next() {
|
||||
let value = args
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing value for {flag}"))?;
|
||||
values.insert(flag, value);
|
||||
}
|
||||
let required = |name: &str| {
|
||||
values
|
||||
.get(name)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("missing {name}"))
|
||||
};
|
||||
let store = PathBuf::from(required("--local-verse-store")?);
|
||||
let runtime_id = required("--runtime-id")?;
|
||||
let actor_id = required("--actor-id")?;
|
||||
let reason = required("--reason")?;
|
||||
let now = Utc::now().to_rfc3339();
|
||||
epiphany_core::engage_epiphany_cultmesh_swarm_brake(
|
||||
&store,
|
||||
&runtime_id,
|
||||
"Fresh runtime begins braked before explicit release.",
|
||||
&actor_id,
|
||||
&now,
|
||||
false,
|
||||
)?;
|
||||
let brake = epiphany_core::release_epiphany_cultmesh_swarm_brake(
|
||||
&store,
|
||||
&runtime_id,
|
||||
reason,
|
||||
actor_id,
|
||||
now,
|
||||
)?;
|
||||
println!("{}", serde_json::to_string_pretty(&brake)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require_resident_git(executable: &str) -> Result<()> {
|
||||
let output = Command::new(executable)
|
||||
.arg("--version")
|
||||
|
|
@ -135,11 +176,7 @@ fn require_resident_git(executable: &str) -> Result<()> {
|
|||
|
||||
fn maintain_resident_runtime(args: &Args, state: &ResidentSelfState) -> Result<()> {
|
||||
retain_runtime_receipts(args, state)?;
|
||||
retain_resident_self_lifecycles(
|
||||
&args.state_store,
|
||||
args.retained_closed_lifecycles,
|
||||
Utc::now().timestamp_millis().max(0) as u64,
|
||||
)?;
|
||||
retain_resident_self_lifecycles(&args.state_store, args.retained_closed_lifecycles)?;
|
||||
retain_runtime_worker_attempts(args)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -268,7 +305,6 @@ fn cycle(
|
|||
bind_runtime_repository_domain(
|
||||
&args.policy.runtime_store,
|
||||
&args.feedback_target_repository,
|
||||
&Utc::now().to_rfc3339(),
|
||||
)?;
|
||||
materialize_resident_self_domain_obligations(
|
||||
&args.policy.runtime_store,
|
||||
|
|
@ -502,7 +538,11 @@ impl Args {
|
|||
runtime_store: path("--runtime-store")?,
|
||||
local_verse_store: path("--local-verse-store")?,
|
||||
artifact_root: path("--artifact-root")?,
|
||||
codex_home: path("--codex-home")?,
|
||||
model_connector_endpoint: value
|
||||
.get("--connector-endpoint")
|
||||
.ok_or_else(|| anyhow!("missing --connector-endpoint"))?
|
||||
.parse()
|
||||
.context("invalid --connector-endpoint")?,
|
||||
mcp_config: path("--mcp-config")?,
|
||||
model_provider: value
|
||||
.get("--model-provider")
|
||||
|
|
@ -783,7 +823,7 @@ mod brake_tests {
|
|||
runtime_store,
|
||||
local_verse_store: verse_store,
|
||||
artifact_root: shared.join("artifacts"),
|
||||
codex_home: shared.join("codex-home"),
|
||||
model_connector_endpoint: "127.0.0.1:17891".parse().unwrap(),
|
||||
mcp_config: shared.join("mcp.toml"),
|
||||
model_provider: "test".into(),
|
||||
model: "test-model".into(),
|
||||
|
|
@ -810,5 +850,4 @@ mod brake_tests {
|
|||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ fn digest_id(prefix: &str, causal_parts: &[&str]) -> String {
|
|||
format!("{prefix}-{:x}", digest.finalize())
|
||||
}
|
||||
|
||||
pub fn proposal_modeling_request_id(
|
||||
pub(crate) fn proposal_modeling_request_id(
|
||||
runtime_id: &str,
|
||||
proposal_id: &str,
|
||||
payload_sha256: &str,
|
||||
|
|
@ -20,7 +20,7 @@ pub fn proposal_modeling_request_id(
|
|||
)
|
||||
}
|
||||
|
||||
pub fn frontier_research_request_id(
|
||||
pub(crate) fn frontier_research_request_id(
|
||||
runtime_id: &str,
|
||||
frontier_item_id: &str,
|
||||
frontier_item_hash: &str,
|
||||
|
|
@ -31,7 +31,7 @@ pub fn frontier_research_request_id(
|
|||
)
|
||||
}
|
||||
|
||||
pub fn frontier_planning_request_id(
|
||||
pub(crate) fn frontier_planning_request_id(
|
||||
runtime_id: &str,
|
||||
frontier_item_id: &str,
|
||||
frontier_item_hash: &str,
|
||||
|
|
@ -42,7 +42,7 @@ pub fn frontier_planning_request_id(
|
|||
)
|
||||
}
|
||||
|
||||
pub fn frontier_plan_mind_request_id(
|
||||
pub(crate) fn frontier_plan_mind_request_id(
|
||||
runtime_id: &str,
|
||||
planning_request_id: &str,
|
||||
imagination_result_id: &str,
|
||||
|
|
@ -59,7 +59,7 @@ pub fn frontier_plan_mind_request_id(
|
|||
)
|
||||
}
|
||||
|
||||
pub fn frontier_verdict_modeling_request_id(
|
||||
pub(crate) fn frontier_verdict_modeling_request_id(
|
||||
runtime_id: &str,
|
||||
soul_verdict_receipt_id: &str,
|
||||
verification_result_id: &str,
|
||||
|
|
@ -76,7 +76,7 @@ pub fn frontier_verdict_modeling_request_id(
|
|||
)
|
||||
}
|
||||
|
||||
pub fn admitted_model_direction_request_id(
|
||||
pub(crate) fn admitted_model_direction_request_id(
|
||||
runtime_id: &str,
|
||||
model_projection_digest: &str,
|
||||
previous_terminal_result_id: Option<&str>,
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
use crate::EpiphanyReorientFindingInterpretation;
|
||||
use cultcache_rs::DatabaseEntry;
|
||||
|
||||
pub const CONTINUITY_RECOVERY_RECEIPT_TYPE: &str = "epiphany.continuity.recovery_receipt";
|
||||
pub const CONTINUITY_RECOVERY_RECEIPT_SCHEMA_VERSION: &str =
|
||||
"epiphany.continuity.recovery_receipt.v0";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.continuity.recovery_receipt",
|
||||
schema = "ContinuityRecoveryReceipt"
|
||||
)]
|
||||
pub struct ContinuityRecoveryReceipt {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub receipt_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub source_result_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub source_job_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub binding_id: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub mode: String,
|
||||
#[cultcache(key = 6)]
|
||||
pub checkpoint_still_valid: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub summary: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub next_safe_move: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub files_inspected: Vec<String>,
|
||||
#[cultcache(key = 10)]
|
||||
pub emitted_at: String,
|
||||
#[cultcache(key = 11)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
pub fn continuity_recovery_receipt_from_reorient_finding(
|
||||
receipt_id: String,
|
||||
binding_id: String,
|
||||
finding: &EpiphanyReorientFindingInterpretation,
|
||||
emitted_at: String,
|
||||
) -> ContinuityRecoveryReceipt {
|
||||
ContinuityRecoveryReceipt {
|
||||
schema_version: CONTINUITY_RECOVERY_RECEIPT_SCHEMA_VERSION.to_string(),
|
||||
receipt_id,
|
||||
source_result_id: finding.runtime_result_id.clone().unwrap_or_default(),
|
||||
source_job_id: finding.runtime_job_id.clone().unwrap_or_default(),
|
||||
binding_id,
|
||||
mode: finding.mode.clone().unwrap_or_else(|| "unknown".to_string()),
|
||||
checkpoint_still_valid: finding
|
||||
.checkpoint_still_valid
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
summary: finding.summary.clone().unwrap_or_default(),
|
||||
next_safe_move: finding.next_safe_move.clone().unwrap_or_default(),
|
||||
files_inspected: finding.files_inspected.clone(),
|
||||
emitted_at,
|
||||
contract: "Continuity recovery emitted from a reviewed reorientation finding; it proves what survived rupture before Mind admits recovery state.".to_string(),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,13 @@
|
|||
use crate::{
|
||||
EpiphanyMindCommitAuthority, EpiphanyMindCommitOutcome, EpiphanyMindCommitReceipt,
|
||||
EpiphanyMindDocumentVersion, EpiphanyMindObjectiveDocument, EpiphanyMindView,
|
||||
MIND_OBJECTIVE_KEY, assemble_mind_view, commit_operator_mind_mutation, runtime_spine_cache,
|
||||
EpiphanyMindDocumentVersion, EpiphanyMindObjectiveDocument, MIND_OBJECTIVE_KEY,
|
||||
assemble_mind_view, commit_operator_mind_mutation, runtime_spine_cache,
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use cultcache_rs::DatabaseEntry;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::Path;
|
||||
|
||||
pub const USER_OBJECTIVE_INTAKE_SCHEMA_VERSION: &str = "gamecult.epiphany.user_objective_intake.v0";
|
||||
pub const USER_OBJECTIVE_INTAKE_TYPE: &str = "epiphany.coordinator.user_objective_intake";
|
||||
pub const USER_OBJECTIVE_INTAKE_CONTRACT: &str = "The human supplies the initial objective. Self records that assertion and the canonical keyed Mind objective in one CAS. Thread identity is provenance only. Repeated identical intake is read-idempotent; replacement requires a separate reviewed adoption flow.";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserObjectiveIntakeInput {
|
||||
pub thread_id: String,
|
||||
|
|
@ -28,29 +24,20 @@ pub struct UserObjectiveIntakeInput {
|
|||
)]
|
||||
pub struct UserObjectiveIntake {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub intake_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub thread_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
#[cultcache(key = 1)]
|
||||
pub objective: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub objective_sha256: String,
|
||||
#[cultcache(key = 5)]
|
||||
#[cultcache(key = 2)]
|
||||
pub source_actor: String,
|
||||
#[cultcache(key = 6)]
|
||||
#[cultcache(key = 3)]
|
||||
pub source_ref: String,
|
||||
#[cultcache(key = 7)]
|
||||
#[cultcache(key = 4)]
|
||||
pub submitted_at: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserObjectiveIntakeApplied {
|
||||
pub intake: UserObjectiveIntake,
|
||||
pub mind: EpiphanyMindView,
|
||||
pub mind_projection_digest: String,
|
||||
pub commit_receipt: EpiphanyMindCommitReceipt,
|
||||
pub changed: bool,
|
||||
}
|
||||
|
|
@ -71,7 +58,6 @@ pub fn intake_user_objective(
|
|||
{
|
||||
return Err(anyhow!("invalid typed user-objective intake"));
|
||||
}
|
||||
let objective_sha256 = format!("{:x}", Sha256::digest(objective.as_bytes()));
|
||||
let intake_id = format!("user-objective-{:x}", Sha256::digest(thread_id.as_bytes()));
|
||||
|
||||
let mut cache = runtime_spine_cache(store)?;
|
||||
|
|
@ -82,7 +68,6 @@ pub fn intake_user_objective(
|
|||
.ok_or_else(|| anyhow!("typed user-objective intake lost its Mind objective"))?;
|
||||
if existing.thread_id != thread_id
|
||||
|| existing.objective != objective
|
||||
|| existing.objective_sha256 != objective_sha256
|
||||
|| existing.source_actor != source_actor
|
||||
|| existing.source_ref != source_ref
|
||||
|| objective_document.objective.trim() != objective
|
||||
|
|
@ -113,8 +98,7 @@ pub fn intake_user_objective(
|
|||
})
|
||||
.ok_or_else(|| anyhow!("typed user-objective intake lost its Mind commit receipt"))?;
|
||||
return Ok(UserObjectiveIntakeApplied {
|
||||
intake: existing,
|
||||
mind: assemble_mind_view(store)?,
|
||||
mind_projection_digest: assemble_mind_view(store)?.projection_digest,
|
||||
commit_receipt: receipt,
|
||||
changed: false,
|
||||
});
|
||||
|
|
@ -128,15 +112,11 @@ pub fn intake_user_objective(
|
|||
));
|
||||
}
|
||||
let intake = UserObjectiveIntake {
|
||||
schema_version: USER_OBJECTIVE_INTAKE_SCHEMA_VERSION.into(),
|
||||
intake_id: intake_id.clone(),
|
||||
thread_id: thread_id.to_string(),
|
||||
objective: objective.to_string(),
|
||||
objective_sha256,
|
||||
source_actor: source_actor.to_string(),
|
||||
source_ref: source_ref.to_string(),
|
||||
submitted_at: input.submitted_at,
|
||||
contract: USER_OBJECTIVE_INTAKE_CONTRACT.into(),
|
||||
};
|
||||
let provenance = cache.prepare_entry(&intake_id, &intake)?.0;
|
||||
let objective_write = crate::mind_documents::prepare_mind_document(
|
||||
|
|
@ -160,8 +140,7 @@ pub fn intake_user_objective(
|
|||
}
|
||||
};
|
||||
Ok(UserObjectiveIntakeApplied {
|
||||
intake,
|
||||
mind: assemble_mind_view(store)?,
|
||||
mind_projection_digest: assemble_mind_view(store)?.projection_digest,
|
||||
commit_receipt,
|
||||
changed: true,
|
||||
})
|
||||
|
|
@ -196,7 +175,10 @@ mod tests {
|
|||
)?;
|
||||
let first = intake_user_objective(&store, input("Map the machine"))?;
|
||||
assert!(first.changed);
|
||||
assert_eq!(first.mind.objective.as_deref(), Some("Map the machine"));
|
||||
assert_eq!(
|
||||
assemble_mind_view(&store)?.objective.as_deref(),
|
||||
Some("Map the machine")
|
||||
);
|
||||
assert_eq!(
|
||||
first.commit_receipt.invariant_owner,
|
||||
"Self.user_objective_intake"
|
||||
|
|
@ -209,11 +191,16 @@ mod tests {
|
|||
.iter()
|
||||
.all(|envelope| envelope.r#type != "epiphany.thread_state")
|
||||
);
|
||||
let after_first = std::fs::read(&store)?;
|
||||
|
||||
let repeated = intake_user_objective(&store, input(" Map the machine "))?;
|
||||
assert!(!repeated.changed);
|
||||
assert_eq!(repeated.intake, first.intake);
|
||||
assert_eq!(
|
||||
repeated.mind_projection_digest,
|
||||
first.mind_projection_digest
|
||||
);
|
||||
assert_eq!(repeated.commit_receipt, first.commit_receipt);
|
||||
assert_eq!(std::fs::read(&store)?, after_first);
|
||||
|
||||
let before = std::fs::read(&store)?;
|
||||
let error = intake_user_objective(&store, input("Replace the machine"))
|
||||
|
|
|
|||
|
|
@ -1,95 +1,68 @@
|
|||
use crate::*;
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EpiphanyReorientResultStatus {
|
||||
Pending,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EpiphanyCoordinatorReorientResultSnapshot {
|
||||
pub status: EpiphanyCrrcResultStatus,
|
||||
pub status: EpiphanyReorientResultStatus,
|
||||
pub finding: Option<EpiphanyReorientFindingInterpretation>,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
pub fn read_runtime_reorient_result(
|
||||
runtime_store_path: Option<&Path>,
|
||||
runtime_store_path: &Path,
|
||||
job_id: &str,
|
||||
) -> EpiphanyCoordinatorReorientResultSnapshot {
|
||||
let Some(runtime_store_path) = runtime_store_path else {
|
||||
return reorient_snapshot(
|
||||
EpiphanyCrrcResultStatus::Pending,
|
||||
None,
|
||||
Some(
|
||||
"Resident Self owns this reorientation worker; no loaded runtime-spine store is available yet.",
|
||||
),
|
||||
);
|
||||
};
|
||||
let snapshot = match runtime_job_snapshot(runtime_store_path, job_id) {
|
||||
Ok(Some(snapshot)) => snapshot,
|
||||
Ok(None) => {
|
||||
return reorient_snapshot(
|
||||
EpiphanyCrrcResultStatus::Pending,
|
||||
) -> Result<EpiphanyCoordinatorReorientResultSnapshot> {
|
||||
let mut cache = runtime_spine_cache(runtime_store_path)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let job = match cache.get::<EpiphanyRuntimeJob>(job_id)? {
|
||||
Some(job) => job,
|
||||
None => {
|
||||
return Ok(reorient_snapshot(
|
||||
EpiphanyReorientResultStatus::Pending,
|
||||
None,
|
||||
Some(&format!(
|
||||
"Resident runtime job {job_id:?} has not reported typed state yet."
|
||||
)),
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
return reorient_snapshot(
|
||||
EpiphanyCrrcResultStatus::BackendUnavailable,
|
||||
None,
|
||||
Some(&format!(
|
||||
"Failed to read resident runtime-spine job {job_id:?}: {error}"
|
||||
)),
|
||||
);
|
||||
));
|
||||
}
|
||||
};
|
||||
let status = reorient_result_status(&snapshot);
|
||||
let status = reorient_result_status(&job.status);
|
||||
let finding = match status {
|
||||
EpiphanyCrrcResultStatus::Completed => {
|
||||
match runtime_reorient_worker_result(runtime_store_path, job_id) {
|
||||
Ok(Some(result)) => Some(interpret_runtime_reorient_worker_result(&result)),
|
||||
Ok(None) => {
|
||||
return reorient_snapshot(
|
||||
EpiphanyCrrcResultStatus::BackendUnavailable,
|
||||
None,
|
||||
Some(&format!(
|
||||
"Resident runtime job {job_id:?} completed without an EpiphanyRuntimeReorientWorkerResult typed document; generic lifecycle receipts are not reviewable findings."
|
||||
)),
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
return reorient_snapshot(
|
||||
EpiphanyCrrcResultStatus::BackendUnavailable,
|
||||
None,
|
||||
Some(&format!(
|
||||
"Failed to read typed reorientation worker result for resident runtime job {job_id:?}: {error}"
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
EpiphanyCrrcResultStatus::Failed | EpiphanyCrrcResultStatus::Cancelled => snapshot
|
||||
.result
|
||||
EpiphanyReorientResultStatus::Completed => Some(interpret_runtime_reorient_worker_result(
|
||||
&cache
|
||||
.get::<EpiphanyRuntimeReorientWorkerResult>(job_id)?
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"resident runtime job {job_id:?} completed without its typed reorientation result"
|
||||
)
|
||||
})?,
|
||||
)),
|
||||
EpiphanyReorientResultStatus::Failed => cache
|
||||
.get::<EpiphanyRuntimeReorientWorkerResult>(job_id)?
|
||||
.as_ref()
|
||||
.map(interpret_reorient_lifecycle_failure),
|
||||
_ => None,
|
||||
.map(interpret_runtime_reorient_worker_result),
|
||||
EpiphanyReorientResultStatus::Pending => None,
|
||||
};
|
||||
reorient_snapshot(status, finding, None)
|
||||
Ok(reorient_snapshot(status, finding, None))
|
||||
}
|
||||
|
||||
fn reorient_snapshot(
|
||||
status: EpiphanyCrrcResultStatus,
|
||||
status: EpiphanyReorientResultStatus,
|
||||
finding: Option<EpiphanyReorientFindingInterpretation>,
|
||||
note_override: Option<&str>,
|
||||
) -> EpiphanyCoordinatorReorientResultSnapshot {
|
||||
let note = note_override.map(str::to_string).unwrap_or_else(|| {
|
||||
render_reorient_result_note(
|
||||
status,
|
||||
finding.as_ref(),
|
||||
finding
|
||||
.as_ref()
|
||||
.and_then(|finding| finding.job_error.as_deref()),
|
||||
)
|
||||
});
|
||||
let note = note_override
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| render_reorient_result_note(status, finding.as_ref()));
|
||||
EpiphanyCoordinatorReorientResultSnapshot {
|
||||
status,
|
||||
finding,
|
||||
|
|
@ -97,80 +70,31 @@ fn reorient_snapshot(
|
|||
}
|
||||
}
|
||||
|
||||
fn interpret_reorient_lifecycle_failure(
|
||||
result: &EpiphanyRuntimeJobResult,
|
||||
) -> EpiphanyReorientFindingInterpretation {
|
||||
EpiphanyReorientFindingInterpretation {
|
||||
mode: None,
|
||||
summary: Some(result.summary.clone()),
|
||||
next_safe_move: nonempty(&result.next_safe_move),
|
||||
checkpoint_still_valid: None,
|
||||
files_inspected: Vec::new(),
|
||||
frontier_node_ids: Vec::new(),
|
||||
evidence_ids: result.evidence_refs.clone(),
|
||||
artifact_refs: result.artifact_refs.clone(),
|
||||
runtime_result_id: Some(result.result_id.clone()),
|
||||
runtime_job_id: Some(result.job_id.clone()),
|
||||
job_error: Some(result.summary.clone()),
|
||||
item_error: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn reorient_result_status(snapshot: &EpiphanyRuntimeJobSnapshot) -> EpiphanyCrrcResultStatus {
|
||||
match snapshot.job.status {
|
||||
EpiphanyRuntimeJobStatus::Queued => EpiphanyCrrcResultStatus::Pending,
|
||||
EpiphanyRuntimeJobStatus::Running | EpiphanyRuntimeJobStatus::WaitingForReview => {
|
||||
EpiphanyCrrcResultStatus::Running
|
||||
}
|
||||
EpiphanyRuntimeJobStatus::Completed if snapshot.result.is_some() => {
|
||||
EpiphanyCrrcResultStatus::Completed
|
||||
}
|
||||
EpiphanyRuntimeJobStatus::Completed => EpiphanyCrrcResultStatus::Pending,
|
||||
EpiphanyRuntimeJobStatus::Failed => EpiphanyCrrcResultStatus::Failed,
|
||||
EpiphanyRuntimeJobStatus::Cancelled => EpiphanyCrrcResultStatus::Cancelled,
|
||||
fn reorient_result_status(status: &EpiphanyRuntimeJobStatus) -> EpiphanyReorientResultStatus {
|
||||
match status {
|
||||
EpiphanyRuntimeJobStatus::Queued => EpiphanyReorientResultStatus::Pending,
|
||||
EpiphanyRuntimeJobStatus::Completed => EpiphanyReorientResultStatus::Completed,
|
||||
EpiphanyRuntimeJobStatus::Failed => EpiphanyReorientResultStatus::Failed,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_reorient_result_note(
|
||||
status: EpiphanyCrrcResultStatus,
|
||||
status: EpiphanyReorientResultStatus,
|
||||
finding: Option<&EpiphanyReorientFindingInterpretation>,
|
||||
item_error: Option<&str>,
|
||||
) -> String {
|
||||
match status {
|
||||
EpiphanyCrrcResultStatus::Completed => finding.map_or_else(
|
||||
|| "Reorientation worker completed, but no structured result was recorded.".to_string(),
|
||||
|finding| {
|
||||
format!(
|
||||
"Reorientation worker completed. Next safe move: {}",
|
||||
finding.next_safe_move.as_deref().unwrap_or("not supplied")
|
||||
)
|
||||
},
|
||||
EpiphanyReorientResultStatus::Completed => format!(
|
||||
"Reorientation worker completed. Next safe move: {}",
|
||||
finding
|
||||
.and_then(|finding| finding.next_safe_move.as_deref())
|
||||
.unwrap_or("not supplied")
|
||||
),
|
||||
EpiphanyCrrcResultStatus::Failed => item_error
|
||||
EpiphanyReorientResultStatus::Failed => finding
|
||||
.and_then(|finding| finding.job_error.as_deref())
|
||||
.map(|error| format!("Reorientation worker failed: {error}"))
|
||||
.unwrap_or_else(|| "Reorientation worker failed.".to_string()),
|
||||
EpiphanyCrrcResultStatus::Cancelled => {
|
||||
"Reorientation worker was cancelled before producing a result.".to_string()
|
||||
}
|
||||
EpiphanyCrrcResultStatus::Running => "Reorientation worker is still running.".to_string(),
|
||||
EpiphanyCrrcResultStatus::Pending => {
|
||||
EpiphanyReorientResultStatus::Pending => {
|
||||
"Reorientation worker has not produced a result yet.".to_string()
|
||||
}
|
||||
EpiphanyCrrcResultStatus::MissingState => {
|
||||
"No authoritative Epiphany state exists for this thread.".to_string()
|
||||
}
|
||||
EpiphanyCrrcResultStatus::MissingBinding => {
|
||||
"No matching Epiphany reorientation worker binding exists.".to_string()
|
||||
}
|
||||
EpiphanyCrrcResultStatus::BackendUnavailable => {
|
||||
"The bound runtime backend is unavailable.".to_string()
|
||||
}
|
||||
EpiphanyCrrcResultStatus::BackendMissing => {
|
||||
"The bound runtime backend job or item is missing.".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn nonempty(value: &str) -> Option<String> {
|
||||
(!value.trim().is_empty()).then(|| value.to_string())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use anyhow::{Context, Result};
|
||||
use crate::{
|
||||
EpiphanyCoordinatorInput, EpiphanyCrrcAction, EpiphanyCurrentWorkProjection,
|
||||
EpiphanyRoleBoardInput, RepoFrontierPlanningLifecycle, RepoFrontierPlanningLifecycleStage,
|
||||
EpiphanyCoordinatorInput, EpiphanyCurrentWorkProjection, EpiphanyRoleBoardInput,
|
||||
RepoFrontierPlanningLifecycle, RepoFrontierPlanningLifecycleStage,
|
||||
RepoFrontierResearchLifecycle, RepoFrontierResearchLifecycleStage, derive_role_board,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use serde_json::{Value, json};
|
||||
use std::{
|
||||
env,
|
||||
|
|
@ -23,38 +23,25 @@ const MAX_OPERATOR_TEXT_CHARS: usize = 1200;
|
|||
pub fn native_coordinator_json(runtime_store: &Path, thread_id: &str) -> Result<Value> {
|
||||
let store_path = absolute_path(runtime_store)?;
|
||||
|
||||
let mind = if store_path.exists() {
|
||||
Some(
|
||||
crate::assemble_mind_view(&store_path).with_context(|| {
|
||||
format!("failed to assemble keyed Mind {}", store_path.display())
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let current_work = if mind.is_some() {
|
||||
crate::project_current_work(&store_path)
|
||||
.context("failed to derive current work from keyed Mind and runtime receipts")?
|
||||
} else {
|
||||
empty_current_work()
|
||||
};
|
||||
let latest_reorientation_decision = mind
|
||||
let projected_work = store_path
|
||||
.exists()
|
||||
.then(|| {
|
||||
crate::project_current_work(&store_path)
|
||||
.context("failed to derive current work from keyed Mind and runtime receipts")
|
||||
})
|
||||
.transpose()?;
|
||||
let mind_present = projected_work.is_some();
|
||||
let projection_digest = projected_work
|
||||
.as_ref()
|
||||
.and_then(|mind| mind.reorientation_decisions.last());
|
||||
let crrc_action =
|
||||
if latest_reorientation_decision.is_some_and(|decision| decision.mode == "regather") {
|
||||
EpiphanyCrrcAction::RegatherManually
|
||||
} else {
|
||||
EpiphanyCrrcAction::Continue
|
||||
};
|
||||
.map(|work| work.mind_projection_digest.clone());
|
||||
let current_work = projected_work.unwrap_or_else(empty_current_work);
|
||||
|
||||
let roles = derive_role_board(EpiphanyRoleBoardInput {
|
||||
mind_present: mind.is_some(),
|
||||
mind_present,
|
||||
current_work: current_work.clone(),
|
||||
});
|
||||
let coordinator = crate::recommend_coordinator_action(EpiphanyCoordinatorInput {
|
||||
mind_present: mind.is_some(),
|
||||
crrc_action,
|
||||
mind_present,
|
||||
current_work: current_work.clone(),
|
||||
});
|
||||
Ok(sanitize_for_operator(json!({
|
||||
|
|
@ -62,8 +49,8 @@ pub fn native_coordinator_json(runtime_store: &Path, thread_id: &str) -> Result<
|
|||
"read": {
|
||||
"source": "native",
|
||||
"mindStore": store_path,
|
||||
"mindPresent": mind.is_some(),
|
||||
"projectionDigest": mind.as_ref().map(|mind| mind.projection_digest.as_str()),
|
||||
"mindPresent": mind_present,
|
||||
"projectionDigest": projection_digest,
|
||||
},
|
||||
"roles": {"threadId": thread_id, "source": "native", "roles": roles},
|
||||
"currentWork": current_work,
|
||||
|
|
@ -74,6 +61,7 @@ pub fn native_coordinator_json(runtime_store: &Path, thread_id: &str) -> Result<
|
|||
fn empty_current_work() -> EpiphanyCurrentWorkProjection {
|
||||
EpiphanyCurrentWorkProjection {
|
||||
mind_projection_digest: String::new(),
|
||||
operator_regather_required: false,
|
||||
body_modeling: None,
|
||||
research: RepoFrontierResearchLifecycle {
|
||||
stage: RepoFrontierResearchLifecycleStage::Terminal,
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ use cultmesh_rs::{CultMesh, CultMeshNode, CultMeshNodeOptions, cultmesh_document
|
|||
use std::path::Path;
|
||||
|
||||
pub const EPIPHANY_CULTMESH_SWARM_BRAKE_TYPE: &str = "epiphany.cultmesh.swarm_brake";
|
||||
pub const EPIPHANY_CULTMESH_SWARM_BRAKE_SCHEMA_VERSION: &str =
|
||||
"epiphany.cultmesh.swarm_brake.v0";
|
||||
pub const EPIPHANY_CULTMESH_SWARM_BRAKE_SCHEMA_VERSION: &str = "epiphany.cultmesh.swarm_brake.v0";
|
||||
pub const EPIPHANY_CULTMESH_SWARM_BRAKE_KEY: &str = "epiphany-local/swarm-brake";
|
||||
pub const EPIPHANY_CANONICAL_SWARM_BRAKE_ID: &str = "epiphany/swarm-brake";
|
||||
pub const EPIPHANY_CANONICAL_SWARM_BRAKE_OWNER: &str = "epiphany.swarm-brake";
|
||||
|
|
@ -66,18 +65,6 @@ pub fn open_epiphany_cultmesh_node(
|
|||
)
|
||||
}
|
||||
|
||||
pub fn canonical_epiphany_swarm_brake_protected_surfaces() -> Vec<String> {
|
||||
vec![
|
||||
"resident.self".to_string(),
|
||||
"coordinator.run".to_string(),
|
||||
"persona.public_speech".to_string(),
|
||||
"hands.consequence".to_string(),
|
||||
"atlas.publish".to_string(),
|
||||
"atlas.project".to_string(),
|
||||
"atlas.impact_ingress".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn default_epiphany_cultmesh_swarm_brake(
|
||||
generated_at_utc: impl Into<String>,
|
||||
) -> EpiphanyCultMeshSwarmBrakeEntry {
|
||||
|
|
@ -90,7 +77,15 @@ pub fn default_epiphany_cultmesh_swarm_brake(
|
|||
.to_string(),
|
||||
operator_agent_id: EPIPHANY_CANONICAL_SWARM_BRAKE_OWNER.to_string(),
|
||||
affected_clusters: Vec::new(),
|
||||
protected_surfaces: canonical_epiphany_swarm_brake_protected_surfaces(),
|
||||
protected_surfaces: vec![
|
||||
"resident.self".into(),
|
||||
"coordinator.run".into(),
|
||||
"persona.public_speech".into(),
|
||||
"hands.consequence".into(),
|
||||
"atlas.publish".into(),
|
||||
"atlas.project".into(),
|
||||
"atlas.impact_ingress".into(),
|
||||
],
|
||||
created_at_utc: generated_at_utc.into(),
|
||||
expires_at_utc: None,
|
||||
private_state_exposed: false,
|
||||
|
|
@ -113,7 +108,9 @@ pub fn engage_epiphany_cultmesh_swarm_brake(
|
|||
return Err(anyhow!("swarm brake engagement requires an actor identity"));
|
||||
}
|
||||
if allow_engaged_adoption && actor_id != "Idunn" {
|
||||
return Err(anyhow!("only Idunn may adopt an already-engaged legacy brake"));
|
||||
return Err(anyhow!(
|
||||
"only Idunn may adopt an already-engaged legacy brake"
|
||||
));
|
||||
}
|
||||
if let Some(current) = load_epiphany_cultmesh_swarm_brake(&store_path, runtime_id.clone())? {
|
||||
let foreign = current.brake_id != EPIPHANY_CANONICAL_SWARM_BRAKE_ID
|
||||
|
|
@ -200,7 +197,9 @@ fn validate_swarm_brake(brake: &EpiphanyCultMeshSwarmBrakeEntry) -> Result<()> {
|
|||
|| brake.runtime_id.trim().is_empty()
|
||||
|| brake.created_at_utc.trim().is_empty()
|
||||
{
|
||||
return Err(anyhow!("swarm brake identity, scope, runtime, and timestamp are required"));
|
||||
return Err(anyhow!(
|
||||
"swarm brake identity, scope, runtime, and timestamp are required"
|
||||
));
|
||||
}
|
||||
if !matches!(brake.status.as_str(), "released" | "engaged") {
|
||||
return Err(anyhow!("swarm brake status must be released or engaged"));
|
||||
|
|
@ -210,7 +209,9 @@ fn validate_swarm_brake(brake: &EpiphanyCultMeshSwarmBrakeEntry) -> Result<()> {
|
|||
|| brake.operator_agent_id.trim().is_empty()
|
||||
|| (brake.affected_clusters.is_empty() && brake.protected_surfaces.is_empty()))
|
||||
{
|
||||
return Err(anyhow!("engaged swarm brake requires operator id, reason, and scope"));
|
||||
return Err(anyhow!(
|
||||
"engaged swarm brake requires operator id, reason, and scope"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -233,7 +234,12 @@ mod tests {
|
|||
)?;
|
||||
assert_eq!(engaged.status, "engaged");
|
||||
assert_eq!(engaged.brake_id, EPIPHANY_CANONICAL_SWARM_BRAKE_ID);
|
||||
assert!(engaged.protected_surfaces.iter().any(|value| value == "hands.consequence"));
|
||||
assert!(
|
||||
engaged
|
||||
.protected_surfaces
|
||||
.iter()
|
||||
.any(|value| value == "hands.consequence")
|
||||
);
|
||||
assert_eq!(
|
||||
load_epiphany_cultmesh_swarm_brake(&store, "runtime")?,
|
||||
Some(engaged)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,168 +0,0 @@
|
|||
use crate::EpiphanyResearchDecision;
|
||||
use crate::EpiphanyRoleFindingInterpretation;
|
||||
use cultcache_rs::DatabaseEntry;
|
||||
|
||||
pub const EYES_SOURCE_LOOKUP_RECEIPT_TYPE: &str = "epiphany.eyes.source_lookup_receipt";
|
||||
pub const EYES_EVIDENCE_PACKET_TYPE: &str = "epiphany.eyes.evidence_packet";
|
||||
pub const EYES_SOURCE_LOOKUP_RECEIPT_SCHEMA_VERSION: &str =
|
||||
"epiphany.eyes.source_lookup_receipt.v0";
|
||||
pub const EYES_EVIDENCE_PACKET_SCHEMA_VERSION: &str = "epiphany.eyes.evidence_packet.v1";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.eyes.source_lookup_receipt",
|
||||
schema = "EyesSourceLookupReceipt"
|
||||
)]
|
||||
pub struct EyesSourceLookupReceipt {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub receipt_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub source_job_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub substrate_grant_receipt_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub tool_intent_id: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub tool_receipt_id: String,
|
||||
#[cultcache(key = 6)]
|
||||
pub provider: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub repository: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub revision: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub path: String,
|
||||
#[cultcache(key = 10)]
|
||||
pub source_ref: String,
|
||||
#[cultcache(key = 11)]
|
||||
pub content_sha256: String,
|
||||
#[cultcache(key = 12)]
|
||||
pub byte_count: u64,
|
||||
#[cultcache(key = 13)]
|
||||
pub observed_at: String,
|
||||
#[cultcache(key = 14)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "epiphany.eyes.evidence_packet", schema = "EyesEvidencePacket")]
|
||||
pub struct EyesEvidencePacket {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub packet_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub source_result_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub source_job_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub source_role_id: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub evidence_ids: Vec<String>,
|
||||
#[cultcache(key = 6)]
|
||||
pub observation_ids: Vec<String>,
|
||||
#[cultcache(key = 7)]
|
||||
pub source_refs: Vec<String>,
|
||||
#[cultcache(key = 8)]
|
||||
pub summary: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub uncertainty: String,
|
||||
#[cultcache(key = 10)]
|
||||
pub emitted_at: String,
|
||||
#[cultcache(key = 11)]
|
||||
pub contract: String,
|
||||
#[cultcache(key = 12, default)]
|
||||
pub source_lookup_receipt_ids: Vec<String>,
|
||||
#[cultcache(key = 13)]
|
||||
pub research_request_id: String,
|
||||
#[cultcache(key = 14)]
|
||||
pub decision_context_id: String,
|
||||
}
|
||||
|
||||
pub fn eyes_evidence_packet_from_research_finding(
|
||||
packet_id: String,
|
||||
research_request_id: String,
|
||||
decision_context_id: String,
|
||||
finding: &EpiphanyRoleFindingInterpretation,
|
||||
decision: &EpiphanyResearchDecision,
|
||||
source_lookups: &[EyesSourceLookupReceipt],
|
||||
emitted_at: String,
|
||||
) -> EyesEvidencePacket {
|
||||
let evidence_ids = decision
|
||||
.evidence
|
||||
.iter()
|
||||
.filter_map(|evidence| non_empty_string(&evidence.id))
|
||||
.collect::<Vec<_>>();
|
||||
let observation_ids = decision
|
||||
.observations
|
||||
.iter()
|
||||
.filter_map(|observation| non_empty_string(&observation.id))
|
||||
.collect::<Vec<_>>();
|
||||
let mut source_refs = Vec::new();
|
||||
for evidence in &decision.evidence {
|
||||
for code_ref in &evidence.code_refs {
|
||||
let mut rendered = code_ref.path.display().to_string();
|
||||
if let Some(start_line) = code_ref.start_line {
|
||||
rendered.push(':');
|
||||
rendered.push_str(&start_line.to_string());
|
||||
}
|
||||
if let Some(end_line) = code_ref.end_line
|
||||
&& Some(end_line) != code_ref.start_line
|
||||
{
|
||||
rendered.push('-');
|
||||
rendered.push_str(&end_line.to_string());
|
||||
}
|
||||
if let Some(symbol) = code_ref.symbol.as_deref().filter(|value| !value.is_empty()) {
|
||||
rendered.push('#');
|
||||
rendered.push_str(symbol);
|
||||
}
|
||||
push_unique(&mut source_refs, rendered);
|
||||
}
|
||||
}
|
||||
for lookup in source_lookups {
|
||||
push_unique(&mut source_refs, lookup.source_ref.clone());
|
||||
}
|
||||
EyesEvidencePacket {
|
||||
schema_version: EYES_EVIDENCE_PACKET_SCHEMA_VERSION.to_string(),
|
||||
packet_id,
|
||||
source_result_id: finding.runtime_result_id.clone().unwrap_or_default(),
|
||||
source_job_id: finding.runtime_job_id.clone().unwrap_or_default(),
|
||||
source_role_id: "research".to_string(),
|
||||
evidence_ids,
|
||||
observation_ids,
|
||||
source_refs,
|
||||
summary: finding.summary.clone().unwrap_or_default(),
|
||||
uncertainty: if finding.evidence_gaps.is_empty() && finding.risks.is_empty() {
|
||||
"no declared research gaps or risks".to_string()
|
||||
} else {
|
||||
finding
|
||||
.evidence_gaps
|
||||
.iter()
|
||||
.chain(finding.risks.iter())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ")
|
||||
},
|
||||
emitted_at,
|
||||
contract: "Eyes packet emitted from a reviewed Research lane finding; it makes the source-gathering evidence claim citable before Mind admission.".to_string(),
|
||||
source_lookup_receipt_ids: source_lookups
|
||||
.iter()
|
||||
.map(|lookup| lookup.receipt_id.clone())
|
||||
.collect(),
|
||||
research_request_id,
|
||||
decision_context_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty_string(value: &str) -> Option<String> {
|
||||
let value = value.trim();
|
||||
(!value.is_empty()).then(|| value.to_string())
|
||||
}
|
||||
|
||||
fn push_unique(out: &mut Vec<String>, value: String) {
|
||||
if !out.contains(&value) {
|
||||
out.push(value);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +1,34 @@
|
|||
use cultcache_rs::DatabaseEntry;
|
||||
|
||||
pub const HANDS_ACTION_INTENT_TYPE: &str = "epiphany.hands.action_intent";
|
||||
pub const HANDS_ACTION_REVIEW_TYPE: &str = "epiphany.hands.action_review";
|
||||
pub const HANDS_COMMAND_RECEIPT_TYPE: &str = "epiphany.hands.command_receipt";
|
||||
pub const HANDS_PATCH_RECEIPT_TYPE: &str = "epiphany.hands.patch_receipt";
|
||||
pub const HANDS_COMMIT_RECEIPT_TYPE: &str = "epiphany.hands.commit_receipt";
|
||||
pub const HANDS_ACTION_INTENT_SCHEMA_VERSION: &str = "epiphany.hands.action_intent.v1";
|
||||
pub const HANDS_ACTION_REVIEW_SCHEMA_VERSION: &str = "epiphany.hands.action_review.v0";
|
||||
pub const HANDS_COMMAND_RECEIPT_SCHEMA_VERSION: &str = "epiphany.hands.command_receipt.v0";
|
||||
pub const HANDS_PATCH_RECEIPT_SCHEMA_VERSION: &str = "epiphany.hands.patch_receipt.v0";
|
||||
pub const HANDS_COMMIT_RECEIPT_SCHEMA_VERSION: &str = "epiphany.hands.commit_receipt.v0";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "epiphany.hands.action_intent", schema = "HandsActionIntent")]
|
||||
pub struct HandsActionIntent {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub intent_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
#[cultcache(key = 1)]
|
||||
pub runtime_job_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub binding_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub role: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub authority_scope: String,
|
||||
#[cultcache(key = 6)]
|
||||
pub requested_action: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub requested_paths: Vec<String>,
|
||||
#[cultcache(key = 8)]
|
||||
pub substrate_gate_grant_receipt_id: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub requested_at: String,
|
||||
#[cultcache(key = 10)]
|
||||
pub contract: String,
|
||||
#[cultcache(key = 11, default)]
|
||||
pub frontier_route_id: String,
|
||||
#[cultcache(key = 12, default)]
|
||||
pub plan_candidate_sha256: String,
|
||||
#[cultcache(key = 13, default)]
|
||||
pub plan_action: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "epiphany.hands.action_review", schema = "HandsActionReview")]
|
||||
pub struct HandsActionReview {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub review_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub intent_id: String,
|
||||
pub requested_paths: Vec<String>,
|
||||
#[cultcache(key = 3)]
|
||||
pub decision: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub allowed_operations: Vec<String>,
|
||||
#[cultcache(key = 5)]
|
||||
pub required_receipts: Vec<String>,
|
||||
#[cultcache(key = 6)]
|
||||
pub reasons: Vec<String>,
|
||||
#[cultcache(key = 7)]
|
||||
pub reviewed_at: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub contract: String,
|
||||
pub substrate_gate_grant_receipt_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "epiphany.hands.patch_receipt", schema = "HandsPatchReceipt")]
|
||||
pub struct HandsPatchReceipt {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub receipt_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
#[cultcache(key = 1)]
|
||||
pub intent_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub review_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub substrate_gate_grant_receipt_id: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub runtime_job_id: String,
|
||||
#[cultcache(key = 6)]
|
||||
#[cultcache(key = 2)]
|
||||
pub changed_paths: Vec<String>,
|
||||
#[cultcache(key = 7)]
|
||||
#[cultcache(key = 3)]
|
||||
pub summary: String,
|
||||
#[cultcache(key = 8)]
|
||||
#[cultcache(key = 4)]
|
||||
pub emitted_at: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
|
|
@ -99,153 +38,38 @@ pub struct HandsPatchReceipt {
|
|||
)]
|
||||
pub struct HandsCommandReceipt {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub receipt_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
#[cultcache(key = 1)]
|
||||
pub intent_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub review_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub substrate_gate_grant_receipt_id: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub runtime_job_id: String,
|
||||
#[cultcache(key = 6)]
|
||||
#[cultcache(key = 2)]
|
||||
pub command: String,
|
||||
#[cultcache(key = 7)]
|
||||
#[cultcache(key = 3)]
|
||||
pub exit_code: String,
|
||||
#[cultcache(key = 8)]
|
||||
#[cultcache(key = 4)]
|
||||
pub stdout_artifact: String,
|
||||
#[cultcache(key = 9)]
|
||||
#[cultcache(key = 5)]
|
||||
pub stderr_artifact: String,
|
||||
#[cultcache(key = 10)]
|
||||
#[cultcache(key = 6)]
|
||||
pub summary: String,
|
||||
#[cultcache(key = 11)]
|
||||
#[cultcache(key = 7)]
|
||||
pub emitted_at: String,
|
||||
#[cultcache(key = 12)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "epiphany.hands.commit_receipt", schema = "HandsCommitReceipt")]
|
||||
pub struct HandsCommitReceipt {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub receipt_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
#[cultcache(key = 1)]
|
||||
pub intent_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub review_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub runtime_job_id: String,
|
||||
#[cultcache(key = 5)]
|
||||
#[cultcache(key = 2)]
|
||||
pub commit_sha: String,
|
||||
#[cultcache(key = 6)]
|
||||
#[cultcache(key = 3)]
|
||||
pub branch: String,
|
||||
#[cultcache(key = 7)]
|
||||
#[cultcache(key = 4)]
|
||||
pub changed_paths: Vec<String>,
|
||||
#[cultcache(key = 8)]
|
||||
#[cultcache(key = 5)]
|
||||
pub summary: String,
|
||||
#[cultcache(key = 9)]
|
||||
#[cultcache(key = 6)]
|
||||
pub emitted_at: String,
|
||||
#[cultcache(key = 10)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
pub fn hands_action_review_for_intent(
|
||||
review_id: String,
|
||||
intent: &HandsActionIntent,
|
||||
decision: String,
|
||||
allowed_operations: Vec<String>,
|
||||
reasons: Vec<String>,
|
||||
reviewed_at: String,
|
||||
) -> HandsActionReview {
|
||||
HandsActionReview {
|
||||
schema_version: HANDS_ACTION_REVIEW_SCHEMA_VERSION.to_string(),
|
||||
review_id,
|
||||
intent_id: intent.intent_id.clone(),
|
||||
decision,
|
||||
allowed_operations,
|
||||
required_receipts: vec![HANDS_PATCH_RECEIPT_TYPE.to_string()],
|
||||
reasons,
|
||||
reviewed_at,
|
||||
contract: "Hands review is the execution decision for a bounded action intent; it depends on Substrate Gate access and does not admit durable Mind state.".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hands_patch_receipt_for_review(
|
||||
receipt_id: String,
|
||||
intent: &HandsActionIntent,
|
||||
review: &HandsActionReview,
|
||||
changed_paths: Vec<String>,
|
||||
summary: String,
|
||||
emitted_at: String,
|
||||
) -> HandsPatchReceipt {
|
||||
HandsPatchReceipt {
|
||||
schema_version: HANDS_PATCH_RECEIPT_SCHEMA_VERSION.to_string(),
|
||||
receipt_id,
|
||||
intent_id: intent.intent_id.clone(),
|
||||
review_id: review.review_id.clone(),
|
||||
substrate_gate_grant_receipt_id: intent.substrate_gate_grant_receipt_id.clone(),
|
||||
runtime_job_id: intent.runtime_job_id.clone(),
|
||||
changed_paths,
|
||||
summary,
|
||||
emitted_at,
|
||||
contract: "Hands patch receipt proves which files changed under the reviewed action and named Substrate Gate grant; Soul and Mind still decide verification and durable admission.".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn hands_command_receipt_for_review(
|
||||
receipt_id: String,
|
||||
intent: &HandsActionIntent,
|
||||
review: &HandsActionReview,
|
||||
command: String,
|
||||
exit_code: String,
|
||||
stdout_artifact: String,
|
||||
stderr_artifact: String,
|
||||
summary: String,
|
||||
emitted_at: String,
|
||||
) -> HandsCommandReceipt {
|
||||
HandsCommandReceipt {
|
||||
schema_version: HANDS_COMMAND_RECEIPT_SCHEMA_VERSION.to_string(),
|
||||
receipt_id,
|
||||
intent_id: intent.intent_id.clone(),
|
||||
review_id: review.review_id.clone(),
|
||||
substrate_gate_grant_receipt_id: intent.substrate_gate_grant_receipt_id.clone(),
|
||||
runtime_job_id: intent.runtime_job_id.clone(),
|
||||
command,
|
||||
exit_code,
|
||||
stdout_artifact,
|
||||
stderr_artifact,
|
||||
summary,
|
||||
emitted_at,
|
||||
contract: "Hands command receipt proves which command ran, where output evidence lives, and which reviewed action plus Substrate Gate grant authorized it.".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hands_commit_receipt_for_review(
|
||||
receipt_id: String,
|
||||
intent: &HandsActionIntent,
|
||||
review: &HandsActionReview,
|
||||
commit_sha: String,
|
||||
branch: String,
|
||||
changed_paths: Vec<String>,
|
||||
summary: String,
|
||||
emitted_at: String,
|
||||
) -> HandsCommitReceipt {
|
||||
HandsCommitReceipt {
|
||||
schema_version: HANDS_COMMIT_RECEIPT_SCHEMA_VERSION.to_string(),
|
||||
receipt_id,
|
||||
intent_id: intent.intent_id.clone(),
|
||||
review_id: review.review_id.clone(),
|
||||
runtime_job_id: intent.runtime_job_id.clone(),
|
||||
commit_sha,
|
||||
branch,
|
||||
changed_paths,
|
||||
summary,
|
||||
emitted_at,
|
||||
contract: "Hands commit receipt proves a repository commit consequence after a reviewed action; it is still subject to Soul verification and Mind admission.".to_string(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,518 +0,0 @@
|
|||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use cultnet_rs::{
|
||||
CultNetMessage, CultNetRawDocumentRecord, CultNetRawPayloadEncoding,
|
||||
CultNetRudpSocketTransportConnection, CultNetRudpSocketTransportOptions, CultNetWireContract,
|
||||
GameCultProviderHealthIdentity, IDUNN_SIGNED_DAEMON_HEALTH_SCHEMA,
|
||||
IdunnSignedDaemonHealthPurpose, IdunnSignedDaemonHealthRecord, ServiceIdentitySigner,
|
||||
encode_cultnet_message_to_vec,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::{SocketAddr, UdpSocket};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub const EPIPHANY_IDUNN_RUNTIME_HEALTH_CONTRACT: &str = "epiphany.cultnet-rudp-runtime-health";
|
||||
pub const CULTNET_RUDP_PROTOCOL_ID: &str = "cultnet.transport.rudp.v0";
|
||||
const IDUNN_HEALTH_RUDP_CONNECTION_ID: u32 = 0x1d0d_0001;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct IdunnDaemonHealthDocument {
|
||||
pub daemon_id: String,
|
||||
pub state: String,
|
||||
pub detail: String,
|
||||
pub observed_at: String,
|
||||
pub health_contract: String,
|
||||
pub publication_source: String,
|
||||
pub transport: String,
|
||||
}
|
||||
|
||||
pub fn sign_epiphany_runtime_health(
|
||||
health: IdunnDaemonHealthDocument,
|
||||
source_runtime_id: &str,
|
||||
release_id: &str,
|
||||
release_witness_sha256: &str,
|
||||
source_commit: &str,
|
||||
deployment_request_id: &str,
|
||||
publisher_incarnation_id: &str,
|
||||
publisher_sequence: u64,
|
||||
signer: &ServiceIdentitySigner<GameCultProviderHealthIdentity>,
|
||||
) -> Result<IdunnSignedDaemonHealthRecord> {
|
||||
validate_health_document(&health)?;
|
||||
let observed_at_unix_millis = chrono::DateTime::parse_from_rfc3339(&health.observed_at)?
|
||||
.timestamp_millis()
|
||||
.try_into()
|
||||
.context("runtime health observation precedes Unix epoch")?;
|
||||
let mut record = IdunnSignedDaemonHealthRecord {
|
||||
schema_version: "idunn.signed_daemon_health.v1".into(),
|
||||
daemon_id: health.daemon_id,
|
||||
health_contract: health.health_contract,
|
||||
source_runtime_id: source_runtime_id.into(),
|
||||
state: health.state,
|
||||
detail: health.detail,
|
||||
signer_identity_id: signer.entry().identity_id.clone(),
|
||||
publisher_incarnation_id: publisher_incarnation_id.into(),
|
||||
publisher_sequence,
|
||||
observed_at_unix_millis,
|
||||
release_id: Some(release_id.into()),
|
||||
release_witness_sha256: Some(release_witness_sha256.into()),
|
||||
source_commit: Some(source_commit.into()),
|
||||
deployment_id: Some(deployment_request_id.into()),
|
||||
signature_algorithm: "ed25519".into(),
|
||||
signature: Vec::new(),
|
||||
private_state_exposed: false,
|
||||
};
|
||||
validate_unsigned_record(&record)?;
|
||||
let canonical = canonical_unsigned_record(&record)?;
|
||||
let proof = signer.sign::<IdunnSignedDaemonHealthPurpose>(&canonical);
|
||||
if proof.identity_id != record.signer_identity_id {
|
||||
bail!("provider-health signer identity disagrees with record")
|
||||
}
|
||||
record.signature = proof.signature;
|
||||
record.validate()?;
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EpiphanyAggregateRuntimeHealthInput {
|
||||
pub daemon_id: String,
|
||||
pub health_contract: String,
|
||||
pub observed_at: String,
|
||||
pub release_authenticated: bool,
|
||||
pub expected_service_count: usize,
|
||||
pub terminal_current_service_count: usize,
|
||||
pub warming_service_count: usize,
|
||||
pub contradictions: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn derive_epiphany_aggregate_runtime_health(
|
||||
input: EpiphanyAggregateRuntimeHealthInput,
|
||||
) -> Result<IdunnDaemonHealthDocument> {
|
||||
require_id(&input.daemon_id, "Idunn daemon id")?;
|
||||
require_id(&input.health_contract, "Idunn health contract")?;
|
||||
require_id(&input.observed_at, "Idunn health observation time")?;
|
||||
if input.expected_service_count == 0 {
|
||||
bail!("aggregate runtime health requires at least one managed service");
|
||||
}
|
||||
if input.terminal_current_service_count > input.expected_service_count
|
||||
|| input.warming_service_count > input.expected_service_count
|
||||
|| input
|
||||
.terminal_current_service_count
|
||||
.saturating_add(input.warming_service_count)
|
||||
> input.expected_service_count
|
||||
{
|
||||
bail!("managed service health counts exceed the expected service count");
|
||||
}
|
||||
let (state, detail) = if !input.contradictions.is_empty() {
|
||||
("failed", "runtime-contradiction")
|
||||
} else if !input.release_authenticated {
|
||||
("failed", "release-authentication-failed")
|
||||
} else if input.terminal_current_service_count == input.expected_service_count {
|
||||
("active", "managed-services-current")
|
||||
} else if input.warming_service_count > 0
|
||||
&& input
|
||||
.terminal_current_service_count
|
||||
.saturating_add(input.warming_service_count)
|
||||
== input.expected_service_count
|
||||
{
|
||||
("warming", "managed-services-warming")
|
||||
} else {
|
||||
("degraded", "managed-services-reconciling")
|
||||
};
|
||||
Ok(IdunnDaemonHealthDocument {
|
||||
daemon_id: input.daemon_id,
|
||||
state: state.into(),
|
||||
detail: detail.into(),
|
||||
observed_at: input.observed_at,
|
||||
health_contract: input.health_contract,
|
||||
publication_source: "daemon-published".into(),
|
||||
transport: CULTNET_RUDP_PROTOCOL_ID.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn publish_idunn_daemon_health_rudp(
|
||||
endpoint: SocketAddr,
|
||||
source_runtime_id: &str,
|
||||
signed: &IdunnSignedDaemonHealthRecord,
|
||||
) -> Result<()> {
|
||||
signed.validate()?;
|
||||
require_id(source_runtime_id, "health publisher runtime id")?;
|
||||
if source_runtime_id != signed.source_runtime_id {
|
||||
bail!("health transport source disagrees with signed source runtime");
|
||||
}
|
||||
let payload = rmp_serde::to_vec(signed).context("encoding canonical signed Idunn health")?;
|
||||
let decoded: IdunnSignedDaemonHealthRecord = rmp_serde::from_slice(&payload)?;
|
||||
if decoded != *signed || rmp_serde::to_vec(&decoded)? != payload {
|
||||
bail!("signed Idunn health encoding is noncanonical");
|
||||
}
|
||||
let message = CultNetMessage::DocumentPutRaw {
|
||||
message_id: format!(
|
||||
"epiphany-signed-health:{}:{}:{}",
|
||||
signed.daemon_id, signed.publisher_incarnation_id, signed.publisher_sequence
|
||||
),
|
||||
document: CultNetRawDocumentRecord {
|
||||
schema_id: IDUNN_SIGNED_DAEMON_HEALTH_SCHEMA.into(),
|
||||
record_key: signed.daemon_id.clone(),
|
||||
stored_at: chrono::DateTime::from_timestamp_millis(
|
||||
signed.observed_at_unix_millis.try_into()?,
|
||||
)
|
||||
.context("signed health observation time is invalid")?
|
||||
.to_rfc3339(),
|
||||
payload_encoding: CultNetRawPayloadEncoding::Messagepack,
|
||||
payload,
|
||||
source_runtime_id: Some(source_runtime_id.into()),
|
||||
source_agent_id: Some(signed.signer_identity_id.clone()),
|
||||
source_role: Some("daemon-health-publisher".into()),
|
||||
tags: Some(vec![CULTNET_RUDP_PROTOCOL_ID.into()]),
|
||||
},
|
||||
};
|
||||
let bind = if endpoint.is_ipv4() {
|
||||
"0.0.0.0:0"
|
||||
} else {
|
||||
"[::]:0"
|
||||
};
|
||||
let socket = UdpSocket::bind(bind)
|
||||
.with_context(|| format!("binding Epiphany Idunn RUDP sender at {bind}"))?;
|
||||
socket.set_read_timeout(Some(Duration::from_millis(100)))?;
|
||||
let mut transport =
|
||||
CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions::client(
|
||||
source_runtime_id,
|
||||
socket,
|
||||
endpoint,
|
||||
IDUNN_HEALTH_RUDP_CONNECTION_ID,
|
||||
))?;
|
||||
transport.connect(Vec::new())?;
|
||||
let deadline = Instant::now() + Duration::from_millis(500);
|
||||
while !transport.connected() {
|
||||
let _ = transport.receive_once()?;
|
||||
transport.poll_resends()?;
|
||||
if Instant::now() >= deadline {
|
||||
return Err(anyhow!(
|
||||
"timed out connecting Epiphany health publisher to {endpoint}"
|
||||
));
|
||||
}
|
||||
}
|
||||
transport.send(
|
||||
"schema",
|
||||
encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0)?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn canonical_unsigned_record(record: &IdunnSignedDaemonHealthRecord) -> Result<Vec<u8>> {
|
||||
let mut unsigned = record.clone();
|
||||
unsigned.signature.clear();
|
||||
validate_unsigned_record(&unsigned)?;
|
||||
rmp_serde::to_vec(&unsigned).context("encoding canonical unsigned Idunn health")
|
||||
}
|
||||
|
||||
fn validate_unsigned_record(record: &IdunnSignedDaemonHealthRecord) -> Result<()> {
|
||||
if !record.signature.is_empty() {
|
||||
bail!("unsigned signing record already contains a signature");
|
||||
}
|
||||
let mut signed_shape = record.clone();
|
||||
signed_shape.signature = vec![0; 64];
|
||||
signed_shape.validate()?;
|
||||
uuid::Uuid::parse_str(&record.publisher_incarnation_id)
|
||||
.context("publisher incarnation id must be UUID")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_health_document(health: &IdunnDaemonHealthDocument) -> Result<()> {
|
||||
require_id(&health.daemon_id, "Idunn daemon id")?;
|
||||
require_id(&health.health_contract, "Idunn health contract")?;
|
||||
require_id(&health.observed_at, "Idunn health observation time")?;
|
||||
if !matches!(
|
||||
health.state.as_str(),
|
||||
"active" | "warming" | "degraded" | "failed"
|
||||
) {
|
||||
bail!("unsupported Idunn daemon health state");
|
||||
}
|
||||
if !matches!(
|
||||
health.detail.as_str(),
|
||||
"runtime-contradiction"
|
||||
| "release-authentication-failed"
|
||||
| "managed-services-current"
|
||||
| "managed-services-warming"
|
||||
| "managed-services-reconciling"
|
||||
) {
|
||||
bail!("Idunn daemon health detail is not a bounded generic reason");
|
||||
}
|
||||
if health.publication_source != "daemon-published"
|
||||
|| health.transport != CULTNET_RUDP_PROTOCOL_ID
|
||||
{
|
||||
bail!("Idunn daemon health publication authority is invalid");
|
||||
}
|
||||
chrono::DateTime::parse_from_rfc3339(&health.observed_at)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require_id(value: &str, label: &str) -> Result<()> {
|
||||
if value.trim().is_empty() || value.len() > 256 || value.chars().any(char::is_control) {
|
||||
bail!("{label} is empty, oversized, or contains control characters");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cultnet_rs::{
|
||||
ServiceIdentityProfile, ServiceIdentitySignature, ServiceSignaturePurpose,
|
||||
decode_cultnet_message_from_slice, enroll_service_identity_at,
|
||||
verify_service_identity_signature,
|
||||
};
|
||||
use std::thread;
|
||||
|
||||
struct WrongPurpose;
|
||||
impl ServiceSignaturePurpose<GameCultProviderHealthIdentity> for WrongPurpose {
|
||||
const PURPOSE: &'static [u8] = b"wrong-purpose";
|
||||
}
|
||||
|
||||
fn input(current: usize) -> EpiphanyAggregateRuntimeHealthInput {
|
||||
EpiphanyAggregateRuntimeHealthInput {
|
||||
daemon_id: "yggdrasil-epiphany".into(),
|
||||
health_contract: EPIPHANY_IDUNN_RUNTIME_HEALTH_CONTRACT.into(),
|
||||
observed_at: "2026-07-16T00:00:00Z".into(),
|
||||
release_authenticated: true,
|
||||
expected_service_count: 2,
|
||||
terminal_current_service_count: current,
|
||||
warming_service_count: 0,
|
||||
contradictions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn signed_health() -> (
|
||||
tempfile::TempDir,
|
||||
ServiceIdentitySigner<GameCultProviderHealthIdentity>,
|
||||
IdunnSignedDaemonHealthRecord,
|
||||
) {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let signer = enroll_service_identity_at::<GameCultProviderHealthIdentity>(
|
||||
&root.path().join("provider.cc"),
|
||||
)
|
||||
.unwrap();
|
||||
let record = sign_epiphany_runtime_health(
|
||||
derive_epiphany_aggregate_runtime_health(input(2)).unwrap(),
|
||||
"epiphany-yggdrasil",
|
||||
"release-test",
|
||||
&format!("sha256-{}", "a".repeat(64)),
|
||||
&"b".repeat(40),
|
||||
"deploy-request-test",
|
||||
"00000000-0000-4000-8000-000000000001",
|
||||
1,
|
||||
&signer,
|
||||
)
|
||||
.unwrap();
|
||||
(root, signer, record)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregate_health_uses_closed_nonprivate_reasons() {
|
||||
assert_eq!(
|
||||
derive_epiphany_aggregate_runtime_health(input(2))
|
||||
.unwrap()
|
||||
.detail,
|
||||
"managed-services-current"
|
||||
);
|
||||
let mut warming = input(1);
|
||||
warming.warming_service_count = 1;
|
||||
assert_eq!(
|
||||
derive_epiphany_aggregate_runtime_health(warming)
|
||||
.unwrap()
|
||||
.detail,
|
||||
"managed-services-warming"
|
||||
);
|
||||
let mut failed = input(2);
|
||||
failed
|
||||
.contradictions
|
||||
.push("secret path and process detail".into());
|
||||
assert_eq!(
|
||||
derive_epiphany_aggregate_runtime_health(failed)
|
||||
.unwrap()
|
||||
.detail,
|
||||
"runtime-contradiction"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_record_binds_exact_release_and_process_continuity() {
|
||||
let (_root, signer, record) = signed_health();
|
||||
assert_eq!(record.source_runtime_id, "epiphany-yggdrasil");
|
||||
assert_eq!(record.release_id.as_deref(), Some("release-test"));
|
||||
assert_eq!(record.deployment_id.as_deref(), Some("deploy-request-test"));
|
||||
assert_eq!(record.publisher_sequence, 1);
|
||||
assert!(!record.private_state_exposed);
|
||||
let statement = canonical_unsigned_record(&record).unwrap();
|
||||
let proof = ServiceIdentitySignature {
|
||||
identity_id: record.signer_identity_id.clone(),
|
||||
signature: record.signature.clone(),
|
||||
};
|
||||
verify_service_identity_signature::<
|
||||
GameCultProviderHealthIdentity,
|
||||
IdunnSignedDaemonHealthPurpose,
|
||||
>(&signer.trust_anchor().unwrap(), &statement, &proof)
|
||||
.unwrap();
|
||||
assert_eq!(&statement[..3], &[0xdc, 0, 17]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_purpose_key_mutation_and_noncanonical_encoding_fail() {
|
||||
let (_root, signer, record) = signed_health();
|
||||
let statement = canonical_unsigned_record(&record).unwrap();
|
||||
let wrong = signer.sign::<WrongPurpose>(&statement);
|
||||
assert!(
|
||||
verify_service_identity_signature::<
|
||||
GameCultProviderHealthIdentity,
|
||||
IdunnSignedDaemonHealthPurpose,
|
||||
>(&signer.trust_anchor().unwrap(), &statement, &wrong)
|
||||
.is_err()
|
||||
);
|
||||
let other_root = tempfile::tempdir().unwrap();
|
||||
let other = enroll_service_identity_at::<GameCultProviderHealthIdentity>(
|
||||
&other_root.path().join("other.cc"),
|
||||
)
|
||||
.unwrap();
|
||||
let proof = ServiceIdentitySignature {
|
||||
identity_id: record.signer_identity_id.clone(),
|
||||
signature: record.signature.clone(),
|
||||
};
|
||||
assert!(
|
||||
verify_service_identity_signature::<
|
||||
GameCultProviderHealthIdentity,
|
||||
IdunnSignedDaemonHealthPurpose,
|
||||
>(&other.trust_anchor().unwrap(), &statement, &proof)
|
||||
.is_err()
|
||||
);
|
||||
let mut mutated = record.clone();
|
||||
mutated.state = "failed".into();
|
||||
assert!(
|
||||
verify_service_identity_signature::<
|
||||
GameCultProviderHealthIdentity,
|
||||
IdunnSignedDaemonHealthPurpose,
|
||||
>(
|
||||
&signer.trust_anchor().unwrap(),
|
||||
&canonical_unsigned_record(&mutated).unwrap(),
|
||||
&proof
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let named = rmp_serde::to_vec(&serde_json::json!({
|
||||
"schema_version": record.schema_version,
|
||||
"daemon_id": record.daemon_id,
|
||||
"health_contract": record.health_contract,
|
||||
"source_runtime_id": record.source_runtime_id,
|
||||
"state": record.state,
|
||||
"detail": record.detail,
|
||||
"signer_identity_id": record.signer_identity_id,
|
||||
"publisher_incarnation_id": record.publisher_incarnation_id,
|
||||
"publisher_sequence": record.publisher_sequence,
|
||||
"observed_at_unix_millis": record.observed_at_unix_millis,
|
||||
"release_id": record.release_id,
|
||||
"release_witness_sha256": record.release_witness_sha256,
|
||||
"source_commit": record.source_commit,
|
||||
"deployment_id": record.deployment_id,
|
||||
"signature_algorithm": record.signature_algorithm,
|
||||
"signature": [],
|
||||
"private_state_exposed": record.private_state_exposed,
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(
|
||||
verify_service_identity_signature::<
|
||||
GameCultProviderHealthIdentity,
|
||||
IdunnSignedDaemonHealthPurpose,
|
||||
>(&signer.trust_anchor().unwrap(), &named, &proof)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequence_and_incarnation_are_strict() {
|
||||
let (_root, signer, record) = signed_health();
|
||||
let health = derive_epiphany_aggregate_runtime_health(input(2)).unwrap();
|
||||
assert!(
|
||||
sign_epiphany_runtime_health(
|
||||
health.clone(),
|
||||
"epiphany-yggdrasil",
|
||||
"r",
|
||||
&format!("sha256-{}", "a".repeat(64)),
|
||||
&"b".repeat(40),
|
||||
"d",
|
||||
"bad",
|
||||
2,
|
||||
&signer
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
sign_epiphany_runtime_health(
|
||||
health,
|
||||
"epiphany-yggdrasil",
|
||||
"r",
|
||||
&format!("sha256-{}", "a".repeat(64)),
|
||||
&"b".repeat(40),
|
||||
"d",
|
||||
&record.publisher_incarnation_id,
|
||||
0,
|
||||
&signer
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rudp_publisher_emits_exact_canonical_tuple() {
|
||||
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_millis(100)))
|
||||
.unwrap();
|
||||
let endpoint = socket.local_addr().unwrap();
|
||||
let server = thread::spawn(move || {
|
||||
let mut transport = CultNetRudpSocketTransportConnection::new(
|
||||
CultNetRudpSocketTransportOptions::server(
|
||||
"idunn-test",
|
||||
socket,
|
||||
IDUNN_HEALTH_RUDP_CONNECTION_ID,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
loop {
|
||||
if let Some(frame) = transport.receive_once().unwrap() {
|
||||
return decode_cultnet_message_from_slice(
|
||||
&frame.payload,
|
||||
CultNetWireContract::CultNetSchemaV0,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
transport.poll_resends().unwrap();
|
||||
assert!(Instant::now() < deadline);
|
||||
}
|
||||
});
|
||||
let (_root, _signer, record) = signed_health();
|
||||
publish_idunn_daemon_health_rudp(endpoint, "epiphany-yggdrasil", &record).unwrap();
|
||||
let CultNetMessage::DocumentPutRaw { document, .. } = server.join().unwrap() else {
|
||||
panic!("wrong message")
|
||||
};
|
||||
assert_eq!(document.schema_id, IDUNN_SIGNED_DAEMON_HEALTH_SCHEMA);
|
||||
assert_eq!(document.schema_id, record.schema_version);
|
||||
assert_eq!(
|
||||
document.source_role.as_deref(),
|
||||
Some("daemon-health-publisher")
|
||||
);
|
||||
let decoded: IdunnSignedDaemonHealthRecord =
|
||||
rmp_serde::from_slice(&document.payload).unwrap();
|
||||
assert_eq!(decoded, record);
|
||||
assert_eq!(rmp_serde::to_vec(&decoded).unwrap(), document.payload);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_profile_is_not_host_identity() {
|
||||
assert_eq!(
|
||||
GameCultProviderHealthIdentity::TRUST_ANCHOR_SCHEMA,
|
||||
"gamecult.provider_health_identity.trust_anchor.v1"
|
||||
);
|
||||
assert_ne!(
|
||||
GameCultProviderHealthIdentity::ID_DOMAIN,
|
||||
b"epiphany.host-incarnation.identity.v0\0"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,9 +8,6 @@ pub const REQUEST_SCHEMA: &str = "epiphany.self.imagination_consideration_reques
|
|||
pub const REQUEST_CONTRACT: &str = "epiphany.imagination_consideration_request.v1";
|
||||
pub const CANDIDATE_SCHEMA: &str = "epiphany.imagination.consideration_candidate.v1";
|
||||
pub const CANDIDATE_CONTRACT: &str = "epiphany.imagination_consideration_candidate.v1";
|
||||
pub const LAUNCH_BINDING_SCHEMA: &str =
|
||||
"epiphany.coordinator.imagination_consideration_launch_binding.v0";
|
||||
pub const REVIEW_REQUEST_SCHEMA: &str = "epiphany.self.imagination_consideration_review_request.v0";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
|
@ -149,52 +146,6 @@ pub struct ImaginationConsiderationCandidate {
|
|||
pub data_classification: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.coordinator.imagination_consideration_launch_binding",
|
||||
schema = "ImaginationConsiderationLaunchBinding"
|
||||
)]
|
||||
pub struct ImaginationConsiderationLaunchBinding {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub binding_record_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub job_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub binding_id: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub runtime_id: String,
|
||||
#[cultcache(key = 6)]
|
||||
pub thread_id: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub launched_at: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub worker_launch_document_sha256: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.self.imagination_consideration_review_request",
|
||||
schema = "ImaginationConsiderationReviewRequest"
|
||||
)]
|
||||
pub struct ImaginationConsiderationReviewRequest {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub review_request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub candidate_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub candidate_sha256: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub requested_review_route: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub selected_at: String,
|
||||
}
|
||||
|
||||
pub fn commit_request(
|
||||
runtime_store: &Path,
|
||||
persona_feedback_store: &Path,
|
||||
|
|
@ -407,52 +358,6 @@ pub fn render_consideration_prompt(request: &ImaginationConsiderationRequest) ->
|
|||
))
|
||||
}
|
||||
|
||||
pub fn request_candidate_modeling_review(
|
||||
runtime_store: &Path,
|
||||
request: &ImaginationConsiderationRequest,
|
||||
candidate: &ImaginationConsiderationCandidate,
|
||||
selected_at: &str,
|
||||
) -> Result<ImaginationConsiderationReviewRequest> {
|
||||
chrono::DateTime::parse_from_rfc3339(selected_at)
|
||||
.map_err(|_| anyhow!("consideration selection timestamp must be RFC3339"))?;
|
||||
let mut cache = crate::runtime_spine_cache(runtime_store)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
validate_current_request(&cache, request)?;
|
||||
validate_candidate(request, candidate)?;
|
||||
if candidate.disposition != ImaginationConsiderationDisposition::Suggest
|
||||
|| candidate.recommended_review_route != ImaginationConsiderationReviewRoute::ModelingReview
|
||||
{
|
||||
bail!("only an explicit suggest/modeling_review candidate may be selected");
|
||||
}
|
||||
let candidate_sha256 = format!("sha256-{:x}", Sha256::digest(rmp_serde::to_vec(candidate)?));
|
||||
let review_request_id = format!(
|
||||
"imagination-consideration-review-{:x}",
|
||||
Sha256::digest(format!("{}:{candidate_sha256}", request.request_id).as_bytes())
|
||||
);
|
||||
let review = ImaginationConsiderationReviewRequest {
|
||||
schema_version: REVIEW_REQUEST_SCHEMA.into(),
|
||||
review_request_id: review_request_id.clone(),
|
||||
candidate_id: candidate.candidate_id.clone(),
|
||||
candidate_sha256,
|
||||
requested_review_route: "modeling_review_proposal_only".into(),
|
||||
selected_at: selected_at.into(),
|
||||
};
|
||||
if let Some(existing) =
|
||||
cache.get::<ImaginationConsiderationReviewRequest>(&review_request_id)?
|
||||
{
|
||||
let mut replay = review;
|
||||
replay.selected_at = existing.selected_at.clone();
|
||||
return if replay == existing {
|
||||
Ok(existing)
|
||||
} else {
|
||||
bail!("consideration review request collision")
|
||||
};
|
||||
}
|
||||
let (entry, _) = cache.prepare_entry(&review_request_id, &review)?;
|
||||
SingleFileMessagePackBackingStore::new(runtime_store).push(&entry)?;
|
||||
Ok(review)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
mod agent_launch;
|
||||
pub mod atlas;
|
||||
mod causal_work_identity;
|
||||
mod continuity_gateway;
|
||||
mod coordinator_objective_intake;
|
||||
mod coordinator_results;
|
||||
pub mod coordinator_status;
|
||||
mod cultmesh_integration;
|
||||
mod current_work;
|
||||
mod eyes_gateway;
|
||||
mod hands_gateway;
|
||||
mod idunn_provider_health;
|
||||
mod idunn_runtime_health;
|
||||
mod imagination_consideration;
|
||||
mod memory_graph;
|
||||
mod mind_documents;
|
||||
|
|
@ -35,6 +31,7 @@ mod runtime_store_backend;
|
|||
mod runtime_worker_attempt;
|
||||
mod soul_gateway;
|
||||
mod state_ledger;
|
||||
mod state_model;
|
||||
mod substrate_gate;
|
||||
mod surfaces;
|
||||
|
||||
|
|
@ -46,7 +43,6 @@ pub use admitted_model_direction_consideration::{
|
|||
RESULT_CONTRACT as ADMITTED_MODEL_DIRECTION_CONSIDERATION_RESULT_CONTRACT,
|
||||
RESULT_SCHEMA as ADMITTED_MODEL_DIRECTION_CONSIDERATION_RESULT_SCHEMA_VERSION,
|
||||
commit_request as commit_admitted_model_direction_consideration_request,
|
||||
render_prompt as render_admitted_model_direction_consideration_prompt,
|
||||
result_id_for_launch as admitted_model_direction_consideration_result_id_for_launch,
|
||||
validate_current_request as validate_current_admitted_model_direction_consideration_request,
|
||||
validate_request as validate_admitted_model_direction_consideration_request,
|
||||
|
|
@ -71,22 +67,16 @@ pub use agent_launch::epiphany_frontier_verdict_modeling_output_schema;
|
|||
pub use agent_launch::epiphany_imagination_consideration_output_schema;
|
||||
pub use agent_launch::epiphany_proposal_modeling_output_schema;
|
||||
pub use agent_launch::epiphany_reorient_launch_output_schema;
|
||||
pub use agent_launch::epiphany_role_binding_id;
|
||||
pub use agent_launch::epiphany_role_label;
|
||||
pub use agent_launch::epiphany_role_launch_output_schema;
|
||||
pub use agent_launch::epiphany_role_owner;
|
||||
pub use agent_launch::unique_strings;
|
||||
pub use atlas::*;
|
||||
pub use causal_work_identity::*;
|
||||
pub use continuity_gateway::*;
|
||||
pub use coordinator_objective_intake::USER_OBJECTIVE_INTAKE_CONTRACT;
|
||||
pub use coordinator_objective_intake::USER_OBJECTIVE_INTAKE_SCHEMA_VERSION;
|
||||
pub use coordinator_objective_intake::USER_OBJECTIVE_INTAKE_TYPE;
|
||||
pub(crate) use causal_work_identity::*;
|
||||
pub use coordinator_objective_intake::UserObjectiveIntake;
|
||||
pub use coordinator_objective_intake::UserObjectiveIntakeApplied;
|
||||
pub use coordinator_objective_intake::UserObjectiveIntakeInput;
|
||||
pub use coordinator_objective_intake::intake_user_objective;
|
||||
pub use coordinator_results::EpiphanyCoordinatorReorientResultSnapshot;
|
||||
pub use coordinator_results::EpiphanyReorientResultStatus;
|
||||
pub use coordinator_results::read_runtime_reorient_result;
|
||||
pub use cultcache_rs::CacheBackingStore;
|
||||
pub use cultcache_rs::CultCache;
|
||||
|
|
@ -100,64 +90,29 @@ pub use cultmesh_integration::EPIPHANY_CANONICAL_SWARM_BRAKE_ID;
|
|||
pub use cultmesh_integration::EPIPHANY_CANONICAL_SWARM_BRAKE_OWNER;
|
||||
pub use cultmesh_integration::{
|
||||
EPIPHANY_CULTMESH_SWARM_BRAKE_KEY, EPIPHANY_CULTMESH_SWARM_BRAKE_SCHEMA_VERSION,
|
||||
EPIPHANY_CULTMESH_SWARM_BRAKE_TYPE, EpiphanyCultMeshDocuments,
|
||||
EpiphanyCultMeshSwarmBrakeEntry, canonical_epiphany_swarm_brake_protected_surfaces,
|
||||
EPIPHANY_CULTMESH_SWARM_BRAKE_TYPE, EpiphanyCultMeshDocuments, EpiphanyCultMeshSwarmBrakeEntry,
|
||||
default_epiphany_cultmesh_swarm_brake, engage_epiphany_cultmesh_swarm_brake,
|
||||
load_epiphany_cultmesh_swarm_brake, open_epiphany_cultmesh_node,
|
||||
release_epiphany_cultmesh_swarm_brake, write_epiphany_cultmesh_swarm_brake,
|
||||
};
|
||||
|
||||
pub use current_work::*;
|
||||
pub use epiphany_state_model::EpiphanyMemoryAnchor;
|
||||
pub use epiphany_state_model::EpiphanyMemoryDomain;
|
||||
pub use epiphany_state_model::EpiphanyMemoryEdge;
|
||||
pub use epiphany_state_model::EpiphanyMemoryLifecycle;
|
||||
pub use epiphany_state_model::EpiphanyMemoryNode;
|
||||
pub use eyes_gateway::EYES_EVIDENCE_PACKET_SCHEMA_VERSION;
|
||||
pub use eyes_gateway::EYES_EVIDENCE_PACKET_TYPE;
|
||||
pub use eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_SCHEMA_VERSION;
|
||||
pub use eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_TYPE;
|
||||
pub use eyes_gateway::EyesEvidencePacket;
|
||||
pub use eyes_gateway::EyesSourceLookupReceipt;
|
||||
pub use eyes_gateway::eyes_evidence_packet_from_research_finding;
|
||||
pub use hands_gateway::*;
|
||||
pub use idunn_provider_health::{
|
||||
EPIPHANY_IDUNN_PROVIDER_HEALTH_ADMISSION_SCHEMA, EPIPHANY_IDUNN_PROVIDER_HEALTH_ADMISSION_TYPE,
|
||||
IdunnProviderHealthAdmission, ProviderReleaseBinding, RequiredProviderHealth,
|
||||
admit_required_idunn_provider_health, provider_health_record_key,
|
||||
read_idunn_provider_health_trust_anchor, required_idunn_provider_health_query,
|
||||
verify_idunn_provider_health_candidate,
|
||||
};
|
||||
pub use idunn_runtime_health::{
|
||||
CULTNET_RUDP_PROTOCOL_ID, EPIPHANY_IDUNN_RUNTIME_HEALTH_CONTRACT,
|
||||
EpiphanyAggregateRuntimeHealthInput, IdunnDaemonHealthDocument,
|
||||
derive_epiphany_aggregate_runtime_health, publish_idunn_daemon_health_rudp,
|
||||
sign_epiphany_runtime_health,
|
||||
};
|
||||
pub use imagination_consideration::{
|
||||
CANDIDATE_CONTRACT as IMAGINATION_CONSIDERATION_CANDIDATE_CONTRACT,
|
||||
CANDIDATE_SCHEMA as IMAGINATION_CONSIDERATION_CANDIDATE_SCHEMA_VERSION,
|
||||
ImaginationConsiderationCandidate, ImaginationConsiderationDisposition,
|
||||
ImaginationConsiderationLaunchBinding, ImaginationConsiderationQuestion,
|
||||
ImaginationConsiderationRequest, ImaginationConsiderationReviewRequest,
|
||||
ImaginationConsiderationReviewRoute, ImaginationOptionDraft,
|
||||
LAUNCH_BINDING_SCHEMA as IMAGINATION_CONSIDERATION_LAUNCH_BINDING_SCHEMA_VERSION,
|
||||
QuotedPersonaFeedbackEvidence, REQUEST_CONTRACT as IMAGINATION_CONSIDERATION_REQUEST_CONTRACT,
|
||||
ImaginationConsiderationQuestion, ImaginationConsiderationRequest,
|
||||
ImaginationConsiderationReviewRoute, ImaginationOptionDraft, QuotedPersonaFeedbackEvidence,
|
||||
REQUEST_CONTRACT as IMAGINATION_CONSIDERATION_REQUEST_CONTRACT,
|
||||
REQUEST_SCHEMA as IMAGINATION_CONSIDERATION_REQUEST_SCHEMA_VERSION,
|
||||
candidate_id_for_launch as imagination_consideration_candidate_id_for_launch,
|
||||
commit_request as commit_imagination_consideration_request,
|
||||
render_consideration_prompt as render_imagination_consideration_prompt,
|
||||
request_candidate_modeling_review as request_imagination_consideration_modeling_review,
|
||||
validate_candidate as validate_imagination_consideration_candidate,
|
||||
validate_current_request as validate_current_imagination_consideration_request,
|
||||
};
|
||||
pub use memory_graph::EpiphanyMemoryEdgeKind;
|
||||
pub use memory_graph::EpiphanyMemoryGraphSnapshot;
|
||||
pub use memory_graph::EpiphanyMemoryGraphValidationError;
|
||||
pub use memory_graph::EpiphanyMemoryNodeKind;
|
||||
pub use memory_graph::RepoFrontierAdoptedPlan;
|
||||
pub use memory_graph::RepoFrontierItem;
|
||||
pub use memory_graph::RepoFrontierStatus;
|
||||
pub use memory_graph::validate_memory_graph_snapshot;
|
||||
pub use mind_documents::*;
|
||||
pub use packaged_release::{
|
||||
|
|
@ -166,23 +121,18 @@ pub use packaged_release::{
|
|||
EpiphanyPackagedReleaseEntry, EpiphanyPackagedReleaseHead, PackageReleaseRequest,
|
||||
authenticate_epiphany_packaged_release, epiphany_packaged_release_binary_path,
|
||||
epiphany_packaged_release_witness_sha256, inspect_epiphany_packaged_release_witness,
|
||||
load_epiphany_packaged_release, load_epiphany_packaged_release_head, package_epiphany_release,
|
||||
publish_epiphany_packaged_release, read_epiphany_packaged_release_witness,
|
||||
required_packaged_release_binaries, validate_epiphany_packaged_release,
|
||||
verify_epiphany_packaged_release_files, write_epiphany_packaged_release_witness,
|
||||
package_epiphany_release, publish_epiphany_packaged_release,
|
||||
};
|
||||
pub use persona_conversation::*;
|
||||
pub use persona_discord_crossing::*;
|
||||
pub use persona_discord_permit::*;
|
||||
pub use persona_feedback_admission::{
|
||||
BIFROST_PERSONA_FEEDBACK_ADMISSION_SCHEMA_VERSION, BIFROST_PERSONA_FEEDBACK_DELIVERY_TYPE,
|
||||
BifrostPersonaFeedbackAdmission,
|
||||
LOCAL_PERSONA_FEEDBACK_SCHEMA_VERSION, LocalAdmittedPersonaFeedback,
|
||||
PERSONA_FEEDBACK_SOCIAL_ADMISSION_SCHEMA_VERSION, PersonaFeedbackPacket,
|
||||
PersonaFeedbackSocialAdmissionReceipt, admit_persona_feedback_to_social,
|
||||
BifrostPersonaFeedbackAdmission, LOCAL_PERSONA_FEEDBACK_SCHEMA_VERSION,
|
||||
LocalAdmittedPersonaFeedback, PERSONA_FEEDBACK_SOCIAL_ADMISSION_SCHEMA_VERSION,
|
||||
PersonaFeedbackPacket, PersonaFeedbackSocialAdmissionReceipt, admit_persona_feedback_to_social,
|
||||
admitted_persona_feedback, import_bifrost_persona_feedback_deliveries,
|
||||
persona_feedback_ready_for_cognition,
|
||||
validate_persona_feedback_store_separation,
|
||||
persona_feedback_ready_for_cognition, validate_persona_feedback_store_separation,
|
||||
};
|
||||
pub use persona_social_state::*;
|
||||
pub use persona_turn::PERSONA_INTERPRETER_EFFECT_DOCUMENT_SCHEMA_VERSION;
|
||||
|
|
@ -225,20 +175,16 @@ pub use repo_model_gateway::*;
|
|||
pub use repository_body_observer::*;
|
||||
pub use resident_readiness::*;
|
||||
pub use resident_self::*;
|
||||
pub use runtime_spine::ARCHIVED_RUNTIME_WORKER_ATTEMPT_SCHEMA_VERSION;
|
||||
pub use runtime_spine::ARCHIVED_RUNTIME_WORKER_ATTEMPT_TYPE;
|
||||
pub use runtime_spine::COORDINATOR_DEATH_RECOVERY_SCHEMA_VERSION;
|
||||
pub use runtime_spine::COORDINATOR_RUN_RECEIPT_SCHEMA_VERSION;
|
||||
pub use runtime_spine::COORDINATOR_RUN_RECEIPT_TYPE;
|
||||
pub use runtime_spine::EPIPHANY_RUNTIME_ROOT_SESSION_ID;
|
||||
pub use runtime_spine::EpiphanyArchivedRuntimeWorkerAttempt;
|
||||
pub use runtime_spine::EpiphanyArchivedRuntimeWorkerDecision;
|
||||
pub use runtime_spine::EpiphanyCoordinatorDeathRecovery;
|
||||
pub use runtime_spine::EpiphanyCoordinatorRunBasis;
|
||||
pub use runtime_spine::EpiphanyCoordinatorRunReceipt;
|
||||
pub use runtime_spine::EpiphanyCoordinatorRunTerminality;
|
||||
pub use runtime_spine::EpiphanyRuntimeIdentity;
|
||||
pub use runtime_spine::EpiphanyRuntimeJob;
|
||||
pub use runtime_spine::EpiphanyRuntimeJobResult;
|
||||
pub use runtime_spine::EpiphanyRuntimeJobSnapshot;
|
||||
pub use runtime_spine::EpiphanyRuntimeJobStatus;
|
||||
pub use runtime_spine::EpiphanyRuntimeModelExecutionBinding;
|
||||
pub use runtime_spine::EpiphanyRuntimeReorientWorkerResult;
|
||||
|
|
@ -252,24 +198,9 @@ pub use runtime_spine::EpiphanyRuntimeWorkerProcessClaim;
|
|||
pub use runtime_spine::ModelPassFailureTerminalOptions;
|
||||
pub use runtime_spine::RUNTIME_IDENTITY_KEY;
|
||||
pub use runtime_spine::RUNTIME_IDENTITY_TYPE;
|
||||
pub use runtime_spine::RUNTIME_JOB_RESULT_TYPE;
|
||||
pub use runtime_spine::RUNTIME_JOB_TYPE;
|
||||
pub use runtime_spine::RUNTIME_MODEL_EXECUTION_BINDING_SCHEMA_VERSION;
|
||||
pub use runtime_spine::RUNTIME_MODEL_EXECUTION_BINDING_TYPE;
|
||||
pub use runtime_spine::RUNTIME_REORIENT_WORKER_RESULT_SCHEMA_VERSION;
|
||||
pub use runtime_spine::RUNTIME_REORIENT_WORKER_RESULT_TYPE;
|
||||
pub use runtime_spine::RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION;
|
||||
pub use runtime_spine::RUNTIME_ROLE_WORKER_RESULT_TYPE;
|
||||
pub use runtime_spine::RUNTIME_SESSION_TYPE;
|
||||
pub use runtime_spine::RUNTIME_SPINE_SCHEMA_VERSION;
|
||||
pub use runtime_spine::RUNTIME_SWARM_BINDING_KEY;
|
||||
pub use runtime_spine::RUNTIME_SWARM_BINDING_SCHEMA_VERSION;
|
||||
pub use runtime_spine::RUNTIME_TOOL_EXECUTION_BINDING_SCHEMA_VERSION;
|
||||
pub use runtime_spine::RUNTIME_TOOL_EXECUTION_BINDING_TYPE;
|
||||
pub use runtime_spine::RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION;
|
||||
pub use runtime_spine::RUNTIME_WORKER_LAUNCH_REQUEST_TYPE;
|
||||
pub use runtime_spine::RUNTIME_WORKER_PROCESS_CLAIM_SCHEMA_VERSION;
|
||||
pub use runtime_spine::RUNTIME_WORKER_PROCESS_CLAIM_TYPE;
|
||||
pub use runtime_spine::RepoFrontierResearchContinuationAction;
|
||||
pub use runtime_spine::RepoFrontierResearchLifecycle;
|
||||
pub use runtime_spine::RepoFrontierResearchLifecycleStage;
|
||||
|
|
@ -277,9 +208,6 @@ pub use runtime_spine::RepoFrontierVerdictModelingLaunchAuthority;
|
|||
pub use runtime_spine::RuntimeSpineHeartbeatJobOptions;
|
||||
pub use runtime_spine::RuntimeSpineInitOptions;
|
||||
pub use runtime_spine::RuntimeSpineJobOptions;
|
||||
pub use runtime_spine::RuntimeSpineJobResultOptions;
|
||||
pub use runtime_spine::RuntimeSpineSessionClosureOptions;
|
||||
pub use runtime_spine::RuntimeSpineSessionOptions;
|
||||
pub use runtime_spine::abandon_unactivated_runtime_worker_process;
|
||||
pub use runtime_spine::activate_runtime_worker_process;
|
||||
pub use runtime_spine::bind_runtime_repository_domain;
|
||||
|
|
@ -287,15 +215,10 @@ pub use runtime_spine::bind_runtime_to_swarm;
|
|||
pub use runtime_spine::canonical_repo_frontier_plan_candidate_id;
|
||||
pub use runtime_spine::claim_runtime_worker_process;
|
||||
pub use runtime_spine::close_runtime_session;
|
||||
pub use runtime_spine::commit_repo_frontier_modeling_request;
|
||||
pub use runtime_spine::commit_repo_frontier_plan_decision;
|
||||
pub use runtime_spine::commit_repo_frontier_plan_mind_request;
|
||||
pub use runtime_spine::complete_runtime_job;
|
||||
pub use runtime_spine::coordinator_run_receipts;
|
||||
pub use runtime_spine::coordinator_run_session_id;
|
||||
pub use runtime_spine::create_runtime_job;
|
||||
pub use runtime_spine::create_runtime_session;
|
||||
pub use runtime_spine::ensure_runtime_session;
|
||||
pub use runtime_spine::finalize_coordinator_run;
|
||||
pub use runtime_spine::initialize_runtime_spine;
|
||||
pub use runtime_spine::model_pass_failure_for_request;
|
||||
|
|
@ -304,7 +227,6 @@ pub use runtime_spine::open_runtime_model_execution;
|
|||
pub use runtime_spine::prepare_runtime_spine_heartbeat_job;
|
||||
pub(crate) use runtime_spine::promote_autonomous_direction_options_for_modeling;
|
||||
pub use runtime_spine::put_hands_action_intent;
|
||||
pub use runtime_spine::put_hands_action_review;
|
||||
pub use runtime_spine::put_hands_command_receipt;
|
||||
pub use runtime_spine::put_hands_commit_receipt;
|
||||
pub use runtime_spine::put_hands_patch_receipt;
|
||||
|
|
@ -320,22 +242,14 @@ pub use runtime_spine::retain_completed_runtime_sessions;
|
|||
pub use runtime_spine::retain_failed_runtime_worker_attempts;
|
||||
pub use runtime_spine::retain_fulfilled_runtime_worker_attempts;
|
||||
pub use runtime_spine::review_repo_frontier_planning_failure;
|
||||
pub(crate) use runtime_spine::runtime_authenticated_public_source_lookups_for_worker;
|
||||
pub use runtime_spine::runtime_hands_command_receipt;
|
||||
pub use runtime_spine::runtime_hands_commit_receipt;
|
||||
pub use runtime_spine::runtime_hands_patch_receipt;
|
||||
pub(crate) use runtime_spine::runtime_has_actionable_hands_frontier;
|
||||
pub use runtime_spine::runtime_job_snapshot;
|
||||
pub use runtime_spine::runtime_identity;
|
||||
pub use runtime_spine::runtime_registered_document_types;
|
||||
pub(crate) use runtime_spine::runtime_reorient_worker_result;
|
||||
pub use runtime_spine::runtime_job;
|
||||
pub use runtime_spine::runtime_reorient_worker_result;
|
||||
pub use runtime_spine::runtime_repo_frontier_planning_lifecycle;
|
||||
pub(crate) use runtime_spine::runtime_repo_frontier_research_lifecycle;
|
||||
pub use runtime_spine::runtime_requested_public_source_refs_for_worker;
|
||||
pub use runtime_spine::runtime_role_worker_result;
|
||||
pub use runtime_spine::runtime_spine_cache;
|
||||
pub(crate) use runtime_spine::runtime_typed_request_attempt_exists;
|
||||
pub(crate) use runtime_spine::runtime_typed_request_fulfillment;
|
||||
pub use runtime_spine::runtime_worker_launch_request;
|
||||
pub use runtime_spine::runtime_worker_process_claim;
|
||||
pub use runtime_spine::runtime_worker_process_claims;
|
||||
|
|
@ -343,6 +257,7 @@ pub use runtime_spine::select_and_commit_repo_frontier_planning_request;
|
|||
pub(crate) use runtime_spine::select_and_commit_repo_frontier_research_request;
|
||||
pub use runtime_spine::select_and_commit_repo_frontier_route;
|
||||
pub use runtime_spine::terminalize_model_pass_failure_session;
|
||||
pub use runtime_spine::terminalize_runtime_job;
|
||||
pub(crate) use runtime_worker_attempt::{RuntimeTypedRequestRef, WorkerProcessStatus};
|
||||
pub use soul_gateway::*;
|
||||
pub use state_ledger::EpiphanyBranchRecord;
|
||||
|
|
@ -352,23 +267,17 @@ pub use state_ledger::add_state_branch;
|
|||
pub use state_ledger::append_state_evidence;
|
||||
pub use state_ledger::close_state_branch;
|
||||
pub use state_ledger::load_state_ledger;
|
||||
pub use state_ledger::state_ledger_status;
|
||||
pub use state_model::*;
|
||||
pub use substrate_gate::SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION;
|
||||
pub use substrate_gate::SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_TYPE;
|
||||
pub use substrate_gate::SubstrateGateRepoAccessGrantReceipt;
|
||||
pub use substrate_gate::substrate_gate_coordinator_implementation_grant;
|
||||
pub use substrate_gate::substrate_gate_operation_for_governed_tool;
|
||||
pub use substrate_gate::substrate_gate_repo_access_grant_for_worker;
|
||||
pub use substrate_gate::substrate_gate_repo_work_planning_grant;
|
||||
pub use surfaces::AdmittedModelDirectionConsiderationContextProjection;
|
||||
pub use surfaces::EpiphanyCoordinatorAction;
|
||||
pub use surfaces::EpiphanyCoordinatorDecision;
|
||||
pub use surfaces::EpiphanyCoordinatorInput;
|
||||
pub use surfaces::EpiphanyCoordinatorRoleResultStatus;
|
||||
pub use surfaces::EpiphanyCoordinatorRoleStatus;
|
||||
pub use surfaces::EpiphanyCrrcAction;
|
||||
pub use surfaces::EpiphanyCrrcResultStatus;
|
||||
pub use surfaces::EpiphanyReorientAction;
|
||||
pub use surfaces::EpiphanyReorientFindingInterpretation;
|
||||
pub use surfaces::EpiphanyReorientWorkerLaunchDocument;
|
||||
pub use surfaces::EpiphanyResearchDecision;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,5 @@
|
|||
pub mod validation;
|
||||
|
||||
pub use epiphany_state_model::EpiphanyMemoryDomain;
|
||||
pub use epiphany_state_model::EpiphanyMemoryEdge;
|
||||
pub use epiphany_state_model::EpiphanyMemoryEdgeKind;
|
||||
pub use epiphany_state_model::EpiphanyMemoryGraphSnapshot;
|
||||
pub use epiphany_state_model::EpiphanyMemoryLifecycle;
|
||||
pub use epiphany_state_model::EpiphanyMemoryNode;
|
||||
pub use epiphany_state_model::EpiphanyMemoryNodeKind;
|
||||
pub use epiphany_state_model::RepoFrontierAdoptedPlan;
|
||||
pub use epiphany_state_model::RepoFrontierItem;
|
||||
pub use epiphany_state_model::RepoFrontierStatus;
|
||||
|
||||
pub use validation::EpiphanyMemoryGraphValidationError;
|
||||
pub(crate) use validation::frontier_item_has_routeable_repository_scope;
|
||||
pub(crate) use validation::repo_paths_are_canonical_and_safe;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use super::EpiphanyMemoryDomain;
|
||||
use super::EpiphanyMemoryEdge;
|
||||
use super::EpiphanyMemoryGraphSnapshot;
|
||||
use super::EpiphanyMemoryLifecycle;
|
||||
use super::EpiphanyMemoryNode;
|
||||
use super::RepoFrontierStatus;
|
||||
use crate::EpiphanyMemoryDomain;
|
||||
use crate::EpiphanyMemoryEdge;
|
||||
use crate::EpiphanyMemoryGraphSnapshot;
|
||||
use crate::EpiphanyMemoryLifecycle;
|
||||
use crate::EpiphanyMemoryNode;
|
||||
use crate::RepoFrontierStatus;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub(crate) fn repo_paths_are_canonical_and_safe(paths: &[String]) -> bool {
|
||||
|
|
@ -22,9 +22,7 @@ pub(crate) fn repo_paths_are_canonical_and_safe(paths: &[String]) -> bool {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn frontier_item_has_routeable_repository_scope(
|
||||
item: &epiphany_state_model::RepoFrontierItem,
|
||||
) -> bool {
|
||||
pub(crate) fn frontier_item_has_routeable_repository_scope(item: &crate::RepoFrontierItem) -> bool {
|
||||
!item.repository_scope.is_empty() && repo_paths_are_canonical_and_safe(&item.repository_scope)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
use std::path::Path;
|
||||
|
||||
use crate::state_model::{
|
||||
EpiphanyEvidenceRecord, EpiphanyInvariant, EpiphanyInvestigationCheckpoint, EpiphanyModeState,
|
||||
EpiphanyObservation, EpiphanySubgoal,
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry};
|
||||
use epiphany_state_model::{
|
||||
EpiphanyBacklogItem, EpiphanyEvidenceRecord, EpiphanyInvariant,
|
||||
EpiphanyInvestigationCheckpoint, EpiphanyModeState, EpiphanyObjectiveDraft,
|
||||
EpiphanyObservation, EpiphanyPlanningCapture, EpiphanyPlanningState, EpiphanyRoadmapStream,
|
||||
EpiphanySubgoal,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::{EpiphanyMindDocumentVersion, runtime_spine_cache};
|
||||
|
||||
pub const MIND_SCHEMA_EPOCH: &str = "epiphany.mind.epoch.v8";
|
||||
pub const MIND_SCHEMA_EPOCH: &str = "epiphany.mind.epoch.v11";
|
||||
pub const MIND_OBJECTIVE_KEY: &str = "objective";
|
||||
pub const MIND_FOCUS_KEY: &str = "focus";
|
||||
pub const MIND_MODE_KEY: &str = "mode";
|
||||
|
|
@ -140,7 +138,7 @@ pub struct EpiphanyMindEvidenceDocument {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.mind.persona_memory.v1",
|
||||
type = "epiphany.mind.persona_memory.v2",
|
||||
schema = "EpiphanyMindPersonaMemoryDocument"
|
||||
)]
|
||||
pub struct EpiphanyMindPersonaMemoryDocument {
|
||||
|
|
@ -157,12 +155,8 @@ pub struct EpiphanyMindPersonaMemoryDocument {
|
|||
#[cultcache(key = 5)]
|
||||
pub confidence: f64,
|
||||
#[cultcache(key = 6)]
|
||||
pub linked_event_ids: Vec<String>,
|
||||
#[cultcache(key = 7)]
|
||||
pub linked_relationship_id: Option<String>,
|
||||
#[cultcache(key = 8)]
|
||||
pub effect_document_id: String,
|
||||
#[cultcache(key = 9)]
|
||||
#[cultcache(key = 7)]
|
||||
pub decision_context_id: String,
|
||||
}
|
||||
|
||||
|
|
@ -297,46 +291,6 @@ pub struct EpiphanyMindInvestigationCheckpointDocument {
|
|||
pub value: EpiphanyInvestigationCheckpoint,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.mind.planning_capture.v1",
|
||||
schema = "EpiphanyMindPlanningCaptureDocument"
|
||||
)]
|
||||
pub struct EpiphanyMindPlanningCaptureDocument {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: EpiphanyPlanningCapture,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.mind.backlog_item.v1",
|
||||
schema = "EpiphanyMindBacklogItemDocument"
|
||||
)]
|
||||
pub struct EpiphanyMindBacklogItemDocument {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: EpiphanyBacklogItem,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.mind.roadmap_stream.v1",
|
||||
schema = "EpiphanyMindRoadmapStreamDocument"
|
||||
)]
|
||||
pub struct EpiphanyMindRoadmapStreamDocument {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: EpiphanyRoadmapStream,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.mind.objective_draft.v1",
|
||||
schema = "EpiphanyMindObjectiveDraftDocument"
|
||||
)]
|
||||
pub struct EpiphanyMindObjectiveDraftDocument {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: EpiphanyObjectiveDraft,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EpiphanyMindView {
|
||||
pub schema_epoch: String,
|
||||
|
|
@ -356,7 +310,6 @@ pub struct EpiphanyMindView {
|
|||
pub agent_pass_admission_refusals: Vec<crate::EpiphanyAgentPassAdmissionRefusal>,
|
||||
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
|
||||
pub mode: Option<EpiphanyModeState>,
|
||||
pub planning: EpiphanyPlanningState,
|
||||
pub repository_body_observation: Option<crate::RepositoryBodyObservationBasis>,
|
||||
pub repo_model: Option<crate::EpiphanyRepoModelView>,
|
||||
}
|
||||
|
|
@ -376,10 +329,6 @@ pub(crate) fn register_mind_document_types(cache: &mut CultCache) -> Result<()>
|
|||
cache.register_entry_type::<crate::EpiphanyMindReorientationDecisionDocument>()?;
|
||||
cache.register_entry_type::<crate::EpiphanyMindReorientationPassFailureDocument>()?;
|
||||
cache.register_entry_type::<EpiphanyMindInvestigationCheckpointDocument>()?;
|
||||
cache.register_entry_type::<EpiphanyMindPlanningCaptureDocument>()?;
|
||||
cache.register_entry_type::<EpiphanyMindBacklogItemDocument>()?;
|
||||
cache.register_entry_type::<EpiphanyMindRoadmapStreamDocument>()?;
|
||||
cache.register_entry_type::<EpiphanyMindObjectiveDraftDocument>()?;
|
||||
cache.register_entry_type::<EpiphanyMindRepositoryBodyObservationDocument>()?;
|
||||
crate::repo_model_documents::register_repo_model_document_types(cache)?;
|
||||
cache.register_entry_type::<crate::RepoFrontierPlanDecisionReceipt>()?;
|
||||
|
|
@ -524,22 +473,6 @@ pub(crate) fn validate_mind_write_envelope(envelope: &CultCacheEnvelope) -> Resu
|
|||
rmp_serde::from_slice::<EpiphanyMindInvestigationCheckpointDocument>(&envelope.payload)?
|
||||
.value
|
||||
.checkpoint_id
|
||||
} else if envelope.r#type == EpiphanyMindPlanningCaptureDocument::TYPE {
|
||||
rmp_serde::from_slice::<EpiphanyMindPlanningCaptureDocument>(&envelope.payload)?
|
||||
.value
|
||||
.id
|
||||
} else if envelope.r#type == EpiphanyMindBacklogItemDocument::TYPE {
|
||||
rmp_serde::from_slice::<EpiphanyMindBacklogItemDocument>(&envelope.payload)?
|
||||
.value
|
||||
.id
|
||||
} else if envelope.r#type == EpiphanyMindRoadmapStreamDocument::TYPE {
|
||||
rmp_serde::from_slice::<EpiphanyMindRoadmapStreamDocument>(&envelope.payload)?
|
||||
.value
|
||||
.id
|
||||
} else if envelope.r#type == EpiphanyMindObjectiveDraftDocument::TYPE {
|
||||
rmp_serde::from_slice::<EpiphanyMindObjectiveDraftDocument>(&envelope.payload)?
|
||||
.value
|
||||
.id
|
||||
} else if envelope.r#type == EpiphanyMindRepositoryBodyObservationDocument::TYPE {
|
||||
let value: EpiphanyMindRepositoryBodyObservationDocument =
|
||||
rmp_serde::from_slice(&envelope.payload)?;
|
||||
|
|
@ -562,6 +495,10 @@ pub(crate) fn validate_mind_write_envelope(envelope: &CultCacheEnvelope) -> Resu
|
|||
pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindView> {
|
||||
let mut cache = runtime_spine_cache(store_path.as_ref())?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
assemble_mind_view_from_cache(&cache)
|
||||
}
|
||||
|
||||
pub(crate) fn assemble_mind_view_from_cache(cache: &CultCache) -> Result<EpiphanyMindView> {
|
||||
let identity = cache
|
||||
.get::<EpiphanyMindIdentity>(MIND_SCHEMA_EPOCH)?
|
||||
.ok_or_else(|| anyhow!("writable Mind store has no v1 identity"))?;
|
||||
|
|
@ -599,14 +536,6 @@ pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindVi
|
|||
}
|
||||
let checkpoints =
|
||||
values::<EpiphanyMindInvestigationCheckpointDocument, _>(&cache, |value| value.value)?;
|
||||
let mut captures =
|
||||
values::<EpiphanyMindPlanningCaptureDocument, _>(&cache, |value| value.value)?;
|
||||
let mut backlog_items =
|
||||
values::<EpiphanyMindBacklogItemDocument, _>(&cache, |value| value.value)?;
|
||||
let mut roadmap_streams =
|
||||
values::<EpiphanyMindRoadmapStreamDocument, _>(&cache, |value| value.value)?;
|
||||
let mut objective_drafts =
|
||||
values::<EpiphanyMindObjectiveDraftDocument, _>(&cache, |value| value.value)?;
|
||||
let mut body_observations = cache.get_all::<EpiphanyMindRepositoryBodyObservationDocument>()?;
|
||||
for observation in &body_observations {
|
||||
observation.validate()?;
|
||||
|
|
@ -629,7 +558,7 @@ pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindVi
|
|||
.get::<crate::EpiphanyRepoModelIdentityDocument>(crate::REPO_MODEL_IDENTITY_KEY)?
|
||||
.is_some()
|
||||
{
|
||||
Some(crate::assemble_repo_model_view(store_path.as_ref())?)
|
||||
Some(crate::repo_model_documents::assemble_repo_model_view_from_cache(cache)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -642,10 +571,6 @@ pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindVi
|
|||
reorientation_decisions.sort_by(|left, right| left.decision_id.cmp(&right.decision_id));
|
||||
reorientation_failures.sort_by(|left, right| left.failure_id.cmp(&right.failure_id));
|
||||
agent_pass_admission_refusals.sort_by(|left, right| left.refusal_id.cmp(&right.refusal_id));
|
||||
captures.sort_by(|left, right| left.id.cmp(&right.id));
|
||||
backlog_items.sort_by(|left, right| left.id.cmp(&right.id));
|
||||
roadmap_streams.sort_by(|left, right| left.id.cmp(&right.id));
|
||||
objective_drafts.sort_by(|left, right| left.id.cmp(&right.id));
|
||||
let investigation_checkpoint = match focus
|
||||
.as_ref()
|
||||
.and_then(|document| document.investigation_checkpoint_id.as_deref())
|
||||
|
|
@ -690,13 +615,6 @@ pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindVi
|
|||
agent_pass_admission_refusals,
|
||||
investigation_checkpoint,
|
||||
mode,
|
||||
planning: EpiphanyPlanningState {
|
||||
workspace_root: None,
|
||||
captures,
|
||||
backlog_items,
|
||||
roadmap_streams,
|
||||
objective_drafts,
|
||||
},
|
||||
repository_body_observation,
|
||||
repo_model,
|
||||
})
|
||||
|
|
@ -732,10 +650,6 @@ fn canonical_mind_versions(
|
|||
| crate::EpiphanyMindReorientationPassFailureDocument::TYPE
|
||||
| crate::EpiphanyAgentPassAdmissionRefusal::TYPE
|
||||
| EpiphanyMindInvestigationCheckpointDocument::TYPE
|
||||
| EpiphanyMindPlanningCaptureDocument::TYPE
|
||||
| EpiphanyMindBacklogItemDocument::TYPE
|
||||
| EpiphanyMindRoadmapStreamDocument::TYPE
|
||||
| EpiphanyMindObjectiveDraftDocument::TYPE
|
||||
| EpiphanyMindRepositoryBodyObservationDocument::TYPE
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ use chrono::Utc;
|
|||
use cultcache_rs::SingleFileMessagePackBackingStore;
|
||||
use std::path::Path;
|
||||
|
||||
pub use epiphany_release_construction::*;
|
||||
mod construction;
|
||||
pub use construction::*;
|
||||
|
||||
const RELEASE_KEY_PREFIX: &str = "epiphany-local/packaged-release/by-id/";
|
||||
const RELEASE_HEAD_KEY: &str = "epiphany-local/packaged-release/current";
|
||||
|
|
@ -52,22 +53,6 @@ pub fn publish_epiphany_packaged_release(
|
|||
Ok(entry)
|
||||
}
|
||||
|
||||
pub fn load_epiphany_packaged_release(
|
||||
store: &Path,
|
||||
runtime_id: &str,
|
||||
release_id: &str,
|
||||
) -> Result<Option<EpiphanyPackagedReleaseEntry>> {
|
||||
open_epiphany_cultmesh_node(store, runtime_id.to_string())?
|
||||
.get(&format!("{RELEASE_KEY_PREFIX}{release_id}"))
|
||||
}
|
||||
|
||||
pub fn load_epiphany_packaged_release_head(
|
||||
store: &Path,
|
||||
runtime_id: &str,
|
||||
) -> Result<Option<EpiphanyPackagedReleaseHead>> {
|
||||
open_epiphany_cultmesh_node(store, runtime_id.to_string())?.get(RELEASE_HEAD_KEY)
|
||||
}
|
||||
|
||||
pub fn authenticate_epiphany_packaged_release(
|
||||
store: &Path,
|
||||
runtime_id: &str,
|
||||
|
|
@ -75,7 +60,8 @@ pub fn authenticate_epiphany_packaged_release(
|
|||
expected_witness_sha256: &str,
|
||||
) -> Result<EpiphanyPackagedReleaseEntry> {
|
||||
validate_epiphany_packaged_release_sha256(expected_witness_sha256)?;
|
||||
let entry = load_epiphany_packaged_release(store, runtime_id, release_id)?
|
||||
let entry = open_epiphany_cultmesh_node(store, runtime_id.to_string())?
|
||||
.get::<EpiphanyPackagedReleaseEntry>(&format!("{RELEASE_KEY_PREFIX}{release_id}"))?
|
||||
.context("packaged release witness is absent")?;
|
||||
validate_epiphany_packaged_release(&entry)?;
|
||||
verify_epiphany_packaged_release_files(&entry)?;
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ pub struct PackageReleaseRequest<'a> {
|
|||
pub target_triple: &'a str,
|
||||
}
|
||||
|
||||
pub fn required_packaged_release_binaries(target_triple: &str) -> Vec<(&'static str, String)> {
|
||||
fn required_packaged_release_binaries(target_triple: &str) -> Vec<(&'static str, String)> {
|
||||
let file_name = |name: &str| target_binary_file_name(target_triple, name);
|
||||
vec![
|
||||
("release-publisher", file_name("epiphany-release")),
|
||||
|
|
@ -572,7 +572,9 @@ fn release_bundle_target_dir(
|
|||
target_root.join(format!("graph-{:x}", digest.finalize()))
|
||||
}
|
||||
|
||||
pub fn validate_epiphany_packaged_release(entry: &EpiphanyPackagedReleaseEntry) -> Result<()> {
|
||||
pub(super) fn validate_epiphany_packaged_release(
|
||||
entry: &EpiphanyPackagedReleaseEntry,
|
||||
) -> Result<()> {
|
||||
if entry.schema_version != EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION {
|
||||
bail!("unsupported packaged release schema");
|
||||
}
|
||||
|
|
@ -630,7 +632,9 @@ pub fn validate_epiphany_packaged_release(entry: &EpiphanyPackagedReleaseEntry)
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn verify_epiphany_packaged_release_files(entry: &EpiphanyPackagedReleaseEntry) -> Result<()> {
|
||||
pub(super) fn verify_epiphany_packaged_release_files(
|
||||
entry: &EpiphanyPackagedReleaseEntry,
|
||||
) -> Result<()> {
|
||||
validate_epiphany_packaged_release(entry)?;
|
||||
let root = canonical_path(&entry.package_root).context("packaged release root is absent")?;
|
||||
if root != PathBuf::from(&entry.package_root) {
|
||||
|
|
@ -672,7 +676,7 @@ pub fn verify_epiphany_packaged_release_files(entry: &EpiphanyPackagedReleaseEnt
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_epiphany_packaged_release_witness(
|
||||
fn write_epiphany_packaged_release_witness(
|
||||
path: &Path,
|
||||
entry: &EpiphanyPackagedReleaseEntry,
|
||||
) -> Result<()> {
|
||||
|
|
@ -686,7 +690,7 @@ pub fn write_epiphany_packaged_release_witness(
|
|||
})
|
||||
}
|
||||
|
||||
pub fn read_epiphany_packaged_release_witness(path: &Path) -> Result<EpiphanyPackagedReleaseEntry> {
|
||||
fn read_epiphany_packaged_release_witness(path: &Path) -> Result<EpiphanyPackagedReleaseEntry> {
|
||||
let bytes = fs::read(path)
|
||||
.with_context(|| format!("failed to read packaged release witness {}", path.display()))?;
|
||||
let entry = rmp_serde::from_slice(&bytes).with_context(|| {
|
||||
|
|
@ -727,7 +731,7 @@ pub fn epiphany_packaged_release_witness_sha256(
|
|||
witness_sha256(entry)
|
||||
}
|
||||
|
||||
pub fn validate_epiphany_packaged_release_sha256(value: &str) -> Result<()> {
|
||||
pub(super) fn validate_epiphany_packaged_release_sha256(value: &str) -> Result<()> {
|
||||
validate_sha256(value)
|
||||
}
|
||||
|
||||
|
|
@ -21,10 +21,8 @@ use crate::{
|
|||
verify_persona_discord_delivery_receipt,
|
||||
};
|
||||
|
||||
pub const PERSONA_DISCORD_DELIVERY_EVIDENCE_SCHEMA_VERSION: &str =
|
||||
"epiphany.persona_discord_delivery_evidence.v0";
|
||||
pub const PERSONA_CONVERSATION_EXECUTION_RECEIPT_SCHEMA_VERSION: &str =
|
||||
"epiphany.persona_conversation_execution_receipt.v1";
|
||||
"epiphany.persona_conversation_execution_receipt.v2";
|
||||
pub const PERSONA_EFFECT_EXECUTION_INTENT_SCHEMA_VERSION: &str =
|
||||
"epiphany.persona_effect_execution_intent.v0";
|
||||
pub const PERSONA_CONVERSATION_STORE_RETIREMENT_RECEIPT_SCHEMA_VERSION: &str =
|
||||
|
|
@ -150,37 +148,7 @@ pub struct PersonaEffectExecutionIntent {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.persona_discord_delivery_evidence.v0",
|
||||
schema = "PersonaDiscordDeliveryEvidence"
|
||||
)]
|
||||
pub struct PersonaDiscordDeliveryEvidence {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub evidence_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub effect_document_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub channel_id: String,
|
||||
#[cultcache(key = 4, default)]
|
||||
pub reply_to_message_id: Option<String>,
|
||||
#[cultcache(key = 5)]
|
||||
pub message_id: String,
|
||||
#[cultcache(key = 6)]
|
||||
pub transport: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub crossing_receipt_id: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub receipt_url: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub bridge_receipt_sha256: String,
|
||||
#[cultcache(key = 10)]
|
||||
pub private_state_exposed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.persona_conversation_execution_receipt.v1",
|
||||
type = "epiphany.persona_conversation_execution_receipt.v2",
|
||||
schema = "PersonaConversationExecutionReceipt"
|
||||
)]
|
||||
pub struct PersonaConversationExecutionReceipt {
|
||||
|
|
@ -198,8 +166,6 @@ pub struct PersonaConversationExecutionReceipt {
|
|||
pub state_effect_status: String,
|
||||
#[cultcache(key = 6, default)]
|
||||
pub state_effect_reasons: Vec<String>,
|
||||
#[cultcache(key = 7, default)]
|
||||
pub delivery_evidence_ids: Vec<String>,
|
||||
#[cultcache(key = 8)]
|
||||
pub social_terminal_receipt_id: Option<String>,
|
||||
#[cultcache(key = 9)]
|
||||
|
|
@ -289,7 +255,7 @@ pub fn poll_persona_discord_crossing(
|
|||
PersonaTurnTerminalOptions {
|
||||
request_id: request_id.into(),
|
||||
outcome: outcome.into(),
|
||||
delivery_evidence: None,
|
||||
delivery_receipt: None,
|
||||
blocked_evidence: None,
|
||||
},
|
||||
)?;
|
||||
|
|
@ -301,7 +267,6 @@ pub fn poll_persona_discord_crossing(
|
|||
outcome: outcome.into(),
|
||||
state_effect_status: state_status,
|
||||
state_effect_reasons: reasons,
|
||||
delivery_evidence_ids: vec![],
|
||||
social_terminal_receipt_id: Some(terminal.receipt_id),
|
||||
private_state_exposed: false,
|
||||
model_terminal_receipt_id: Some(model_terminal.receipt_id.clone()),
|
||||
|
|
@ -379,32 +344,12 @@ pub fn poll_persona_discord_crossing(
|
|||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let evidence = (outcome == "delivered").then(|| PersonaDiscordDeliveryEvidence {
|
||||
schema_version: PERSONA_DISCORD_DELIVERY_EVIDENCE_SCHEMA_VERSION.into(),
|
||||
evidence_id: format!(
|
||||
"persona-delivery:{}:{}",
|
||||
effects.document_id, delivery.message_id
|
||||
),
|
||||
effect_document_id: effects.document_id.clone(),
|
||||
channel_id: delivery.channel_id.clone(),
|
||||
reply_to_message_id: (!delivery.reply_to_message_id.is_empty())
|
||||
.then(|| delivery.reply_to_message_id.clone()),
|
||||
message_id: delivery.message_id.clone(),
|
||||
transport: delivery.transport.clone(),
|
||||
crossing_receipt_id: delivery.crossing_receipt_id.clone(),
|
||||
receipt_url: delivery.receipt_url.clone(),
|
||||
bridge_receipt_sha256: signed_receipt_sha256,
|
||||
private_state_exposed: false,
|
||||
});
|
||||
if let Some(value) = &evidence {
|
||||
put_runtime_document(runtime_store, &value.evidence_id, value)?;
|
||||
}
|
||||
let terminal = complete_persona_social_turn(
|
||||
social_store,
|
||||
PersonaTurnTerminalOptions {
|
||||
request_id: request_id.into(),
|
||||
outcome: outcome.into(),
|
||||
delivery_evidence: evidence.clone(),
|
||||
delivery_receipt: (outcome == "delivered").then(|| delivery.clone()),
|
||||
blocked_evidence,
|
||||
},
|
||||
)?;
|
||||
|
|
@ -422,10 +367,6 @@ pub fn poll_persona_discord_crossing(
|
|||
outcome: outcome.into(),
|
||||
state_effect_status: "admitted_before_delivery_request".into(),
|
||||
state_effect_reasons: vec![],
|
||||
delivery_evidence_ids: evidence
|
||||
.iter()
|
||||
.map(|value| value.evidence_id.clone())
|
||||
.collect(),
|
||||
social_terminal_receipt_id: Some(terminal.receipt_id),
|
||||
private_state_exposed: false,
|
||||
model_terminal_receipt_id: Some(model_terminal.receipt_id.clone()),
|
||||
|
|
@ -448,7 +389,7 @@ fn terminalize_local_effect_quarantine(
|
|||
PersonaTurnTerminalOptions {
|
||||
request_id: request.request_id.clone(),
|
||||
outcome: "blocked".into(),
|
||||
delivery_evidence: None,
|
||||
delivery_receipt: None,
|
||||
blocked_evidence: Some(crate::PersonaTurnBlockedEvidence {
|
||||
evidence_source: "local_effect".into(),
|
||||
crossing_status: "unknown".into(),
|
||||
|
|
@ -466,7 +407,6 @@ fn terminalize_local_effect_quarantine(
|
|||
outcome: "blocked".into(),
|
||||
state_effect_status: "quarantined_ambiguous_local_effect".into(),
|
||||
state_effect_reasons: vec![error.to_string()],
|
||||
delivery_evidence_ids: vec![],
|
||||
social_terminal_receipt_id: Some(terminal.receipt_id),
|
||||
private_state_exposed: false,
|
||||
model_terminal_receipt_id: Some(model_terminal.receipt_id),
|
||||
|
|
@ -519,7 +459,6 @@ pub fn reconcile_terminal_persona_conversation(
|
|||
outcome: terminal.outcome.clone(),
|
||||
state_effect_status: "reconciled_from_social_terminal".into(),
|
||||
state_effect_reasons: vec![],
|
||||
delivery_evidence_ids: terminal.delivery_evidence_id.clone().into_iter().collect(),
|
||||
social_terminal_receipt_id: Some(terminal.receipt_id.clone()),
|
||||
private_state_exposed: false,
|
||||
model_terminal_receipt_id: Some(model_terminal.receipt_id),
|
||||
|
|
@ -756,22 +695,6 @@ fn build_persona_retention_member(
|
|||
}
|
||||
runtime_ids.push((<PersonaEffectExecutionIntent as DatabaseEntry>::TYPE, id));
|
||||
}
|
||||
if let Some(evidence_id) = &terminal.delivery_evidence_id {
|
||||
let Some(evidence) =
|
||||
runtime_document::<PersonaDiscordDeliveryEvidence>(runtime_store, evidence_id)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if conversation.delivery_evidence_ids != [evidence_id.clone()]
|
||||
|| evidence.private_state_exposed
|
||||
{
|
||||
return Err(anyhow!("Persona retained delivery evidence is invalid"));
|
||||
}
|
||||
} else if terminal.outcome == "delivered" || !conversation.delivery_evidence_ids.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"delivered Persona retention candidate lacks evidence"
|
||||
));
|
||||
}
|
||||
let mut runtime_cache = runtime_spine_cache(runtime_store)?;
|
||||
runtime_cache.pull_all_backing_stores()?;
|
||||
let runtime_snapshot = runtime_cache.snapshot_envelopes();
|
||||
|
|
@ -801,7 +724,16 @@ fn build_persona_retention_member(
|
|||
&crossing_request,
|
||||
receipt_anchor,
|
||||
)?;
|
||||
if crossing_receipt.status != "completed" {
|
||||
let receipt_sha256 = format!(
|
||||
"sha256:{:x}",
|
||||
Sha256::digest(rmp_serde::to_vec(&crossing_receipt)?)
|
||||
);
|
||||
if crossing_receipt.status != "completed"
|
||||
|| terminal.delivery_message_id.as_deref() != Some(crossing_receipt.message_id.as_str())
|
||||
|| terminal.crossing_receipt_id.as_deref()
|
||||
!= Some(crossing_receipt.crossing_receipt_id.as_str())
|
||||
|| terminal.bridge_receipt_sha256.as_deref() != Some(receipt_sha256.as_str())
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
// The signed request and consequence receipt remain durable audit
|
||||
|
|
@ -1026,7 +958,7 @@ fn require_store_retirement_receipt(
|
|||
})?;
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<PersonaConversationStoreRetirementReceipt>()?;
|
||||
cache.load_envelope::<PersonaConversationStoreRetirementReceipt>(envelope.clone())?;
|
||||
cache.put_envelope::<PersonaConversationStoreRetirementReceipt>(envelope.clone())?;
|
||||
if cache
|
||||
.get::<PersonaConversationStoreRetirementReceipt>(&expected.receipt_id)?
|
||||
.as_ref()
|
||||
|
|
@ -1110,8 +1042,6 @@ pub(crate) fn admit_persona_state_notes(
|
|||
summary: summary.clone(),
|
||||
salience: 0.7,
|
||||
confidence: confidence.unwrap_or(0.7),
|
||||
linked_event_ids: vec![document.document_id.clone()],
|
||||
linked_relationship_id: None,
|
||||
effect_document_id: document.document_id.clone(),
|
||||
decision_context_id: document.decision_context_id.clone(),
|
||||
};
|
||||
|
|
@ -1530,7 +1460,6 @@ mod tests {
|
|||
outcome: "dropped".into(),
|
||||
state_effect_status: "none".into(),
|
||||
state_effect_reasons: vec![],
|
||||
delivery_evidence_ids: vec![],
|
||||
social_terminal_receipt_id: Some(terminal.receipt_id),
|
||||
private_state_exposed: false,
|
||||
model_terminal_receipt_id: Some(format!("persona-terminal:{request_id}")),
|
||||
|
|
@ -1898,7 +1827,6 @@ mod tests {
|
|||
outcome: "dropped".into(),
|
||||
state_effect_status: "none".into(),
|
||||
state_effect_reasons: vec![],
|
||||
delivery_evidence_ids: vec![],
|
||||
social_terminal_receipt_id: Some("retry:terminal".into()),
|
||||
private_state_exposed: false,
|
||||
model_terminal_receipt_id: Some("persona-terminal:retry".into()),
|
||||
|
|
|
|||
|
|
@ -1042,7 +1042,7 @@ mod tests {
|
|||
let delivery = signed_delivery(&signer, "epiphany")?;
|
||||
let mut source = CultCache::new();
|
||||
source.register_entry_type::<BifrostPersonaFeedbackAdmission>()?;
|
||||
source.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&source_store));
|
||||
source.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&source_store))?;
|
||||
source.put(&delivery.admission_id, &delivery)?;
|
||||
let source_lock = source_store.with_file_name(format!(
|
||||
"{}.lock",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use std::path::Path;
|
|||
|
||||
pub const PERSONA_TURN_REQUEST_SCHEMA_VERSION: &str = "epiphany.persona_turn_request.v1";
|
||||
pub const PERSONA_TURN_TERMINAL_RECEIPT_SCHEMA_VERSION: &str =
|
||||
"epiphany.persona_turn_terminal_receipt.v1";
|
||||
"epiphany.persona_turn_terminal_receipt.v2";
|
||||
pub const PERSONA_CONVERSATION_RETENTION_HEAD_SCHEMA_VERSION: &str =
|
||||
"epiphany.persona_conversation_retention_head.v1";
|
||||
pub const PERSONA_CONVERSATION_RETENTION_PLAN_SCHEMA_VERSION: &str =
|
||||
|
|
@ -21,7 +21,6 @@ pub const PERSONA_CONVERSATION_RETENTION_PLAN_SCHEMA_VERSION: &str =
|
|||
pub struct PersonaSocialMention {
|
||||
pub id: String,
|
||||
pub target_role_id: String,
|
||||
pub target_agent_id: String,
|
||||
pub source_surface: String,
|
||||
pub channel_id: String,
|
||||
pub message_id: String,
|
||||
|
|
@ -73,7 +72,7 @@ pub struct PersonaTurnTerminalReceipt {
|
|||
pub mention_ids: Vec<String>,
|
||||
pub mention_cargo_sha256: String,
|
||||
#[serde(default)]
|
||||
pub delivery_evidence_id: Option<String>,
|
||||
pub delivery_message_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub crossing_receipt_id: Option<String>,
|
||||
#[serde(default)]
|
||||
|
|
@ -90,7 +89,7 @@ pub struct PersonaTurnTerminalReceipt {
|
|||
pub struct PersonaTurnTerminalOptions {
|
||||
pub request_id: String,
|
||||
pub outcome: String,
|
||||
pub delivery_evidence: Option<crate::PersonaDiscordDeliveryEvidence>,
|
||||
pub delivery_receipt: Option<crate::PersonaDiscordDeliveryReceipt>,
|
||||
pub blocked_evidence: Option<PersonaTurnBlockedEvidence>,
|
||||
}
|
||||
|
||||
|
|
@ -146,30 +145,9 @@ pub struct PersonaTurnBlockedEvidence {
|
|||
pub bridge_receipt_sha256: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonaBlockedConversationPressure {
|
||||
pub schema_version: String,
|
||||
pub quarantine_id: String,
|
||||
pub request_id: String,
|
||||
pub terminal_receipt_id: String,
|
||||
pub crossing_status: String,
|
||||
pub evidence_source: String,
|
||||
pub reason: String,
|
||||
pub mentions: Vec<PersonaSocialMention>,
|
||||
pub mention_cargo_sha256: String,
|
||||
#[serde(default)]
|
||||
pub crossing_receipt_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub bridge_receipt_sha256: Option<String>,
|
||||
pub quarantined_at: String,
|
||||
pub private_state_exposed: bool,
|
||||
}
|
||||
|
||||
pub const PERSONA_SOCIAL_MENTION_TYPE: &str = "epiphany.persona.social_mention.v1";
|
||||
pub const PERSONA_SOCIAL_MENTION_TYPE: &str = "epiphany.persona.social_mention.v2";
|
||||
pub const PERSONA_SOCIAL_TURN_REQUEST_TYPE: &str = "epiphany.persona.turn_request.v1";
|
||||
pub const PERSONA_SOCIAL_TURN_TERMINAL_TYPE: &str = "epiphany.persona.turn_terminal.v1";
|
||||
pub const PERSONA_SOCIAL_QUARANTINE_TYPE: &str = "epiphany.persona.quarantine.v1";
|
||||
pub const PERSONA_SOCIAL_TURN_TERMINAL_TYPE: &str = "epiphany.persona.turn_terminal.v2";
|
||||
pub const PERSONA_SOCIAL_RETENTION_HEAD_TYPE: &str = "epiphany.persona.retention_head.v1";
|
||||
pub const PERSONA_SOCIAL_RETENTION_PLAN_TYPE: &str = "epiphany.persona.retention_plan.v1";
|
||||
pub const PERSONA_SOCIAL_RETENTION_HEAD_KEY: &str = "persona";
|
||||
|
|
@ -196,7 +174,7 @@ pub struct PersonaSocialQueueMentionOptions {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.persona.social_mention.v1",
|
||||
type = "epiphany.persona.social_mention.v2",
|
||||
schema = "PersonaSocialMentionDocument"
|
||||
)]
|
||||
pub struct PersonaSocialMentionDocument {
|
||||
|
|
@ -220,7 +198,7 @@ pub struct PersonaSocialTurnRequestDocument {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.persona.turn_terminal.v1",
|
||||
type = "epiphany.persona.turn_terminal.v2",
|
||||
schema = "PersonaSocialTurnTerminalDocument"
|
||||
)]
|
||||
pub struct PersonaSocialTurnTerminalDocument {
|
||||
|
|
@ -228,16 +206,6 @@ pub struct PersonaSocialTurnTerminalDocument {
|
|||
pub receipt: PersonaTurnTerminalReceipt,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.persona.quarantine.v1",
|
||||
schema = "PersonaSocialQuarantineDocument"
|
||||
)]
|
||||
pub struct PersonaSocialQuarantineDocument {
|
||||
#[cultcache(key = 0)]
|
||||
pub pressure: PersonaBlockedConversationPressure,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.persona.retention_head.v1",
|
||||
|
|
@ -265,6 +233,11 @@ pub fn persona_social_cache(store_path: impl AsRef<Path>) -> Result<CultCache> {
|
|||
register_persona_social_types(&mut cache)?;
|
||||
let mut identities = HashSet::new();
|
||||
for envelope in SingleFileMessagePackBackingStore::new(store_path.as_ref()).pull_all()? {
|
||||
if envelope.r#type == "epiphany.persona.social_mention.v1" {
|
||||
return Err(anyhow!(
|
||||
"Persona social store contains an obsolete writable mention epoch"
|
||||
));
|
||||
}
|
||||
if !is_persona_social_type(&envelope.r#type) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -320,7 +293,6 @@ pub fn queue_persona_social_mention(
|
|||
mention: PersonaSocialMention {
|
||||
id: mention_id.clone(),
|
||||
target_role_id: options.target_role_id.clone(),
|
||||
target_agent_id: "epiphany.Persona".into(),
|
||||
source_surface: options.source_surface,
|
||||
channel_id: options.channel_id,
|
||||
message_id: options.message_id,
|
||||
|
|
@ -522,30 +494,33 @@ pub fn complete_persona_social_turn(
|
|||
existing.outcome
|
||||
));
|
||||
}
|
||||
let delivery_evidence = options.delivery_evidence.as_ref();
|
||||
let delivery_receipt = options.delivery_receipt.as_ref();
|
||||
let blocked_evidence = options.blocked_evidence.as_ref();
|
||||
if options.outcome == "delivered" {
|
||||
let evidence = delivery_evidence.ok_or_else(|| {
|
||||
anyhow!("delivered Persona turn requires typed Discord delivery evidence")
|
||||
let receipt = delivery_receipt.ok_or_else(|| {
|
||||
anyhow!("delivered Persona turn requires a typed Discord delivery receipt")
|
||||
})?;
|
||||
if evidence.schema_version != crate::PERSONA_DISCORD_DELIVERY_EVIDENCE_SCHEMA_VERSION
|
||||
|| evidence.private_state_exposed
|
||||
|| evidence.evidence_id.trim().is_empty()
|
||||
|| evidence.message_id.trim().is_empty()
|
||||
|| evidence.crossing_receipt_id.trim().is_empty()
|
||||
|| evidence.bridge_receipt_sha256.trim().is_empty()
|
||||
if receipt.schema_version != crate::PERSONA_DISCORD_DELIVERY_RECEIPT_SCHEMA_VERSION
|
||||
|| receipt.status != "completed"
|
||||
|| receipt.private_state_exposed
|
||||
|| receipt.receipt_id != receipt.request_id
|
||||
|| !receipt
|
||||
.request_id
|
||||
.starts_with(&format!("persona-discord:{}:", request.request_id))
|
||||
|| receipt.message_id.trim().is_empty()
|
||||
|| receipt.crossing_receipt_id.trim().is_empty()
|
||||
|| !request
|
||||
.mentions
|
||||
.iter()
|
||||
.any(|mention| mention.channel_id == evidence.channel_id)
|
||||
.any(|mention| mention.channel_id == receipt.channel_id)
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Discord delivery evidence is not bound to the Persona turn"
|
||||
"Discord delivery receipt is not bound to the Persona turn"
|
||||
));
|
||||
}
|
||||
} else if delivery_evidence.is_some() {
|
||||
} else if delivery_receipt.is_some() {
|
||||
return Err(anyhow!(
|
||||
"non-delivered Persona terminal outcome must not carry delivery evidence"
|
||||
"non-delivered Persona terminal outcome must not carry a delivery receipt"
|
||||
));
|
||||
}
|
||||
if options.outcome == "blocked" {
|
||||
|
|
@ -596,10 +571,12 @@ pub fn complete_persona_social_turn(
|
|||
"sha256-{:x}",
|
||||
sha2::Sha256::digest(rmp_serde::to_vec(&request.mentions)?)
|
||||
),
|
||||
delivery_evidence_id: delivery_evidence.map(|evidence| evidence.evidence_id.clone()),
|
||||
crossing_receipt_id: delivery_evidence.map(|evidence| evidence.crossing_receipt_id.clone()),
|
||||
bridge_receipt_sha256: delivery_evidence
|
||||
.map(|evidence| evidence.bridge_receipt_sha256.clone()),
|
||||
delivery_message_id: delivery_receipt.map(|receipt| receipt.message_id.clone()),
|
||||
crossing_receipt_id: delivery_receipt.map(|receipt| receipt.crossing_receipt_id.clone()),
|
||||
bridge_receipt_sha256: delivery_receipt
|
||||
.map(rmp_serde::to_vec)
|
||||
.transpose()?
|
||||
.map(|bytes| format!("sha256:{:x}", sha2::Sha256::digest(bytes))),
|
||||
blocked_crossing_status: blocked_evidence.map(|evidence| evidence.crossing_status.clone()),
|
||||
blocked_reason: blocked_evidence.map(|evidence| evidence.reason.clone()),
|
||||
completed_at: now_iso(),
|
||||
|
|
@ -644,33 +621,6 @@ pub fn complete_persona_social_turn(
|
|||
document.terminal_receipt_id = Some(receipt.receipt_id.clone());
|
||||
writes.push(cache.prepare_entry(&mention.id, &document)?.0);
|
||||
}
|
||||
if mention_disposition == "quarantined" {
|
||||
let evidence = blocked_evidence.expect("validated blocked evidence");
|
||||
let pressure = PersonaBlockedConversationPressure {
|
||||
schema_version: "epiphany.persona_blocked_conversation_pressure.v1".into(),
|
||||
quarantine_id: format!("{}:quarantine", request.request_id),
|
||||
request_id: request.request_id.clone(),
|
||||
terminal_receipt_id: receipt.receipt_id.clone(),
|
||||
crossing_status: evidence.crossing_status.clone(),
|
||||
evidence_source: evidence.evidence_source.clone(),
|
||||
reason: evidence.reason.clone(),
|
||||
mentions: request.mentions.clone(),
|
||||
mention_cargo_sha256: receipt.mention_cargo_sha256.clone(),
|
||||
crossing_receipt_id: evidence.crossing_receipt_id.clone(),
|
||||
bridge_receipt_sha256: evidence.bridge_receipt_sha256.clone(),
|
||||
quarantined_at: receipt.completed_at.clone(),
|
||||
private_state_exposed: false,
|
||||
};
|
||||
let quarantine_id = pressure.quarantine_id.clone();
|
||||
writes.push(
|
||||
cache
|
||||
.prepare_entry(
|
||||
&quarantine_id,
|
||||
&PersonaSocialQuarantineDocument { pressure },
|
||||
)?
|
||||
.0,
|
||||
);
|
||||
}
|
||||
if !persona_social_backing(store_path).compare_and_swap_batch(&expected, writes)? {
|
||||
return Err(anyhow!(
|
||||
"Persona terminal decision lost exact document compare-and-swap"
|
||||
|
|
@ -742,19 +692,6 @@ pub fn persona_turn_request_source(
|
|||
crate::EpiphanyMindDocumentVersion::from_envelope("epiphany-persona-social", &envelope)
|
||||
}
|
||||
|
||||
pub fn blocked_persona_pressures(
|
||||
store_path: impl AsRef<Path>,
|
||||
) -> Result<Vec<PersonaBlockedConversationPressure>> {
|
||||
let cache = persona_social_cache(store_path)?;
|
||||
let mut pressures = cache
|
||||
.get_all::<PersonaSocialQuarantineDocument>()?
|
||||
.into_iter()
|
||||
.map(|document| document.pressure)
|
||||
.collect::<Vec<_>>();
|
||||
pressures.sort_by(|left, right| left.quarantine_id.cmp(&right.quarantine_id));
|
||||
Ok(pressures)
|
||||
}
|
||||
|
||||
pub fn persona_retention_head(
|
||||
store_path: impl AsRef<Path>,
|
||||
) -> Result<Option<PersonaConversationRetentionHead>> {
|
||||
|
|
@ -890,7 +827,6 @@ fn register_persona_social_types(cache: &mut CultCache) -> Result<()> {
|
|||
cache.register_entry_type::<PersonaSocialMentionDocument>()?;
|
||||
cache.register_entry_type::<PersonaSocialTurnRequestDocument>()?;
|
||||
cache.register_entry_type::<PersonaSocialTurnTerminalDocument>()?;
|
||||
cache.register_entry_type::<PersonaSocialQuarantineDocument>()?;
|
||||
cache.register_entry_type::<PersonaSocialRetentionHeadDocument>()?;
|
||||
cache.register_entry_type::<PersonaSocialRetentionPlanDocument>()?;
|
||||
Ok(())
|
||||
|
|
@ -901,7 +837,6 @@ fn is_persona_social_type(document_type: &str) -> bool {
|
|||
PERSONA_SOCIAL_MENTION_TYPE,
|
||||
PERSONA_SOCIAL_TURN_REQUEST_TYPE,
|
||||
PERSONA_SOCIAL_TURN_TERMINAL_TYPE,
|
||||
PERSONA_SOCIAL_QUARANTINE_TYPE,
|
||||
PERSONA_SOCIAL_RETENTION_HEAD_TYPE,
|
||||
PERSONA_SOCIAL_RETENTION_PLAN_TYPE,
|
||||
]
|
||||
|
|
@ -911,22 +846,19 @@ fn is_persona_social_type(document_type: &str) -> bool {
|
|||
fn load_persona_social_envelope(cache: &mut CultCache, envelope: CultCacheEnvelope) -> Result<()> {
|
||||
match envelope.r#type.as_str() {
|
||||
PERSONA_SOCIAL_MENTION_TYPE => {
|
||||
cache.load_envelope::<PersonaSocialMentionDocument>(envelope)?;
|
||||
cache.put_envelope::<PersonaSocialMentionDocument>(envelope)?;
|
||||
}
|
||||
PERSONA_SOCIAL_TURN_REQUEST_TYPE => {
|
||||
cache.load_envelope::<PersonaSocialTurnRequestDocument>(envelope)?;
|
||||
cache.put_envelope::<PersonaSocialTurnRequestDocument>(envelope)?;
|
||||
}
|
||||
PERSONA_SOCIAL_TURN_TERMINAL_TYPE => {
|
||||
cache.load_envelope::<PersonaSocialTurnTerminalDocument>(envelope)?;
|
||||
}
|
||||
PERSONA_SOCIAL_QUARANTINE_TYPE => {
|
||||
cache.load_envelope::<PersonaSocialQuarantineDocument>(envelope)?;
|
||||
cache.put_envelope::<PersonaSocialTurnTerminalDocument>(envelope)?;
|
||||
}
|
||||
PERSONA_SOCIAL_RETENTION_HEAD_TYPE => {
|
||||
cache.load_envelope::<PersonaSocialRetentionHeadDocument>(envelope)?;
|
||||
cache.put_envelope::<PersonaSocialRetentionHeadDocument>(envelope)?;
|
||||
}
|
||||
PERSONA_SOCIAL_RETENTION_PLAN_TYPE => {
|
||||
cache.load_envelope::<PersonaSocialRetentionPlanDocument>(envelope)?;
|
||||
cache.put_envelope::<PersonaSocialRetentionPlanDocument>(envelope)?;
|
||||
}
|
||||
_ => unreachable!("caller filtered Persona social document types"),
|
||||
}
|
||||
|
|
@ -959,3 +891,50 @@ fn stable_pending_mention_id(
|
|||
fn now_iso() -> String {
|
||||
chrono::Utc::now().to_rfc3339()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn persona_social_cache_reads_without_writing_the_store() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("social.cc");
|
||||
let cache = persona_social_cache(&store)?;
|
||||
let head = PersonaSocialRetentionHeadDocument {
|
||||
head: PersonaConversationRetentionHead::default(),
|
||||
};
|
||||
SingleFileMessagePackBackingStore::new(&store).push(
|
||||
&cache
|
||||
.prepare_entry(PERSONA_SOCIAL_RETENTION_HEAD_KEY, &head)?
|
||||
.0,
|
||||
)?;
|
||||
let past = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
|
||||
std::fs::File::options()
|
||||
.write(true)
|
||||
.open(&store)?
|
||||
.set_modified(past)?;
|
||||
let listing = || -> Result<Vec<(std::ffi::OsString, u64, std::time::SystemTime)>> {
|
||||
let mut entries = std::fs::read_dir(temp.path())?
|
||||
.map(|entry| {
|
||||
let entry = entry?;
|
||||
let metadata = entry.metadata()?;
|
||||
Ok((entry.file_name(), metadata.len(), metadata.modified()?))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
entries.sort();
|
||||
Ok(entries)
|
||||
};
|
||||
let before = listing()?;
|
||||
let names = before.iter().map(|entry| entry.0.clone()).collect::<Vec<_>>();
|
||||
assert_eq!(names, ["social.cc", "social.cc.lock"]);
|
||||
let reopened = persona_social_cache(&store)?;
|
||||
assert_eq!(
|
||||
reopened
|
||||
.get::<PersonaSocialRetentionHeadDocument>(PERSONA_SOCIAL_RETENTION_HEAD_KEY)?,
|
||||
Some(head)
|
||||
);
|
||||
assert_eq!(listing()?, before, "a read attached or wrote a store");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -282,6 +282,7 @@ pub fn build_persona_projector_prompt_with_transcript(
|
|||
typed_context: &typed_context,
|
||||
visible_stimulus: &render_transcript(transcript),
|
||||
domain_guidance: "Project personhood rather than a job label: values, mood, dignity, pressure, needs, fascinations, wounds, bonds, obligations, fatigue, and what repo-body motion feels like from inside. The dependency web may be felt but grants no organ authority. Mind alone admits durable state; Substrate Gate alone grants repo access.",
|
||||
word_budget: 180,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -292,6 +293,7 @@ pub fn build_persona_turn_prompt(input: &PersonaTurnInput) -> String {
|
|||
identity: &input.identity.display_name,
|
||||
lived_stream: input.projected_state.trim(),
|
||||
domain_guidance: "This is Epiphany Persona cognition. Speak, hold silence, wonder, disagree, or form a private thought naturally. A parent Interpreter may propose bounded memory, public speech, or silence; Mind and external gates retain all consequence authority.",
|
||||
word_budget: 180,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -339,7 +341,7 @@ Pending addressed pressure:
|
|||
typed_context: &typed_context,
|
||||
lived_stream: input.persona_prompt.trim(),
|
||||
persona_output: input.persona_output.trim(),
|
||||
output_schema: &persona_interpreter_effect_set_json_schema(),
|
||||
output_schema: Some(&persona_interpreter_effect_set_json_schema()),
|
||||
domain_guidance: &domain_guidance,
|
||||
},
|
||||
)
|
||||
|
|
@ -572,7 +574,6 @@ fn fallback<'a>(value: &'a str, fallback: &'a str) -> &'a str {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn typed_interpreter_effects_reject_channel_escape_and_mixed_drop() {
|
||||
let valid = r#"{"schemaVersion":"epiphany.persona_interpreter_effect_set.v0","effects":[{"kind":"state_note","memory_kind":"memory","summary":"The operator expects a native conversational nerve."},{"kind":"say","channel_id":"aquarium","content":"The nerve is live.","speech_act":"status","register":"concise","safety_notes":[]}]}"#;
|
||||
|
|
|
|||
|
|
@ -109,8 +109,8 @@ mod platform {
|
|||
use chrono::{DateTime, SecondsFormat, Utc};
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
use windows_sys::Win32::Foundation::{
|
||||
CloseHandle, ERROR_ACCESS_DENIED, ERROR_NO_MORE_FILES, FILETIME,
|
||||
GetLastError, INVALID_HANDLE_VALUE, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT,
|
||||
CloseHandle, ERROR_ACCESS_DENIED, ERROR_NO_MORE_FILES, FILETIME, GetLastError,
|
||||
INVALID_HANDLE_VALUE, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT,
|
||||
};
|
||||
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
|
||||
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
|
||||
|
|
@ -375,6 +375,14 @@ mod platform {
|
|||
Ok((starttime, state))
|
||||
}
|
||||
|
||||
fn is_missing_error(error: &anyhow::Error) -> bool {
|
||||
error.chain().any(|source| {
|
||||
source
|
||||
.downcast_ref::<std::io::Error>()
|
||||
.is_some_and(|error| error.kind() == std::io::ErrorKind::NotFound)
|
||||
})
|
||||
}
|
||||
|
||||
fn created_at_rfc3339(starttime: u64) -> Result<String> {
|
||||
let ticks_per_second = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
|
||||
if ticks_per_second <= 0 {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,22 +9,20 @@ use crate::{
|
|||
};
|
||||
|
||||
pub const REORIENTATION_REQUEST_SCHEMA_VERSION: &str = "epiphany.self.reorientation_request.v1";
|
||||
pub const REORIENTATION_LAUNCH_BINDING_SCHEMA_VERSION: &str =
|
||||
"epiphany.runtime.reorientation_launch.v1";
|
||||
pub const MIND_REORIENTATION_DECISION_SCHEMA_VERSION: &str =
|
||||
"epiphany.mind.reorientation_decision.v1";
|
||||
pub const MIND_REORIENTATION_PASS_FAILURE_SCHEMA_VERSION: &str =
|
||||
"epiphany.mind.reorientation_pass_failure.v1";
|
||||
"epiphany.mind.reorientation_pass_failure.v2";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EpiphanyReorientationStateProjection {
|
||||
pub runtime_id: String,
|
||||
pub objective: Option<String>,
|
||||
pub active_subgoal_id: Option<String>,
|
||||
pub investigation_checkpoint: Option<epiphany_state_model::EpiphanyInvestigationCheckpoint>,
|
||||
pub mode: Option<epiphany_state_model::EpiphanyModeState>,
|
||||
pub investigation_checkpoint: Option<crate::EpiphanyInvestigationCheckpoint>,
|
||||
pub mode: Option<crate::EpiphanyModeState>,
|
||||
pub repository_body_observation: Option<crate::RepositoryBodyObservationBasis>,
|
||||
pub frontier: Vec<epiphany_state_model::RepoFrontierItem>,
|
||||
pub frontier: Vec<crate::RepoFrontierItem>,
|
||||
pub projection_digest: String,
|
||||
}
|
||||
|
||||
|
|
@ -48,26 +46,6 @@ pub struct EpiphanyReorientationRequest {
|
|||
pub requested_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.runtime.reorientation_launch.v1",
|
||||
schema = "EpiphanyReorientationLaunchBinding"
|
||||
)]
|
||||
pub struct EpiphanyReorientationLaunchBinding {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub binding_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub job_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub attempt_ordinal: u64,
|
||||
#[cultcache(key = 5)]
|
||||
pub launched_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.mind.reorientation_decision.v1",
|
||||
|
|
@ -100,7 +78,7 @@ pub struct EpiphanyMindReorientationDecisionDocument {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.mind.reorientation_pass_failure.v1",
|
||||
type = "epiphany.mind.reorientation_pass_failure.v2",
|
||||
schema = "EpiphanyMindReorientationPassFailureDocument"
|
||||
)]
|
||||
pub struct EpiphanyMindReorientationPassFailureDocument {
|
||||
|
|
@ -113,14 +91,12 @@ pub struct EpiphanyMindReorientationPassFailureDocument {
|
|||
#[cultcache(key = 3)]
|
||||
pub job_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub runtime_result_id: String,
|
||||
pub model_pass_failure_id: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub decision_context_id: String,
|
||||
#[cultcache(key = 6)]
|
||||
pub verdict: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub summary: String,
|
||||
#[cultcache(key = 8)]
|
||||
#[cultcache(key = 7)]
|
||||
pub failed_at: String,
|
||||
}
|
||||
|
||||
|
|
@ -129,6 +105,7 @@ pub struct EpiphanyReorientationWorkProjection {
|
|||
pub request: EpiphanyReorientationRequest,
|
||||
#[serde(flatten)]
|
||||
pub attempt: crate::EpiphanyAgentPassAttemptProjection,
|
||||
pub terminal_failure: bool,
|
||||
}
|
||||
|
||||
pub fn request_current_reorientation(
|
||||
|
|
@ -138,7 +115,9 @@ pub fn request_current_reorientation(
|
|||
chrono::DateTime::parse_from_rfc3339(requested_at)
|
||||
.map_err(|_| anyhow!("reorientation request time is invalid"))?;
|
||||
let store_path = store_path.as_ref();
|
||||
let mind = crate::assemble_mind_view(store_path)?;
|
||||
let mut cache = crate::runtime_spine_cache(store_path)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let mind = crate::mind_documents::assemble_mind_view_from_cache(&cache)?;
|
||||
let mut source_documents = mind
|
||||
.source_documents
|
||||
.iter()
|
||||
|
|
@ -192,8 +171,6 @@ pub fn request_current_reorientation(
|
|||
requested_at: requested_at.into(),
|
||||
};
|
||||
validate_reorientation_request_intrinsic(&request)?;
|
||||
let mut cache = crate::runtime_spine_cache(store_path)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
if let Some(existing) = cache.get::<EpiphanyReorientationRequest>(&request_id)? {
|
||||
validate_reorientation_request_current(&cache, &existing)?;
|
||||
return Ok(existing);
|
||||
|
|
@ -210,6 +187,70 @@ pub fn request_current_reorientation(
|
|||
Ok(request)
|
||||
}
|
||||
|
||||
fn reorientation_attempt_ordinal(request_id: &str, job_id: &str) -> Result<usize> {
|
||||
let prefix = format!("reorientation-{request_id}-attempt-");
|
||||
job_id
|
||||
.strip_prefix(&prefix)
|
||||
.ok_or_else(|| anyhow!("reorientation job identity is not canonical"))?
|
||||
.parse::<usize>()
|
||||
.map_err(|_| anyhow!("reorientation attempt ordinal is invalid"))
|
||||
}
|
||||
|
||||
fn reorientation_launch_document(
|
||||
launch: &crate::EpiphanyRuntimeWorkerLaunchRequest,
|
||||
) -> Result<crate::EpiphanyReorientWorkerLaunchDocument> {
|
||||
let document = match launch.launch_document()? {
|
||||
crate::EpiphanyWorkerLaunchDocument::Reorient(document) => document,
|
||||
crate::EpiphanyWorkerLaunchDocument::Role(_) => {
|
||||
return Err(anyhow!("reorientation owner carried a role launch"));
|
||||
}
|
||||
};
|
||||
if launch.role != crate::EPIPHANY_REORIENT_OWNER_ROLE
|
||||
|| launch.binding_id != crate::EPIPHANY_REORIENT_LAUNCH_BINDING_ID
|
||||
|| document.schema_version != "epiphany.reorientation_launch_projection.v1"
|
||||
|| document.request_id.trim().is_empty()
|
||||
|| document.creation_thread_id != launch.job_id
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"reorientation immutable launch crossed family authority"
|
||||
));
|
||||
}
|
||||
reorientation_attempt_ordinal(&document.request_id, &launch.job_id)?;
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
fn reorientation_launches(
|
||||
cache: &CultCache,
|
||||
request_id: &str,
|
||||
) -> Result<Vec<(usize, crate::EpiphanyRuntimeWorkerLaunchRequest)>> {
|
||||
let mut launches = Vec::new();
|
||||
for launch in cache
|
||||
.get_all::<crate::EpiphanyRuntimeWorkerLaunchRequest>()?
|
||||
.into_iter()
|
||||
.filter(|launch| {
|
||||
launch.role == crate::EPIPHANY_REORIENT_OWNER_ROLE
|
||||
|| launch.binding_id == crate::EPIPHANY_REORIENT_LAUNCH_BINDING_ID
|
||||
})
|
||||
{
|
||||
let document = reorientation_launch_document(&launch)?;
|
||||
if document.request_id == request_id {
|
||||
launches.push((
|
||||
reorientation_attempt_ordinal(request_id, &launch.job_id)?,
|
||||
launch,
|
||||
));
|
||||
}
|
||||
}
|
||||
launches.sort_by_key(|(ordinal, _)| *ordinal);
|
||||
for (expected, (ordinal, _)) in launches.iter().enumerate() {
|
||||
if *ordinal != expected {
|
||||
return Err(anyhow!(
|
||||
"reorientation request has noncontiguous attempt identity"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(launches)
|
||||
}
|
||||
|
||||
pub(crate) fn current_reorientation_work(
|
||||
cache: &CultCache,
|
||||
) -> Result<Option<EpiphanyReorientationWorkProjection>> {
|
||||
|
|
@ -225,52 +266,58 @@ pub(crate) fn current_reorientation_work(
|
|||
{
|
||||
continue;
|
||||
}
|
||||
let mut bindings = cache
|
||||
.get_all::<EpiphanyReorientationLaunchBinding>()?
|
||||
.into_iter()
|
||||
.filter(|binding| binding.request_id == request.request_id)
|
||||
.collect::<Vec<_>>();
|
||||
bindings.sort_by_key(|binding| binding.attempt_ordinal);
|
||||
let Some(binding) = bindings.last() else {
|
||||
let launches = reorientation_launches(cache, &request.request_id)?;
|
||||
let Some((_, launch)) = launches.last() else {
|
||||
return Ok(Some(EpiphanyReorientationWorkProjection {
|
||||
request,
|
||||
attempt: crate::EpiphanyAgentPassAttemptProjection::unattempted(),
|
||||
terminal_failure: false,
|
||||
}));
|
||||
};
|
||||
let job = cache
|
||||
.get::<crate::EpiphanyRuntimeJob>(&binding.job_id)?
|
||||
.get::<crate::EpiphanyRuntimeJob>(&launch.job_id)?
|
||||
.ok_or_else(|| anyhow!("reorientation launch lost its runtime job"))?;
|
||||
let result = cache.get::<crate::EpiphanyRuntimeReorientWorkerResult>(&binding.job_id)?;
|
||||
let result = cache.get::<crate::EpiphanyRuntimeReorientWorkerResult>(&launch.job_id)?;
|
||||
let terminal_failure = job.status == EpiphanyRuntimeJobStatus::Failed;
|
||||
let action = match job.status {
|
||||
EpiphanyRuntimeJobStatus::Completed if result.is_some() => {
|
||||
EpiphanyAgentPassContinuationAction::Review
|
||||
}
|
||||
EpiphanyRuntimeJobStatus::Failed => {
|
||||
let terminal = exact_runtime_job_result(cache, &binding.job_id)?;
|
||||
match terminal.decision_context_id.as_deref() {
|
||||
Some(context_id)
|
||||
let model_failures = cache
|
||||
.get_all::<crate::EpiphanyModelPassFailure>()?
|
||||
.into_iter()
|
||||
.filter(|failure| failure.pass_id == launch.job_id)
|
||||
.collect::<Vec<_>>();
|
||||
match model_failures.as_slice() {
|
||||
[] => EpiphanyAgentPassContinuationAction::Launch,
|
||||
[model_failure]
|
||||
if failures.iter().any(|failure| {
|
||||
failure.request_id == request.request_id
|
||||
&& failure.job_id == binding.job_id
|
||||
&& failure.runtime_result_id == terminal.result_id
|
||||
&& failure.decision_context_id == context_id
|
||||
&& failure.job_id == launch.job_id
|
||||
&& failure.model_pass_failure_id == model_failure.failure_id
|
||||
&& failure.decision_context_id == model_failure.decision_context_id
|
||||
}) =>
|
||||
{
|
||||
EpiphanyAgentPassContinuationAction::Launch
|
||||
}
|
||||
Some(_) => EpiphanyAgentPassContinuationAction::Review,
|
||||
None => EpiphanyAgentPassContinuationAction::Launch,
|
||||
[_] => EpiphanyAgentPassContinuationAction::Review,
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"reorientation pass has multiple model-failure authorities"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
EpiphanyRuntimeJobStatus::Cancelled => EpiphanyAgentPassContinuationAction::Launch,
|
||||
_ => EpiphanyAgentPassContinuationAction::Wait,
|
||||
};
|
||||
return Ok(Some(EpiphanyReorientationWorkProjection {
|
||||
request,
|
||||
attempt: crate::EpiphanyAgentPassAttemptProjection::with(
|
||||
action,
|
||||
Some(binding.job_id.clone()),
|
||||
Some(launch.job_id.clone()),
|
||||
),
|
||||
terminal_failure,
|
||||
}));
|
||||
}
|
||||
Ok(None)
|
||||
|
|
@ -283,13 +330,12 @@ pub fn record_reorientation_pass_failure(
|
|||
let store_path = store_path.as_ref();
|
||||
let mut cache = crate::runtime_spine_cache(store_path)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let binding = cache
|
||||
.get_all::<EpiphanyReorientationLaunchBinding>()?
|
||||
.into_iter()
|
||||
.find(|binding| binding.job_id == job_id)
|
||||
.ok_or_else(|| anyhow!("reorientation failure has no exact launch binding"))?;
|
||||
let launch = cache
|
||||
.get::<crate::EpiphanyRuntimeWorkerLaunchRequest>(job_id)?
|
||||
.ok_or_else(|| anyhow!("reorientation failure has no immutable launch"))?;
|
||||
let launch_document = reorientation_launch_document(&launch)?;
|
||||
let request = cache
|
||||
.get::<EpiphanyReorientationRequest>(&binding.request_id)?
|
||||
.get::<EpiphanyReorientationRequest>(&launch_document.request_id)?
|
||||
.ok_or_else(|| anyhow!("reorientation failure lost its request"))?;
|
||||
validate_reorientation_request_intrinsic(&request)?;
|
||||
let job = cache
|
||||
|
|
@ -298,21 +344,24 @@ pub fn record_reorientation_pass_failure(
|
|||
if job.status != EpiphanyRuntimeJobStatus::Failed {
|
||||
return Err(anyhow!("reorientation pass is not a failed model pass"));
|
||||
}
|
||||
let terminal = exact_runtime_job_result(&cache, job_id)?;
|
||||
let decision_context_id = terminal
|
||||
.decision_context_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("model-backed reorientation failure has no decision context"))?;
|
||||
let model_failures = cache
|
||||
.get_all::<crate::EpiphanyModelPassFailure>()?
|
||||
.into_iter()
|
||||
.filter(|failure| failure.pass_id == job_id)
|
||||
.collect::<Vec<_>>();
|
||||
let [model_failure] = model_failures.as_slice() else {
|
||||
return Err(anyhow!(
|
||||
"reorientation failure requires one exact model-pass failure"
|
||||
));
|
||||
};
|
||||
let context = cache
|
||||
.get::<crate::EpiphanyDecisionContext>(decision_context_id)?
|
||||
.get::<crate::EpiphanyDecisionContext>(&model_failure.decision_context_id)?
|
||||
.ok_or_else(|| anyhow!("reorientation failure lost its decision context"))?;
|
||||
let basis = cache
|
||||
.get::<crate::EpiphanyReasoningBasis>(&context.basis_id)?
|
||||
.ok_or_else(|| anyhow!("reorientation failure lost its reasoning basis"))?;
|
||||
context.validate(&basis)?;
|
||||
let model_failure =
|
||||
crate::model_pass_failure_for_request(store_path, &context.terminal_request_id)?
|
||||
.ok_or_else(|| anyhow!("reorientation failure has no exact model-pass failure"))?;
|
||||
model_failure.validate(&basis, &context)?;
|
||||
if model_failure.decision_context_id != context.context_id {
|
||||
return Err(anyhow!(
|
||||
"reorientation failure crossed its model-pass failure context"
|
||||
|
|
@ -325,14 +374,13 @@ pub fn record_reorientation_pass_failure(
|
|||
}
|
||||
let failure = EpiphanyMindReorientationPassFailureDocument {
|
||||
schema_version: MIND_REORIENTATION_PASS_FAILURE_SCHEMA_VERSION.into(),
|
||||
failure_id: format!("reorientation-pass-failure-{}", terminal.result_id),
|
||||
failure_id: format!("reorientation-pass-failure-{}", model_failure.failure_id),
|
||||
request_id: request.request_id.clone(),
|
||||
job_id: job_id.into(),
|
||||
runtime_result_id: terminal.result_id.clone(),
|
||||
decision_context_id: decision_context_id.into(),
|
||||
verdict: terminal.verdict.clone(),
|
||||
summary: terminal.summary.clone(),
|
||||
failed_at: terminal.completed_at.clone(),
|
||||
model_pass_failure_id: model_failure.failure_id.clone(),
|
||||
decision_context_id: context.context_id.clone(),
|
||||
summary: model_failure.summary.clone(),
|
||||
failed_at: model_failure.failed_at.clone(),
|
||||
};
|
||||
if let Some(existing) =
|
||||
cache.get::<EpiphanyMindReorientationPassFailureDocument>(&failure.failure_id)?
|
||||
|
|
@ -340,7 +388,7 @@ pub fn record_reorientation_pass_failure(
|
|||
if existing != failure {
|
||||
return Err(anyhow!("reorientation failure replay was substituted"));
|
||||
}
|
||||
return mind_receipt_for_failure_context(&cache, decision_context_id);
|
||||
return mind_receipt_for_failure_context(&cache, &context.context_id);
|
||||
}
|
||||
let snapshot = cache.snapshot_envelopes();
|
||||
let mut strong_reads = Vec::new();
|
||||
|
|
@ -349,17 +397,12 @@ pub fn record_reorientation_pass_failure(
|
|||
EpiphanyReorientationRequest::TYPE,
|
||||
request.request_id.as_str(),
|
||||
),
|
||||
(
|
||||
EpiphanyReorientationLaunchBinding::TYPE,
|
||||
binding.binding_id.as_str(),
|
||||
),
|
||||
(crate::EpiphanyRuntimeWorkerLaunchRequest::TYPE, job_id),
|
||||
(crate::EpiphanyRuntimeJob::TYPE, job_id),
|
||||
(
|
||||
crate::EpiphanyRuntimeJobResult::TYPE,
|
||||
terminal.result_id.as_str(),
|
||||
crate::EpiphanyDecisionContext::TYPE,
|
||||
context.context_id.as_str(),
|
||||
),
|
||||
(crate::EpiphanyDecisionContext::TYPE, decision_context_id),
|
||||
(
|
||||
crate::EpiphanyModelPassFailure::TYPE,
|
||||
model_failure.failure_id.as_str(),
|
||||
|
|
@ -375,14 +418,13 @@ pub fn record_reorientation_pass_failure(
|
|||
}
|
||||
let write =
|
||||
crate::mind_documents::prepare_mind_document(&cache, &failure.failure_id, &failure)?;
|
||||
match crate::reasoning_context::commit_mind_mutation_with_derived_companions(
|
||||
match crate::reasoning_context::commit_mind_mutation(
|
||||
store_path,
|
||||
decision_context_id,
|
||||
&context.context_id,
|
||||
"Continuity.reorientation_failure",
|
||||
strong_reads,
|
||||
vec![write],
|
||||
Vec::new(),
|
||||
&terminal.completed_at,
|
||||
&model_failure.failed_at,
|
||||
)? {
|
||||
crate::EpiphanyMindCommitOutcome::Committed(receipt) => Ok(receipt),
|
||||
crate::EpiphanyMindCommitOutcome::Conflict {
|
||||
|
|
@ -408,11 +450,8 @@ pub fn launch_current_reorientation_work(
|
|||
return Err(anyhow!("reorientation request is not launchable"));
|
||||
}
|
||||
validate_reorientation_request_current(&cache, &work.request)?;
|
||||
let attempt_ordinal = cache
|
||||
.get_all::<EpiphanyReorientationLaunchBinding>()?
|
||||
.into_iter()
|
||||
.filter(|binding| binding.request_id == work.request.request_id)
|
||||
.count() as u64;
|
||||
let prior_launches = reorientation_launches(&cache, &work.request.request_id)?;
|
||||
let attempt_ordinal = prior_launches.len();
|
||||
let job_id = format!(
|
||||
"reorientation-{}-attempt-{attempt_ordinal}",
|
||||
work.request.request_id
|
||||
|
|
@ -429,9 +468,6 @@ pub fn launch_current_reorientation_work(
|
|||
&cache,
|
||||
crate::RuntimeSpineHeartbeatJobOptions {
|
||||
runtime_id: work.request.runtime_id.clone(),
|
||||
session_id: crate::EPIPHANY_RUNTIME_ROOT_SESSION_ID.into(),
|
||||
objective: "Reorient from one exact keyed Mind projection".into(),
|
||||
coordinator_note: "Reorientation current-work owner opened this pass.".into(),
|
||||
job_id: job_id.clone(),
|
||||
role: crate::EPIPHANY_REORIENT_OWNER_ROLE.into(),
|
||||
binding_id: crate::EPIPHANY_REORIENT_LAUNCH_BINDING_ID.into(),
|
||||
|
|
@ -439,39 +475,61 @@ pub fn launch_current_reorientation_work(
|
|||
instruction: "Decide resume or regather from only the sealed typed continuity projection. Return the reorientation result contract; do not invent live state.".into(),
|
||||
launch_document,
|
||||
output_contract_id: output_contract_id.clone(),
|
||||
proposal_modeling_request_id: None,
|
||||
frontier_planning_request_id: None,
|
||||
frontier_plan_mind_request_id: None,
|
||||
imagination_consideration_request_id: None,
|
||||
admitted_model_direction_consideration_request_id: None,
|
||||
repo_frontier_modeling_request_id: None,
|
||||
repo_frontier_research_request_id: None,
|
||||
repo_frontier_verification_request_id: None,
|
||||
created_at: created_at.into(),
|
||||
},
|
||||
)?;
|
||||
let binding = EpiphanyReorientationLaunchBinding {
|
||||
schema_version: REORIENTATION_LAUNCH_BINDING_SCHEMA_VERSION.into(),
|
||||
binding_id: format!("reorientation-launch-{job_id}"),
|
||||
request_id: work.request.request_id.clone(),
|
||||
job_id: job_id.clone(),
|
||||
attempt_ordinal,
|
||||
launched_at: created_at.into(),
|
||||
};
|
||||
let expected = exact_source_envelopes(&cache, &work.request.source_documents)?
|
||||
let mut expected = exact_source_envelopes(&cache, &work.request.source_documents)?
|
||||
.into_iter()
|
||||
.chain(
|
||||
cache
|
||||
.get_envelope::<EpiphanyReorientationRequest>(&work.request.request_id)?
|
||||
.into_iter(),
|
||||
)
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
for (_, launch) in &prior_launches {
|
||||
expected.push(
|
||||
cache
|
||||
.get_envelope::<crate::EpiphanyRuntimeWorkerLaunchRequest>(&launch.job_id)?
|
||||
.ok_or_else(|| anyhow!("reorientation retry lost its prior launch envelope"))?,
|
||||
);
|
||||
}
|
||||
if let Some((_, latest)) = prior_launches.last() {
|
||||
expected.push(
|
||||
cache
|
||||
.get_envelope::<crate::EpiphanyRuntimeJob>(&latest.job_id)?
|
||||
.ok_or_else(|| anyhow!("reorientation retry lost its prior job envelope"))?,
|
||||
);
|
||||
for failure in cache
|
||||
.get_all::<EpiphanyMindReorientationPassFailureDocument>()?
|
||||
.into_iter()
|
||||
.filter(|failure| {
|
||||
failure.request_id == work.request.request_id && failure.job_id == latest.job_id
|
||||
})
|
||||
{
|
||||
expected.push(
|
||||
cache
|
||||
.get_envelope::<EpiphanyMindReorientationPassFailureDocument>(
|
||||
&failure.failure_id,
|
||||
)?
|
||||
.ok_or_else(|| anyhow!("reorientation retry lost its failure envelope"))?,
|
||||
);
|
||||
expected.push(
|
||||
cache
|
||||
.get_envelope::<crate::EpiphanyModelPassFailure>(
|
||||
&failure.model_pass_failure_id,
|
||||
)?
|
||||
.ok_or_else(|| {
|
||||
anyhow!("reorientation retry lost its model-failure envelope")
|
||||
})?,
|
||||
);
|
||||
}
|
||||
}
|
||||
crate::current_work::commit_current_work_launch(
|
||||
store_path,
|
||||
&cache,
|
||||
expected,
|
||||
prepared,
|
||||
vec![cache.prepare_entry(&binding.binding_id, &binding)?.0],
|
||||
Vec::new(),
|
||||
"reorientation",
|
||||
)?;
|
||||
Ok(job_id)
|
||||
|
|
@ -487,13 +545,12 @@ pub fn accept_reorientation_result(
|
|||
let store_path = store_path.as_ref();
|
||||
let mut cache = crate::runtime_spine_cache(store_path)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let binding = cache
|
||||
.get_all::<EpiphanyReorientationLaunchBinding>()?
|
||||
.into_iter()
|
||||
.find(|binding| binding.job_id == job_id)
|
||||
.ok_or_else(|| anyhow!("reorientation result has no exact launch binding"))?;
|
||||
let launch = cache
|
||||
.get::<crate::EpiphanyRuntimeWorkerLaunchRequest>(job_id)?
|
||||
.ok_or_else(|| anyhow!("reorientation result has no immutable launch"))?;
|
||||
let launch_document = reorientation_launch_document(&launch)?;
|
||||
let request = cache
|
||||
.get::<EpiphanyReorientationRequest>(&binding.request_id)?
|
||||
.get::<EpiphanyReorientationRequest>(&launch_document.request_id)?
|
||||
.ok_or_else(|| anyhow!("reorientation result lost its request"))?;
|
||||
validate_reorientation_request_intrinsic(&request)?;
|
||||
let job = cache
|
||||
|
|
@ -554,23 +611,12 @@ pub fn accept_reorientation_result(
|
|||
next_safe_move: result.next_safe_move.clone(),
|
||||
decided_at: accepted_at.into(),
|
||||
};
|
||||
let finding = crate::interpret_runtime_reorient_worker_result(&result);
|
||||
let recovery = crate::continuity_recovery_receipt_from_reorient_finding(
|
||||
format!("continuity-recovery-{}", request.request_id),
|
||||
crate::EPIPHANY_REORIENT_LAUNCH_BINDING_ID.into(),
|
||||
&finding,
|
||||
accepted_at.into(),
|
||||
);
|
||||
let mut strong_reads = exact_source_envelopes(&cache, &request.source_documents)?;
|
||||
for (document_type, document_key) in [
|
||||
(
|
||||
EpiphanyReorientationRequest::TYPE,
|
||||
request.request_id.as_str(),
|
||||
),
|
||||
(
|
||||
EpiphanyReorientationLaunchBinding::TYPE,
|
||||
binding.binding_id.as_str(),
|
||||
),
|
||||
(crate::EpiphanyRuntimeWorkerLaunchRequest::TYPE, job_id),
|
||||
(crate::EpiphanyRuntimeJob::TYPE, job_id),
|
||||
(crate::EpiphanyRuntimeReorientWorkerResult::TYPE, job_id),
|
||||
|
|
@ -589,14 +635,12 @@ pub fn accept_reorientation_result(
|
|||
}
|
||||
let write =
|
||||
crate::mind_documents::prepare_mind_document(&cache, &decision.decision_id, &decision)?;
|
||||
let companion = cache.prepare_entry(&recovery.receipt_id, &recovery)?.0;
|
||||
match crate::reasoning_context::commit_mind_mutation_with_derived_companions(
|
||||
match crate::reasoning_context::commit_mind_mutation(
|
||||
store_path,
|
||||
&result.decision_context_id,
|
||||
"Continuity.reorientation",
|
||||
strong_reads,
|
||||
vec![write],
|
||||
vec![companion],
|
||||
accepted_at,
|
||||
)? {
|
||||
crate::EpiphanyMindCommitOutcome::Committed(receipt) => Ok(receipt),
|
||||
|
|
@ -826,26 +870,6 @@ fn mind_receipt_for_failure_context(
|
|||
}
|
||||
}
|
||||
|
||||
fn exact_runtime_job_result(
|
||||
cache: &CultCache,
|
||||
job_id: &str,
|
||||
) -> Result<crate::EpiphanyRuntimeJobResult> {
|
||||
let mut results = cache
|
||||
.get_all::<crate::EpiphanyRuntimeJobResult>()?
|
||||
.into_iter()
|
||||
.filter(|result| result.job_id == job_id)
|
||||
.collect::<Vec<_>>();
|
||||
match results.len() {
|
||||
1 => Ok(results.remove(0)),
|
||||
0 => Err(anyhow!(
|
||||
"reorientation attempt lost its terminal runtime result"
|
||||
)),
|
||||
_ => Err(anyhow!(
|
||||
"reorientation attempt has multiple terminal runtime results"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -934,22 +958,11 @@ mod tests {
|
|||
)?);
|
||||
crate::open_runtime_model_execution(
|
||||
&store,
|
||||
crate::RuntimeSpineSessionOptions {
|
||||
session_id: "reorientation-model-session".into(),
|
||||
objective: "Reorient from exact Mind state".into(),
|
||||
created_at: "2026-08-18T10:00:03Z".into(),
|
||||
coordinator_note: "test".into(),
|
||||
},
|
||||
crate::RuntimeSpineJobOptions {
|
||||
job_id: "reorientation-model-job".into(),
|
||||
session_id: "reorientation-model-session".into(),
|
||||
role: "openai-model".into(),
|
||||
created_at: "2026-08-18T10:00:03Z".into(),
|
||||
summary: "Bound reorientation inference".into(),
|
||||
artifact_refs: Vec::new(),
|
||||
},
|
||||
&native,
|
||||
"2026-08-18T10:00:03Z",
|
||||
)?;
|
||||
let context = crate::EpiphanyDecisionContext::new(&basis, native, Vec::new())?;
|
||||
crate::put_decision_context(&store, &context)?;
|
||||
|
|
@ -975,13 +988,12 @@ mod tests {
|
|||
"2026-08-18T10:00:05Z",
|
||||
)?;
|
||||
let result = crate::EpiphanyRuntimeReorientWorkerResult {
|
||||
schema_version: crate::RUNTIME_REORIENT_WORKER_RESULT_SCHEMA_VERSION.into(),
|
||||
result_id: "reorientation-result".into(),
|
||||
job_id: job_id.clone(),
|
||||
mode: "resume".into(),
|
||||
summary: "The sealed checkpoint remains coherent.".into(),
|
||||
next_safe_move: "Continue the keyed migration.".into(),
|
||||
checkpoint_still_valid: Some(true),
|
||||
mode: "regather".into(),
|
||||
summary: "The sealed checkpoint requires fresh external evidence.".into(),
|
||||
next_safe_move: "Regather before resuming the keyed migration.".into(),
|
||||
checkpoint_still_valid: Some(false),
|
||||
files_inspected: Vec::new(),
|
||||
frontier_node_ids: Vec::new(),
|
||||
evidence_ids: Vec::new(),
|
||||
|
|
@ -993,26 +1005,29 @@ mod tests {
|
|||
decision_context_id: context.context_id.clone(),
|
||||
};
|
||||
crate::put_runtime_reorient_worker_result(&store, &result)?;
|
||||
crate::complete_runtime_job(
|
||||
&store,
|
||||
crate::RuntimeSpineJobResultOptions {
|
||||
result_id: format!("runtime-result-{job_id}"),
|
||||
job_id: job_id.clone(),
|
||||
completed_at: "2026-08-18T10:00:06Z".into(),
|
||||
verdict: "resume".into(),
|
||||
summary: result.summary.clone(),
|
||||
next_safe_move: result.next_safe_move.clone(),
|
||||
evidence_refs: Vec::new(),
|
||||
artifact_refs: Vec::new(),
|
||||
decision_context_id: Some(context.context_id.clone()),
|
||||
},
|
||||
)?;
|
||||
let projected_result = crate::read_runtime_reorient_result(&store, &job_id)?;
|
||||
assert_eq!(
|
||||
projected_result.status,
|
||||
crate::EpiphanyReorientResultStatus::Completed
|
||||
);
|
||||
assert_eq!(
|
||||
projected_result.finding.and_then(|finding| finding.mode),
|
||||
Some("regather".into())
|
||||
);
|
||||
let review_work = crate::project_current_work(&store)?
|
||||
.reorientation
|
||||
.expect("completed Reorientation work");
|
||||
assert_eq!(
|
||||
review_work.attempt.action,
|
||||
EpiphanyAgentPassContinuationAction::Review
|
||||
);
|
||||
assert!(!review_work.terminal_failure);
|
||||
|
||||
cache.pull_all_backing_stores()?;
|
||||
cache.put(
|
||||
"unrelated-observation",
|
||||
&crate::EpiphanyMindObservationDocument {
|
||||
value: epiphany_state_model::EpiphanyObservation {
|
||||
value: crate::EpiphanyObservation {
|
||||
id: "unrelated-observation".into(),
|
||||
summary: "Persona observed an unrelated social fact.".into(),
|
||||
..Default::default()
|
||||
|
|
@ -1028,7 +1043,17 @@ mod tests {
|
|||
accept_reorientation_result(&store, &job_id, "2026-08-18T10:00:07Z")?,
|
||||
receipt
|
||||
);
|
||||
assert!(crate::project_current_work(&store)?.reorientation.is_none());
|
||||
let current_work = crate::project_current_work(&store)?;
|
||||
assert!(current_work.reorientation.is_none());
|
||||
assert!(current_work.operator_regather_required);
|
||||
assert_eq!(
|
||||
crate::recommend_coordinator_action(crate::EpiphanyCoordinatorInput {
|
||||
mind_present: true,
|
||||
current_work,
|
||||
})
|
||||
.action,
|
||||
crate::EpiphanyCoordinatorAction::RegatherManually
|
||||
);
|
||||
let mut accepted = crate::runtime_spine_cache(&store)?;
|
||||
accepted.pull_all_backing_stores()?;
|
||||
assert!(
|
||||
|
|
@ -1068,22 +1093,11 @@ mod tests {
|
|||
)?);
|
||||
crate::open_runtime_model_execution(
|
||||
&store,
|
||||
crate::RuntimeSpineSessionOptions {
|
||||
session_id: "failed-reorientation-model-session".into(),
|
||||
objective: "Fail from exact Mind state".into(),
|
||||
created_at: "2026-08-18T10:00:10Z".into(),
|
||||
coordinator_note: "test".into(),
|
||||
},
|
||||
crate::RuntimeSpineJobOptions {
|
||||
job_id: "failed-reorientation-model-job".into(),
|
||||
session_id: "failed-reorientation-model-session".into(),
|
||||
role: "openai-model".into(),
|
||||
created_at: "2026-08-18T10:00:10Z".into(),
|
||||
summary: "Bound failing inference".into(),
|
||||
artifact_refs: Vec::new(),
|
||||
},
|
||||
&failed_native,
|
||||
"2026-08-18T10:00:10Z",
|
||||
)?;
|
||||
let failed_context =
|
||||
crate::EpiphanyDecisionContext::new(&failed_basis, failed_native, Vec::new())?;
|
||||
|
|
@ -1119,27 +1133,21 @@ mod tests {
|
|||
failed_activation,
|
||||
"2026-08-18T10:00:12Z",
|
||||
)?;
|
||||
crate::complete_runtime_job(
|
||||
crate::terminalize_runtime_job(
|
||||
&store,
|
||||
crate::RuntimeSpineJobResultOptions {
|
||||
result_id: format!("runtime-result-{failed_job}"),
|
||||
job_id: failed_job.clone(),
|
||||
completed_at: "2026-08-18T10:00:14Z".into(),
|
||||
verdict: "failed".into(),
|
||||
summary: "Provider failed after the terminal request was sealed.".into(),
|
||||
next_safe_move: "Review the typed failure, then retry.".into(),
|
||||
evidence_refs: Vec::new(),
|
||||
artifact_refs: Vec::new(),
|
||||
decision_context_id: Some(failed_context.context_id.clone()),
|
||||
},
|
||||
&failed_job,
|
||||
EpiphanyRuntimeJobStatus::Failed,
|
||||
"2026-08-18T10:00:14Z",
|
||||
&model_failure.failure_id,
|
||||
)?;
|
||||
let failed_review = crate::project_current_work(&store)?
|
||||
.reorientation
|
||||
.expect("failed Reorientation work");
|
||||
assert_eq!(
|
||||
crate::project_current_work(&store)?
|
||||
.reorientation
|
||||
.as_ref()
|
||||
.map(|work| work.attempt.action),
|
||||
Some(EpiphanyAgentPassContinuationAction::Review)
|
||||
failed_review.attempt.action,
|
||||
EpiphanyAgentPassContinuationAction::Review
|
||||
);
|
||||
assert!(failed_review.terminal_failure);
|
||||
let failure_receipt = record_reorientation_pass_failure(&store, &failed_job)?;
|
||||
assert_eq!(
|
||||
record_reorientation_pass_failure(&store, &failed_job)?,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry};
|
||||
use epiphany_state_model::{
|
||||
use crate::state_model::{
|
||||
EpiphanyMemoryDomain, EpiphanyMemoryEdge, EpiphanyMemoryGraphSnapshot, EpiphanyMemoryNode,
|
||||
RepoFrontierItem,
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
|
|
@ -179,15 +179,6 @@ impl EpiphanyRepoModelBasis {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_current(&self, store_path: impl AsRef<Path>) -> Result<()> {
|
||||
self.validate()?;
|
||||
let current = assemble_repo_model_view(store_path)?.reasoning_basis();
|
||||
if current != *self {
|
||||
return Err(anyhow!("RepoModel basis is stale"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_against_cache(&self, cache: &CultCache) -> Result<()> {
|
||||
self.validate()?;
|
||||
let live = cache.snapshot_envelopes();
|
||||
|
|
@ -835,7 +826,7 @@ pub fn plan_repo_model_mutation(
|
|||
{
|
||||
insert_strong_envelope(&mut strong, existing)?;
|
||||
}
|
||||
node.lifecycle = epiphany_state_model::EpiphanyMemoryLifecycle::Retired;
|
||||
node.lifecycle = crate::EpiphanyMemoryLifecycle::Retired;
|
||||
nodes.insert(node_id.clone(), node.clone());
|
||||
insert_envelope(
|
||||
&mut writes,
|
||||
|
|
@ -879,7 +870,7 @@ pub fn plan_repo_model_mutation(
|
|||
.get_envelope::<EpiphanyRepoModelEdgeDocument>(edge_id)?
|
||||
.ok_or_else(|| anyhow!("RepoModel edge lost its envelope"))?;
|
||||
insert_strong_envelope(&mut strong, existing)?;
|
||||
edge.lifecycle = epiphany_state_model::EpiphanyMemoryLifecycle::Retired;
|
||||
edge.lifecycle = crate::EpiphanyMemoryLifecycle::Retired;
|
||||
edges.insert(edge_id.clone(), edge.clone());
|
||||
insert_envelope(
|
||||
&mut writes,
|
||||
|
|
@ -1319,9 +1310,9 @@ fn require_semantic_id(value: &str, label: &str) -> Result<()> {
|
|||
fn frontier_is_unresolved(item: &RepoFrontierItem) -> bool {
|
||||
matches!(
|
||||
item.status,
|
||||
epiphany_state_model::RepoFrontierStatus::Proposed
|
||||
| epiphany_state_model::RepoFrontierStatus::Active
|
||||
| epiphany_state_model::RepoFrontierStatus::Blocked
|
||||
crate::RepoFrontierStatus::Proposed
|
||||
| crate::RepoFrontierStatus::Active
|
||||
| crate::RepoFrontierStatus::Blocked
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1509,9 +1500,9 @@ fn validate_claim_obligations(
|
|||
if !item.target_claim_ids.contains(&obligation.node_id)
|
||||
|| !matches!(
|
||||
item.status,
|
||||
epiphany_state_model::RepoFrontierStatus::Proposed
|
||||
| epiphany_state_model::RepoFrontierStatus::Active
|
||||
| epiphany_state_model::RepoFrontierStatus::Blocked
|
||||
crate::RepoFrontierStatus::Proposed
|
||||
| crate::RepoFrontierStatus::Active
|
||||
| crate::RepoFrontierStatus::Blocked
|
||||
)
|
||||
{
|
||||
return Err(anyhow!("RepoModel claim obligation is not live"));
|
||||
|
|
@ -1521,9 +1512,9 @@ fn validate_claim_obligations(
|
|||
for item in frontier.iter().filter(|item| {
|
||||
matches!(
|
||||
item.status,
|
||||
epiphany_state_model::RepoFrontierStatus::Proposed
|
||||
| epiphany_state_model::RepoFrontierStatus::Active
|
||||
| epiphany_state_model::RepoFrontierStatus::Blocked
|
||||
crate::RepoFrontierStatus::Proposed
|
||||
| crate::RepoFrontierStatus::Active
|
||||
| crate::RepoFrontierStatus::Blocked
|
||||
)
|
||||
}) {
|
||||
for node_id in &item.target_claim_ids {
|
||||
|
|
@ -1541,10 +1532,10 @@ fn validate_claim_obligations(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{RuntimeSpineInitOptions, initialize_runtime_spine};
|
||||
use epiphany_state_model::{
|
||||
use crate::state_model::{
|
||||
EpiphanyMemoryEdgeKind, EpiphanyMemoryLifecycle, EpiphanyMemoryNodeKind, RepoFrontierStatus,
|
||||
};
|
||||
use crate::{RuntimeSpineInitOptions, initialize_runtime_spine};
|
||||
|
||||
fn bind_test_body(
|
||||
store: &Path,
|
||||
|
|
@ -2251,7 +2242,10 @@ mod tests {
|
|||
source.document_type == crate::AtlasSurfaceOffer::TYPE
|
||||
&& source.document_key == offer_id.to_string()
|
||||
}));
|
||||
offer_view.reasoning_basis().validate_current(&store)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
offer_view
|
||||
.reasoning_basis()
|
||||
.validate_against_cache(&cache)?;
|
||||
|
||||
let eve = crate::AtlasRepositoryIdentity::new("gamecult-local", "eve")?;
|
||||
let eve_surface = uuid::Uuid::new_v4();
|
||||
|
|
|
|||
|
|
@ -33,58 +33,7 @@ use cultcache_rs::DatabaseEntry;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub const REPO_FRONTIER_ROUTE_SCHEMA_VERSION: &str = "epiphany.self.repo_frontier_route.v2";
|
||||
pub const REPO_FRONTIER_HANDS_AUTHORITY_SCHEMA_VERSION: &str =
|
||||
"epiphany.hands.repo_frontier_authority.v0";
|
||||
pub const REPO_FRONTIER_ROUTE_CONTRACT: &str = "epiphany.repo_frontier_route.v2";
|
||||
pub const REPO_FRONTIER_HANDS_AUTHORITY_CONTRACT: &str =
|
||||
"epiphany.repo_frontier_hands_authority.v0";
|
||||
pub const REPO_FRONTIER_MODELING_REQUEST_SCHEMA_VERSION: &str =
|
||||
"epiphany.modeling.repo_frontier_verdict_request.v1";
|
||||
pub const REPO_FRONTIER_MODELING_REQUEST_CONTRACT: &str =
|
||||
"epiphany.repo_frontier_verdict_modeling_request.v1";
|
||||
pub const REPO_FRONTIER_WORK_PROPOSAL_SCHEMA_VERSION: &str =
|
||||
"epiphany.repo_frontier_work_proposal.v3";
|
||||
pub const REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION: &str =
|
||||
"epiphany.self.repo_frontier_planning_request.v4";
|
||||
pub const REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION: &str =
|
||||
"epiphany.imagination.repo_frontier_plan_candidate.v0";
|
||||
pub const REPO_FRONTIER_PLAN_DECISION_RECEIPT_SCHEMA_VERSION: &str =
|
||||
"epiphany.mind.repo_frontier_plan_decision_receipt.v2";
|
||||
pub const REPO_FRONTIER_PLAN_DECISION_CONTRACT: &str = "epiphany.repo_frontier_plan_decision.v0";
|
||||
pub const REPO_FRONTIER_PLANNING_CONTRACT: &str = "epiphany.repo_frontier_planning.v4";
|
||||
pub const REPO_FRONTIER_PLANNING_FAILURE_REVIEW_SCHEMA_VERSION: &str =
|
||||
"epiphany.self.repo_frontier_planning_failure_review.v0";
|
||||
pub const REPO_FRONTIER_RESEARCH_REQUEST_SCHEMA_VERSION: &str =
|
||||
"epiphany.self.repo_frontier_research_request.v3";
|
||||
pub const REPO_FRONTIER_RESEARCH_REQUEST_CONTRACT: &str =
|
||||
"epiphany.repo_frontier_research_request.v3";
|
||||
pub const REPO_FRONTIER_WORK_PROPOSAL_CONTRACT: &str =
|
||||
"epiphany.repo_frontier_work_proposal.inert.v3";
|
||||
pub const REPO_FRONTIER_AUTONOMOUS_PROPOSAL_BINDING_SCHEMA_VERSION: &str =
|
||||
"epiphany.self.repo_frontier_autonomous_proposal_binding.v2";
|
||||
pub const RUNTIME_REPOSITORY_DOMAIN_BINDING_KEY: &str = "runtime-repository-domain-binding";
|
||||
pub const RUNTIME_REPOSITORY_DOMAIN_BINDING_SCHEMA_VERSION: &str =
|
||||
"epiphany.runtime.repository_domain_binding.v0";
|
||||
pub const RUNTIME_REPOSITORY_DOMAIN_BINDING_CONTRACT: &str = "deployment configuration binds one organizational repository name to one exact authenticated repository Body; Self may consume but not relabel it";
|
||||
pub const REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_SCHEMA_VERSION: &str =
|
||||
"epiphany.coordinator.repo_frontier_proposal_modeling_request.v0";
|
||||
pub const REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_CONTRACT: &str =
|
||||
"epiphany.repo_frontier_proposal_modeling_request.v0";
|
||||
pub const REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_SCHEMA_VERSION: &str =
|
||||
"epiphany.coordinator.repo_frontier_proposal_modeling_launch_binding.v2";
|
||||
pub const REPO_FRONTIER_PLANNING_LAUNCH_BINDING_SCHEMA_VERSION: &str =
|
||||
"epiphany.coordinator.repo_frontier_planning_launch_binding.v0";
|
||||
pub const REPO_FRONTIER_PLANNING_LAUNCH_BINDING_CONTRACT: &str =
|
||||
"epiphany.repo_frontier_planning_launch_binding.v0";
|
||||
pub const REPO_FRONTIER_PLAN_MIND_REQUEST_SCHEMA_VERSION: &str =
|
||||
"epiphany.self.repo_frontier_plan_mind_request.v1";
|
||||
pub const REPO_FRONTIER_PLAN_MIND_REQUEST_CONTRACT: &str =
|
||||
"epiphany.repo_frontier_plan_mind_request.v1";
|
||||
pub const REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_SCHEMA_VERSION: &str =
|
||||
"epiphany.coordinator.repo_frontier_plan_mind_launch_binding.v0";
|
||||
pub const REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_CONTRACT: &str =
|
||||
"epiphany.repo_frontier_plan_mind_launch_binding.v0";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
|
|
@ -92,8 +41,6 @@ pub const REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_CONTRACT: &str =
|
|||
schema = "RepoFrontierWorkProposal"
|
||||
)]
|
||||
pub struct RepoFrontierWorkProposal {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub proposal_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
|
|
@ -106,8 +53,6 @@ pub struct RepoFrontierWorkProposal {
|
|||
pub constraints: Vec<String>,
|
||||
#[cultcache(key = 6)]
|
||||
pub evidence_refs: Vec<String>,
|
||||
#[cultcache(key = 7)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
pub fn repo_frontier_proposal_payload_sha256(
|
||||
|
|
@ -120,26 +65,6 @@ pub fn repo_frontier_proposal_payload_sha256(
|
|||
Ok(format!("{:x}", Sha256::digest(content)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.self.repo_frontier_autonomous_proposal_binding",
|
||||
schema = "RepoFrontierAutonomousProposalBinding"
|
||||
)]
|
||||
pub struct RepoFrontierAutonomousProposalBinding {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub direction_result_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub option_ordinal: u32,
|
||||
#[cultcache(key = 3)]
|
||||
pub direction_worker_job_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub direction_worker_result_sha256: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub direction_worker_launch_sha256: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.runtime.repository_domain_binding",
|
||||
|
|
@ -147,23 +72,9 @@ pub struct RepoFrontierAutonomousProposalBinding {
|
|||
)]
|
||||
pub struct RuntimeRepositoryDomainBinding {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub binding_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub repository_full_name: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub runtime_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub swarm_id: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub workspace_id: String,
|
||||
#[cultcache(key = 6)]
|
||||
#[cultcache(key = 1)]
|
||||
pub body_binding_sha256: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub bound_at: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
|
|
@ -172,8 +83,6 @@ pub struct RuntimeRepositoryDomainBinding {
|
|||
schema = "RepoFrontierProposalModelingRequest"
|
||||
)]
|
||||
pub struct RepoFrontierProposalModelingRequest {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
|
|
@ -190,24 +99,12 @@ pub struct RepoFrontierProposalModelingRequest {
|
|||
pub workspace: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub selected_at: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.coordinator.repo_frontier_proposal_modeling_launch_binding",
|
||||
schema = "RepoFrontierProposalModelingLaunchBinding"
|
||||
)]
|
||||
pub struct RepoFrontierProposalModelingLaunchBinding {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub proposal_modeling_request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub job_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub worker_launch_document_sha256: String,
|
||||
#[cultcache(key = 10)]
|
||||
pub direction_result_id: String,
|
||||
#[cultcache(key = 11)]
|
||||
pub direction_option_ordinal: u32,
|
||||
#[cultcache(key = 12)]
|
||||
pub direction_worker_job_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
|
|
@ -216,8 +113,6 @@ pub struct RepoFrontierProposalModelingLaunchBinding {
|
|||
schema = "RepoFrontierPlanningRequest"
|
||||
)]
|
||||
pub struct RepoFrontierPlanningRequest {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
|
|
@ -234,8 +129,6 @@ pub struct RepoFrontierPlanningRequest {
|
|||
pub repository_scope: Vec<String>,
|
||||
#[cultcache(key = 8)]
|
||||
pub requested_at: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub contract: String,
|
||||
#[cultcache(key = 10)]
|
||||
pub runtime_id: String,
|
||||
/// Exact current documents that own whether this planning work remains
|
||||
|
|
@ -248,46 +141,12 @@ pub struct RepoFrontierPlanningRequest {
|
|||
pub claim_obligation_documents: Vec<crate::EpiphanyMindDocumentVersion>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.coordinator.repo_frontier_planning_launch_binding",
|
||||
schema = "RepoFrontierPlanningLaunchBinding"
|
||||
)]
|
||||
pub struct RepoFrontierPlanningLaunchBinding {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub binding_record_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub planning_request_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub job_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub binding_id: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub runtime_id: String,
|
||||
#[cultcache(key = 6)]
|
||||
pub thread_id: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub launched_at: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub worker_launch_document_sha256: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub contract: String,
|
||||
#[cultcache(key = 10, default)]
|
||||
pub attempt_ordinal: u64,
|
||||
#[cultcache(key = 11, default)]
|
||||
pub superseded_failure_result_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.self.repo_frontier_research_request",
|
||||
schema = "RepoFrontierResearchRequest"
|
||||
)]
|
||||
pub struct RepoFrontierResearchRequest {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
|
|
@ -304,8 +163,6 @@ pub struct RepoFrontierResearchRequest {
|
|||
pub requested_at: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub runtime_id: String,
|
||||
#[cultcache(key = 10)]
|
||||
pub contract: String,
|
||||
/// Immutable public source identities already owned by the admitted
|
||||
/// frontier. This is dedicated retrieval authority, not worker-authored
|
||||
/// evidence or a search hint.
|
||||
|
|
@ -324,8 +181,6 @@ pub struct RepoFrontierResearchRequest {
|
|||
schema = "RepoFrontierPlanCandidate"
|
||||
)]
|
||||
pub struct RepoFrontierPlanCandidate {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub candidate_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
|
|
@ -354,8 +209,6 @@ pub struct RepoFrontierPlanCandidate {
|
|||
pub commit_message: String,
|
||||
#[cultcache(key = 14)]
|
||||
pub proposed_at: String,
|
||||
#[cultcache(key = 15)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -401,8 +254,6 @@ pub struct RepoFrontierPlanningLifecycle {
|
|||
schema = "RepoFrontierPlanMindRequest"
|
||||
)]
|
||||
pub struct RepoFrontierPlanMindRequest {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
|
|
@ -419,40 +270,6 @@ pub struct RepoFrontierPlanMindRequest {
|
|||
pub runtime_id: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub requested_at: String,
|
||||
#[cultcache(key = 10)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.coordinator.repo_frontier_plan_mind_launch_binding",
|
||||
schema = "RepoFrontierPlanMindLaunchBinding"
|
||||
)]
|
||||
pub struct RepoFrontierPlanMindLaunchBinding {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub binding_record_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub mind_request_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub job_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub binding_id: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub runtime_id: String,
|
||||
#[cultcache(key = 6)]
|
||||
pub thread_id: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub launched_at: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub worker_launch_document_sha256: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub contract: String,
|
||||
#[cultcache(key = 10, default)]
|
||||
pub attempt_ordinal: u64,
|
||||
#[cultcache(key = 11, default)]
|
||||
pub superseded_failure_result_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -473,8 +290,6 @@ pub struct RepoFrontierPlanMindDecision {
|
|||
schema = "RepoFrontierPlanDecisionReceipt"
|
||||
)]
|
||||
pub struct RepoFrontierPlanDecisionReceipt {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub decision_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
|
|
@ -497,8 +312,6 @@ pub struct RepoFrontierPlanDecisionReceipt {
|
|||
pub rationale: String,
|
||||
#[cultcache(key = 11)]
|
||||
pub decided_at: String,
|
||||
#[cultcache(key = 12)]
|
||||
pub contract: String,
|
||||
#[cultcache(key = 13)]
|
||||
pub decision_source: RepoFrontierPlanDecisionSource,
|
||||
}
|
||||
|
|
@ -522,8 +335,6 @@ pub enum RepoFrontierVerdictDisposition {
|
|||
schema = "RepoFrontierModelingRequest"
|
||||
)]
|
||||
pub struct RepoFrontierModelingRequest {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
|
|
@ -548,8 +359,6 @@ pub struct RepoFrontierModelingRequest {
|
|||
pub allowed_disposition: RepoFrontierVerdictDisposition,
|
||||
#[cultcache(key = 12)]
|
||||
pub requested_at: String,
|
||||
#[cultcache(key = 13)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
|
|
@ -558,8 +367,6 @@ pub struct RepoFrontierModelingRequest {
|
|||
schema = "RepoFrontierPlanningFailureReview"
|
||||
)]
|
||||
pub struct RepoFrontierPlanningFailureReview {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub review_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
|
|
@ -588,8 +395,6 @@ pub enum RepoFrontierNextOrgan {
|
|||
schema = "RepoFrontierRoute"
|
||||
)]
|
||||
pub struct RepoFrontierRoute {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub route_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
|
|
@ -613,11 +418,9 @@ pub struct RepoFrontierRoute {
|
|||
#[cultcache(key = 11)]
|
||||
pub authorized_paths: Vec<String>,
|
||||
#[cultcache(key = 12, default)]
|
||||
pub adopted_plan: Option<epiphany_state_model::RepoFrontierAdoptedPlan>,
|
||||
pub adopted_plan: Option<crate::RepoFrontierAdoptedPlan>,
|
||||
#[cultcache(key = 13)]
|
||||
pub selected_at: String,
|
||||
#[cultcache(key = 14)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
|
|
@ -626,30 +429,12 @@ pub struct RepoFrontierRoute {
|
|||
schema = "RepoFrontierHandsAuthority"
|
||||
)]
|
||||
pub struct RepoFrontierHandsAuthority {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub authority_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub route_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub model_projection_digest: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub model_source_documents: Vec<crate::EpiphanyMindDocumentVersion>,
|
||||
#[cultcache(key = 5)]
|
||||
pub frontier_item_id: String,
|
||||
#[cultcache(key = 6)]
|
||||
pub frontier_item_hash: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub hands_intent_id: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub hands_review_id: String,
|
||||
#[cultcache(key = 9)]
|
||||
#[cultcache(key = 4)]
|
||||
pub substrate_grant_receipt_id: String,
|
||||
#[cultcache(key = 10)]
|
||||
pub requested_paths: Vec<String>,
|
||||
#[cultcache(key = 11)]
|
||||
pub granted_at: String,
|
||||
#[cultcache(key = 12)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,7 +214,6 @@ pub struct RepositoryBodyManifest {
|
|||
pub entries: Vec<RepositoryBodyManifestEntry>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "epiphany.repository_body.head", schema = "RepositoryBodyHead")]
|
||||
pub struct RepositoryBodyHead {
|
||||
|
|
@ -331,7 +330,7 @@ pub fn admit_repository_body_observation(
|
|||
BODY_OBSERVATION_TYPE,
|
||||
&basis.observation_id,
|
||||
)
|
||||
.ok_or_else(|| anyhow!("repository Body observation source is absent"))?;
|
||||
.ok_or_else(|| anyhow!("repository Body observation source is absent"))?;
|
||||
let source_observation =
|
||||
crate::EpiphanyMindDocumentVersion::from_envelope("epiphany-repository-body", source)?;
|
||||
let document = crate::EpiphanyMindRepositoryBodyObservationDocument {
|
||||
|
|
@ -367,12 +366,6 @@ pub fn admit_repository_body_observation(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn current_mind_repository_body_observation(
|
||||
runtime_store: &Path,
|
||||
) -> Result<Option<RepositoryBodyObservationBasis>> {
|
||||
Ok(crate::assemble_mind_view(runtime_store)?.repository_body_observation)
|
||||
}
|
||||
|
||||
/// Loads the authenticated current Repository Body basis already persisted for
|
||||
/// a runtime without observing the worktree or advancing the Body generation.
|
||||
pub fn load_current_runtime_repository_body_basis(
|
||||
|
|
@ -986,7 +979,7 @@ fn sanitized_git_command() -> Command {
|
|||
sanitize_git_command(&mut command);
|
||||
command
|
||||
}
|
||||
fn repository_git_command(repo: &Path) -> Result<Command> {
|
||||
pub(crate) fn repository_git_command(repo: &Path) -> Result<Command> {
|
||||
let canonical = std::fs::canonicalize(repo)
|
||||
.with_context(|| format!("failed to canonicalize Git repository {}", repo.display()))?;
|
||||
let mut command = sanitized_git_command();
|
||||
|
|
@ -1362,7 +1355,7 @@ fn validate_runtime_binding_entries(
|
|||
EpiphanyRuntimeSwarmBinding::TYPE,
|
||||
RUNTIME_SWARM_BINDING_KEY,
|
||||
)
|
||||
.ok_or_else(|| anyhow!("runtime store has no immutable swarm binding"))?,
|
||||
.ok_or_else(|| anyhow!("runtime store has no immutable swarm binding"))?,
|
||||
)?;
|
||||
if binding.schema_version != RUNTIME_SWARM_BINDING_SCHEMA_VERSION
|
||||
|| binding.binding_id != RUNTIME_SWARM_BINDING_KEY
|
||||
|
|
@ -1586,7 +1579,7 @@ mod tests {
|
|||
run(d.path(), &["add", "."])?;
|
||||
let observed = observe_runtime_repository_body_basis(&runtime)?;
|
||||
assert_eq!(
|
||||
current_mind_repository_body_observation(&runtime)?,
|
||||
crate::assemble_mind_view(&runtime)?.repository_body_observation,
|
||||
Some(observed.clone())
|
||||
);
|
||||
let mut mind = crate::runtime_spine_cache(&runtime)?;
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ pub struct ResidentProviderReadiness {
|
|||
}
|
||||
|
||||
impl ResidentProviderReadiness {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
fn validate(&self) -> Result<()> {
|
||||
if self.schema_version != RESIDENT_PROVIDER_READINESS_SCHEMA_VERSION {
|
||||
bail!("resident provider readiness schema is unsupported");
|
||||
}
|
||||
|
|
@ -295,7 +295,7 @@ pub fn publish_resident_provider_readiness(
|
|||
Ok(readiness)
|
||||
}
|
||||
|
||||
pub fn load_resident_provider_readiness(store: &Path) -> Result<Option<ResidentProviderReadiness>> {
|
||||
fn load_resident_provider_readiness(store: &Path) -> Result<Option<ResidentProviderReadiness>> {
|
||||
Ok(readiness_snapshot(store)?.1)
|
||||
}
|
||||
|
||||
|
|
@ -520,72 +520,6 @@ fn classify_active_lease_observation(
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResidentProviderHealth {
|
||||
pub terminal_current: usize,
|
||||
pub warming: usize,
|
||||
pub contradictions: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn authenticate_resident_provider(
|
||||
release: &crate::EpiphanyPackagedReleaseEntry,
|
||||
witness: &str,
|
||||
resident_store: &Path,
|
||||
freshness_millis: u64,
|
||||
) -> ResidentProviderHealth {
|
||||
let mut result = ResidentProviderHealth {
|
||||
terminal_current: 0,
|
||||
warming: 0,
|
||||
contradictions: Vec::new(),
|
||||
};
|
||||
for (owner, store, role) in [("resident-self", resident_store, "swarm")] {
|
||||
let executable = epiphany_packaged_release_binary_path(release, role).ok();
|
||||
let value = match load_resident_provider_readiness(store) {
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => {
|
||||
result.warming += 1;
|
||||
continue;
|
||||
}
|
||||
Err(error) => {
|
||||
result
|
||||
.contradictions
|
||||
.push(format!("{owner} provider readiness is invalid: {error:#}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let role_owner_present = match resident_process_singleton_is_owned(store) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
result.contradictions.push(format!(
|
||||
"{owner} provider singleton observation failed: {error:#}"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if provider_matches_authority(
|
||||
&value,
|
||||
owner,
|
||||
&release.runtime_id,
|
||||
&release.release_id,
|
||||
witness,
|
||||
Some(&release.source_commit_sha),
|
||||
executable.as_deref(),
|
||||
current_time_millis(),
|
||||
freshness_millis,
|
||||
role_owner_present,
|
||||
) {
|
||||
result.terminal_current += 1;
|
||||
} else if value.runtime_id == release.runtime_id && value.release_id == release.release_id {
|
||||
result.warming += 1;
|
||||
} else {
|
||||
result
|
||||
.contradictions
|
||||
.push(format!("{owner} provider readiness names another release"));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn provider_is_fresh(
|
||||
store: &Path,
|
||||
owner: &str,
|
||||
|
|
@ -684,34 +618,6 @@ fn provider_authority_failures(
|
|||
failures
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn provider_matches_authority(
|
||||
value: &ResidentProviderReadiness,
|
||||
owner: &str,
|
||||
runtime_id: &str,
|
||||
release_id: &str,
|
||||
witness: &str,
|
||||
source_commit: Option<&str>,
|
||||
executable: Option<&Path>,
|
||||
now_millis: u64,
|
||||
freshness_millis: u64,
|
||||
role_owner_present: bool,
|
||||
) -> bool {
|
||||
provider_authority_failures(
|
||||
value,
|
||||
owner,
|
||||
runtime_id,
|
||||
release_id,
|
||||
witness,
|
||||
source_commit,
|
||||
executable,
|
||||
now_millis,
|
||||
freshness_millis,
|
||||
role_owner_present,
|
||||
)
|
||||
.is_empty()
|
||||
}
|
||||
|
||||
fn canonical_or_absolute(path: &Path) -> PathBuf {
|
||||
fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
|
||||
}
|
||||
|
|
@ -728,20 +634,12 @@ fn directory_ready(path: &Path) -> bool {
|
|||
fs::metadata(path).is_ok_and(|value| value.is_dir() && !value.permissions().readonly())
|
||||
}
|
||||
|
||||
fn codex_credentials_ready(codex_home: &Path) -> bool {
|
||||
directory_ready(codex_home) && credential_file_ready(&codex_home.join("auth.json"))
|
||||
}
|
||||
|
||||
fn model_provider_credential_ready(policy: &ResidentSelfPolicy) -> bool {
|
||||
match policy.model_provider.as_str() {
|
||||
"openai-codex" | "openai" => codex_credentials_ready(&policy.codex_home),
|
||||
"openrouter" => policy
|
||||
"openai-codex" | "openai" | "openrouter" => policy
|
||||
.provider_credential_path
|
||||
.as_deref()
|
||||
.is_some_and(|path| {
|
||||
fs::metadata(path).is_ok_and(|metadata| metadata.is_file())
|
||||
&& fs::File::open(path).is_ok()
|
||||
}),
|
||||
.is_some_and(credential_file_ready),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
|
@ -845,60 +743,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_wrong_release_and_absent_role_owner_cannot_be_ready() {
|
||||
let value = provider();
|
||||
let executable = std::env::current_exe().unwrap();
|
||||
assert!(provider_matches_authority(
|
||||
&value,
|
||||
"resident-self",
|
||||
"ygg",
|
||||
"release-a",
|
||||
&value.release_witness_sha256,
|
||||
Some("commit-a"),
|
||||
Some(&executable),
|
||||
1_050,
|
||||
100,
|
||||
true
|
||||
));
|
||||
assert!(!provider_matches_authority(
|
||||
&value,
|
||||
"resident-self",
|
||||
"ygg",
|
||||
"release-b",
|
||||
&value.release_witness_sha256,
|
||||
Some("commit-a"),
|
||||
Some(&executable),
|
||||
1_050,
|
||||
100,
|
||||
true
|
||||
));
|
||||
assert!(!provider_matches_authority(
|
||||
&value,
|
||||
"resident-self",
|
||||
"ygg",
|
||||
"release-a",
|
||||
&value.release_witness_sha256,
|
||||
Some("commit-a"),
|
||||
Some(&executable),
|
||||
1_101,
|
||||
100,
|
||||
true
|
||||
));
|
||||
assert!(!provider_matches_authority(
|
||||
&value,
|
||||
"resident-self",
|
||||
"ygg",
|
||||
"release-a",
|
||||
&value.release_witness_sha256,
|
||||
Some("commit-a"),
|
||||
Some(&executable),
|
||||
1_050,
|
||||
100,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn role_singleton_probe_observes_only_a_live_owner() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
|
|
@ -944,7 +788,7 @@ mod tests {
|
|||
let store = temp.path().join("mixed.cc");
|
||||
let mut mixed = CultCache::new();
|
||||
mixed.register_entry_type::<ForeignOwnerEntry>()?;
|
||||
mixed.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store));
|
||||
mixed.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store))?;
|
||||
let foreign = ForeignOwnerEntry {
|
||||
value: "must survive readiness CAS".into(),
|
||||
};
|
||||
|
|
@ -972,30 +816,6 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn unix_credentials_reject_group_or_other_access() -> Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let temp = tempfile::tempdir()?;
|
||||
let auth = temp.path().join("auth.json");
|
||||
fs::write(&auth, b"{}")?;
|
||||
fs::set_permissions(&auth, fs::Permissions::from_mode(0o400))?;
|
||||
assert!(!codex_credentials_ready(temp.path()));
|
||||
fs::set_permissions(&auth, fs::Permissions::from_mode(0o644))?;
|
||||
assert!(!codex_credentials_ready(temp.path()));
|
||||
fs::set_permissions(&auth, fs::Permissions::from_mode(0o600))?;
|
||||
assert!(codex_credentials_ready(temp.path()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_credentials_file_cannot_impersonate_canonical_auth() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
fs::write(temp.path().join("credentials.json"), b"{}")?;
|
||||
assert!(!codex_credentials_ready(temp.path()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readiness_uses_mounted_cognitive_identity_for_projection_and_brake() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
|
|
@ -1026,7 +846,7 @@ mod tests {
|
|||
runtime_store,
|
||||
local_verse_store,
|
||||
artifact_root: shared.join("artifacts"),
|
||||
codex_home: shared.join("codex-home"),
|
||||
model_connector_endpoint: "127.0.0.1:17891".parse().unwrap(),
|
||||
mcp_config: shared.join("mcp.toml"),
|
||||
model_provider: "test".into(),
|
||||
model: "test-model".into(),
|
||||
|
|
@ -1116,79 +936,22 @@ mod tests {
|
|||
let mut value = provider();
|
||||
value.observed_at_millis = 1_500;
|
||||
let executable = PathBuf::from(&value.process_executable_path);
|
||||
assert!(provider_matches_authority(
|
||||
&value,
|
||||
"resident-self",
|
||||
"ygg",
|
||||
"release-a",
|
||||
&value.release_witness_sha256,
|
||||
Some("commit-a"),
|
||||
Some(&executable),
|
||||
1_000,
|
||||
100,
|
||||
true,
|
||||
));
|
||||
let failures = |value: &ResidentProviderReadiness| {
|
||||
provider_authority_failures(
|
||||
value,
|
||||
"resident-self",
|
||||
"ygg",
|
||||
"release-a",
|
||||
&value.release_witness_sha256,
|
||||
Some("commit-a"),
|
||||
Some(&executable),
|
||||
1_000,
|
||||
100,
|
||||
true,
|
||||
)
|
||||
};
|
||||
assert!(failures(&value).is_empty());
|
||||
value.observed_at_millis = 2_001;
|
||||
assert!(!provider_matches_authority(
|
||||
&value,
|
||||
"resident-self",
|
||||
"ygg",
|
||||
"release-a",
|
||||
&value.release_witness_sha256,
|
||||
Some("commit-a"),
|
||||
Some(&executable),
|
||||
1_000,
|
||||
100,
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregate_health_warms_on_missing_and_rejects_wrong_release() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let resident = temp.path().join("resident.cc");
|
||||
let executable = std::env::current_exe()?;
|
||||
let binary = |role: &str| crate::EpiphanyPackagedReleaseBinary {
|
||||
role: role.into(),
|
||||
file_name: executable.file_name().unwrap().to_string_lossy().into(),
|
||||
canonical_path: executable.display().to_string(),
|
||||
sha256: format!("sha256-{}", "b".repeat(64)),
|
||||
byte_len: 1,
|
||||
};
|
||||
let release = crate::EpiphanyPackagedReleaseEntry {
|
||||
schema_version: crate::EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION.into(),
|
||||
release_id: "release-a".into(),
|
||||
runtime_id: "ygg".into(),
|
||||
source_commit_sha: "commit-a".into(),
|
||||
target_triple: "test".into(),
|
||||
cargo_profile: "release".into(),
|
||||
toolchain_fingerprint: "toolchain".into(),
|
||||
created_at_utc: "now".into(),
|
||||
package_root: temp.path().display().to_string(),
|
||||
binaries: vec![binary("swarm")],
|
||||
private_state_exposed: false,
|
||||
};
|
||||
let witness = format!("sha256-{}", "a".repeat(64));
|
||||
let missing = authenticate_resident_provider(&release, &witness, &resident, 10);
|
||||
assert_eq!(missing.warming, 1);
|
||||
let process = crate::capture_process_instance(std::process::id())?;
|
||||
let mut wrong = provider();
|
||||
wrong.runtime_id = "ygg".into();
|
||||
wrong.release_id = "other-release".into();
|
||||
wrong.release_witness_sha256 = witness.clone();
|
||||
wrong.source_commit = "commit-a".into();
|
||||
wrong.observed_at_millis = 10;
|
||||
wrong.process_id = process.process_id;
|
||||
wrong.process_creation_token = process.creation_token;
|
||||
wrong.process_executable_path = process.executable_path.display().to_string();
|
||||
publish_resident_provider_readiness(&resident, wrong)?;
|
||||
let rejected = authenticate_resident_provider(&release, &witness, &resident, 10);
|
||||
assert!(
|
||||
rejected
|
||||
.contradictions
|
||||
.iter()
|
||||
.any(|reason| reason.contains("another release"))
|
||||
);
|
||||
Ok(())
|
||||
assert_eq!(failures(&value), vec!["future-observation"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ use cultcache_rs::{
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const RESIDENT_SELF_STATE_KEY: &str = "resident-self";
|
||||
pub const RESIDENT_SELF_STATE_SCHEMA_VERSION: &str = "epiphany.resident_self.state.v1";
|
||||
pub const RESIDENT_SELF_RUNTIME_RECEIPT_SCHEMA_VERSION: &str =
|
||||
"epiphany.resident_self.runtime_receipt.v0";
|
||||
pub const RESIDENT_SELF_STATE_SCHEMA_VERSION: &str = "epiphany.resident_self.state.v3";
|
||||
pub const RESIDENT_SELF_PRESSURE_SCHEMA_VERSION: &str = "epiphany.resident_self.pressure.v0";
|
||||
pub const RESIDENT_SELF_COORDINATOR_CONTINUATION_PRESSURE_KIND: &str =
|
||||
"coordinator-internal-continuation";
|
||||
|
|
@ -26,7 +25,7 @@ pub const RESIDENT_SELF_TERMINAL_RECEIPT_SCHEMA_VERSION: &str =
|
|||
"epiphany.resident_self.terminal_receipt.v1";
|
||||
pub const RESIDENT_SELF_CHILD_CLAIM_SCHEMA_VERSION: &str = "epiphany.resident_self.child_claim.v0";
|
||||
pub const RESIDENT_SELF_RETENTION_HEAD_SCHEMA_VERSION: &str =
|
||||
"epiphany.resident_self.retention_head.v0";
|
||||
"epiphany.resident_self.retention_head.v1";
|
||||
pub const RESIDENT_SELF_RETENTION_HEAD_KEY: &str = "resident-self-retention";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
|
|
@ -241,7 +240,7 @@ pub struct ResidentSelfChildClaim {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.resident_self.retention_head.v0",
|
||||
type = "epiphany.resident_self.retention_head.v1",
|
||||
schema = "ResidentSelfRetentionHead"
|
||||
)]
|
||||
pub struct ResidentSelfRetentionHead {
|
||||
|
|
@ -255,9 +254,7 @@ pub struct ResidentSelfRetentionHead {
|
|||
pub retired_envelope_count: u64,
|
||||
#[cultcache(key = 4)]
|
||||
pub retired_chain_digest: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub retained_at_millis: u64,
|
||||
#[cultcache(key = 6, default)]
|
||||
#[cultcache(key = 5, default)]
|
||||
pub private_state_exposed: bool,
|
||||
}
|
||||
|
||||
|
|
@ -277,7 +274,7 @@ pub struct ResidentSelfPolicy {
|
|||
pub runtime_store: PathBuf,
|
||||
pub local_verse_store: PathBuf,
|
||||
pub artifact_root: PathBuf,
|
||||
pub codex_home: PathBuf,
|
||||
pub model_connector_endpoint: SocketAddr,
|
||||
pub mcp_config: PathBuf,
|
||||
pub model_provider: String,
|
||||
pub model: String,
|
||||
|
|
@ -305,7 +302,6 @@ impl ResidentSelfPolicy {
|
|||
("runtime store", &self.runtime_store),
|
||||
("local Verse store", &self.local_verse_store),
|
||||
("artifact root", &self.artifact_root),
|
||||
("Codex home", &self.codex_home),
|
||||
("MCP config", &self.mcp_config),
|
||||
("release store", &self.release_store),
|
||||
] {
|
||||
|
|
@ -324,6 +320,13 @@ impl ResidentSelfPolicy {
|
|||
path.display()
|
||||
));
|
||||
}
|
||||
if !self.model_connector_endpoint.ip().is_loopback()
|
||||
|| self.model_connector_endpoint.port() == 0
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"resident Self model connector must use a nonzero loopback endpoint"
|
||||
));
|
||||
}
|
||||
if self.model_provider.trim().is_empty()
|
||||
|| self.model.trim().is_empty()
|
||||
|| self.max_steps == 0
|
||||
|
|
@ -530,32 +533,6 @@ impl Default for ResidentSelfState {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.resident_self.runtime_receipt.v0",
|
||||
schema = "ResidentSelfRuntimeReceipt"
|
||||
)]
|
||||
pub struct ResidentSelfRuntimeReceipt {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub receipt_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub occurred_at_millis: u64,
|
||||
#[cultcache(key = 3)]
|
||||
pub status: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub reason: String,
|
||||
#[cultcache(key = 5, default)]
|
||||
pub turn_id: Option<String>,
|
||||
#[cultcache(key = 6, default)]
|
||||
pub coordinator_receipt_id: Option<String>,
|
||||
#[cultcache(key = 7, default)]
|
||||
pub process_id: Option<u32>,
|
||||
#[cultcache(key = 8, default)]
|
||||
pub private_state_exposed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ChildObservation {
|
||||
Running,
|
||||
|
|
@ -647,8 +624,8 @@ pub fn coordinator_argv(
|
|||
turn_id.into(),
|
||||
"--cwd".into(),
|
||||
policy.workspace.display().to_string(),
|
||||
"--codex-home".into(),
|
||||
policy.codex_home.display().to_string(),
|
||||
"--connector-endpoint".into(),
|
||||
policy.model_connector_endpoint.to_string(),
|
||||
"--mcp-config".into(),
|
||||
policy.mcp_config.display().to_string(),
|
||||
"--artifact-dir".into(),
|
||||
|
|
@ -785,7 +762,6 @@ pub fn resident_prepared_launch_thread_id(prepared: &ResidentSelfPreparedLaunch)
|
|||
fn state_cache(path: &Path) -> Result<CultCache> {
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<ResidentSelfState>()?;
|
||||
cache.register_entry_type::<ResidentSelfRuntimeReceipt>()?;
|
||||
cache.register_entry_type::<ResidentSelfPressure>()?;
|
||||
cache.register_entry_type::<ResidentSelfGrant>()?;
|
||||
cache.register_entry_type::<ResidentSelfTerminalReceipt>()?;
|
||||
|
|
@ -795,14 +771,15 @@ fn state_cache(path: &Path) -> Result<CultCache> {
|
|||
for envelope in SingleFileMessagePackBackingStore::new(path).pull_all()? {
|
||||
if matches!(
|
||||
envelope.r#type.as_str(),
|
||||
"epiphany.resident_self.heartbeat_grant.v0" | "epiphany.resident_self.terminal_ack.v0"
|
||||
"epiphany.resident_self.heartbeat_grant.v0"
|
||||
| "epiphany.resident_self.terminal_ack.v0"
|
||||
| "epiphany.resident_self.retention_head.v0"
|
||||
) {
|
||||
return Err(anyhow!(
|
||||
"resident Self store contains a pre-cut heartbeat lifecycle document"
|
||||
));
|
||||
}
|
||||
let owned = envelope.r#type == ResidentSelfState::TYPE
|
||||
|| envelope.r#type == ResidentSelfRuntimeReceipt::TYPE
|
||||
|| envelope.r#type == ResidentSelfPressure::TYPE
|
||||
|| envelope.r#type == ResidentSelfGrant::TYPE
|
||||
|| envelope.r#type == ResidentSelfTerminalReceipt::TYPE
|
||||
|
|
@ -820,25 +797,22 @@ fn state_cache(path: &Path) -> Result<CultCache> {
|
|||
}
|
||||
match envelope.r#type.as_str() {
|
||||
ResidentSelfState::TYPE => {
|
||||
cache.load_envelope::<ResidentSelfState>(envelope)?;
|
||||
}
|
||||
ResidentSelfRuntimeReceipt::TYPE => {
|
||||
cache.load_envelope::<ResidentSelfRuntimeReceipt>(envelope)?;
|
||||
cache.put_envelope::<ResidentSelfState>(envelope)?;
|
||||
}
|
||||
ResidentSelfPressure::TYPE => {
|
||||
cache.load_envelope::<ResidentSelfPressure>(envelope)?;
|
||||
cache.put_envelope::<ResidentSelfPressure>(envelope)?;
|
||||
}
|
||||
ResidentSelfGrant::TYPE => {
|
||||
cache.load_envelope::<ResidentSelfGrant>(envelope)?;
|
||||
cache.put_envelope::<ResidentSelfGrant>(envelope)?;
|
||||
}
|
||||
ResidentSelfTerminalReceipt::TYPE => {
|
||||
cache.load_envelope::<ResidentSelfTerminalReceipt>(envelope)?;
|
||||
cache.put_envelope::<ResidentSelfTerminalReceipt>(envelope)?;
|
||||
}
|
||||
ResidentSelfChildClaim::TYPE => {
|
||||
cache.load_envelope::<ResidentSelfChildClaim>(envelope)?;
|
||||
cache.put_envelope::<ResidentSelfChildClaim>(envelope)?;
|
||||
}
|
||||
ResidentSelfRetentionHead::TYPE => {
|
||||
cache.load_envelope::<ResidentSelfRetentionHead>(envelope)?;
|
||||
cache.put_envelope::<ResidentSelfRetentionHead>(envelope)?;
|
||||
}
|
||||
_ => unreachable!("owned resident Self type was matched above"),
|
||||
};
|
||||
|
|
@ -995,7 +969,6 @@ pub(crate) fn resident_self_current_work_action(
|
|||
let projection_digest = current_work.projection_digest()?;
|
||||
let decision = crate::recommend_coordinator_action(crate::EpiphanyCoordinatorInput {
|
||||
mind_present: true,
|
||||
crrc_action: crate::EpiphanyCrrcAction::Continue,
|
||||
current_work,
|
||||
});
|
||||
if !matches!(
|
||||
|
|
@ -1241,8 +1214,16 @@ pub fn verify_resident_self_grant_fulfillment(
|
|||
let Some(request) = request else {
|
||||
return Ok(ResidentSelfGrantFulfillment::Fulfilled);
|
||||
};
|
||||
let mut runtime = crate::runtime_spine_cache(runtime_store)?;
|
||||
runtime.pull_all_backing_stores()?;
|
||||
Ok(
|
||||
if crate::runtime_typed_request_fulfillment(runtime_store, request)?.is_some() {
|
||||
if crate::runtime_spine::runtime_typed_request_fulfillment_from_cache(
|
||||
runtime_store,
|
||||
&runtime,
|
||||
request,
|
||||
)?
|
||||
.is_some()
|
||||
{
|
||||
ResidentSelfGrantFulfillment::Fulfilled
|
||||
} else {
|
||||
ResidentSelfGrantFulfillment::Pending
|
||||
|
|
@ -1349,11 +1330,12 @@ pub fn recover_dead_resident_typed_worker(
|
|||
};
|
||||
let mut cache = crate::runtime_spine_cache(runtime_store)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let mut launches = cache
|
||||
.get_all::<crate::EpiphanyRuntimeWorkerLaunchRequest>()?
|
||||
.into_iter()
|
||||
.filter(|launch| request.matches_launch(launch))
|
||||
.collect::<Vec<_>>();
|
||||
let mut launches = Vec::new();
|
||||
for launch in cache.get_all::<crate::EpiphanyRuntimeWorkerLaunchRequest>()? {
|
||||
if request.matches_launch(&launch)? {
|
||||
launches.push(launch);
|
||||
}
|
||||
}
|
||||
launches.sort_by(|a, b| a.job_id.cmp(&b.job_id));
|
||||
let mut retryable = Vec::new();
|
||||
for launch in &launches {
|
||||
|
|
@ -1838,7 +1820,6 @@ pub fn recover_receipt_free_dead_coordinator_session(
|
|||
return Ok(None);
|
||||
}
|
||||
let recovery = crate::EpiphanyCoordinatorDeathRecovery {
|
||||
schema_version: crate::runtime_spine::COORDINATOR_DEATH_RECOVERY_SCHEMA_VERSION.into(),
|
||||
recovery_id: format!("coordinator-death-recovery-{session_id}"),
|
||||
session_id,
|
||||
thread_id: lease.turn_id.clone(),
|
||||
|
|
@ -1888,7 +1869,7 @@ fn digest_parts(parts: impl IntoIterator<Item = impl AsRef<[u8]>>) -> String {
|
|||
format!("sha256:{:x}", hash.finalize())
|
||||
}
|
||||
|
||||
pub fn resident_self_policy_digest(policy: &ResidentSelfPolicy) -> String {
|
||||
fn resident_self_policy_digest(policy: &ResidentSelfPolicy) -> String {
|
||||
digest_parts([
|
||||
policy.workspace.display().to_string(),
|
||||
policy.coordinator_bin.display().to_string(),
|
||||
|
|
@ -2333,20 +2314,6 @@ pub fn validate_resident_self_prepared_objective(
|
|||
))
|
||||
}
|
||||
|
||||
pub fn validate_resident_self_coordinator_receipt(
|
||||
lease: &ResidentSelfTurnLease,
|
||||
coordinator: &crate::EpiphanyCoordinatorRunReceipt,
|
||||
) -> Result<()> {
|
||||
validate_resident_self_coordinator_receipt_binding(lease, coordinator)?;
|
||||
if !matches!(
|
||||
coordinator.status.as_str(),
|
||||
"planned" | "needsReview" | "completed"
|
||||
) {
|
||||
return Err(anyhow!("coordinator terminal receipt is not successful"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_resident_self_coordinator_receipt_binding(
|
||||
lease: &ResidentSelfTurnLease,
|
||||
coordinator: &crate::EpiphanyCoordinatorRunReceipt,
|
||||
|
|
@ -2372,7 +2339,7 @@ pub fn validate_resident_self_coordinator_receipt_binding(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn complete_resident_self_turn_after_death(
|
||||
fn complete_resident_self_turn_after_death(
|
||||
path: &Path,
|
||||
lease: &ResidentSelfTurnLease,
|
||||
recovery: &crate::EpiphanyCoordinatorDeathRecovery,
|
||||
|
|
@ -2566,17 +2533,9 @@ pub fn cancel_resident_self_turn(
|
|||
Ok(terminal)
|
||||
}
|
||||
|
||||
pub fn resident_self_terminal_receipts(path: &Path) -> Result<Vec<ResidentSelfTerminalReceipt>> {
|
||||
Ok(state_cache(path)?
|
||||
.get_all::<ResidentSelfTerminalReceipt>()?
|
||||
.into_iter()
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn retain_resident_self_lifecycles(
|
||||
path: &Path,
|
||||
retain_closed: usize,
|
||||
now_millis: u64,
|
||||
) -> Result<Option<ResidentSelfRetentionHead>> {
|
||||
let cache = state_cache(path)?;
|
||||
let state = cache
|
||||
|
|
@ -2718,7 +2677,6 @@ pub fn retain_resident_self_lifecycles(
|
|||
head.retired_envelope_count + deletions.len() as u64
|
||||
}),
|
||||
retired_chain_digest: digest_parts(digest_inputs),
|
||||
retained_at_millis: now_millis,
|
||||
private_state_exposed: false,
|
||||
};
|
||||
let (replacement, _) = cache.prepare_entry(RESIDENT_SELF_RETENTION_HEAD_KEY, &head)?;
|
||||
|
|
@ -2756,6 +2714,61 @@ mod pressure_replay_tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn obsolete_state_epoch_refuses_without_mutation() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("resident.cc");
|
||||
let cache = state_cache(&store)?;
|
||||
let mut state = ResidentSelfState::default();
|
||||
state.schema_version = "epiphany.resident_self.state.v2".into();
|
||||
SingleFileMessagePackBackingStore::new(&store)
|
||||
.push(&cache.prepare_entry(RESIDENT_SELF_STATE_KEY, &state)?.0)?;
|
||||
let before = std::fs::read(&store)?;
|
||||
let error = load_resident_self_state(&store).unwrap_err();
|
||||
assert!(error.to_string().contains("obsolete writable state epoch"));
|
||||
assert_eq!(std::fs::read(&store)?, before);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_cache_reads_without_writing_the_store() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("resident.cc");
|
||||
let cache = state_cache(&store)?;
|
||||
SingleFileMessagePackBackingStore::new(&store).push(
|
||||
&cache
|
||||
.prepare_entry(RESIDENT_SELF_STATE_KEY, &ResidentSelfState::default())?
|
||||
.0,
|
||||
)?;
|
||||
let past = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
|
||||
std::fs::File::options()
|
||||
.write(true)
|
||||
.open(&store)?
|
||||
.set_modified(past)?;
|
||||
let listing = || -> Result<Vec<(std::ffi::OsString, u64, std::time::SystemTime)>> {
|
||||
let mut entries = std::fs::read_dir(temp.path())?
|
||||
.map(|entry| {
|
||||
let entry = entry?;
|
||||
let metadata = entry.metadata()?;
|
||||
Ok((entry.file_name(), metadata.len(), metadata.modified()?))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
entries.sort();
|
||||
Ok(entries)
|
||||
};
|
||||
let before = listing()?;
|
||||
let names = before.iter().map(|entry| entry.0.clone()).collect::<Vec<_>>();
|
||||
assert_eq!(names, ["resident.cc", "resident.cc.lock"]);
|
||||
let reopened = state_cache(&store)?;
|
||||
assert!(
|
||||
reopened
|
||||
.get::<ResidentSelfState>(RESIDENT_SELF_STATE_KEY)?
|
||||
.is_some()
|
||||
);
|
||||
assert_eq!(listing()?, before, "a read attached or wrote a store");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_operator_pressure_replay_preserves_the_first_document() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
|
|
@ -2796,7 +2809,7 @@ mod coordinator_launch_contract_tests {
|
|||
runtime_store,
|
||||
local_verse_store: root.join("local-verse.cc"),
|
||||
artifact_root: root.join("artifacts"),
|
||||
codex_home: root.join("codex-home"),
|
||||
model_connector_endpoint: "127.0.0.1:17891".parse().unwrap(),
|
||||
mcp_config: root.join("mcp.toml"),
|
||||
model_provider: "openrouter".into(),
|
||||
model: "stealth/ox-alpha".into(),
|
||||
|
|
@ -2825,7 +2838,7 @@ mod coordinator_launch_contract_tests {
|
|||
runtime_store: PathBuf::from("/epiphany/state/runtime.cc"),
|
||||
local_verse_store: PathBuf::from("/epiphany/state/local-verse.cc"),
|
||||
artifact_root: PathBuf::from("/epiphany/artifacts"),
|
||||
codex_home: PathBuf::from("/epiphany/codex-home"),
|
||||
model_connector_endpoint: "127.0.0.1:17891".parse().unwrap(),
|
||||
mcp_config: PathBuf::from("/epiphany/mcp.toml"),
|
||||
model_provider: "openrouter".into(),
|
||||
model: "stealth/ox-alpha".into(),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -74,21 +74,6 @@ impl RuntimeSpineBackingStore {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replace_and_append_if_snapshot_unchanged(
|
||||
&self,
|
||||
expected_snapshot: &[CultCacheEnvelope],
|
||||
replacements: Vec<CultCacheEnvelope>,
|
||||
) -> Result<bool> {
|
||||
match self.selected()? {
|
||||
SelectedBackingStore::Snapshot(store) => {
|
||||
store.replace_and_append_if_snapshot_unchanged(expected_snapshot, replacements)
|
||||
}
|
||||
SelectedBackingStore::Keyed(store) => {
|
||||
store.replace_and_append_if_snapshot_unchanged(expected_snapshot, replacements)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheBackingStore for RuntimeSpineBackingStore {
|
||||
|
|
@ -123,7 +108,11 @@ impl CacheBackingStore for RuntimeSpineBackingStore {
|
|||
|
||||
pub(crate) fn runtime_spine_backing_store(path: &Path) -> Result<RuntimeSpineBackingStore> {
|
||||
match path.extension().and_then(|extension| extension.to_str()) {
|
||||
Some("cc" | "msgpack" | "redb") => Ok(RuntimeSpineBackingStore::new(path)),
|
||||
Some("cc" | "msgpack" | "redb") => {
|
||||
#[cfg(test)]
|
||||
record_backing_store_resolution(path);
|
||||
Ok(RuntimeSpineBackingStore::new(path))
|
||||
}
|
||||
extension => Err(anyhow!(
|
||||
"runtime spine store must use an explicit .cc, .msgpack, or .redb extension; got {:?}",
|
||||
extension
|
||||
|
|
@ -131,6 +120,39 @@ pub(crate) fn runtime_spine_backing_store(path: &Path) -> Result<RuntimeSpineBac
|
|||
}
|
||||
}
|
||||
|
||||
/// Every resolution this process has made, in order, recorded only under test.
|
||||
///
|
||||
/// A handle carries nothing but its path today, so a second handle to the same
|
||||
/// path is behaviourally invisible and no assertion about files or contents can
|
||||
/// see it. The `.redb` arm makes that difference real: redb permits one
|
||||
/// writable handle per path. This log is the probe that lets a caller pin
|
||||
/// "resolved once and reused" rather than the much weaker "only one store was
|
||||
/// written".
|
||||
#[cfg(test)]
|
||||
static RESOLVED_BACKING_STORES: std::sync::Mutex<Vec<PathBuf>> =
|
||||
std::sync::Mutex::new(Vec::new());
|
||||
|
||||
#[cfg(test)]
|
||||
fn record_backing_store_resolution(path: &Path) {
|
||||
RESOLVED_BACKING_STORES
|
||||
.lock()
|
||||
.expect("the backing-store resolution log is never poisoned")
|
||||
.push(path.to_path_buf());
|
||||
}
|
||||
|
||||
/// How many times this path has been resolved into a handle. Tests run in
|
||||
/// parallel, so a caller compares this around its own call against its own
|
||||
/// temporary path rather than reading a global total.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn backing_store_resolutions(path: &Path) -> usize {
|
||||
RESOLVED_BACKING_STORES
|
||||
.lock()
|
||||
.expect("the backing-store resolution log is never poisoned")
|
||||
.iter()
|
||||
.filter(|resolved| resolved.as_path() == path)
|
||||
.count()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -84,77 +84,50 @@ impl<'a> RuntimeTypedRequestRef<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn matches_launch(self, launch: &crate::EpiphanyRuntimeWorkerLaunchRequest) -> bool {
|
||||
match self {
|
||||
Self::ProposalModeling(id) => {
|
||||
launch.proposal_modeling_request_id.as_deref() == Some(id)
|
||||
}
|
||||
Self::FrontierVerdictModeling(id) => {
|
||||
launch.repo_frontier_modeling_request_id.as_deref() == Some(id)
|
||||
}
|
||||
Self::FrontierResearch(id) => {
|
||||
launch.repo_frontier_research_request_id.as_deref() == Some(id)
|
||||
}
|
||||
Self::FrontierVerification(id) => {
|
||||
launch.repo_frontier_verification_request_id.as_deref() == Some(id)
|
||||
}
|
||||
Self::ImaginationConsideration(id) => {
|
||||
launch.imagination_consideration_request_id.as_deref() == Some(id)
|
||||
}
|
||||
Self::AdmittedModelDirection(id) => {
|
||||
launch
|
||||
.admitted_model_direction_consideration_request_id
|
||||
.as_deref()
|
||||
== Some(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn matches_result(self, result: &crate::EpiphanyRuntimeRoleWorkerResult) -> bool {
|
||||
match self {
|
||||
Self::ProposalModeling(id) => {
|
||||
result.proposal_modeling_request_id.as_deref() == Some(id)
|
||||
}
|
||||
Self::FrontierVerdictModeling(id) => {
|
||||
result.repo_frontier_modeling_request_id.as_deref() == Some(id)
|
||||
}
|
||||
Self::FrontierResearch(id) => {
|
||||
result.repo_frontier_research_request_id.as_deref() == Some(id)
|
||||
}
|
||||
Self::FrontierVerification(id) => result.verification_request_id.as_deref() == Some(id),
|
||||
Self::ImaginationConsideration(id) => {
|
||||
result.imagination_consideration_request_id.as_deref() == Some(id)
|
||||
}
|
||||
Self::AdmittedModelDirection(id) => {
|
||||
result
|
||||
.admitted_model_direction_consideration_request_id
|
||||
.as_deref()
|
||||
== Some(id)
|
||||
}
|
||||
}
|
||||
pub(crate) fn matches_launch(
|
||||
self,
|
||||
launch: &crate::EpiphanyRuntimeWorkerLaunchRequest,
|
||||
) -> Result<bool> {
|
||||
let document = launch.launch_document()?;
|
||||
Ok(document.typed_request_ref()? == Some(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::EpiphanyRuntimeWorkerLaunchRequest {
|
||||
impl crate::EpiphanyWorkerLaunchDocument {
|
||||
pub(crate) fn typed_request_ref(&self) -> Result<Option<RuntimeTypedRequestRef<'_>>> {
|
||||
let crate::EpiphanyWorkerLaunchDocument::Role(document) = self else {
|
||||
return Ok(None);
|
||||
};
|
||||
let requests = [
|
||||
self.proposal_modeling_request_id
|
||||
.as_deref()
|
||||
document
|
||||
.proposal_modeling_context
|
||||
.as_ref()
|
||||
.map(|context| context.request_id.as_str())
|
||||
.map(RuntimeTypedRequestRef::ProposalModeling),
|
||||
self.repo_frontier_modeling_request_id
|
||||
.as_deref()
|
||||
document
|
||||
.frontier_verdict_modeling_context
|
||||
.as_ref()
|
||||
.map(|context| context.request.request_id.as_str())
|
||||
.map(RuntimeTypedRequestRef::FrontierVerdictModeling),
|
||||
self.repo_frontier_research_request_id
|
||||
.as_deref()
|
||||
document
|
||||
.frontier_research_context
|
||||
.as_ref()
|
||||
.map(|context| context.request_id.as_str())
|
||||
.map(RuntimeTypedRequestRef::FrontierResearch),
|
||||
self.repo_frontier_verification_request_id
|
||||
.as_deref()
|
||||
document
|
||||
.frontier_verification_context
|
||||
.as_ref()
|
||||
.map(|context| context.request.request_id.as_str())
|
||||
.map(RuntimeTypedRequestRef::FrontierVerification),
|
||||
self.imagination_consideration_request_id
|
||||
.as_deref()
|
||||
document
|
||||
.imagination_consideration_context
|
||||
.as_ref()
|
||||
.map(|context| context.request.request_id.as_str())
|
||||
.map(RuntimeTypedRequestRef::ImaginationConsideration),
|
||||
self.admitted_model_direction_consideration_request_id
|
||||
.as_deref()
|
||||
document
|
||||
.admitted_model_direction_consideration_context
|
||||
.as_ref()
|
||||
.map(|context| context.request.request_id.as_str())
|
||||
.map(RuntimeTypedRequestRef::AdmittedModelDirection),
|
||||
]
|
||||
.into_iter()
|
||||
|
|
|
|||
|
|
@ -1,19 +1,8 @@
|
|||
use cultcache_rs::DatabaseEntry;
|
||||
|
||||
pub const SOUL_VERDICT_RECEIPT_TYPE: &str = "epiphany.soul.verdict_receipt";
|
||||
pub const REPO_FRONTIER_VERIFICATION_REQUEST_TYPE: &str =
|
||||
"epiphany.soul.repo_frontier_verification_request";
|
||||
pub const REPO_FRONTIER_VERIFICATION_REQUEST_SCHEMA_VERSION: &str =
|
||||
"epiphany.soul.repo_frontier_verification_request.v2";
|
||||
pub const REPO_FRONTIER_VERIFICATION_REQUEST_CONTRACT: &str =
|
||||
"epiphany.repo_frontier_verification_request.v2";
|
||||
pub const SOUL_VERDICT_RECEIPT_SCHEMA_VERSION: &str = "epiphany.soul.verdict_receipt.v1";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "epiphany.soul.verdict_receipt", schema = "SoulVerdictReceipt")]
|
||||
pub struct SoulVerdictReceipt {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub receipt_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
|
|
@ -30,8 +19,6 @@ pub struct SoulVerdictReceipt {
|
|||
pub risks: Vec<String>,
|
||||
#[cultcache(key = 8)]
|
||||
pub emitted_at: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub contract: String,
|
||||
#[cultcache(key = 10, default)]
|
||||
pub verification_request_id: String,
|
||||
#[cultcache(key = 11, default)]
|
||||
|
|
@ -44,8 +31,6 @@ pub struct SoulVerdictReceipt {
|
|||
schema = "RepoFrontierVerificationRequest"
|
||||
)]
|
||||
pub struct RepoFrontierVerificationRequest {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
|
|
@ -61,17 +46,13 @@ pub struct RepoFrontierVerificationRequest {
|
|||
#[cultcache(key = 7)]
|
||||
pub hands_intent_id: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub hands_review_id: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub hands_patch_receipt_id: String,
|
||||
#[cultcache(key = 10)]
|
||||
#[cultcache(key = 9)]
|
||||
pub hands_command_receipt_id: String,
|
||||
#[cultcache(key = 11)]
|
||||
#[cultcache(key = 10)]
|
||||
pub hands_commit_receipt_id: String,
|
||||
#[cultcache(key = 12)]
|
||||
#[cultcache(key = 11)]
|
||||
pub requested_at: String,
|
||||
#[cultcache(key = 13)]
|
||||
pub contract: String,
|
||||
#[cultcache(key = 14)]
|
||||
#[cultcache(key = 12)]
|
||||
pub frontier_authority_documents: Vec<crate::EpiphanyMindDocumentVersion>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ use serde::Serialize;
|
|||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
|
||||
pub const STATE_LEDGER_STORE_TYPE: &str = "epiphany.state_ledger";
|
||||
pub const STATE_LEDGER_KEY: &str = "default";
|
||||
pub const STATE_LEDGER_SCHEMA_VERSION: &str = "epiphany.state_ledger.v0";
|
||||
|
||||
|
|
@ -43,34 +42,6 @@ pub struct EpiphanyLedgerEvidenceRecord {
|
|||
pub branch: Option<String>,
|
||||
}
|
||||
|
||||
pub fn state_ledger_status(store_path: impl AsRef<Path>) -> Result<Value> {
|
||||
let store_path = store_path.as_ref();
|
||||
if !store_path.exists() {
|
||||
return Ok(serde_json::json!({
|
||||
"ok": false,
|
||||
"present": false,
|
||||
"store": store_path,
|
||||
"entryType": STATE_LEDGER_STORE_TYPE,
|
||||
}));
|
||||
}
|
||||
let entry = load_state_ledger(store_path)?;
|
||||
let active = entry
|
||||
.branches
|
||||
.iter()
|
||||
.filter(|branch| branch.status == "active")
|
||||
.count();
|
||||
Ok(serde_json::json!({
|
||||
"ok": true,
|
||||
"present": true,
|
||||
"store": store_path,
|
||||
"entryType": STATE_LEDGER_STORE_TYPE,
|
||||
"branches": entry.branches.len(),
|
||||
"activeBranches": active,
|
||||
"evidence": entry.evidence.len(),
|
||||
"latestEvidence": entry.evidence.iter().rev().take(8).collect::<Vec<_>>(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn load_state_ledger(store_path: impl AsRef<Path>) -> Result<EpiphanyStateLedgerEntry> {
|
||||
let store_path = store_path.as_ref();
|
||||
let mut cache = state_ledger_cache(store_path)?;
|
||||
|
|
@ -162,7 +133,7 @@ pub fn close_state_branch(
|
|||
fn state_ledger_cache(store_path: &Path) -> Result<CultCache> {
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<EpiphanyStateLedgerEntry>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path))?;
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
|
|
|
|||
302
epiphany-core/src/state_model.rs
Normal file
302
epiphany-core/src/state_model.rs
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
//! Core-owned semantic state contracts.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanySubgoal {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub status: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyInvariant {
|
||||
pub id: String,
|
||||
pub description: String,
|
||||
pub status: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub rationale: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyCodeRef {
|
||||
pub path: PathBuf,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub start_line: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub end_line: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub symbol: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EpiphanyInvestigationDisposition {
|
||||
#[default]
|
||||
ResumeReady,
|
||||
RegatherRequired,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyInvestigationCheckpoint {
|
||||
pub checkpoint_id: String,
|
||||
pub kind: String,
|
||||
pub disposition: EpiphanyInvestigationDisposition,
|
||||
pub focus: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub summary: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub next_action: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub captured_at_turn_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub open_questions: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub code_refs: Vec<EpiphanyCodeRef>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub evidence_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyObservation {
|
||||
pub id: String,
|
||||
pub summary: String,
|
||||
pub source_kind: String,
|
||||
pub status: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub code_refs: Vec<EpiphanyCodeRef>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub evidence_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyEvidenceRecord {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub status: String,
|
||||
pub summary: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub code_refs: Vec<EpiphanyCodeRef>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyModeState {
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub kind: Option<EpiphanyModeKind>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EpiphanyModeKind {
|
||||
Plan,
|
||||
#[default]
|
||||
#[serde(
|
||||
alias = "code",
|
||||
alias = "pair_programming",
|
||||
alias = "execute",
|
||||
alias = "custom"
|
||||
)]
|
||||
Default,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyMemoryGraphSnapshot {
|
||||
pub graph_id: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub domains: Vec<EpiphanyMemoryDomain>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub nodes: Vec<EpiphanyMemoryNode>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub edges: Vec<EpiphanyMemoryEdge>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub frontier: Vec<RepoFrontierItem>,
|
||||
}
|
||||
|
||||
/// A durable piece of unfinished repository anatomy. This is Modeling-owned state,
|
||||
/// not a scheduler job or a presentation card.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct RepoFrontierItem {
|
||||
pub id: String,
|
||||
pub migration_body: String,
|
||||
pub question: String,
|
||||
pub gap: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub target_claim_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
/// Canonical repository-relative path ceiling for this wound. This is the
|
||||
/// scope that downstream Planning may narrow and Hands may eventually
|
||||
/// change; inspected files and evidence provenance belong elsewhere.
|
||||
pub repository_scope: Vec<String>,
|
||||
pub recommended_next_organ: String,
|
||||
/// The exact plan admitted by Mind for this frontier item. Imagination may
|
||||
/// propose this payload, but only the dedicated model transition may make
|
||||
/// it part of Modeling-owned truth.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub adopted_plan: Option<RepoFrontierAdoptedPlan>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub dependency_item_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub status: RepoFrontierStatus,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub evidence_refs: Vec<String>,
|
||||
/// Exact immutable public sources selected upstream for Eyes. These are
|
||||
/// causal retrieval inputs, not evidence produced by Modeling.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub public_source_refs: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub updated_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub retired_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub superseded_by: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct RepoFrontierAdoptedPlan {
|
||||
pub planning_request_id: String,
|
||||
pub result_id: String,
|
||||
pub job_id: String,
|
||||
pub candidate_id: String,
|
||||
pub candidate_sha256: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub safe_paths: Vec<String>,
|
||||
pub action: String,
|
||||
pub command: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub checks: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub stop_conditions: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub rollback_steps: Vec<String>,
|
||||
pub commit_message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RepoFrontierStatus {
|
||||
#[default]
|
||||
Proposed,
|
||||
Active,
|
||||
Blocked,
|
||||
Resolved,
|
||||
Retired,
|
||||
Superseded,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyMemoryDomain {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub lifecycle: EpiphanyMemoryLifecycle,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EpiphanyMemoryLifecycle {
|
||||
#[default]
|
||||
Observed,
|
||||
Proposed,
|
||||
Accepted,
|
||||
Retired,
|
||||
Stale,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EpiphanyMemoryNodeKind {
|
||||
#[default]
|
||||
Domain,
|
||||
Module,
|
||||
Crate,
|
||||
Binary,
|
||||
Schema,
|
||||
RuntimeContract,
|
||||
Adapter,
|
||||
TestSeam,
|
||||
StateStore,
|
||||
Summary,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyMemoryNode {
|
||||
pub id: String,
|
||||
pub domain_id: String,
|
||||
pub kind: EpiphanyMemoryNodeKind,
|
||||
pub title: String,
|
||||
pub claim: String,
|
||||
pub question: String,
|
||||
pub tension: String,
|
||||
pub action_implication: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub anchors: Vec<EpiphanyMemoryAnchor>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub source_hashes: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub lifecycle: EpiphanyMemoryLifecycle,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EpiphanyMemoryEdgeKind {
|
||||
#[default]
|
||||
Owns,
|
||||
Reads,
|
||||
Writes,
|
||||
Derives,
|
||||
Adapts,
|
||||
Persists,
|
||||
Launches,
|
||||
Verifies,
|
||||
Supports,
|
||||
Contradicts,
|
||||
Grounds,
|
||||
Triggers,
|
||||
DependsOn,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyMemoryEdge {
|
||||
pub id: String,
|
||||
pub source_id: String,
|
||||
pub target_id: String,
|
||||
pub kind: EpiphanyMemoryEdgeKind,
|
||||
pub claim: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub anchors: Vec<EpiphanyMemoryAnchor>,
|
||||
#[serde(default)]
|
||||
pub lifecycle: EpiphanyMemoryLifecycle,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct EpiphanyMemoryAnchor {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub target: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub code_ref: Option<EpiphanyCodeRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub evidence_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub source_hash: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
use cultcache_rs::DatabaseEntry;
|
||||
|
||||
pub const SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_TYPE: &str =
|
||||
"epiphany.substrate_gate.repo_access_grant_receipt";
|
||||
pub const SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION: &str =
|
||||
"epiphany.substrate_gate.repo_access_grant_receipt.v0";
|
||||
pub const SUBSTRATE_GATE_SOURCE_READ_OPERATION: &str = "read";
|
||||
|
|
@ -71,10 +69,7 @@ pub fn substrate_gate_operation_for_governed_tool(
|
|||
tool_name: &str,
|
||||
) -> Option<&'static str> {
|
||||
match (server, tool_name) {
|
||||
(
|
||||
"epiphany_source",
|
||||
"read_file" | "directory_inventory" | "git_show" | "read_hands_receipt",
|
||||
)
|
||||
("epiphany_source", "read_file" | "directory_inventory" | "git_show")
|
||||
| ("epiphany_state", "resident_grant_lifecycle") => {
|
||||
Some(SUBSTRATE_GATE_SOURCE_READ_OPERATION)
|
||||
}
|
||||
|
|
@ -83,26 +78,6 @@ pub fn substrate_gate_operation_for_governed_tool(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn substrate_gate_repo_work_planning_grant(
|
||||
receipt_id: String,
|
||||
runtime_job_id: String,
|
||||
granted_paths: Vec<String>,
|
||||
granted_at: String,
|
||||
) -> SubstrateGateRepoAccessGrantReceipt {
|
||||
SubstrateGateRepoAccessGrantReceipt {
|
||||
schema_version: SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION.to_string(),
|
||||
receipt_id,
|
||||
runtime_job_id,
|
||||
binding_id: "repo-work-runner".to_string(),
|
||||
role: "epiphany-hands".to_string(),
|
||||
authority_scope: "repo.branch_local_work".to_string(),
|
||||
granted_operations: vec!["read".to_string(), "snapshot".to_string()],
|
||||
granted_paths,
|
||||
granted_at,
|
||||
contract: "Substrate Gate grants read/snapshot access for repo-work planning only; mutation awaits an approved Hands review.".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn substrate_gate_coordinator_implementation_grant(
|
||||
receipt_id: String,
|
||||
runtime_job_id: String,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use super::EpiphanyCrrcAction;
|
||||
use crate::EpiphanyCurrentWorkProjection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -12,20 +11,6 @@ pub enum EpiphanyCoordinatorRoleStatus {
|
|||
Blocked,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EpiphanyCoordinatorRoleResultStatus {
|
||||
MissingState,
|
||||
MissingBinding,
|
||||
BackendUnavailable,
|
||||
BackendMissing,
|
||||
Pending,
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EpiphanyCoordinatorAction {
|
||||
|
|
@ -60,7 +45,6 @@ pub enum EpiphanyCoordinatorAction {
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EpiphanyCoordinatorInput {
|
||||
pub mind_present: bool,
|
||||
pub crrc_action: EpiphanyCrrcAction,
|
||||
pub current_work: EpiphanyCurrentWorkProjection,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
EpiphanyAgentPassContinuationAction, EpiphanyCoordinatorAction, EpiphanyCoordinatorDecision,
|
||||
EpiphanyCoordinatorInput, EpiphanyCrrcAction, RepoFrontierPlanningLifecycleStage,
|
||||
EpiphanyCoordinatorInput, RepoFrontierPlanningLifecycleStage,
|
||||
RepoFrontierResearchContinuationAction,
|
||||
};
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ pub fn recommend_coordinator_action(
|
|||
);
|
||||
}
|
||||
|
||||
if input.crrc_action == EpiphanyCrrcAction::RegatherManually {
|
||||
if input.current_work.operator_regather_required {
|
||||
return decision(
|
||||
EpiphanyCoordinatorAction::RegatherManually,
|
||||
"The accepted continuity decision requires explicit operator regather.",
|
||||
|
|
@ -134,10 +134,9 @@ fn continuation_decision(
|
|||
launch,
|
||||
&format!("The exact {label} obligation has no live attempt."),
|
||||
),
|
||||
EpiphanyAgentPassContinuationAction::Wait => decision(
|
||||
wait,
|
||||
&format!("The exact {label} attempt is still live."),
|
||||
),
|
||||
EpiphanyAgentPassContinuationAction::Wait => {
|
||||
decision(wait, &format!("The exact {label} attempt is still live."))
|
||||
}
|
||||
EpiphanyAgentPassContinuationAction::Review => decision(
|
||||
review,
|
||||
&format!("The exact {label} result awaits its family admission owner."),
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EpiphanyCrrcResultStatus {
|
||||
MissingState,
|
||||
MissingBinding,
|
||||
BackendUnavailable,
|
||||
BackendMissing,
|
||||
Pending,
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EpiphanyCrrcAction {
|
||||
Continue,
|
||||
LaunchReorientWorker,
|
||||
WaitForReorientWorker,
|
||||
ReviewReorientResult,
|
||||
AcceptReorientResult,
|
||||
RegatherManually,
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
mod coordinator_contract;
|
||||
mod coordinator_decision;
|
||||
mod crrc;
|
||||
mod reorient;
|
||||
mod research_decision;
|
||||
mod role_board;
|
||||
mod role_result;
|
||||
|
|
@ -9,9 +7,6 @@ mod worker_launch;
|
|||
|
||||
pub use coordinator_contract::*;
|
||||
pub use coordinator_decision::recommend_coordinator_action;
|
||||
pub use crrc::EpiphanyCrrcAction;
|
||||
pub use crrc::EpiphanyCrrcResultStatus;
|
||||
pub use reorient::EpiphanyReorientAction;
|
||||
pub use research_decision::EpiphanyResearchDecision;
|
||||
pub use role_board::EpiphanyRoleBoardInput;
|
||||
pub use role_board::EpiphanyRoleBoardLane;
|
||||
|
|
@ -21,6 +16,8 @@ pub use role_result::EpiphanyRoleFindingInterpretation;
|
|||
pub use role_result::EpiphanyRoleResultRoleId;
|
||||
pub use role_result::interpret_runtime_reorient_worker_result;
|
||||
pub use role_result::interpret_runtime_role_worker_result;
|
||||
pub use worker_launch::ADMITTED_MODEL_DIRECTION_CONSIDERATION_CONTEXT_CONTRACT;
|
||||
pub use worker_launch::ADMITTED_MODEL_DIRECTION_CONSIDERATION_CONTEXT_SCHEMA_VERSION;
|
||||
pub use worker_launch::AdmittedModelDirectionConsiderationContextProjection;
|
||||
pub use worker_launch::EpiphanyReorientWorkerLaunchDocument;
|
||||
pub use worker_launch::EpiphanyRoleWorkerLaunchDocument;
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Display projection of an accepted keyed continuity decision.
|
||||
///
|
||||
/// This enum does not decide whether Reorientation should run. The keyed
|
||||
/// current-work owner in `reorientation_work` owns launch, wait, review, and
|
||||
/// admission behavior.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EpiphanyReorientAction {
|
||||
Resume,
|
||||
Regather,
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::state_model::EpiphanyEvidenceRecord;
|
||||
use crate::state_model::EpiphanyInvestigationCheckpoint;
|
||||
use crate::state_model::EpiphanyObservation;
|
||||
use anyhow::{Result, anyhow};
|
||||
use epiphany_state_model::EpiphanyEvidenceRecord;
|
||||
use epiphany_state_model::EpiphanyInvestigationCheckpoint;
|
||||
use epiphany_state_model::EpiphanyObservation;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,20 +21,18 @@ pub struct EpiphanyRoleBoardLane {
|
|||
|
||||
pub fn derive_role_board(input: EpiphanyRoleBoardInput) -> Vec<EpiphanyRoleBoardLane> {
|
||||
let missing = !input.mind_present;
|
||||
let lane = |title: &str, status, note: &str| {
|
||||
EpiphanyRoleBoardLane {
|
||||
title: title.into(),
|
||||
status: if missing {
|
||||
EpiphanyCoordinatorRoleStatus::Blocked
|
||||
} else {
|
||||
status
|
||||
},
|
||||
note: if missing {
|
||||
"Canonical keyed Mind is missing.".into()
|
||||
} else {
|
||||
note.into()
|
||||
},
|
||||
}
|
||||
let lane = |title: &str, status, note: &str| EpiphanyRoleBoardLane {
|
||||
title: title.into(),
|
||||
status: if missing {
|
||||
EpiphanyCoordinatorRoleStatus::Blocked
|
||||
} else {
|
||||
status
|
||||
},
|
||||
note: if missing {
|
||||
"Canonical keyed Mind is missing.".into()
|
||||
} else {
|
||||
note.into()
|
||||
},
|
||||
};
|
||||
|
||||
let modeling_action = input
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ pub struct EpiphanyRoleFindingInterpretation {
|
|||
pub frontier_node_ids: Vec<String>,
|
||||
pub evidence_ids: Vec<String>,
|
||||
pub artifact_refs: Vec<String>,
|
||||
pub runtime_result_id: Option<String>,
|
||||
pub result_id: Option<String>,
|
||||
pub runtime_job_id: Option<String>,
|
||||
pub open_questions: Vec<String>,
|
||||
pub evidence_gaps: Vec<String>,
|
||||
|
|
@ -32,9 +32,6 @@ pub struct EpiphanyRoleFindingInterpretation {
|
|||
pub repo_model_mutation_proposal: Option<crate::EpiphanyRepoModelMutationProposal>,
|
||||
pub job_error: Option<String>,
|
||||
pub item_error: Option<String>,
|
||||
pub verification_request_id: Option<String>,
|
||||
pub frontier_route_id: Option<String>,
|
||||
pub proposal_modeling_request_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -48,7 +45,7 @@ pub struct EpiphanyReorientFindingInterpretation {
|
|||
pub frontier_node_ids: Vec<String>,
|
||||
pub evidence_ids: Vec<String>,
|
||||
pub artifact_refs: Vec<String>,
|
||||
pub runtime_result_id: Option<String>,
|
||||
pub result_id: Option<String>,
|
||||
pub runtime_job_id: Option<String>,
|
||||
pub job_error: Option<String>,
|
||||
pub item_error: Option<String>,
|
||||
|
|
@ -92,7 +89,7 @@ pub fn interpret_runtime_role_worker_result(
|
|||
frontier_node_ids: result.frontier_node_ids.clone(),
|
||||
evidence_ids: result.evidence_ids.clone(),
|
||||
artifact_refs: result.artifact_refs.clone(),
|
||||
runtime_result_id: Some(result.result_id.clone()),
|
||||
result_id: Some(result.result_id.clone()),
|
||||
runtime_job_id: Some(result.job_id.clone()),
|
||||
open_questions: result.open_questions.clone(),
|
||||
evidence_gaps: result.evidence_gaps.clone(),
|
||||
|
|
@ -122,9 +119,6 @@ pub fn interpret_runtime_role_worker_result(
|
|||
| EpiphanyRoleResultRoleId::Verification
|
||||
| EpiphanyRoleResultRoleId::Reorientation => item_error,
|
||||
},
|
||||
verification_request_id: result.verification_request_id.clone(),
|
||||
frontier_route_id: result.frontier_route_id.clone(),
|
||||
proposal_modeling_request_id: result.proposal_modeling_request_id.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +134,7 @@ pub fn interpret_runtime_reorient_worker_result(
|
|||
frontier_node_ids: result.frontier_node_ids.clone(),
|
||||
evidence_ids: result.evidence_ids.clone(),
|
||||
artifact_refs: result.artifact_refs.clone(),
|
||||
runtime_result_id: Some(result.result_id.clone()),
|
||||
result_id: Some(result.result_id.clone()),
|
||||
runtime_job_id: Some(result.job_id.clone()),
|
||||
job_error: None,
|
||||
item_error: result.item_error.clone(),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v4";
|
||||
pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v5";
|
||||
pub const REORIENT_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.reorient_result.v0";
|
||||
pub const REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION: &str =
|
||||
"epiphany.worker.repo_frontier_proposal_modeling_context.v4";
|
||||
|
|
@ -16,9 +16,9 @@ pub const REPO_FRONTIER_RESEARCH_CONTEXT_SCHEMA_VERSION: &str =
|
|||
pub const REPO_FRONTIER_RESEARCH_CONTEXT_CONTRACT: &str =
|
||||
"epiphany.repo_frontier_research_context.v3";
|
||||
pub const REPO_FRONTIER_VERIFICATION_CONTEXT_SCHEMA_VERSION: &str =
|
||||
"epiphany.worker.repo_frontier_verification_context.v1";
|
||||
"epiphany.worker.repo_frontier_verification_context.v2";
|
||||
pub const REPO_FRONTIER_VERIFICATION_CONTEXT_CONTRACT: &str =
|
||||
"epiphany.repo_frontier_verification_context.v1";
|
||||
"epiphany.repo_frontier_verification_context.v2";
|
||||
pub const REPO_FRONTIER_PLAN_MIND_CONTEXT_SCHEMA_VERSION: &str =
|
||||
"epiphany.worker.repo_frontier_plan_mind_context.v0";
|
||||
pub const REPO_FRONTIER_PLAN_MIND_CONTEXT_CONTRACT: &str =
|
||||
|
|
@ -102,6 +102,25 @@ pub struct EpiphanyRoleWorkerLaunchDocument {
|
|||
Option<AdmittedModelDirectionConsiderationContextProjection>,
|
||||
}
|
||||
|
||||
impl EpiphanyRoleWorkerLaunchDocument {
|
||||
pub(crate) fn family_context_count(&self) -> usize {
|
||||
[
|
||||
self.proposal_modeling_context.is_some(),
|
||||
self.frontier_verdict_modeling_context.is_some(),
|
||||
self.frontier_planning_context.is_some(),
|
||||
self.frontier_research_context.is_some(),
|
||||
self.frontier_verification_context.is_some(),
|
||||
self.frontier_plan_mind_context.is_some(),
|
||||
self.imagination_consideration_context.is_some(),
|
||||
self.admitted_model_direction_consideration_context
|
||||
.is_some(),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|present| *present)
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RepoFrontierResearchContextProjection {
|
||||
|
|
@ -222,7 +241,6 @@ pub struct RepoFrontierVerificationContextProjection {
|
|||
pub route: crate::RepoFrontierRoute,
|
||||
pub hands_authority: crate::RepoFrontierHandsAuthority,
|
||||
pub hands_intent: crate::HandsActionIntent,
|
||||
pub hands_review: crate::HandsActionReview,
|
||||
pub patch_receipt: crate::HandsPatchReceipt,
|
||||
pub command_receipt: crate::HandsCommandReceipt,
|
||||
pub commit_receipt: crate::HandsCommitReceipt,
|
||||
|
|
|
|||
|
|
@ -10,5 +10,9 @@ name = "epiphany_model_adapter"
|
|||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
cultcache-rs = { path = "../vendor/cultcache-rs" }
|
||||
anyhow = "1"
|
||||
codex-connector = { git = "https://github.com/GameCult/CodexConnector", rev = "be1776ab455fd79bd74b393518ab07211dc9c1f0", default-features = false }
|
||||
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
|
|
|
|||
|
|
@ -1,202 +1,5 @@
|
|||
use cultcache_rs::DatabaseEntry;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
mod native;
|
||||
mod provider;
|
||||
|
||||
pub const MODEL_ADAPTER_REQUEST_SCHEMA_ID: &str = "epiphany.model_request.v0";
|
||||
pub const MODEL_ADAPTER_EVENT_SCHEMA_ID: &str = "epiphany.model_stream_event.v0";
|
||||
pub const MODEL_ADAPTER_RECEIPT_SCHEMA_ID: &str = "epiphany.model_receipt.v0";
|
||||
pub const MODEL_ADAPTER_STATUS_SCHEMA_ID: &str = "epiphany.model_adapter_status.v0";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.model_adapter_status.v0",
|
||||
schema = "EpiphanyModelAdapterStatus"
|
||||
)]
|
||||
pub struct EpiphanyModelAdapterStatus {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub adapter_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub provider: String,
|
||||
#[cultcache(key = 3, default)]
|
||||
pub default_model: Option<String>,
|
||||
#[cultcache(key = 4)]
|
||||
pub streaming_supported: bool,
|
||||
#[cultcache(key = 5)]
|
||||
pub provider_transport_attached: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(type = "epiphany.model_request.v0", schema = "EpiphanyModelRequest")]
|
||||
pub struct EpiphanyModelRequest {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub conversation_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub provider: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub model: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub instructions: String,
|
||||
#[cultcache(key = 6, default)]
|
||||
pub input: Vec<EpiphanyModelInputItem>,
|
||||
#[cultcache(key = 7, default)]
|
||||
pub reasoning_effort: Option<String>,
|
||||
#[cultcache(key = 8, default)]
|
||||
pub reasoning_summary: Option<String>,
|
||||
#[cultcache(key = 9, default)]
|
||||
pub service_tier: Option<String>,
|
||||
#[cultcache(key = 10, default)]
|
||||
pub output_contract_id: Option<String>,
|
||||
#[cultcache(key = 11, default)]
|
||||
pub previous_response_id: Option<String>,
|
||||
#[cultcache(key = 12, default)]
|
||||
pub tools: Vec<EpiphanyModelToolDefinition>,
|
||||
#[cultcache(key = 13, default)]
|
||||
pub output_schema_json: Option<String>,
|
||||
#[cultcache(key = 14, default)]
|
||||
pub source_worker_job_id: Option<String>,
|
||||
#[cultcache(key = 15, default)]
|
||||
pub reasoning_basis_id: Option<String>,
|
||||
}
|
||||
|
||||
impl EpiphanyModelRequest {
|
||||
pub fn new(
|
||||
request_id: impl Into<String>,
|
||||
conversation_id: impl Into<String>,
|
||||
provider: impl Into<String>,
|
||||
model: impl Into<String>,
|
||||
instructions: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
schema_id: MODEL_ADAPTER_REQUEST_SCHEMA_ID.to_string(),
|
||||
request_id: request_id.into(),
|
||||
conversation_id: conversation_id.into(),
|
||||
provider: provider.into(),
|
||||
model: model.into(),
|
||||
instructions: instructions.into(),
|
||||
input: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
reasoning_summary: None,
|
||||
service_tier: None,
|
||||
output_contract_id: None,
|
||||
previous_response_id: None,
|
||||
tools: Vec::new(),
|
||||
output_schema_json: None,
|
||||
source_worker_job_id: None,
|
||||
reasoning_basis_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EpiphanyModelToolDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters_json: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum EpiphanyModelInputItem {
|
||||
UserText {
|
||||
text: String,
|
||||
},
|
||||
AssistantText {
|
||||
text: String,
|
||||
},
|
||||
ToolCall {
|
||||
call_id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
},
|
||||
ToolResult {
|
||||
call_id: String,
|
||||
output: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.model_stream_event.v0",
|
||||
schema = "EpiphanyModelStreamEvent"
|
||||
)]
|
||||
pub struct EpiphanyModelStreamEvent {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub provider: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub sequence: u64,
|
||||
#[cultcache(key = 4)]
|
||||
pub payload: EpiphanyModelStreamPayload,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum EpiphanyModelStreamPayload {
|
||||
TextDelta {
|
||||
text: String,
|
||||
},
|
||||
ReasoningDelta {
|
||||
text: String,
|
||||
},
|
||||
ToolCall {
|
||||
call_id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
},
|
||||
Completed {
|
||||
receipt: EpiphanyModelReceipt,
|
||||
},
|
||||
Failed {
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "epiphany.model_receipt.v0", schema = "EpiphanyModelReceipt")]
|
||||
pub struct EpiphanyModelReceipt {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub provider: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub model: String,
|
||||
#[cultcache(key = 4, default)]
|
||||
pub provider_response_id: Option<String>,
|
||||
#[cultcache(key = 5, default)]
|
||||
pub input_tokens: Option<u64>,
|
||||
#[cultcache(key = 6, default)]
|
||||
pub output_tokens: Option<u64>,
|
||||
#[cultcache(key = 7, default)]
|
||||
pub reasoning_output_tokens: Option<u64>,
|
||||
#[cultcache(key = 8, default)]
|
||||
pub transport: Option<String>,
|
||||
}
|
||||
|
||||
impl EpiphanyModelReceipt {
|
||||
pub fn new(
|
||||
request_id: impl Into<String>,
|
||||
provider: impl Into<String>,
|
||||
model: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
schema_id: MODEL_ADAPTER_RECEIPT_SCHEMA_ID.to_string(),
|
||||
request_id: request_id.into(),
|
||||
provider: provider.into(),
|
||||
model: model.into(),
|
||||
provider_response_id: None,
|
||||
input_tokens: None,
|
||||
output_tokens: None,
|
||||
reasoning_output_tokens: None,
|
||||
transport: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use native::*;
|
||||
pub use provider::*;
|
||||
|
|
|
|||
192
epiphany-model-adapter/src/native.rs
Normal file
192
epiphany-model-adapter/src/native.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
use cultcache_rs::DatabaseEntry;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub const MODEL_ADAPTER_REQUEST_SCHEMA_ID: &str = "epiphany.model_request.v0";
|
||||
pub const MODEL_ADAPTER_EVENT_SCHEMA_ID: &str = "epiphany.model_stream_event.v0";
|
||||
pub const MODEL_ADAPTER_RECEIPT_SCHEMA_ID: &str = "epiphany.model_receipt.v1";
|
||||
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(type = "epiphany.model_request.v0", schema = "EpiphanyModelRequest")]
|
||||
pub struct EpiphanyModelRequest {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub conversation_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub provider: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub model: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub instructions: String,
|
||||
#[cultcache(key = 6, default)]
|
||||
pub input: Vec<EpiphanyModelInputItem>,
|
||||
#[cultcache(key = 7, default)]
|
||||
pub reasoning_effort: Option<String>,
|
||||
#[cultcache(key = 8, default)]
|
||||
pub reasoning_summary: Option<String>,
|
||||
#[cultcache(key = 9, default)]
|
||||
pub service_tier: Option<String>,
|
||||
#[cultcache(key = 10, default)]
|
||||
pub output_contract_id: Option<String>,
|
||||
#[cultcache(key = 11, default)]
|
||||
pub previous_response_id: Option<String>,
|
||||
#[cultcache(key = 12, default)]
|
||||
pub tools: Vec<EpiphanyModelToolDefinition>,
|
||||
#[cultcache(key = 13, default)]
|
||||
pub output_schema_json: Option<String>,
|
||||
#[cultcache(key = 14, default)]
|
||||
pub source_worker_job_id: Option<String>,
|
||||
#[cultcache(key = 15, default)]
|
||||
pub reasoning_basis_id: Option<String>,
|
||||
}
|
||||
|
||||
impl EpiphanyModelRequest {
|
||||
pub fn new(
|
||||
request_id: impl Into<String>,
|
||||
conversation_id: impl Into<String>,
|
||||
provider: impl Into<String>,
|
||||
model: impl Into<String>,
|
||||
instructions: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
schema_id: MODEL_ADAPTER_REQUEST_SCHEMA_ID.to_string(),
|
||||
request_id: request_id.into(),
|
||||
conversation_id: conversation_id.into(),
|
||||
provider: provider.into(),
|
||||
model: model.into(),
|
||||
instructions: instructions.into(),
|
||||
input: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
reasoning_summary: None,
|
||||
service_tier: None,
|
||||
output_contract_id: None,
|
||||
previous_response_id: None,
|
||||
tools: Vec::new(),
|
||||
output_schema_json: None,
|
||||
source_worker_job_id: None,
|
||||
reasoning_basis_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EpiphanyModelToolDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters_json: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum EpiphanyModelInputItem {
|
||||
UserText {
|
||||
text: String,
|
||||
},
|
||||
AssistantText {
|
||||
text: String,
|
||||
},
|
||||
ToolCall {
|
||||
call_id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
},
|
||||
ToolResult {
|
||||
call_id: String,
|
||||
output: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.model_stream_event.v0",
|
||||
schema = "EpiphanyModelStreamEvent"
|
||||
)]
|
||||
pub struct EpiphanyModelStreamEvent {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub provider: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub sequence: u64,
|
||||
#[cultcache(key = 4)]
|
||||
pub payload: EpiphanyModelStreamPayload,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum EpiphanyModelStreamPayload {
|
||||
TextDelta {
|
||||
text: String,
|
||||
},
|
||||
ReasoningDelta {
|
||||
text: String,
|
||||
},
|
||||
ToolCall {
|
||||
call_id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
},
|
||||
Completed {
|
||||
receipt: Box<EpiphanyModelReceipt>,
|
||||
},
|
||||
Failed {
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "epiphany.model_receipt.v1", schema = "EpiphanyModelReceipt")]
|
||||
pub struct EpiphanyModelReceipt {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub provider: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub model: String,
|
||||
#[cultcache(key = 4, default)]
|
||||
pub provider_response_id: Option<String>,
|
||||
#[cultcache(key = 5, default)]
|
||||
pub input_tokens: Option<u64>,
|
||||
#[cultcache(key = 6, default)]
|
||||
pub output_tokens: Option<u64>,
|
||||
#[cultcache(key = 7, default)]
|
||||
pub reasoning_output_tokens: Option<u64>,
|
||||
#[cultcache(key = 8, default)]
|
||||
pub transport: Option<String>,
|
||||
#[cultcache(key = 9, default)]
|
||||
pub caller_runtime_id: Option<String>,
|
||||
#[cultcache(key = 10, default)]
|
||||
pub native_request_sha256: Option<String>,
|
||||
#[cultcache(key = 11, default)]
|
||||
pub provider_request_sha256: Option<String>,
|
||||
#[cultcache(key = 12, default)]
|
||||
pub cached_input_tokens: Option<u64>,
|
||||
}
|
||||
|
||||
impl EpiphanyModelReceipt {
|
||||
pub fn new(
|
||||
request_id: impl Into<String>,
|
||||
provider: impl Into<String>,
|
||||
model: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
schema_id: MODEL_ADAPTER_RECEIPT_SCHEMA_ID.to_string(),
|
||||
request_id: request_id.into(),
|
||||
provider: provider.into(),
|
||||
model: model.into(),
|
||||
provider_response_id: None,
|
||||
input_tokens: None,
|
||||
output_tokens: None,
|
||||
reasoning_output_tokens: None,
|
||||
transport: None,
|
||||
caller_runtime_id: None,
|
||||
native_request_sha256: None,
|
||||
provider_request_sha256: None,
|
||||
cached_input_tokens: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
922
epiphany-model-adapter/src/provider.rs
Normal file
922
epiphany-model-adapter/src/provider.rs
Normal file
|
|
@ -0,0 +1,922 @@
|
|||
use crate::{
|
||||
EpiphanyModelInputItem, EpiphanyModelReceipt, EpiphanyModelRequest, EpiphanyModelStreamEvent,
|
||||
EpiphanyModelStreamPayload, EpiphanyModelToolDefinition, MODEL_ADAPTER_EVENT_SCHEMA_ID,
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use codex_connector::{CodexInputItem, CodexProviderRequest, CodexToolChoice, CodexToolDefinition};
|
||||
use cultcache_rs::DatabaseEntry;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.provider_request.v2",
|
||||
schema = "EpiphanyProviderRequest"
|
||||
)]
|
||||
pub struct EpiphanyProviderRequest {
|
||||
#[cultcache(key = 0)]
|
||||
pub payload: EpiphanyProviderRequestPayload,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum EpiphanyProviderRequestPayload {
|
||||
Codex(CodexProviderRequest),
|
||||
OpenRouter(EpiphanyOpenRouterRequest),
|
||||
}
|
||||
|
||||
impl EpiphanyProviderRequest {
|
||||
pub fn request_id(&self) -> &str {
|
||||
match &self.payload {
|
||||
EpiphanyProviderRequestPayload::Codex(request) => &request.request_id,
|
||||
EpiphanyProviderRequestPayload::OpenRouter(request) => &request.request_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn conversation_id(&self) -> &str {
|
||||
match &self.payload {
|
||||
EpiphanyProviderRequestPayload::Codex(request) => &request.conversation_id,
|
||||
EpiphanyProviderRequestPayload::OpenRouter(request) => &request.conversation_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn model(&self) -> &str {
|
||||
match &self.payload {
|
||||
EpiphanyProviderRequestPayload::Codex(request) => &request.model,
|
||||
EpiphanyProviderRequestPayload::OpenRouter(request) => &request.model,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EpiphanyOpenRouterRequest {
|
||||
pub request_id: String,
|
||||
pub conversation_id: String,
|
||||
pub model: String,
|
||||
pub instructions: String,
|
||||
pub input: Vec<EpiphanyModelInputItem>,
|
||||
pub reasoning_effort: Option<String>,
|
||||
pub reasoning_summary: Option<String>,
|
||||
pub service_tier: Option<String>,
|
||||
pub output_contract_id: Option<String>,
|
||||
pub previous_response_id: Option<String>,
|
||||
pub tools: Vec<EpiphanyModelToolDefinition>,
|
||||
pub output_schema_json: Option<String>,
|
||||
}
|
||||
|
||||
impl EpiphanyOpenRouterRequest {
|
||||
pub fn new(
|
||||
request_id: impl Into<String>,
|
||||
conversation_id: impl Into<String>,
|
||||
model: impl Into<String>,
|
||||
instructions: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
request_id: request_id.into(),
|
||||
conversation_id: conversation_id.into(),
|
||||
model: model.into(),
|
||||
instructions: instructions.into(),
|
||||
input: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
reasoning_summary: None,
|
||||
service_tier: None,
|
||||
output_contract_id: None,
|
||||
previous_response_id: None,
|
||||
tools: Vec::new(),
|
||||
output_schema_json: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The only lowering from a native model request to an exact provider request.
|
||||
pub fn request_from_native(request: &EpiphanyModelRequest) -> Result<EpiphanyProviderRequest> {
|
||||
let payload = match request.provider.as_str() {
|
||||
"openai-codex" | "openai" => {
|
||||
EpiphanyProviderRequestPayload::Codex(codex_request_from_native(request)?)
|
||||
}
|
||||
"openrouter" => {
|
||||
EpiphanyProviderRequestPayload::OpenRouter(openrouter_request_from_native(request))
|
||||
}
|
||||
_ => return Err(anyhow!("unsupported model provider {:?}", request.provider)),
|
||||
};
|
||||
Ok(EpiphanyProviderRequest { payload })
|
||||
}
|
||||
|
||||
fn openrouter_request_from_native(request: &EpiphanyModelRequest) -> EpiphanyOpenRouterRequest {
|
||||
EpiphanyOpenRouterRequest {
|
||||
request_id: request.request_id.clone(),
|
||||
conversation_id: request.conversation_id.clone(),
|
||||
model: request.model.clone(),
|
||||
instructions: request.instructions.clone(),
|
||||
input: request.input.clone(),
|
||||
reasoning_effort: request.reasoning_effort.clone(),
|
||||
reasoning_summary: request.reasoning_summary.clone(),
|
||||
service_tier: request.service_tier.clone(),
|
||||
output_contract_id: request.output_contract_id.clone(),
|
||||
previous_response_id: request.previous_response_id.clone(),
|
||||
tools: request.tools.clone(),
|
||||
output_schema_json: request.output_schema_json.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_request_from_native(request: &EpiphanyModelRequest) -> Result<CodexProviderRequest> {
|
||||
let has_tool_result = request
|
||||
.input
|
||||
.iter()
|
||||
.any(|item| matches!(item, EpiphanyModelInputItem::ToolResult { .. }));
|
||||
let output_format_name = request.output_schema_json.as_ref().map(|_| {
|
||||
provider_format_name(
|
||||
request
|
||||
.output_contract_id
|
||||
.as_deref()
|
||||
.unwrap_or(&request.request_id),
|
||||
)
|
||||
});
|
||||
let output_schema_json = request
|
||||
.output_schema_json
|
||||
.as_deref()
|
||||
.map(strict_provider_schema)
|
||||
.transpose()?
|
||||
.map(|schema| serde_json::to_string(&schema))
|
||||
.transpose()?;
|
||||
let tools = request
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
Ok(CodexToolDefinition {
|
||||
name: tool.name.clone(),
|
||||
description: tool.description.clone(),
|
||||
parameters_json: serde_json::to_string(&strict_provider_schema(
|
||||
&tool.parameters_json,
|
||||
)?)?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let mut provider = CodexProviderRequest::new(
|
||||
&request.request_id,
|
||||
&request.conversation_id,
|
||||
&request.model,
|
||||
&request.instructions,
|
||||
);
|
||||
provider.input = request.input.iter().map(codex_input_from_native).collect();
|
||||
provider.reasoning_effort = request.reasoning_effort.clone();
|
||||
provider.reasoning_summary = request.reasoning_summary.clone();
|
||||
provider.service_tier = request.service_tier.clone();
|
||||
provider.output_format_name = output_format_name;
|
||||
provider.previous_response_id = request.previous_response_id.clone();
|
||||
provider.tools = tools;
|
||||
provider.tool_choice = if !provider.tools.is_empty() && !has_tool_result {
|
||||
CodexToolChoice::Required
|
||||
} else {
|
||||
CodexToolChoice::Auto
|
||||
};
|
||||
provider.output_schema_json = output_schema_json;
|
||||
provider.validate()?;
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
fn codex_input_from_native(input: &EpiphanyModelInputItem) -> CodexInputItem {
|
||||
match input {
|
||||
EpiphanyModelInputItem::UserText { text } => {
|
||||
CodexInputItem::UserText { text: text.clone() }
|
||||
}
|
||||
EpiphanyModelInputItem::AssistantText { text } => {
|
||||
CodexInputItem::AssistantText { text: text.clone() }
|
||||
}
|
||||
EpiphanyModelInputItem::ToolCall {
|
||||
call_id,
|
||||
name,
|
||||
arguments,
|
||||
} => CodexInputItem::ToolCall {
|
||||
call_id: provider_call_id(call_id),
|
||||
name: name.clone(),
|
||||
arguments: arguments.clone(),
|
||||
},
|
||||
EpiphanyModelInputItem::ToolResult { call_id, output } => CodexInputItem::ToolResult {
|
||||
call_id: provider_call_id(call_id),
|
||||
output: output.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Project Epiphany's canonical JSON Schema into the strict subset accepted by
|
||||
/// model providers. Native decoding remains the final validation authority.
|
||||
pub fn strict_provider_schema(schema_json: &str) -> anyhow::Result<serde_json::Value> {
|
||||
let mut schema = serde_json::from_str(schema_json)
|
||||
.map_err(|error| anyhow::anyhow!("provider schema is not valid JSON Schema: {error}"))?;
|
||||
lower_provider_schema(&mut schema);
|
||||
close_provider_objects(&mut schema, "$")?;
|
||||
if !provider_schema_is_strict(&schema) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"projected provider output schema is not strict"
|
||||
));
|
||||
}
|
||||
Ok(schema)
|
||||
}
|
||||
|
||||
/// Provider-safe output-format identity derived from the native contract.
|
||||
pub fn provider_format_name(value: &str) -> String {
|
||||
let mut name = value
|
||||
.chars()
|
||||
.map(|ch| {
|
||||
if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-') {
|
||||
ch
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.take(64)
|
||||
.collect::<String>();
|
||||
if name.is_empty() {
|
||||
name.push_str("epiphany_worker_result");
|
||||
}
|
||||
name
|
||||
}
|
||||
|
||||
/// Responses call IDs have a narrow alphabet and byte bound. Preserve already
|
||||
/// legal IDs and derive a stable identity for every other native call ID.
|
||||
pub fn provider_call_id(value: &str) -> String {
|
||||
if !value.is_empty()
|
||||
&& value.len() <= 64
|
||||
&& value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
|
||||
{
|
||||
return value.to_string();
|
||||
}
|
||||
let digest = Sha256::digest(value.as_bytes());
|
||||
let mut projected = String::with_capacity(64);
|
||||
for byte in digest {
|
||||
use std::fmt::Write as _;
|
||||
write!(&mut projected, "{byte:02x}").expect("writing to a String cannot fail");
|
||||
}
|
||||
projected
|
||||
}
|
||||
|
||||
pub const OPENROUTER_TERMINAL_TOOL_NAME: &str = "epiphany_submit_typed_result";
|
||||
|
||||
/// Lower one typed Epiphany request to OpenRouter's terminal-tool dialect.
|
||||
pub fn openrouter_request_body(
|
||||
request: &EpiphanyOpenRouterRequest,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let has_tool_result = request
|
||||
.input
|
||||
.iter()
|
||||
.any(|item| matches!(item, EpiphanyModelInputItem::ToolResult { .. }));
|
||||
let output_schema = request
|
||||
.output_schema_json
|
||||
.as_deref()
|
||||
.map(strict_provider_schema)
|
||||
.transpose()?;
|
||||
let include_terminal_tool =
|
||||
output_schema.is_some() && (request.tools.is_empty() || has_tool_result);
|
||||
if request
|
||||
.tools
|
||||
.iter()
|
||||
.any(|tool| tool.name == OPENROUTER_TERMINAL_TOOL_NAME)
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"native tool name collides with the OpenRouter terminal decision tool"
|
||||
));
|
||||
}
|
||||
|
||||
let mut messages = vec![serde_json::json!({
|
||||
"role": "system",
|
||||
"content": request.instructions,
|
||||
})];
|
||||
for item in &request.input {
|
||||
messages.push(match item {
|
||||
EpiphanyModelInputItem::UserText { text } => {
|
||||
serde_json::json!({"role": "user", "content": text})
|
||||
}
|
||||
EpiphanyModelInputItem::AssistantText { text } => {
|
||||
serde_json::json!({"role": "assistant", "content": text})
|
||||
}
|
||||
EpiphanyModelInputItem::ToolCall {
|
||||
call_id,
|
||||
name,
|
||||
arguments,
|
||||
} => serde_json::json!({
|
||||
"role": "assistant",
|
||||
"tool_calls": [{
|
||||
"id": call_id,
|
||||
"type": "function",
|
||||
"function": {"name": name, "arguments": arguments},
|
||||
}],
|
||||
}),
|
||||
EpiphanyModelInputItem::ToolResult { call_id, output } => serde_json::json!({
|
||||
"role": "tool",
|
||||
"tool_call_id": call_id,
|
||||
"content": output,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
let mut tools = request
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
Ok(serde_json::json!({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"strict": true,
|
||||
"parameters": strict_provider_schema(&tool.parameters_json)?,
|
||||
},
|
||||
}))
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
if include_terminal_tool {
|
||||
tools.push(serde_json::json!({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": OPENROUTER_TERMINAL_TOOL_NAME,
|
||||
"description": "Submit the final typed Epiphany decision. Call this only when the pass is complete.",
|
||||
"strict": true,
|
||||
"parameters": output_schema.expect("terminal schema exists"),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
let mut body = serde_json::json!({
|
||||
"model": request.model,
|
||||
"messages": messages,
|
||||
"stream": false,
|
||||
"parallel_tool_calls": false,
|
||||
});
|
||||
if !tools.is_empty() {
|
||||
body["tools"] = serde_json::Value::Array(tools);
|
||||
body["tool_choice"] = serde_json::Value::String(
|
||||
if include_terminal_tool || (!request.tools.is_empty() && !has_tool_result) {
|
||||
"required"
|
||||
} else {
|
||||
"auto"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if let Some(effort) = request.reasoning_effort.as_deref() {
|
||||
body["reasoning"] = serde_json::json!({"effort": effort});
|
||||
}
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Interpret one non-streaming OpenRouter response into Epiphany's transient
|
||||
/// event family. The model transcript remains optional; the terminal receipt
|
||||
/// is authoritative transport evidence.
|
||||
pub fn openrouter_events_from_response(
|
||||
request: &EpiphanyOpenRouterRequest,
|
||||
response_bytes: &[u8],
|
||||
) -> anyhow::Result<Vec<EpiphanyModelStreamEvent>> {
|
||||
let response: OpenRouterResponse = serde_json::from_slice(response_bytes)
|
||||
.map_err(|error| anyhow::anyhow!("OpenRouter returned an invalid response: {error}"))?;
|
||||
let choice = response
|
||||
.choices
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("OpenRouter response contained no completion choice"))?;
|
||||
let mut events = Vec::new();
|
||||
if let Some(reasoning) = choice.message.reasoning.filter(|value| !value.is_empty()) {
|
||||
push_openrouter_event(
|
||||
&mut events,
|
||||
&request.request_id,
|
||||
EpiphanyModelStreamPayload::ReasoningDelta { text: reasoning },
|
||||
);
|
||||
}
|
||||
let terminal_calls = choice
|
||||
.message
|
||||
.tool_calls
|
||||
.iter()
|
||||
.filter(|call| call.function.name == OPENROUTER_TERMINAL_TOOL_NAME)
|
||||
.collect::<Vec<_>>();
|
||||
if !terminal_calls.is_empty() {
|
||||
if terminal_calls.len() != 1 || choice.message.tool_calls.len() != 1 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"OpenRouter mixed the terminal decision tool with another tool call"
|
||||
));
|
||||
}
|
||||
let arguments = &terminal_calls[0].function.arguments;
|
||||
if !matches!(
|
||||
serde_json::from_str::<serde_json::Value>(arguments),
|
||||
Ok(serde_json::Value::Object(_))
|
||||
) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"OpenRouter terminal decision tool returned non-object arguments"
|
||||
));
|
||||
}
|
||||
push_openrouter_event(
|
||||
&mut events,
|
||||
&request.request_id,
|
||||
EpiphanyModelStreamPayload::TextDelta {
|
||||
text: arguments.clone(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if let Some(content) = choice.message.content.filter(|value| !value.is_empty()) {
|
||||
push_openrouter_event(
|
||||
&mut events,
|
||||
&request.request_id,
|
||||
EpiphanyModelStreamPayload::TextDelta { text: content },
|
||||
);
|
||||
}
|
||||
for call in choice.message.tool_calls {
|
||||
push_openrouter_event(
|
||||
&mut events,
|
||||
&request.request_id,
|
||||
EpiphanyModelStreamPayload::ToolCall {
|
||||
call_id: call.id,
|
||||
name: call.function.name,
|
||||
arguments: call.function.arguments,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if events.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"OpenRouter response contained neither text nor tool calls"
|
||||
));
|
||||
}
|
||||
let usage = response.usage;
|
||||
let mut receipt = EpiphanyModelReceipt::new(
|
||||
&request.request_id,
|
||||
"openrouter",
|
||||
response.model.unwrap_or_else(|| request.model.clone()),
|
||||
);
|
||||
receipt.provider_response_id = response.id;
|
||||
receipt.input_tokens = usage.as_ref().and_then(|usage| usage.prompt_tokens);
|
||||
receipt.output_tokens = usage.as_ref().and_then(|usage| usage.completion_tokens);
|
||||
receipt.reasoning_output_tokens = usage
|
||||
.as_ref()
|
||||
.and_then(|usage| usage.completion_tokens_details.as_ref())
|
||||
.and_then(|details| details.reasoning_tokens);
|
||||
receipt.transport = Some("openrouter-chat-completions".into());
|
||||
push_openrouter_event(
|
||||
&mut events,
|
||||
&request.request_id,
|
||||
EpiphanyModelStreamPayload::Completed {
|
||||
receipt: Box::new(receipt),
|
||||
},
|
||||
);
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenRouterResponse {
|
||||
id: Option<String>,
|
||||
model: Option<String>,
|
||||
choices: Vec<OpenRouterChoice>,
|
||||
usage: Option<OpenRouterUsage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenRouterChoice {
|
||||
message: OpenRouterMessage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenRouterMessage {
|
||||
content: Option<String>,
|
||||
#[serde(default)]
|
||||
reasoning: Option<String>,
|
||||
#[serde(default)]
|
||||
tool_calls: Vec<OpenRouterToolCall>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenRouterToolCall {
|
||||
id: String,
|
||||
function: OpenRouterFunctionCall,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenRouterFunctionCall {
|
||||
name: String,
|
||||
arguments: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenRouterUsage {
|
||||
prompt_tokens: Option<u64>,
|
||||
completion_tokens: Option<u64>,
|
||||
completion_tokens_details: Option<OpenRouterCompletionTokenDetails>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OpenRouterCompletionTokenDetails {
|
||||
reasoning_tokens: Option<u64>,
|
||||
}
|
||||
|
||||
fn push_openrouter_event(
|
||||
events: &mut Vec<EpiphanyModelStreamEvent>,
|
||||
request_id: &str,
|
||||
payload: EpiphanyModelStreamPayload,
|
||||
) {
|
||||
events.push(EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id: request_id.into(),
|
||||
provider: "openrouter".into(),
|
||||
sequence: events.len() as u64,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
fn provider_schema_is_strict(schema: &serde_json::Value) -> bool {
|
||||
match schema {
|
||||
serde_json::Value::Object(map) => {
|
||||
if (map.contains_key("const") || map.contains_key("enum")) && !map.contains_key("type")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if schema_map_describes_object(map) {
|
||||
if map.get("additionalProperties") != Some(&serde_json::Value::Bool(false)) {
|
||||
return false;
|
||||
}
|
||||
let Some(properties) = map.get("properties").and_then(serde_json::Value::as_object)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(required) = map.get("required").and_then(serde_json::Value::as_array)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if properties
|
||||
.keys()
|
||||
.any(|key| !required.iter().any(|item| item.as_str() == Some(key)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
map.values().all(provider_schema_is_strict)
|
||||
}
|
||||
serde_json::Value::Array(values) => values.iter().all(provider_schema_is_strict),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn close_provider_objects(schema: &mut serde_json::Value, path: &str) -> anyhow::Result<()> {
|
||||
match schema {
|
||||
serde_json::Value::Object(map) => {
|
||||
let describes_object = schema_map_describes_object(map);
|
||||
if describes_object {
|
||||
map.insert("type".into(), serde_json::Value::String("object".into()));
|
||||
let properties = map
|
||||
.get("properties")
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let canonical_required = map
|
||||
.get("required")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
for required in &canonical_required {
|
||||
let required = required.as_str().ok_or_else(|| {
|
||||
anyhow::anyhow!("provider schema {path} has a non-string required key")
|
||||
})?;
|
||||
if !properties.contains_key(required) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"provider schema {path} requires undeclared property {required:?}"
|
||||
));
|
||||
}
|
||||
}
|
||||
let canonical_required = canonical_required
|
||||
.into_iter()
|
||||
.filter_map(|value| value.as_str().map(ToOwned::to_owned))
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
let mut projected = serde_json::Map::new();
|
||||
for (name, mut property) in properties {
|
||||
close_provider_objects(&mut property, &format!("{path}.properties.{name}"))?;
|
||||
if !canonical_required.contains(&name) {
|
||||
property = nullable_provider_property(property);
|
||||
}
|
||||
projected.insert(name, property);
|
||||
}
|
||||
let required = projected
|
||||
.keys()
|
||||
.cloned()
|
||||
.map(serde_json::Value::String)
|
||||
.collect();
|
||||
map.insert("properties".into(), serde_json::Value::Object(projected));
|
||||
map.insert("required".into(), serde_json::Value::Array(required));
|
||||
map.insert(
|
||||
"additionalProperties".into(),
|
||||
serde_json::Value::Bool(false),
|
||||
);
|
||||
}
|
||||
for (name, value) in map.iter_mut() {
|
||||
if name != "properties" || !describes_object {
|
||||
close_provider_objects(value, &format!("{path}.{name}"))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
serde_json::Value::Array(values) => {
|
||||
for (index, value) in values.iter_mut().enumerate() {
|
||||
close_provider_objects(value, &format!("{path}[{index}]"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn schema_map_describes_object(map: &serde_json::Map<String, serde_json::Value>) -> bool {
|
||||
map.get("type").and_then(serde_json::Value::as_str) == Some("object")
|
||||
|| [
|
||||
"properties",
|
||||
"required",
|
||||
"additionalProperties",
|
||||
"patternProperties",
|
||||
"propertyNames",
|
||||
"minProperties",
|
||||
"maxProperties",
|
||||
]
|
||||
.iter()
|
||||
.any(|keyword| map.contains_key(*keyword))
|
||||
}
|
||||
|
||||
fn parent_relative_object_alternatives(value: &serde_json::Value) -> bool {
|
||||
let Some(alternatives) = value.as_array() else {
|
||||
return false;
|
||||
};
|
||||
!alternatives.is_empty()
|
||||
&& alternatives.iter().all(|alternative| {
|
||||
let Some(map) = alternative.as_object() else {
|
||||
return false;
|
||||
};
|
||||
!map.contains_key("type")
|
||||
&& !map.contains_key("$ref")
|
||||
&& map
|
||||
.keys()
|
||||
.any(|key| matches!(key.as_str(), "properties" | "required"))
|
||||
&& map.keys().all(|key| {
|
||||
matches!(
|
||||
key.as_str(),
|
||||
"properties" | "required" | "title" | "description" | "$comment"
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn infer_literal_type(map: &mut serde_json::Map<String, serde_json::Value>) {
|
||||
if map.contains_key("type") {
|
||||
return;
|
||||
}
|
||||
let mut types = std::collections::BTreeSet::new();
|
||||
if let Some(value) = map.get("const") {
|
||||
types.insert(json_type(value));
|
||||
} else if let Some(values) = map.get("enum").and_then(serde_json::Value::as_array) {
|
||||
types.extend(values.iter().map(json_type));
|
||||
}
|
||||
match types.len() {
|
||||
0 => {}
|
||||
1 => {
|
||||
map.insert(
|
||||
"type".into(),
|
||||
serde_json::Value::String(types.into_iter().next().expect("one type").into()),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
map.insert(
|
||||
"type".into(),
|
||||
serde_json::Value::Array(
|
||||
types
|
||||
.into_iter()
|
||||
.map(|value| serde_json::Value::String(value.into()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn json_type(value: &serde_json::Value) -> &'static str {
|
||||
match value {
|
||||
serde_json::Value::Null => "null",
|
||||
serde_json::Value::Bool(_) => "boolean",
|
||||
serde_json::Value::Number(number) if number.is_i64() || number.is_u64() => "integer",
|
||||
serde_json::Value::Number(_) => "number",
|
||||
serde_json::Value::String(_) => "string",
|
||||
serde_json::Value::Array(_) => "array",
|
||||
serde_json::Value::Object(_) => "object",
|
||||
}
|
||||
}
|
||||
|
||||
fn nullable_provider_property(property: serde_json::Value) -> serde_json::Value {
|
||||
if property
|
||||
.get("anyOf")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.is_some_and(|variants| {
|
||||
variants.iter().any(|variant| {
|
||||
variant.get("type").and_then(serde_json::Value::as_str) == Some("null")
|
||||
})
|
||||
})
|
||||
{
|
||||
property
|
||||
} else {
|
||||
serde_json::json!({"anyOf": [property, {"type": "null"}]})
|
||||
}
|
||||
}
|
||||
|
||||
const UNSUPPORTED_PROVIDER_SCHEMA_KEYWORDS: &[&str] = &[
|
||||
"allOf",
|
||||
"not",
|
||||
"dependentRequired",
|
||||
"dependentSchemas",
|
||||
"if",
|
||||
"then",
|
||||
"else",
|
||||
"patternProperties",
|
||||
"propertyNames",
|
||||
"minProperties",
|
||||
"maxProperties",
|
||||
"unevaluatedProperties",
|
||||
"uniqueItems",
|
||||
"contains",
|
||||
"minContains",
|
||||
"maxContains",
|
||||
"prefixItems",
|
||||
"unevaluatedItems",
|
||||
"default",
|
||||
"examples",
|
||||
"readOnly",
|
||||
"writeOnly",
|
||||
"$schema",
|
||||
"$id",
|
||||
"$anchor",
|
||||
"$dynamicAnchor",
|
||||
"$dynamicRef",
|
||||
"$vocabulary",
|
||||
];
|
||||
|
||||
fn lower_provider_schema(schema: &mut serde_json::Value) {
|
||||
let serde_json::Value::Object(map) = schema else {
|
||||
return;
|
||||
};
|
||||
infer_literal_type(map);
|
||||
if map.get("format").and_then(serde_json::Value::as_str) == Some("uuid") {
|
||||
map.remove("format");
|
||||
map.entry("pattern").or_insert_with(|| {
|
||||
serde_json::Value::String(
|
||||
"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
||||
.into(),
|
||||
)
|
||||
});
|
||||
}
|
||||
for keyword in UNSUPPORTED_PROVIDER_SCHEMA_KEYWORDS {
|
||||
map.remove(*keyword);
|
||||
}
|
||||
if let Some(one_of) = map.remove("oneOf") {
|
||||
map.insert("anyOf".into(), one_of);
|
||||
}
|
||||
if map
|
||||
.get("anyOf")
|
||||
.is_some_and(parent_relative_object_alternatives)
|
||||
{
|
||||
map.remove("anyOf");
|
||||
}
|
||||
for collection in ["properties", "$defs", "definitions"] {
|
||||
if let Some(serde_json::Value::Object(children)) = map.get_mut(collection) {
|
||||
for child in children.values_mut() {
|
||||
lower_provider_schema(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(items) = map.get_mut("items") {
|
||||
match items {
|
||||
serde_json::Value::Array(items) => {
|
||||
for item in items {
|
||||
lower_provider_schema(item);
|
||||
}
|
||||
}
|
||||
item => lower_provider_schema(item),
|
||||
}
|
||||
}
|
||||
if let Some(serde_json::Value::Array(alternatives)) = map.get_mut("anyOf") {
|
||||
for alternative in alternatives {
|
||||
lower_provider_schema(alternative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn native_provider_identity_selects_one_exact_request_contract() {
|
||||
let openai = EpiphanyModelRequest::new(
|
||||
"openai-request",
|
||||
"conversation",
|
||||
"openai-codex",
|
||||
"gpt-test",
|
||||
"decide",
|
||||
);
|
||||
let openrouter = EpiphanyModelRequest::new(
|
||||
"openrouter-request",
|
||||
"conversation",
|
||||
"openrouter",
|
||||
"stealth/ox-alpha",
|
||||
"decide",
|
||||
);
|
||||
|
||||
let openai = request_from_native(&openai).unwrap();
|
||||
let openrouter = request_from_native(&openrouter).unwrap();
|
||||
assert!(matches!(
|
||||
openai.payload,
|
||||
EpiphanyProviderRequestPayload::Codex(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
openrouter.payload,
|
||||
EpiphanyProviderRequestPayload::OpenRouter(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_projection_closes_objects_without_keeping_parent_conditions() {
|
||||
let schema = strict_provider_schema(
|
||||
r#"{
|
||||
"type":"object",
|
||||
"properties":{"choice":{"type":"string"},"note":{"type":"string"}},
|
||||
"required":["choice"],
|
||||
"allOf":[{"if":{"properties":{"choice":{"const":"x"}}}}],
|
||||
"anyOf":[{"required":["choice"]},{"required":["note"]}]
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(schema["additionalProperties"], false);
|
||||
assert_eq!(schema["required"], serde_json::json!(["choice", "note"]));
|
||||
assert!(schema.get("allOf").is_none());
|
||||
assert!(schema.get("anyOf").is_none());
|
||||
assert_eq!(schema["properties"]["note"]["anyOf"][1]["type"], "null");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_projection_types_literals_and_lowers_uuid() {
|
||||
let schema = strict_provider_schema(
|
||||
r#"{
|
||||
"type":"object",
|
||||
"properties":{
|
||||
"kind":{"const":"ready"},
|
||||
"id":{"type":"string","format":"uuid"},
|
||||
"items":{"type":"array","uniqueItems":true,"items":{"enum":[1,2]}}
|
||||
},
|
||||
"required":["kind","id","items"]
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(schema["properties"]["kind"]["type"], "string");
|
||||
assert!(schema["properties"]["id"].get("format").is_none());
|
||||
assert!(schema["properties"]["id"]["pattern"].is_string());
|
||||
assert!(schema["properties"]["items"].get("uniqueItems").is_none());
|
||||
assert_eq!(schema["properties"]["items"]["items"]["type"], "integer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_identities_are_stable_and_bounded() {
|
||||
assert_eq!(provider_format_name("role/result.v1"), "role_result_v1");
|
||||
assert_eq!(provider_call_id("already-safe_1"), "already-safe_1");
|
||||
let first = provider_call_id("unsafe/call identity");
|
||||
assert_eq!(first, provider_call_id("unsafe/call identity"));
|
||||
assert_eq!(first.len(), 64);
|
||||
assert!(first.bytes().all(|byte| byte.is_ascii_hexdigit()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openrouter_terminal_tool_preserves_structured_decision() {
|
||||
let mut request = EpiphanyOpenRouterRequest::new("request", "conversation", "ox", "decide");
|
||||
request.output_schema_json = Some(
|
||||
r#"{"type":"object","properties":{"verdict":{"type":"string"}},"required":["verdict"]}"#.into(),
|
||||
);
|
||||
let body = openrouter_request_body(&request).unwrap();
|
||||
assert_eq!(body["tool_choice"], "required");
|
||||
assert_eq!(
|
||||
body["tools"][0]["function"]["name"],
|
||||
OPENROUTER_TERMINAL_TOOL_NAME
|
||||
);
|
||||
|
||||
let response = serde_json::json!({
|
||||
"id": "response",
|
||||
"model": "ox",
|
||||
"choices": [{"message": {"tool_calls": [{
|
||||
"id": "terminal",
|
||||
"function": {"name": OPENROUTER_TERMINAL_TOOL_NAME, "arguments": "{\"verdict\":\"pass\"}"}
|
||||
}]}}]
|
||||
});
|
||||
let events =
|
||||
openrouter_events_from_response(&request, &serde_json::to_vec(&response).unwrap())
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
&events[0].payload,
|
||||
EpiphanyModelStreamPayload::TextDelta { text } if text == "{\"verdict\":\"pass\"}"
|
||||
));
|
||||
assert!(matches!(
|
||||
&events[1].payload,
|
||||
EpiphanyModelStreamPayload::Completed { receipt }
|
||||
if receipt.transport.as_deref() == Some("openrouter-chat-completions")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
[package]
|
||||
name = "epiphany-openai-adapter"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "epiphany_openai_adapter"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
cultcache-rs = { path = "../vendor/cultcache-rs" }
|
||||
epiphany-model-adapter = { path = "../epiphany-model-adapter" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
|
@ -1,309 +0,0 @@
|
|||
use cultcache_rs::DatabaseEntry;
|
||||
use epiphany_model_adapter::{EpiphanyModelInputItem, EpiphanyModelRequest};
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub const OPENAI_ADAPTER_REQUEST_SCHEMA_ID: &str = "epiphany.openai_model_request.v1";
|
||||
pub const OPENAI_ADAPTER_EVENT_SCHEMA_ID: &str = "epiphany.openai_model_stream_event.v0";
|
||||
pub const OPENAI_ADAPTER_RECEIPT_SCHEMA_ID: &str = "epiphany.openai_model_receipt.v0";
|
||||
pub const OPENAI_ADAPTER_STATUS_SCHEMA_ID: &str = "epiphany.openai_adapter_status.v0";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum EpiphanyOpenAiAuthMode {
|
||||
ChatGptSubscription,
|
||||
ApiKey,
|
||||
ExternalBearer,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum EpiphanyOpenAiWireDialect {
|
||||
Responses,
|
||||
ChatCompletionsTerminalTool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.openai_adapter_status.v0",
|
||||
schema = "EpiphanyOpenAiAdapterStatus"
|
||||
)]
|
||||
pub struct EpiphanyOpenAiAdapterStatus {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub adapter_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub auth_mode: EpiphanyOpenAiAuthMode,
|
||||
#[cultcache(key = 3, default)]
|
||||
pub account_id: Option<String>,
|
||||
#[cultcache(key = 4, default)]
|
||||
pub plan_type: Option<String>,
|
||||
#[cultcache(key = 5, default)]
|
||||
pub default_model: Option<String>,
|
||||
#[cultcache(key = 6)]
|
||||
pub supports_websockets: bool,
|
||||
#[cultcache(key = 7)]
|
||||
pub codex_transport_attached: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.openai_model_request.v1",
|
||||
schema = "EpiphanyOpenAiModelRequest"
|
||||
)]
|
||||
pub struct EpiphanyOpenAiModelRequest {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub conversation_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub model: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub instructions: String,
|
||||
#[cultcache(key = 5, default)]
|
||||
pub input: Vec<EpiphanyOpenAiInputItem>,
|
||||
#[cultcache(key = 6, default)]
|
||||
pub reasoning_effort: Option<String>,
|
||||
#[cultcache(key = 7, default)]
|
||||
pub reasoning_summary: Option<String>,
|
||||
#[cultcache(key = 8, default)]
|
||||
pub service_tier: Option<String>,
|
||||
#[cultcache(key = 9, default)]
|
||||
pub output_contract_id: Option<String>,
|
||||
#[cultcache(key = 10, default)]
|
||||
pub previous_response_id: Option<String>,
|
||||
#[cultcache(key = 11, default)]
|
||||
pub tools: Vec<EpiphanyOpenAiToolDefinition>,
|
||||
#[cultcache(key = 12, default)]
|
||||
pub output_schema_json: Option<String>,
|
||||
#[cultcache(key = 13)]
|
||||
pub provider_id: String,
|
||||
#[cultcache(key = 14)]
|
||||
pub wire_dialect: EpiphanyOpenAiWireDialect,
|
||||
}
|
||||
|
||||
impl EpiphanyOpenAiModelRequest {
|
||||
pub fn new(
|
||||
request_id: impl Into<String>,
|
||||
conversation_id: impl Into<String>,
|
||||
model: impl Into<String>,
|
||||
instructions: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
schema_id: OPENAI_ADAPTER_REQUEST_SCHEMA_ID.to_string(),
|
||||
request_id: request_id.into(),
|
||||
conversation_id: conversation_id.into(),
|
||||
model: model.into(),
|
||||
instructions: instructions.into(),
|
||||
input: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
reasoning_summary: None,
|
||||
service_tier: None,
|
||||
output_contract_id: None,
|
||||
previous_response_id: None,
|
||||
tools: Vec::new(),
|
||||
output_schema_json: None,
|
||||
provider_id: "openai-codex".to_string(),
|
||||
wire_dialect: EpiphanyOpenAiWireDialect::Responses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The only lowering from a native model request to the OpenAI transport.
|
||||
/// Internal audit identity stays on the native request; every provider-bearing
|
||||
/// byte is derived here.
|
||||
pub fn request_from_native(request: &EpiphanyModelRequest) -> EpiphanyOpenAiModelRequest {
|
||||
EpiphanyOpenAiModelRequest {
|
||||
schema_id: OPENAI_ADAPTER_REQUEST_SCHEMA_ID.to_string(),
|
||||
request_id: request.request_id.clone(),
|
||||
conversation_id: request.conversation_id.clone(),
|
||||
model: request.model.clone(),
|
||||
instructions: request.instructions.clone(),
|
||||
input: request.input.iter().map(input_from_native).collect(),
|
||||
reasoning_effort: request.reasoning_effort.clone(),
|
||||
reasoning_summary: request.reasoning_summary.clone(),
|
||||
service_tier: request.service_tier.clone(),
|
||||
output_contract_id: request.output_contract_id.clone(),
|
||||
previous_response_id: request.previous_response_id.clone(),
|
||||
tools: request
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| EpiphanyOpenAiToolDefinition {
|
||||
name: tool.name.clone(),
|
||||
description: tool.description.clone(),
|
||||
parameters_json: tool.parameters_json.clone(),
|
||||
})
|
||||
.collect(),
|
||||
output_schema_json: request.output_schema_json.clone(),
|
||||
provider_id: request.provider.clone(),
|
||||
wire_dialect: if request.provider == "openrouter" {
|
||||
EpiphanyOpenAiWireDialect::ChatCompletionsTerminalTool
|
||||
} else {
|
||||
EpiphanyOpenAiWireDialect::Responses
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn input_from_native(input: &EpiphanyModelInputItem) -> EpiphanyOpenAiInputItem {
|
||||
match input {
|
||||
EpiphanyModelInputItem::UserText { text } => {
|
||||
EpiphanyOpenAiInputItem::UserText { text: text.clone() }
|
||||
}
|
||||
EpiphanyModelInputItem::AssistantText { text } => {
|
||||
EpiphanyOpenAiInputItem::AssistantText { text: text.clone() }
|
||||
}
|
||||
EpiphanyModelInputItem::ToolCall {
|
||||
call_id,
|
||||
name,
|
||||
arguments,
|
||||
} => EpiphanyOpenAiInputItem::ToolCall {
|
||||
call_id: call_id.clone(),
|
||||
name: name.clone(),
|
||||
arguments: arguments.clone(),
|
||||
},
|
||||
EpiphanyModelInputItem::ToolResult { call_id, output } => {
|
||||
EpiphanyOpenAiInputItem::ToolResult {
|
||||
call_id: call_id.clone(),
|
||||
output: output.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EpiphanyOpenAiToolDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters_json: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum EpiphanyOpenAiInputItem {
|
||||
UserText {
|
||||
text: String,
|
||||
},
|
||||
AssistantText {
|
||||
text: String,
|
||||
},
|
||||
ToolCall {
|
||||
call_id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
},
|
||||
ToolResult {
|
||||
call_id: String,
|
||||
output: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.openai_model_stream_event.v0",
|
||||
schema = "EpiphanyOpenAiStreamEvent"
|
||||
)]
|
||||
pub struct EpiphanyOpenAiStreamEvent {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub sequence: u64,
|
||||
#[cultcache(key = 3)]
|
||||
pub payload: EpiphanyOpenAiStreamPayload,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum EpiphanyOpenAiStreamPayload {
|
||||
TextDelta {
|
||||
text: String,
|
||||
},
|
||||
ReasoningDelta {
|
||||
text: String,
|
||||
},
|
||||
ToolCall {
|
||||
call_id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
},
|
||||
Completed {
|
||||
receipt: EpiphanyOpenAiModelReceipt,
|
||||
},
|
||||
Failed {
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.openai_model_receipt.v0",
|
||||
schema = "EpiphanyOpenAiModelReceipt"
|
||||
)]
|
||||
pub struct EpiphanyOpenAiModelReceipt {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub model: String,
|
||||
#[cultcache(key = 3, default)]
|
||||
pub response_id: Option<String>,
|
||||
#[cultcache(key = 4, default)]
|
||||
pub input_tokens: Option<u64>,
|
||||
#[cultcache(key = 5, default)]
|
||||
pub output_tokens: Option<u64>,
|
||||
#[cultcache(key = 6, default)]
|
||||
pub reasoning_output_tokens: Option<u64>,
|
||||
#[cultcache(key = 7, default)]
|
||||
pub transport: Option<String>,
|
||||
}
|
||||
|
||||
impl EpiphanyOpenAiModelReceipt {
|
||||
pub fn new(request_id: impl Into<String>, model: impl Into<String>) -> Self {
|
||||
Self {
|
||||
schema_id: OPENAI_ADAPTER_RECEIPT_SCHEMA_ID.to_string(),
|
||||
request_id: request_id.into(),
|
||||
model: model.into(),
|
||||
response_id: None,
|
||||
input_tokens: None,
|
||||
output_tokens: None,
|
||||
reasoning_output_tokens: None,
|
||||
transport: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn native_provider_identity_selects_one_exact_wire_dialect() {
|
||||
let openai = EpiphanyModelRequest::new(
|
||||
"openai-request",
|
||||
"conversation",
|
||||
"openai-codex",
|
||||
"gpt-test",
|
||||
"decide",
|
||||
);
|
||||
let openrouter = EpiphanyModelRequest::new(
|
||||
"openrouter-request",
|
||||
"conversation",
|
||||
"openrouter",
|
||||
"stealth/ox-alpha",
|
||||
"decide",
|
||||
);
|
||||
|
||||
let openai = request_from_native(&openai);
|
||||
let openrouter = request_from_native(&openrouter);
|
||||
assert_eq!(openai.provider_id, "openai-codex");
|
||||
assert_eq!(openai.wire_dialect, EpiphanyOpenAiWireDialect::Responses);
|
||||
assert_eq!(openrouter.provider_id, "openrouter");
|
||||
assert_eq!(
|
||||
openrouter.wire_dialect,
|
||||
EpiphanyOpenAiWireDialect::ChatCompletionsTerminalTool
|
||||
);
|
||||
assert_ne!(openai.schema_id, "epiphany.openai_model_request.v0");
|
||||
}
|
||||
}
|
||||
1
epiphany-openai-auth-spine/.gitignore
vendored
1
epiphany-openai-auth-spine/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
/target/
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
[package]
|
||||
name = "epiphany-openai-codex-spine"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "epiphany_openai_codex_spine"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
codex-client = { path = "../vendor/codex/codex-rs/codex-client" }
|
||||
codex-login = { path = "../vendor/codex/codex-rs/login" }
|
||||
epiphany-openai-adapter = { path = "../epiphany-openai-adapter" }
|
||||
futures = "0.3"
|
||||
http = "1"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -13,11 +13,9 @@ path = "src/lib.rs"
|
|||
[dependencies]
|
||||
anyhow = "1"
|
||||
chrono = "0.4.44"
|
||||
cultcache-rs = { path = "../vendor/cultcache-rs" }
|
||||
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
|
||||
epiphany-core = { path = "../epiphany-core" }
|
||||
epiphany-model-adapter = { path = "../epiphany-model-adapter" }
|
||||
epiphany-openai-adapter = { path = "../epiphany-openai-adapter" }
|
||||
epiphany-openai-codex-spine = { path = "../epiphany-openai-codex-spine" }
|
||||
epiphany-tool-adapter = { path = "../epiphany-tool-adapter" }
|
||||
rmp-serde = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
|
@ -27,5 +25,4 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
|||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
epiphany-state-model = { path = "../epiphany-state-model" }
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
include!("epiphany-openai-runtime.rs");
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
use std::{env, net::SocketAddr, path::PathBuf, thread, time::Duration};
|
||||
use std::{env, future::Future, net::SocketAddr, path::PathBuf, pin::Pin, thread, time::Duration};
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use cultcache_rs::DatabaseEntry;
|
||||
|
|
@ -17,10 +17,87 @@ use epiphany_core::{
|
|||
pulse_persona_social, reconcile_terminal_persona_conversation,
|
||||
retain_terminal_persona_conversations, validate_persona_discord_request_anchor,
|
||||
};
|
||||
use epiphany_model_adapter::EpiphanyModelRequest;
|
||||
use epiphany_openai_runtime::{
|
||||
NativePersonaModelRunner, PersonaModelExecutionPlan, execute_persona_model_turn,
|
||||
DEFAULT_CODEX_CONNECTOR_ENDPOINT, EpiphanyOpenAiRuntimeOptions, PersonaModelExecutionPlan,
|
||||
PersonaModelRunner, assistant_text_from_model_events, execute_persona_model_turn,
|
||||
};
|
||||
|
||||
#[path = "../provider_transport.rs"]
|
||||
mod provider_transport;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct NativePersonaModelRunner {
|
||||
store_path: PathBuf,
|
||||
provider_credential_path: Option<PathBuf>,
|
||||
connector_endpoint: SocketAddr,
|
||||
caller_runtime_id: String,
|
||||
provider: String,
|
||||
model: String,
|
||||
turn_timeout: Duration,
|
||||
}
|
||||
|
||||
impl PersonaModelRunner for NativePersonaModelRunner {
|
||||
fn run<'a>(
|
||||
&'a mut self,
|
||||
_store_path: &'a PathBuf,
|
||||
stage: &'a str,
|
||||
turn_id: &'a str,
|
||||
request: EpiphanyModelRequest,
|
||||
) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
if request.provider != self.provider || request.model != self.model {
|
||||
return Err(anyhow!("Persona execution plan and model runner disagree"));
|
||||
}
|
||||
let replay = assistant_text_from_model_events(&self.store_path, &request.request_id)?;
|
||||
if !replay.trim().is_empty() {
|
||||
return Ok(replay);
|
||||
}
|
||||
let options = EpiphanyOpenAiRuntimeOptions {
|
||||
store_path: self.store_path.clone(),
|
||||
provider_credential_path: self.provider_credential_path.clone(),
|
||||
connector_endpoint: Some(self.connector_endpoint),
|
||||
caller_runtime_id: self.caller_runtime_id.clone(),
|
||||
session_id: format!("persona-turn-{turn_id}"),
|
||||
job_id: format!("persona-{stage}-{turn_id}"),
|
||||
objective: format!("Run Persona {stage} stage for {turn_id}"),
|
||||
coordinator_note: "Native Persona model executor; transport owns inference only."
|
||||
.to_string(),
|
||||
default_model: Some(self.model.clone()),
|
||||
request_timeout: None,
|
||||
};
|
||||
let summary = tokio::time::timeout(
|
||||
self.turn_timeout,
|
||||
provider_transport::run_model_turn(
|
||||
&self.provider,
|
||||
options.clone(),
|
||||
request.clone(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
anyhow!(
|
||||
"Persona {stage} stage exceeded its {} second outer turn budget",
|
||||
self.turn_timeout.as_secs()
|
||||
)
|
||||
})??;
|
||||
let output = assistant_text_from_model_events(&self.store_path, &request.request_id)?;
|
||||
if summary.verdict != "pass" || output.trim().is_empty() {
|
||||
return Err(anyhow!(if summary.verdict != "pass" {
|
||||
summary.summary
|
||||
} else {
|
||||
format!("Persona {stage} stage completed without assistant text")
|
||||
}));
|
||||
}
|
||||
Ok(output)
|
||||
})
|
||||
}
|
||||
|
||||
fn recover(&mut self, request_id: &str) -> Result<String> {
|
||||
assistant_text_from_model_events(&self.store_path, request_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let options = Options::parse()?;
|
||||
|
|
@ -98,28 +175,30 @@ async fn poll_once(options: &Options) -> Result<bool> {
|
|||
)?;
|
||||
let mut runner = NativePersonaModelRunner {
|
||||
store_path: options.runtime_store.clone(),
|
||||
codex_home: options.codex_home.clone(),
|
||||
provider_credential_path: options.provider_credential_path.clone(),
|
||||
connector_endpoint: options.connector_endpoint,
|
||||
caller_runtime_id: options.runtime_id.clone(),
|
||||
provider: options.provider.clone(),
|
||||
model: options.model.clone(),
|
||||
turn_timeout: Duration::from_secs(options.turn_timeout_seconds),
|
||||
};
|
||||
let model_terminal = match execute_persona_model_turn(&plan, &mut runner).await {
|
||||
Ok(receipt) => receipt,
|
||||
Err(error) if error.to_string().contains("braked") => return Ok(false),
|
||||
Err(error) => {
|
||||
complete_persona_social_turn(
|
||||
&options.social_store,
|
||||
PersonaTurnTerminalOptions {
|
||||
request_id: request.request_id,
|
||||
outcome: "failed".into(),
|
||||
delivery_evidence: None,
|
||||
blocked_evidence: None,
|
||||
},
|
||||
)?;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let model_terminal =
|
||||
match execute_persona_model_turn(&options.runtime_store, &plan, &mut runner).await {
|
||||
Ok(receipt) => receipt,
|
||||
Err(error) if error.to_string().contains("braked") => return Ok(false),
|
||||
Err(error) => {
|
||||
complete_persona_social_turn(
|
||||
&options.social_store,
|
||||
PersonaTurnTerminalOptions {
|
||||
request_id: request.request_id,
|
||||
outcome: "failed".into(),
|
||||
delivery_receipt: None,
|
||||
blocked_evidence: None,
|
||||
},
|
||||
)?;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let signer = open_persona_discord_request_identity(&options.mouth_identity_store)?;
|
||||
let request_anchor = load_persona_discord_service_anchor(&options.mouth_request_anchor)?;
|
||||
validate_persona_discord_request_anchor(&request_anchor, &options.runtime_id)?;
|
||||
|
|
@ -289,8 +368,8 @@ struct Options {
|
|||
runtime_store: PathBuf,
|
||||
social_store: PathBuf,
|
||||
cultmesh_store: PathBuf,
|
||||
codex_home: PathBuf,
|
||||
provider_credential_path: Option<PathBuf>,
|
||||
connector_endpoint: SocketAddr,
|
||||
runtime_id: String,
|
||||
provider: String,
|
||||
model: String,
|
||||
|
|
@ -336,8 +415,14 @@ impl Options {
|
|||
runtime_store: path("--runtime-store")?,
|
||||
social_store: path("--persona-social-store")?,
|
||||
cultmesh_store: path("--cultmesh-store")?,
|
||||
codex_home: path("--codex-home")?,
|
||||
provider_credential_path: values.get("--provider-credential").map(PathBuf::from),
|
||||
connector_endpoint: value(
|
||||
&values,
|
||||
"--connector-endpoint",
|
||||
&DEFAULT_CODEX_CONNECTOR_ENDPOINT.to_string(),
|
||||
)
|
||||
.parse()
|
||||
.context("--connector-endpoint must be an IP:port socket address")?,
|
||||
_repo_root: path("--repo-root")?,
|
||||
mouth_request_store: path("--mouth-request-store")?,
|
||||
mouth_receipt_store: path("--mouth-receipt-store")?,
|
||||
|
|
@ -435,8 +520,8 @@ mod tests {
|
|||
runtime_store: runtime_store.clone(),
|
||||
social_store: absent.join("persona-social.cc"),
|
||||
cultmesh_store: absent.join("cultmesh.cc"),
|
||||
codex_home: absent.join("codex-home"),
|
||||
provider_credential_path: None,
|
||||
connector_endpoint: DEFAULT_CODEX_CONNECTOR_ENDPOINT,
|
||||
runtime_id: "persona-reentry".into(),
|
||||
provider: "test".into(),
|
||||
model: "test-model".into(),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,6 @@
|
|||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use chrono::SecondsFormat;
|
||||
|
|
@ -11,18 +10,14 @@ use epiphany_core::{
|
|||
PERSONA_MODEL_STAGE_RECEIPT_SCHEMA_VERSION, PERSONA_MODEL_TERMINAL_RECEIPT_SCHEMA_VERSION,
|
||||
PersonaInterpreterEffectDocument, PersonaInterpreterInput, PersonaModelStageReceipt,
|
||||
PersonaModelTerminalReceipt, PersonaProjectorInput, PersonaTranscriptMessage, PersonaTurnInput,
|
||||
RuntimeSpineSessionClosureOptions, build_persona_interpreter_prompt,
|
||||
build_persona_projector_prompt_with_transcript, build_persona_turn_prompt,
|
||||
close_runtime_session, model_pass_failure_for_request,
|
||||
build_persona_interpreter_prompt, build_persona_projector_prompt_with_transcript,
|
||||
build_persona_turn_prompt, close_runtime_session, model_pass_failure_for_request,
|
||||
parse_and_validate_persona_interpreter_effect_set, persona_interpreter_effect_set_json_schema,
|
||||
runtime_spine_cache,
|
||||
terminalize_model_pass_failure_session,
|
||||
runtime_spine_cache, terminalize_model_pass_failure_session,
|
||||
};
|
||||
use epiphany_model_adapter::{EpiphanyModelInputItem, EpiphanyModelRequest};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::{EpiphanyOpenAiRuntimeOptions, assistant_text_from_model_events, run_model_turn};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PersonaModelExecutionPlan {
|
||||
turn_id: String,
|
||||
|
|
@ -62,16 +57,6 @@ impl PersonaModelExecutionPlan {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NativePersonaModelRunner {
|
||||
pub store_path: PathBuf,
|
||||
pub codex_home: PathBuf,
|
||||
pub provider_credential_path: Option<PathBuf>,
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
pub turn_timeout: Duration,
|
||||
}
|
||||
|
||||
pub trait PersonaModelRunner {
|
||||
fn run<'a>(
|
||||
&'a mut self,
|
||||
|
|
@ -83,97 +68,23 @@ pub trait PersonaModelRunner {
|
|||
fn recover(&mut self, request_id: &str) -> Result<String>;
|
||||
}
|
||||
|
||||
impl PersonaModelRunner for NativePersonaModelRunner {
|
||||
fn run<'a>(
|
||||
&'a mut self,
|
||||
_store_path: &'a PathBuf,
|
||||
stage: &'a str,
|
||||
turn_id: &'a str,
|
||||
request: EpiphanyModelRequest,
|
||||
) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
let replay = assistant_text_from_model_events(&self.store_path, &request.request_id)?;
|
||||
if !replay.trim().is_empty() {
|
||||
return Ok(replay);
|
||||
}
|
||||
let options = EpiphanyOpenAiRuntimeOptions {
|
||||
store_path: self.store_path.clone(),
|
||||
codex_home: self.codex_home.clone(),
|
||||
provider_credential_path: self.provider_credential_path.clone(),
|
||||
session_id: format!("persona-turn-{turn_id}"),
|
||||
job_id: format!("persona-{stage}-{turn_id}"),
|
||||
objective: format!("Run Persona {stage} stage for {turn_id}"),
|
||||
coordinator_note: "Native Persona model executor; transport owns inference only."
|
||||
.to_string(),
|
||||
default_model: Some(self.model.clone()),
|
||||
// Persona owns one outer typed turn budget. A second transport
|
||||
// timer would give timeout policy two conflicting owners.
|
||||
request_timeout: None,
|
||||
};
|
||||
let summary = tokio::time::timeout(
|
||||
self.turn_timeout,
|
||||
run_model_turn(&self.provider, options.clone(), request.clone()),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
anyhow!(
|
||||
"Persona {stage} stage exceeded its {} second outer turn budget",
|
||||
self.turn_timeout.as_secs()
|
||||
)
|
||||
})??;
|
||||
let output = assistant_text_from_model_events(&self.store_path, &request.request_id)?;
|
||||
if summary.verdict != "pass" || output.trim().is_empty() {
|
||||
let failure_summary = if summary.verdict != "pass" {
|
||||
summary.summary.clone()
|
||||
} else {
|
||||
format!("Persona {stage} stage completed without assistant text")
|
||||
};
|
||||
return Err(anyhow!(failure_summary));
|
||||
}
|
||||
Ok(output)
|
||||
})
|
||||
}
|
||||
|
||||
fn recover(&mut self, request_id: &str) -> Result<String> {
|
||||
assistant_text_from_model_events(&self.store_path, request_id)
|
||||
}
|
||||
}
|
||||
|
||||
struct CompletedPersonaStage {
|
||||
receipt: PersonaModelStageReceipt,
|
||||
output: String,
|
||||
}
|
||||
|
||||
pub async fn execute_persona_model_turn(
|
||||
plan: &PersonaModelExecutionPlan,
|
||||
runner: &mut NativePersonaModelRunner,
|
||||
) -> Result<PersonaModelTerminalReceipt> {
|
||||
if runner.provider != plan.provider || runner.model != plan.model {
|
||||
return Err(anyhow!("Persona execution plan and model runner disagree"));
|
||||
}
|
||||
let store_path = runner.store_path.clone();
|
||||
execute_persona_model_turn_owned(&store_path, plan, runner).await
|
||||
}
|
||||
|
||||
async fn execute_persona_model_turn_owned<R: PersonaModelRunner>(
|
||||
pub async fn execute_persona_model_turn<R: PersonaModelRunner>(
|
||||
store_path: &PathBuf,
|
||||
plan: &PersonaModelExecutionPlan,
|
||||
runner: &mut R,
|
||||
) -> Result<PersonaModelTerminalReceipt> {
|
||||
match execute_persona_model_turn_with_runner(store_path, plan, runner).await {
|
||||
Ok(terminal) => {
|
||||
close_persona_session_if_active(
|
||||
store_path,
|
||||
plan,
|
||||
format!(
|
||||
"Persona turn {} reached terminal decision {}.",
|
||||
plan.turn_id, terminal.receipt_id
|
||||
),
|
||||
)?;
|
||||
close_persona_session_if_active(store_path, plan)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
Err(error) => {
|
||||
terminalize_persona_execution_error(store_path, plan, &error)?;
|
||||
terminalize_persona_execution_error(store_path, plan)?;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
|
|
@ -182,7 +93,6 @@ async fn execute_persona_model_turn_owned<R: PersonaModelRunner>(
|
|||
fn terminalize_persona_execution_error(
|
||||
store_path: &Path,
|
||||
plan: &PersonaModelExecutionPlan,
|
||||
error: &anyhow::Error,
|
||||
) -> Result<()> {
|
||||
for stage in ["interpreter", "persona", "projector"] {
|
||||
let request_id = stage_request_id(&plan.turn_id, stage);
|
||||
|
|
@ -190,17 +100,12 @@ fn terminalize_persona_execution_error(
|
|||
return Ok(());
|
||||
}
|
||||
}
|
||||
close_persona_session_if_active(
|
||||
store_path,
|
||||
plan,
|
||||
format!("Persona turn orchestration refused: {error:#}"),
|
||||
)
|
||||
close_persona_session_if_active(store_path, plan)
|
||||
}
|
||||
|
||||
fn close_persona_session_if_active(
|
||||
store_path: &Path,
|
||||
plan: &PersonaModelExecutionPlan,
|
||||
summary: String,
|
||||
) -> Result<()> {
|
||||
let session_id = format!("persona-turn-{}", plan.turn_id);
|
||||
let mut cache = runtime_spine_cache(store_path)?;
|
||||
|
|
@ -211,14 +116,7 @@ fn close_persona_session_if_active(
|
|||
session.status == epiphany_core::EpiphanyRuntimeSessionStatus::Active
|
||||
})
|
||||
{
|
||||
close_runtime_session(
|
||||
store_path,
|
||||
RuntimeSpineSessionClosureOptions {
|
||||
session_id,
|
||||
completed_at: now(),
|
||||
summary,
|
||||
},
|
||||
)?;
|
||||
close_runtime_session(store_path, &session_id)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -692,22 +590,11 @@ mod tests {
|
|||
}
|
||||
epiphany_core::open_runtime_model_execution(
|
||||
store_path,
|
||||
epiphany_core::RuntimeSpineSessionOptions {
|
||||
session_id: format!("persona-turn-{turn_id}"),
|
||||
objective: format!("Run Persona {stage} test stage"),
|
||||
created_at: "2026-08-14T00:00:00Z".into(),
|
||||
coordinator_note: "Persona test".into(),
|
||||
},
|
||||
epiphany_core::RuntimeSpineJobOptions {
|
||||
job_id: format!("persona-{stage}-{turn_id}"),
|
||||
session_id: format!("persona-turn-{turn_id}"),
|
||||
role: "openai-model-adapter".into(),
|
||||
created_at: "2026-08-14T00:00:00Z".into(),
|
||||
summary: format!("Run Persona {stage} test stage"),
|
||||
artifact_refs: Vec::new(),
|
||||
},
|
||||
&request,
|
||||
"2026-08-14T00:00:00Z",
|
||||
)?;
|
||||
let output = match stage {
|
||||
"projector" => "Epiphany feels the direct call as a live obligation, while remaining tired and sharply curious about her own machinery.".to_string(),
|
||||
|
|
@ -715,19 +602,12 @@ mod tests {
|
|||
"interpreter" => r#"{"schemaVersion":"epiphany.persona_interpreter_effect_set.v0","effects":[{"kind":"say","channel_id":"aquarium","reply_to_message_id":"m1","content":"The native conversational nerve is being connected, and I can show the receipts.","speech_act":"status","register":"concise","target_audience":"the addressed Discord room","safety_notes":[]}]}"#.to_string(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
epiphany_core::complete_runtime_job(
|
||||
epiphany_core::terminalize_runtime_job(
|
||||
store_path,
|
||||
epiphany_core::RuntimeSpineJobResultOptions {
|
||||
result_id: format!("persona-{stage}-{turn_id}-transport-result"),
|
||||
job_id: format!("persona-{stage}-{turn_id}"),
|
||||
completed_at: "2026-08-14T00:00:01Z".into(),
|
||||
verdict: "completed".into(),
|
||||
summary: format!("Persona {stage} test transport completed"),
|
||||
next_safe_move: "Seal the stage decision context".into(),
|
||||
evidence_refs: Vec::new(),
|
||||
artifact_refs: Vec::new(),
|
||||
decision_context_id: None,
|
||||
},
|
||||
&format!("persona-{stage}-{turn_id}"),
|
||||
epiphany_core::EpiphanyRuntimeJobStatus::Completed,
|
||||
"2026-08-14T00:00:01Z",
|
||||
&request.request_id,
|
||||
)?;
|
||||
Ok(output)
|
||||
})
|
||||
|
|
@ -807,7 +687,7 @@ mod tests {
|
|||
PersonaModelExecutionPlan::from_admitted_input(
|
||||
store,
|
||||
"turn-1",
|
||||
"test",
|
||||
crate::DEFAULT_MODEL_PROVIDER,
|
||||
"test-model",
|
||||
cultmesh_store,
|
||||
"epiphany-test",
|
||||
|
|
@ -831,7 +711,7 @@ mod tests {
|
|||
release_brake(&cultmesh)?;
|
||||
let mut runner = FakeRunner { calls: vec![] };
|
||||
let first =
|
||||
execute_persona_model_turn_owned(&store, &plan(&store, cultmesh.clone())?, &mut runner)
|
||||
execute_persona_model_turn(&store, &plan(&store, cultmesh.clone())?, &mut runner)
|
||||
.await?;
|
||||
assert_eq!(runner.calls, ["projector", "persona", "interpreter"]);
|
||||
drop(runner);
|
||||
|
|
@ -840,8 +720,7 @@ mod tests {
|
|||
let admitted_source = restarted_plan.source_documents[0].clone();
|
||||
let mut restarted_runner = FakeRunner { calls: vec![] };
|
||||
let second =
|
||||
execute_persona_model_turn_owned(&store, &restarted_plan, &mut restarted_runner)
|
||||
.await?;
|
||||
execute_persona_model_turn(&store, &restarted_plan, &mut restarted_runner).await?;
|
||||
assert_eq!(first, second);
|
||||
assert!(restarted_runner.calls.is_empty());
|
||||
for receipt_id in &second.stage_receipt_ids {
|
||||
|
|
@ -879,7 +758,7 @@ mod tests {
|
|||
let mut runner = FakeRunner { calls: vec![] };
|
||||
let escaped = plan_with_channels(&store, cultmesh, vec!["elsewhere".into()])?;
|
||||
assert!(
|
||||
execute_persona_model_turn_owned(&store, &escaped, &mut runner)
|
||||
execute_persona_model_turn(&store, &escaped, &mut runner)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
|
|
|||
308
epiphany-openai-runtime/src/provider_transport.rs
Normal file
308
epiphany-openai-runtime/src/provider_transport.rs
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use codex_connector::{
|
||||
CodexConnectorClient, CodexProviderRequest, CodexTransportDisposition,
|
||||
CodexTransportEventPayload, CodexTransportInvocation, CodexTransportOutcome,
|
||||
};
|
||||
use epiphany_model_adapter::{
|
||||
EpiphanyModelReceipt, EpiphanyModelRequest, EpiphanyModelStreamEvent,
|
||||
EpiphanyModelStreamPayload, MODEL_ADAPTER_EVENT_SCHEMA_ID,
|
||||
};
|
||||
use epiphany_model_adapter::{
|
||||
EpiphanyOpenRouterRequest, EpiphanyProviderRequestPayload, openrouter_events_from_response,
|
||||
openrouter_request_body,
|
||||
};
|
||||
use epiphany_openai_runtime::{
|
||||
EpiphanyOpenAiRuntimeOptions, EpiphanyOpenAiRuntimeRunSummary, OPENROUTER_MODEL_PROVIDER,
|
||||
open_model_turn, record_model_turn_events,
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
const OPENROUTER_COMPLETIONS_URL: &str = "https://openrouter.ai/api/v1/chat/completions";
|
||||
const MAX_CONNECTOR_FRAME_BYTES: usize = 32 * 1024 * 1024;
|
||||
const MAX_OPENROUTER_RESPONSE_BYTES: u64 = 16 * 1024 * 1024;
|
||||
const INVOCATION_ADMISSION_WINDOW: Duration = Duration::from_secs(60);
|
||||
|
||||
pub async fn run_model_turn(
|
||||
provider: &str,
|
||||
options: EpiphanyOpenAiRuntimeOptions,
|
||||
request: EpiphanyModelRequest,
|
||||
) -> Result<EpiphanyOpenAiRuntimeRunSummary> {
|
||||
let provider_request = open_model_turn(provider, &options, &request)?;
|
||||
let request_id = request.request_id.clone();
|
||||
let events = match (provider, provider_request.payload) {
|
||||
("openai-codex" | "openai", EpiphanyProviderRequestPayload::Codex(codex_request)) => {
|
||||
collect_transport_events(
|
||||
execute_codex_connector(&options, &request, codex_request),
|
||||
&request_id,
|
||||
provider,
|
||||
)
|
||||
.await
|
||||
}
|
||||
(OPENROUTER_MODEL_PROVIDER, EpiphanyProviderRequestPayload::OpenRouter(request)) => {
|
||||
collect_transport_events(execute_openrouter(&options, request), &request_id, provider)
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"provider request contract disagrees with selected provider"
|
||||
));
|
||||
}
|
||||
};
|
||||
record_model_turn_events(&options.store_path, &options, &request, &events)
|
||||
}
|
||||
|
||||
async fn execute_codex_connector(
|
||||
options: &EpiphanyOpenAiRuntimeOptions,
|
||||
native_request: &EpiphanyModelRequest,
|
||||
provider_request: CodexProviderRequest,
|
||||
) -> Result<Vec<EpiphanyModelStreamEvent>> {
|
||||
let endpoint = options
|
||||
.connector_endpoint
|
||||
.ok_or_else(|| anyhow!("Codex provider requires an explicit connector endpoint"))?;
|
||||
let caller_runtime_id = options.caller_runtime_id.clone();
|
||||
if caller_runtime_id.trim().is_empty() || caller_runtime_id.trim() != caller_runtime_id {
|
||||
return Err(anyhow!(
|
||||
"Codex connector caller runtime identity is invalid"
|
||||
));
|
||||
}
|
||||
let connection_key = Zeroizing::new(read_static_provider_credential(
|
||||
options.provider_credential_path.as_deref(),
|
||||
"codex-connector",
|
||||
)?);
|
||||
let native_request_sha256: [u8; 32] = Sha256::digest(rmp_serde::to_vec(native_request)?).into();
|
||||
let expires_at_unix_ms = unix_time_ms()?
|
||||
.checked_add(INVOCATION_ADMISSION_WINDOW.as_millis() as u64)
|
||||
.ok_or_else(|| anyhow!("Codex connector invocation expiry overflowed"))?;
|
||||
let invocation = CodexTransportInvocation::new(
|
||||
caller_runtime_id,
|
||||
expires_at_unix_ms,
|
||||
native_request_sha256,
|
||||
provider_request,
|
||||
)?;
|
||||
let client = CodexConnectorClient::new(
|
||||
endpoint,
|
||||
connection_key.as_str().to_string(),
|
||||
MAX_CONNECTOR_FRAME_BYTES,
|
||||
options.request_timeout,
|
||||
)?;
|
||||
let provider = native_request.provider.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let result = client.execute(&invocation)?;
|
||||
events_from_connector_result(&provider, result)
|
||||
})
|
||||
.await
|
||||
.context("Codex connector client task failed")?
|
||||
}
|
||||
|
||||
pub(super) fn events_from_connector_result(
|
||||
provider: &str,
|
||||
result: codex_connector::CodexTransportResult,
|
||||
) -> Result<Vec<EpiphanyModelStreamEvent>> {
|
||||
let request_id = result.request_id.clone();
|
||||
match result.disposition {
|
||||
CodexTransportDisposition::Refused(reason) => Err(anyhow!(
|
||||
"Codex connector refused request {request_id:?}: {reason:?}"
|
||||
)),
|
||||
CodexTransportDisposition::Transported { events, receipt } => {
|
||||
let mut projected = events
|
||||
.into_iter()
|
||||
.map(|event| EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id: request_id.clone(),
|
||||
provider: provider.to_string(),
|
||||
sequence: event.sequence,
|
||||
payload: match event.payload {
|
||||
CodexTransportEventPayload::TextDelta { text } => {
|
||||
EpiphanyModelStreamPayload::TextDelta { text }
|
||||
}
|
||||
CodexTransportEventPayload::ToolCall {
|
||||
call_id,
|
||||
name,
|
||||
arguments,
|
||||
} => EpiphanyModelStreamPayload::ToolCall {
|
||||
call_id,
|
||||
name,
|
||||
arguments,
|
||||
},
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut terminal = EpiphanyModelReceipt::new(&request_id, provider, &receipt.model);
|
||||
terminal.transport = Some(receipt.transport.clone());
|
||||
terminal.caller_runtime_id = Some(receipt.caller_runtime_id.clone());
|
||||
terminal.native_request_sha256 = Some(hex_digest(receipt.native_request_sha256));
|
||||
terminal.provider_request_sha256 = Some(hex_digest(receipt.provider_request_sha256));
|
||||
match receipt.outcome {
|
||||
CodexTransportOutcome::Completed {
|
||||
provider_response_id,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
reasoning_output_tokens,
|
||||
cached_input_tokens,
|
||||
} => {
|
||||
terminal.provider_response_id = provider_response_id;
|
||||
terminal.input_tokens = input_tokens;
|
||||
terminal.output_tokens = output_tokens;
|
||||
terminal.reasoning_output_tokens = reasoning_output_tokens;
|
||||
terminal.cached_input_tokens = cached_input_tokens;
|
||||
projected.push(EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id,
|
||||
provider: provider.to_string(),
|
||||
sequence: projected.len() as u64,
|
||||
payload: EpiphanyModelStreamPayload::Completed {
|
||||
receipt: Box::new(terminal),
|
||||
},
|
||||
});
|
||||
}
|
||||
CodexTransportOutcome::Failed {
|
||||
failure_kind,
|
||||
message,
|
||||
} => {
|
||||
projected.push(EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id,
|
||||
provider: provider.to_string(),
|
||||
sequence: projected.len() as u64,
|
||||
payload: EpiphanyModelStreamPayload::Failed {
|
||||
message: format!("Codex connector {failure_kind}: {message}"),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(projected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_openrouter(
|
||||
options: &EpiphanyOpenAiRuntimeOptions,
|
||||
request: EpiphanyOpenRouterRequest,
|
||||
) -> Result<Vec<EpiphanyModelStreamEvent>> {
|
||||
let api_key = Zeroizing::new(read_static_provider_credential(
|
||||
options.provider_credential_path.as_deref(),
|
||||
OPENROUTER_MODEL_PROVIDER,
|
||||
)?);
|
||||
let request_timeout = options.request_timeout;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let body = serde_json::to_vec(&openrouter_request_body(&request)?)?;
|
||||
let agent: ureq::Agent = ureq::Agent::config_builder()
|
||||
.timeout_connect(Some(Duration::from_secs(10)))
|
||||
.timeout_recv_response(request_timeout)
|
||||
.timeout_recv_body(request_timeout)
|
||||
.max_redirects(0)
|
||||
.build()
|
||||
.into();
|
||||
let authorization = Zeroizing::new(format!("Bearer {}", api_key.as_str()));
|
||||
let response = agent
|
||||
.post(OPENROUTER_COMPLETIONS_URL)
|
||||
.header("authorization", authorization.as_str())
|
||||
.header("content-type", "application/json")
|
||||
.header("accept", "application/json")
|
||||
.send(body)
|
||||
.context("OpenRouter request failed")?;
|
||||
let mut bytes = Vec::new();
|
||||
response
|
||||
.into_body()
|
||||
.into_reader()
|
||||
.take(MAX_OPENROUTER_RESPONSE_BYTES + 1)
|
||||
.read_to_end(&mut bytes)
|
||||
.context("OpenRouter response read failed")?;
|
||||
if bytes.len() as u64 > MAX_OPENROUTER_RESPONSE_BYTES {
|
||||
return Err(anyhow!("OpenRouter response exceeded its byte bound"));
|
||||
}
|
||||
openrouter_events_from_response(&request, &bytes)
|
||||
})
|
||||
.await
|
||||
.context("OpenRouter client task failed")?
|
||||
}
|
||||
|
||||
async fn collect_transport_events<F>(
|
||||
future: F,
|
||||
request_id: &str,
|
||||
provider: &str,
|
||||
) -> Vec<EpiphanyModelStreamEvent>
|
||||
where
|
||||
F: std::future::Future<Output = Result<Vec<EpiphanyModelStreamEvent>>>,
|
||||
{
|
||||
match future.await {
|
||||
Ok(events) => events,
|
||||
Err(error) => vec![EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id: request_id.to_string(),
|
||||
provider: provider.to_string(),
|
||||
sequence: 0,
|
||||
payload: EpiphanyModelStreamPayload::Failed {
|
||||
message: format!("{error:#}"),
|
||||
},
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn read_static_provider_credential(path: Option<&Path>, provider: &str) -> Result<String> {
|
||||
let path = path.ok_or_else(|| {
|
||||
anyhow!("model provider {provider:?} requires an explicit credential file")
|
||||
})?;
|
||||
let raw = std::fs::read_to_string(path).with_context(|| {
|
||||
format!(
|
||||
"failed to read {provider} credential file {}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
let credential = raw.trim();
|
||||
if credential.is_empty() || raw.trim_matches(['\r', '\n']) != credential {
|
||||
return Err(anyhow!(
|
||||
"model provider {provider:?} credential file is empty or contains surrounding whitespace"
|
||||
));
|
||||
}
|
||||
Ok(credential.to_string())
|
||||
}
|
||||
|
||||
fn unix_time_ms() -> Result<u64> {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.context("system clock predates Unix epoch")?
|
||||
.as_millis()
|
||||
.try_into()
|
||||
.context("Unix time does not fit u64 milliseconds")
|
||||
}
|
||||
|
||||
fn hex_digest(digest: [u8; 32]) -> String {
|
||||
let mut value = String::with_capacity(64);
|
||||
for byte in digest {
|
||||
use std::fmt::Write as _;
|
||||
write!(&mut value, "{byte:02x}").expect("writing to a String cannot fail");
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn failed_transport_event_preserves_the_bounded_error_chain() {
|
||||
let events = collect_transport_events(
|
||||
async {
|
||||
Err(anyhow!("socket timed out"))
|
||||
.context("Connector client could not read its response")
|
||||
},
|
||||
"request-1",
|
||||
"openai-codex",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(events.len(), 1);
|
||||
let EpiphanyModelStreamPayload::Failed { message } = &events[0].payload else {
|
||||
panic!("transport failure must project one failed event");
|
||||
};
|
||||
assert_eq!(
|
||||
message,
|
||||
"Connector client could not read its response: socket timed out"
|
||||
);
|
||||
}
|
||||
}
|
||||
27
epiphany-pipeline/Cargo.toml
Normal file
27
epiphany-pipeline/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "epiphany-pipeline"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
autobins = false
|
||||
license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "epiphany_pipeline"
|
||||
path = "src/lib.rs"
|
||||
|
||||
# A leaf type library. Every dependency here is one the document types or the
|
||||
# write path need in a non-test build: the organ prepares, decodes and
|
||||
# validates envelopes through this crate, so `anyhow` (the cache's error type)
|
||||
# and `rmp-serde` (the payload decode) are normal dependencies. Only the
|
||||
# schema-publication check needs JSON.
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
chrono = "0.4.44"
|
||||
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "8fc74c709444b884a2bd339e0c306bf9484d99d2", package = "cultcache-rs" }
|
||||
rmp-serde = "1"
|
||||
schemars = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
3187
epiphany-pipeline/src/lib.rs
Normal file
3187
epiphany-pipeline/src/lib.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +0,0 @@
|
|||
[package]
|
||||
name = "epiphany-release-construction"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
autobins = false
|
||||
license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "epiphany_release_construction"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
chrono = "0.4.44"
|
||||
cultcache-rs = { path = "../vendor/cultcache-rs" }
|
||||
fs2 = "0.4"
|
||||
rmp-serde = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
[package]
|
||||
name = "epiphany-state-model"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "epiphany_state_model"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
schemars = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
ts-rs = { version = "11", features = [
|
||||
"uuid-impl",
|
||||
"serde-json-impl",
|
||||
"no-serde-warnings",
|
||||
] }
|
||||
|
|
@ -1,920 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyAcceptanceReceipt {
|
||||
pub id: String,
|
||||
pub result_id: String,
|
||||
pub job_id: String,
|
||||
pub binding_id: String,
|
||||
pub surface: String,
|
||||
pub role_id: String,
|
||||
pub status: String,
|
||||
pub accepted_at: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub accepted_observation_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub accepted_evidence_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyRuntimeLink {
|
||||
pub id: String,
|
||||
pub binding_id: String,
|
||||
pub surface: String,
|
||||
pub role_id: String,
|
||||
pub authority_scope: String,
|
||||
pub runtime_job_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub runtime_result_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub linked_subgoal_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub linked_graph_node_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanySubgoal {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub status: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyInvariant {
|
||||
pub id: String,
|
||||
pub description: String,
|
||||
pub status: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub rationale: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyGraphs {
|
||||
#[serde(default, skip_serializing_if = "EpiphanyGraph::is_empty")]
|
||||
pub architecture: EpiphanyGraph,
|
||||
#[serde(default, skip_serializing_if = "EpiphanyGraph::is_empty")]
|
||||
pub dataflow: EpiphanyGraph,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub links: Vec<EpiphanyGraphLink>,
|
||||
}
|
||||
|
||||
impl EpiphanyGraphs {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.architecture.is_empty() && self.dataflow.is_empty() && self.links.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyGraph {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub nodes: Vec<EpiphanyGraphNode>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub edges: Vec<EpiphanyGraphEdge>,
|
||||
}
|
||||
|
||||
impl EpiphanyGraph {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.nodes.is_empty() && self.edges.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyGraphNode {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub purpose: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub mechanism: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub metaphor: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub status: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyCodeRef>")]
|
||||
pub code_refs: Vec<EpiphanyCodeRef>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyGraphEdge {
|
||||
pub source_id: String,
|
||||
pub target_id: String,
|
||||
pub kind: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub label: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub mechanism: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyCodeRef>")]
|
||||
pub code_refs: Vec<EpiphanyCodeRef>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyGraphLink {
|
||||
pub dataflow_node_id: String,
|
||||
pub architecture_node_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub relationship: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyCodeRef>")]
|
||||
pub code_refs: Vec<EpiphanyCodeRef>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyCodeRef {
|
||||
pub path: PathBuf,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "number | null")]
|
||||
pub start_line: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "number | null")]
|
||||
pub end_line: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub symbol: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyGraphFrontier {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub active_node_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub active_edge_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub open_question_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub open_gap_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub dirty_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyGraphCheckpoint {
|
||||
pub checkpoint_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub summary: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub frontier_node_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub open_question_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub open_gap_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyRetrievalState {
|
||||
pub workspace_root: PathBuf,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub index_revision: Option<String>,
|
||||
#[serde(default)]
|
||||
pub status: EpiphanyRetrievalStatus,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
#[ts(type = "boolean")]
|
||||
pub semantic_available: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "bigint | null")]
|
||||
pub last_indexed_at_unix_seconds: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "number | null")]
|
||||
pub indexed_file_count: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "number | null")]
|
||||
pub indexed_chunk_count: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyRetrievalShardSummary>")]
|
||||
pub shards: Vec<EpiphanyRetrievalShardSummary>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub dirty_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
pub enum EpiphanyRetrievalStatus {
|
||||
#[default]
|
||||
Unavailable,
|
||||
Ready,
|
||||
Stale,
|
||||
Indexing,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyRetrievalShardSummary {
|
||||
pub shard_id: String,
|
||||
pub path_prefix: PathBuf,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "number | null")]
|
||||
pub indexed_file_count: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "number | null")]
|
||||
pub indexed_chunk_count: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub status: EpiphanyRetrievalStatus,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
#[ts(type = "boolean")]
|
||||
pub exact_available: bool,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
#[ts(type = "boolean")]
|
||||
pub semantic_available: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyScratchPad {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub summary: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub hypothesis: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub next_probe: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
pub enum EpiphanyInvestigationDisposition {
|
||||
#[default]
|
||||
ResumeReady,
|
||||
RegatherRequired,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyInvestigationCheckpoint {
|
||||
pub checkpoint_id: String,
|
||||
pub kind: String,
|
||||
pub disposition: EpiphanyInvestigationDisposition,
|
||||
pub focus: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub summary: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub next_action: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub captured_at_turn_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub open_questions: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyCodeRef>")]
|
||||
pub code_refs: Vec<EpiphanyCodeRef>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub evidence_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
pub enum EpiphanyJobKind {
|
||||
Indexing,
|
||||
Remap,
|
||||
Verification,
|
||||
Specialist,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct EpiphanyJobBinding {
|
||||
pub id: String,
|
||||
pub kind: EpiphanyJobKind,
|
||||
pub scope: String,
|
||||
pub owner_role: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub authority_scope: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub linked_subgoal_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub linked_graph_node_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub blocking_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyObservation {
|
||||
pub id: String,
|
||||
pub summary: String,
|
||||
pub source_kind: String,
|
||||
pub status: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub code_refs: Vec<EpiphanyCodeRef>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub evidence_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyEvidenceRecord {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub status: String,
|
||||
pub summary: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub code_refs: Vec<EpiphanyCodeRef>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyChurnState {
|
||||
pub understanding_status: String,
|
||||
pub diff_pressure: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub warning: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "number | null")]
|
||||
pub unexplained_writes: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyModeState {
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "ModeKind | null")]
|
||||
pub kind: Option<EpiphanyModeKind>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, JsonSchema, TS, Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EpiphanyModeKind {
|
||||
Plan,
|
||||
#[default]
|
||||
#[serde(
|
||||
alias = "code",
|
||||
alias = "pair_programming",
|
||||
alias = "execute",
|
||||
alias = "custom"
|
||||
)]
|
||||
Default,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyPlanningState {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub workspace_root: Option<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyPlanningCapture>")]
|
||||
pub captures: Vec<EpiphanyPlanningCapture>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyBacklogItem>")]
|
||||
pub backlog_items: Vec<EpiphanyBacklogItem>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyRoadmapStream>")]
|
||||
pub roadmap_streams: Vec<EpiphanyRoadmapStream>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyObjectiveDraft>")]
|
||||
pub objective_drafts: Vec<EpiphanyObjectiveDraft>,
|
||||
}
|
||||
|
||||
impl EpiphanyPlanningState {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.workspace_root.is_none()
|
||||
&& self.captures.is_empty()
|
||||
&& self.backlog_items.is_empty()
|
||||
&& self.roadmap_streams.is_empty()
|
||||
&& self.objective_drafts.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyPlanningCapture {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub body: Option<String>,
|
||||
pub confidence: String,
|
||||
pub status: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub speaker: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub tags: Vec<String>,
|
||||
pub source: EpiphanyPlanningSourceRef,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub created_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyPlanningSourceRef {
|
||||
pub kind: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub provider: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub uri: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub external_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub repo: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "number | null")]
|
||||
pub issue_number: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub node_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "bigint | null")]
|
||||
pub database_id: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub url: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub state: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub labels: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub milestone: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub assignees: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub author: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub created_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub updated_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub closed_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "boolean | null")]
|
||||
pub is_pull_request: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub imported_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyBacklogItem {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub kind: String,
|
||||
pub summary: String,
|
||||
pub status: String,
|
||||
pub horizon: String,
|
||||
pub priority: EpiphanyPlanningPriority,
|
||||
pub confidence: String,
|
||||
pub product_area: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub lane_hints: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub dependencies: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub blockers: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub acceptance_sketch: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub evidence_refs: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyPlanningSourceRef>")]
|
||||
pub source_refs: Vec<EpiphanyPlanningSourceRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub duplicate_of: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyPlanningPriority {
|
||||
pub value: String,
|
||||
pub rationale: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub impact: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub urgency: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub confidence: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub effort: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub unblocks: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub blocks: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyRoadmapStream {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub purpose: String,
|
||||
pub status: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub item_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub near_term_focus: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub blocked_by: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub review_cadence: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyObjectiveDraft {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub summary: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub source_item_ids: Vec<String>,
|
||||
pub scope: EpiphanyObjectiveDraftScope,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub acceptance_criteria: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub evidence_required: Vec<String>,
|
||||
pub lane_plan: EpiphanyObjectiveDraftLanePlan,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub dependencies: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub risks: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub review_gates: Vec<String>,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyObjectiveDraftScope {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub includes: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub excludes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyObjectiveDraftLanePlan {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub imagination: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub eyes: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub body: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub hands: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub soul: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub life: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyMemoryGraphSnapshot {
|
||||
pub graph_id: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyMemoryDomain>")]
|
||||
pub domains: Vec<EpiphanyMemoryDomain>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyMemoryNode>")]
|
||||
pub nodes: Vec<EpiphanyMemoryNode>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyMemoryEdge>")]
|
||||
pub edges: Vec<EpiphanyMemoryEdge>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<RepoFrontierItem>")]
|
||||
pub frontier: Vec<RepoFrontierItem>,
|
||||
}
|
||||
|
||||
/// A durable piece of unfinished repository anatomy. This is Modeling-owned state,
|
||||
/// not a scheduler job or a presentation card.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct RepoFrontierItem {
|
||||
pub id: String,
|
||||
pub migration_body: String,
|
||||
pub question: String,
|
||||
pub gap: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub target_claim_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
/// Canonical repository-relative path ceiling for this wound. This is the
|
||||
/// scope that downstream Planning may narrow and Hands may eventually
|
||||
/// change; inspected files and evidence provenance belong elsewhere.
|
||||
pub repository_scope: Vec<String>,
|
||||
pub recommended_next_organ: String,
|
||||
/// The exact plan admitted by Mind for this frontier item. Imagination may
|
||||
/// propose this payload, but only the dedicated model transition may make
|
||||
/// it part of Modeling-owned truth.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "RepoFrontierAdoptedPlan | null")]
|
||||
pub adopted_plan: Option<RepoFrontierAdoptedPlan>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub dependency_item_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub status: RepoFrontierStatus,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub evidence_refs: Vec<String>,
|
||||
/// Exact immutable public sources selected upstream for Eyes. These are
|
||||
/// causal retrieval inputs, not evidence produced by Modeling.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub public_source_refs: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub created_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub updated_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub retired_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub superseded_by: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct RepoFrontierAdoptedPlan {
|
||||
pub planning_request_id: String,
|
||||
pub result_id: String,
|
||||
pub job_id: String,
|
||||
pub candidate_id: String,
|
||||
pub candidate_sha256: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub safe_paths: Vec<String>,
|
||||
pub action: String,
|
||||
pub command: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub checks: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub stop_conditions: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub rollback_steps: Vec<String>,
|
||||
pub commit_message: String,
|
||||
}
|
||||
|
||||
impl RepoFrontierAdoptedPlan {
|
||||
pub fn effective_action(&self) -> &str {
|
||||
self.action.as_str()
|
||||
}
|
||||
|
||||
pub fn effective_command(&self) -> &str {
|
||||
self.command.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
pub enum RepoFrontierStatus {
|
||||
#[default]
|
||||
Proposed,
|
||||
Active,
|
||||
Blocked,
|
||||
Resolved,
|
||||
Retired,
|
||||
Superseded,
|
||||
}
|
||||
|
||||
pub fn reorient_checkpoint_from_admitted_repo_model(
|
||||
snapshot: &EpiphanyMemoryGraphSnapshot,
|
||||
obligation_id: &str,
|
||||
) -> EpiphanyInvestigationCheckpoint {
|
||||
EpiphanyInvestigationCheckpoint {
|
||||
checkpoint_id: obligation_id.to_string(),
|
||||
kind: "admitted_repo_model".to_string(),
|
||||
disposition: EpiphanyInvestigationDisposition::ResumeReady,
|
||||
focus: "Current Mind-admitted RepoModel frontier".to_string(),
|
||||
summary: Some(
|
||||
"Derived from the authenticated current keyed RepoModel view.".to_string(),
|
||||
),
|
||||
next_action: Some("Resume from the current Mind-admitted RepoModel frontier.".to_string()),
|
||||
captured_at_turn_id: None,
|
||||
open_questions: Vec::new(),
|
||||
code_refs: snapshot
|
||||
.frontier
|
||||
.iter()
|
||||
.filter(|item| item.status == RepoFrontierStatus::Active)
|
||||
.flat_map(|item| item.repository_scope.iter())
|
||||
.map(|path| EpiphanyCodeRef {
|
||||
path: path.into(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
symbol: None,
|
||||
note: Some("Current admitted RepoModel frontier scope".to_string()),
|
||||
})
|
||||
.collect(),
|
||||
evidence_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyMemoryDomain {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub lifecycle: EpiphanyMemoryLifecycle,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, JsonSchema, TS, Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
pub enum EpiphanyMemoryLifecycle {
|
||||
#[default]
|
||||
Observed,
|
||||
Proposed,
|
||||
Accepted,
|
||||
Retired,
|
||||
Stale,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, JsonSchema, TS, Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
pub enum EpiphanyMemoryNodeKind {
|
||||
#[default]
|
||||
Domain,
|
||||
Module,
|
||||
Crate,
|
||||
Binary,
|
||||
Schema,
|
||||
RuntimeContract,
|
||||
Adapter,
|
||||
TestSeam,
|
||||
StateStore,
|
||||
Summary,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyMemoryNode {
|
||||
pub id: String,
|
||||
pub domain_id: String,
|
||||
pub kind: EpiphanyMemoryNodeKind,
|
||||
pub title: String,
|
||||
pub claim: String,
|
||||
pub question: String,
|
||||
pub tension: String,
|
||||
pub action_implication: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyMemoryAnchor>")]
|
||||
pub anchors: Vec<EpiphanyMemoryAnchor>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<string>")]
|
||||
pub source_hashes: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub lifecycle: EpiphanyMemoryLifecycle,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub created_at: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, JsonSchema, TS, Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
pub enum EpiphanyMemoryEdgeKind {
|
||||
#[default]
|
||||
Owns,
|
||||
Reads,
|
||||
Writes,
|
||||
Derives,
|
||||
Adapts,
|
||||
Persists,
|
||||
Launches,
|
||||
Verifies,
|
||||
Supports,
|
||||
Contradicts,
|
||||
Grounds,
|
||||
Triggers,
|
||||
DependsOn,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyMemoryEdge {
|
||||
pub id: String,
|
||||
pub source_id: String,
|
||||
pub target_id: String,
|
||||
pub kind: EpiphanyMemoryEdgeKind,
|
||||
pub claim: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyMemoryAnchor>")]
|
||||
pub anchors: Vec<EpiphanyMemoryAnchor>,
|
||||
#[serde(default)]
|
||||
pub lifecycle: EpiphanyMemoryLifecycle,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyMemoryAnchor {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub target: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "EpiphanyCodeRef | null")]
|
||||
pub code_ref: Option<EpiphanyCodeRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub evidence_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub source_hash: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
|
@ -8,6 +8,6 @@ name = "epiphany_tool_adapter"
|
|||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
cultcache-rs = { path = "../vendor/cultcache-rs" }
|
||||
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use cultcache_rs::DatabaseEntry;
|
||||
|
||||
pub const TOOL_ADAPTER_INVOCATION_INTENT_SCHEMA_ID: &str = "epiphany.tool_invocation_intent.v0";
|
||||
pub const TOOL_ADAPTER_INVOCATION_RECEIPT_SCHEMA_ID: &str = "epiphany.tool_invocation_receipt.v0";
|
||||
pub const TOOL_ADAPTER_INVOCATION_RECEIPT_SCHEMA_ID: &str = "epiphany.tool_invocation_receipt.v1";
|
||||
pub const EPIPHANY_TOOL_RUNTIME_ADAPTER_ID: &str = "epiphany-tools";
|
||||
|
||||
pub fn tool_invocation_intent_key(intent_id: &str) -> String {
|
||||
|
|
@ -102,7 +102,7 @@ impl EpiphanyToolInvocationIntent {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.tool_invocation_receipt.v0",
|
||||
type = "epiphany.tool_invocation_receipt.v1",
|
||||
schema = "EpiphanyToolInvocationReceipt"
|
||||
)]
|
||||
pub struct EpiphanyToolInvocationReceipt {
|
||||
|
|
@ -126,8 +126,6 @@ pub struct EpiphanyToolInvocationReceipt {
|
|||
pub result_json: Option<String>,
|
||||
#[cultcache(key = 9, default)]
|
||||
pub error: Option<String>,
|
||||
#[cultcache(key = 10, default)]
|
||||
pub sealed_raw_result_ref: Option<String>,
|
||||
}
|
||||
|
||||
impl EpiphanyToolInvocationReceipt {
|
||||
|
|
@ -151,7 +149,6 @@ impl EpiphanyToolInvocationReceipt {
|
|||
completed_at: completed_at.into(),
|
||||
result_json: None,
|
||||
error: None,
|
||||
sealed_raw_result_ref: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ anyhow = "1"
|
|||
chrono = "0.4.44"
|
||||
epiphany-tool-adapter = { path = "../epiphany-tool-adapter" }
|
||||
epiphany-core = { path = "../epiphany-core" }
|
||||
cultcache-rs = { path = "../vendor/cultcache-rs" }
|
||||
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
|
||||
http = "1"
|
||||
reqwest = { version = "0.13.2", default-features = false, features = ["json", "rustls"] }
|
||||
sha2 = "0.10"
|
||||
|
|
|
|||
|
|
@ -198,9 +198,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parses_only_the_owned_stdio_and_http_subset() -> Result<()> {
|
||||
let config = McpRuntimeConfig::from_toml(
|
||||
&format!(
|
||||
r#"
|
||||
let config = McpRuntimeConfig::from_toml(&format!(
|
||||
r#"
|
||||
[mcp_servers.local]
|
||||
command = "{ABSOLUTE_COMMAND}"
|
||||
args = ["--stdio"]
|
||||
|
|
@ -211,8 +210,7 @@ url = "https://example.test/mcp"
|
|||
bearer_token_env_var = "MCP_TOKEN"
|
||||
tool_timeout_sec = 4
|
||||
"#
|
||||
),
|
||||
)?;
|
||||
))?;
|
||||
assert!(matches!(
|
||||
config.server("local")?.transport,
|
||||
McpTransportConfig::Stdio { .. }
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ use std::collections::HashMap;
|
|||
use std::process::Stdio;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub const MCP_CLIENT_NAME: &str = "epiphany-tool-mcp-runtime";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct InvocationOutcome {
|
||||
pub receipt: EpiphanyToolInvocationReceipt,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use anyhow::{Context, Result, anyhow};
|
||||
use epiphany_tool_adapter::{
|
||||
EPIPHANY_TOOL_RUNTIME_ADAPTER_ID, EpiphanyToolInvocationIntent, EpiphanyToolInvocationReceipt,
|
||||
tool_invocation_intent_key,
|
||||
EpiphanyToolInvocationIntent, EpiphanyToolInvocationReceipt, tool_invocation_intent_key,
|
||||
};
|
||||
use epiphany_tool_mcp_runtime::{
|
||||
McpRuntimeConfig, current_utc_timestamp, execute_epiphany_public, execute_epiphany_source,
|
||||
|
|
@ -79,7 +78,7 @@ async fn execute_to_receipt(
|
|||
Some(path) => path.clone(),
|
||||
None => std::env::current_dir()?,
|
||||
};
|
||||
execute_epiphany_source(intent, &options.store, &cwd)
|
||||
execute_epiphany_source(intent, &cwd)
|
||||
} else if intent.server == "epiphany_public" {
|
||||
execute_epiphany_public(intent).await
|
||||
} else if intent.server == "epiphany_state" {
|
||||
|
|
@ -216,8 +215,38 @@ fn bound(value: &str, limit: usize) -> String {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use epiphany_tool_adapter::EPIPHANY_TOOL_RUNTIME_ADAPTER_ID;
|
||||
use epiphany_tool_adapter::tool_invocation_receipt_key;
|
||||
|
||||
fn seed_test_tool_job(store: &Path, session_id: &str, job_id: &str) -> Result<()> {
|
||||
let created_at = "2026-08-10T01:00:01Z";
|
||||
let mut cache = epiphany_core::runtime_spine_cache(store)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
cache.put(
|
||||
session_id,
|
||||
&epiphany_core::EpiphanyRuntimeSession {
|
||||
session_id: session_id.into(),
|
||||
objective: "Exercise the native tool boundary.".into(),
|
||||
status: epiphany_core::EpiphanyRuntimeSessionStatus::Active,
|
||||
created_at: created_at.into(),
|
||||
updated_at: created_at.into(),
|
||||
coordinator_note: "test fixture".into(),
|
||||
},
|
||||
)?;
|
||||
cache.put(
|
||||
job_id,
|
||||
&epiphany_core::EpiphanyRuntimeJob {
|
||||
job_id: job_id.into(),
|
||||
session_id: session_id.into(),
|
||||
role: "tool-runtime".into(),
|
||||
status: epiphany_core::EpiphanyRuntimeJobStatus::Queued,
|
||||
created_at: created_at.into(),
|
||||
updated_at: created_at.into(),
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_production_cli_without_legacy_codex_flags() -> Result<()> {
|
||||
let options = parse_cli(vec![
|
||||
|
|
@ -269,32 +298,12 @@ mod tests {
|
|||
"prove provider-independent native tools",
|
||||
"now",
|
||||
);
|
||||
epiphany_core::create_runtime_session(
|
||||
&store,
|
||||
epiphany_core::RuntimeSpineSessionOptions {
|
||||
session_id: "native-tool-session".into(),
|
||||
objective: "Prove owned native tool execution.".into(),
|
||||
created_at: "2026-08-10T01:00:01Z".into(),
|
||||
coordinator_note: "test".into(),
|
||||
},
|
||||
)?;
|
||||
epiphany_core::create_runtime_job(
|
||||
&store,
|
||||
epiphany_core::RuntimeSpineJobOptions {
|
||||
job_id: "native-tool-job".into(),
|
||||
session_id: "native-tool-session".into(),
|
||||
role: "tool-runtime".into(),
|
||||
created_at: "2026-08-10T01:00:02Z".into(),
|
||||
summary: "Owned native tool execution.".into(),
|
||||
artifact_refs: Vec::new(),
|
||||
},
|
||||
)?;
|
||||
seed_test_tool_job(&store, "native-tool-session", "native-tool-job")?;
|
||||
epiphany_core::put_runtime_tool_execution_intent(
|
||||
&store,
|
||||
"native-tool-session",
|
||||
"native-tool-job",
|
||||
&intent,
|
||||
"2026-08-10T01:00:03Z",
|
||||
)?;
|
||||
|
||||
let summary = run(RunOptions {
|
||||
|
|
@ -344,8 +353,7 @@ mod tests {
|
|||
private_state_exposed: false,
|
||||
},
|
||||
)?;
|
||||
let grant = epiphany_core::issue_resident_self_grant(&resident_store, 2)?
|
||||
.expect("grant");
|
||||
let grant = epiphany_core::issue_resident_self_grant(&resident_store, 2)?.expect("grant");
|
||||
let intent = EpiphanyToolInvocationIntent::new(
|
||||
"native-state-read",
|
||||
EPIPHANY_TOOL_RUNTIME_ADAPTER_ID,
|
||||
|
|
@ -356,32 +364,12 @@ mod tests {
|
|||
"prove grant-owned state observation",
|
||||
"now",
|
||||
);
|
||||
epiphany_core::create_runtime_session(
|
||||
&runtime_store,
|
||||
epiphany_core::RuntimeSpineSessionOptions {
|
||||
session_id: "state-tool-session".into(),
|
||||
objective: "Observe resident state.".into(),
|
||||
created_at: "2026-08-11T00:00:00Z".into(),
|
||||
coordinator_note: "test".into(),
|
||||
},
|
||||
)?;
|
||||
epiphany_core::create_runtime_job(
|
||||
&runtime_store,
|
||||
epiphany_core::RuntimeSpineJobOptions {
|
||||
job_id: "state-tool-job".into(),
|
||||
session_id: "state-tool-session".into(),
|
||||
role: "tool-runtime".into(),
|
||||
created_at: "2026-08-11T00:00:01Z".into(),
|
||||
summary: "Observe resident state.".into(),
|
||||
artifact_refs: vec![],
|
||||
},
|
||||
)?;
|
||||
seed_test_tool_job(&runtime_store, "state-tool-session", "state-tool-job")?;
|
||||
epiphany_core::put_runtime_tool_execution_intent(
|
||||
&runtime_store,
|
||||
"state-tool-session",
|
||||
"state-tool-job",
|
||||
&intent,
|
||||
"2026-08-11T00:00:02Z",
|
||||
)?;
|
||||
|
||||
let unbound = execute_to_receipt(
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::process::{Command, Stdio};
|
||||
use std::thread;
|
||||
|
||||
pub fn execute_epiphany_source(
|
||||
intent: &EpiphanyToolInvocationIntent,
|
||||
store: &Path,
|
||||
cwd: &Path,
|
||||
) -> Result<Value> {
|
||||
pub fn execute_epiphany_source(intent: &EpiphanyToolInvocationIntent, cwd: &Path) -> Result<Value> {
|
||||
let arguments: Value =
|
||||
serde_json::from_str(&intent.arguments_json).context("arguments_json is not valid JSON")?;
|
||||
if !arguments.is_object() {
|
||||
|
|
@ -21,7 +17,6 @@ pub fn execute_epiphany_source(
|
|||
"read_file" => read_file(cwd, &arguments),
|
||||
"directory_inventory" => directory_inventory(cwd, &arguments),
|
||||
"git_show" => git_show(cwd, &arguments),
|
||||
"read_hands_receipt" => read_hands_receipt(store, &arguments),
|
||||
other => Err(anyhow!("unknown epiphany_source tool {other:?}")),
|
||||
}
|
||||
}
|
||||
|
|
@ -220,28 +215,6 @@ fn read_retained(mut input: impl Read, limit: usize) -> Result<String> {
|
|||
Ok(text)
|
||||
}
|
||||
|
||||
fn read_hands_receipt(store: &Path, arguments: &Value) -> Result<Value> {
|
||||
let id = required_string(arguments, "receiptId")?;
|
||||
match required_string(arguments, "kind")? {
|
||||
"patch" => {
|
||||
let r = epiphany_core::runtime_hands_patch_receipt(store, id)?
|
||||
.ok_or_else(|| anyhow!("Hands patch receipt {id:?} not found"))?;
|
||||
serde_json::to_value(r).context("encoding Hands patch receipt")
|
||||
}
|
||||
"command" => {
|
||||
let r = epiphany_core::runtime_hands_command_receipt(store, id)?
|
||||
.ok_or_else(|| anyhow!("Hands command receipt {id:?} not found"))?;
|
||||
serde_json::to_value(r).context("encoding Hands command receipt")
|
||||
}
|
||||
"commit" => {
|
||||
let r = epiphany_core::runtime_hands_commit_receipt(store, id)?
|
||||
.ok_or_else(|| anyhow!("Hands commit receipt {id:?} not found"))?;
|
||||
serde_json::to_value(r).context("encoding Hands commit receipt")
|
||||
}
|
||||
other => Err(anyhow!("unsupported Hands receipt kind {other:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn confined_path(cwd: &Path, requested: &str) -> Result<PathBuf> {
|
||||
let root = cwd
|
||||
.canonicalize()
|
||||
|
|
@ -313,16 +286,11 @@ mod tests {
|
|||
r#"{"path":"body.txt","startLine":2,"maxLines":2}"#,
|
||||
),
|
||||
dir.path(),
|
||||
dir.path(),
|
||||
)?;
|
||||
assert_eq!(value["content"], "2: two\n3: three");
|
||||
assert!(
|
||||
execute_epiphany_source(
|
||||
&intent("read_file", r#"{"path":"../escape"}"#),
|
||||
dir.path(),
|
||||
dir.path()
|
||||
)
|
||||
.is_err()
|
||||
execute_epiphany_source(&intent("read_file", r#"{"path":"../escape"}"#), dir.path())
|
||||
.is_err()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -347,7 +315,6 @@ mod tests {
|
|||
r#"{"path":"artifacts","maxDepth":2,"maxEntries":10,"maxSamples":10}"#,
|
||||
),
|
||||
dir.path(),
|
||||
dir.path(),
|
||||
)?;
|
||||
assert_eq!(value["complete"], true);
|
||||
assert_eq!(value["entryCount"], 4);
|
||||
|
|
@ -359,7 +326,6 @@ mod tests {
|
|||
execute_epiphany_source(
|
||||
&intent("directory_inventory", r#"{"path":"../escape"}"#),
|
||||
dir.path(),
|
||||
dir.path(),
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
|
@ -379,7 +345,6 @@ mod tests {
|
|||
r#"{"path":"artifacts","maxEntries":2}"#,
|
||||
),
|
||||
dir.path(),
|
||||
dir.path(),
|
||||
)?;
|
||||
assert_eq!(value["complete"], false);
|
||||
assert_eq!(value["entryCount"], 2);
|
||||
|
|
@ -393,7 +358,6 @@ mod tests {
|
|||
r#"{"path":"nested","maxDepth":0,"maxEntries":10}"#,
|
||||
),
|
||||
dir.path(),
|
||||
dir.path(),
|
||||
)?;
|
||||
assert_eq!(depth_limited["complete"], false);
|
||||
assert_eq!(depth_limited["depthLimitHit"], true);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ contract docs.
|
|||
- `epiphany-anatomy.md`: current organ/sub-agent/substrate/protocol split.
|
||||
- `fresh-workspace-handoff.md`: compact re-entry orientation and current live state.
|
||||
- `epiphany-current-algorithmic-map.md`: broad source-grounded machine map.
|
||||
- `codex-auth-spine-inventory.md`: retained Codex auth/model transport keeper list.
|
||||
- `epiphany-fork-implementation-plan.md`: current campaign plan.
|
||||
- `epiphany-safety-architecture.md`: capability, autonomy, and governance boundaries.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,200 +0,0 @@
|
|||
# Codex Auth Spine Inventory
|
||||
|
||||
This is the keeper list for the vestigial Codex organ. Anything not justified
|
||||
here is suspect until proven otherwise. The organ must stay honest: Epiphany is
|
||||
a modified Codex-derived backend with a native CultCache/CultNet body, not a
|
||||
clean-room client pretending to be Codex.
|
||||
|
||||
## Compliance Invariant
|
||||
|
||||
- Keep Codex-compatible auth identity anchored in vendored Codex-derived
|
||||
machinery. Do not fully replace Codex auth with a lookalike native
|
||||
implementation unless OpenAI provides an explicit public path or permission
|
||||
for that use.
|
||||
- It is fine to cut Codex apps, skills, marketplace, plugin UX, broad
|
||||
app-server workflow, and generic JSON control surfaces. Those are product and
|
||||
workflow organs, not subscription-auth legitimacy.
|
||||
- The retained auth/model organ should be brutally thin, but it must be real:
|
||||
load/refresh credentials, preserve required client headers/identity,
|
||||
respect subscription limits, and call the allowed Codex/OpenAI model route.
|
||||
|
||||
## Live Spine
|
||||
|
||||
### Inputs
|
||||
|
||||
- User authentication material from Codex auth storage or an external auth
|
||||
provider.
|
||||
- Model name and optional endpoint override from Epiphany-owned request/config
|
||||
surfaces.
|
||||
- Per-turn model request data: prompt instructions, formatted input, tools,
|
||||
output schema, reasoning controls, service tier, and model metadata.
|
||||
- Session identity: conversation/thread id, installation id, window id,
|
||||
session source, optional turn metadata, and optional parent/subagent headers.
|
||||
|
||||
### Durable Stores
|
||||
|
||||
- Codex auth storage under the configured Codex home. The legal/compliance
|
||||
authority should remain vendored Codex-derived auth machinery such as
|
||||
`vendor/codex/codex-rs/login/src/auth/manager.rs`, even when Epiphany wraps
|
||||
it in typed status documents.
|
||||
- Auth token state in Codex-compatible auth records, including refreshable
|
||||
ChatGPT tokens and account metadata.
|
||||
- No durable model/provider state is owned by the current OpenAI spine. The
|
||||
model string is request data; catalog/default selection must become a typed
|
||||
Epiphany document before it can be keeper machinery.
|
||||
|
||||
### Transformations
|
||||
|
||||
- `epiphany-openai-codex-spine` imports the narrow auth types and default-client
|
||||
construction it needs directly from vendored `codex-login`, without cloning
|
||||
env/file/keyring/token-refresh behavior or wrapping re-exports in another crate.
|
||||
Subscription credential authority remains in retained Codex-compatible
|
||||
machinery; Epiphany owns only the typed status/request/event/runtime surfaces
|
||||
around it.
|
||||
- `epiphany-openai-codex-spine` converts `CodexAuth` directly into
|
||||
authorization/account headers, chooses the ChatGPT Codex backend or OpenAI API
|
||||
base URL from auth mode, builds a local serializable Responses request body,
|
||||
opens an HTTP/SSE stream through `codex-client`, and parses Responses frames
|
||||
into typed Epiphany stream events.
|
||||
- `codex-login` remains in `vendor/codex` and should be treated as keeper
|
||||
auth machinery or the source for a thin vendored auth organ. Removing it from
|
||||
the native request graph may be an overcut if it makes Epiphany look like an
|
||||
impersonating client instead of a modified Codex backend.
|
||||
|
||||
### Outputs
|
||||
|
||||
- Bearer-authenticated OpenAI/ChatGPT-compatible model calls.
|
||||
- Streaming model response events.
|
||||
- No compaction/memory/realtime helper calls are part of this spine unless a
|
||||
typed Epiphany caller explicitly earns them later.
|
||||
- No model catalog or model metadata store is keeper machinery yet.
|
||||
- Auth and transport telemetry/errors needed to recover or report failure.
|
||||
|
||||
## Keep
|
||||
|
||||
- `codex-login`: keep as the legal authority for Codex-compatible subscription
|
||||
credentials. The Epiphany transport imports only its required auth surface.
|
||||
- `codex-client`: keep only as a plain HTTP/SSE transport helper for now. It
|
||||
does not own Epiphany request shape, model policy, stream semantics, or durable
|
||||
state.
|
||||
|
||||
## Cut Or Seal
|
||||
|
||||
- Codex app-server routing is not part of auth/model transport. It can host
|
||||
compatibility wrappers while Aquarium/CLI still speak JSON-RPC, but it must
|
||||
not be the native Epiphany operator surface.
|
||||
- Codex apps, skills, marketplace, plugin UX, and MCP OAuth handlers are not
|
||||
required for OpenAI subscription compatibility. MCP may survive as a separate
|
||||
CultNet adapter, not as a reason to keep the Codex app-server brain.
|
||||
- `codex-core::client` helper calls for memories/realtime/compaction are
|
||||
suspicious unless Epiphany deliberately needs that OpenAI endpoint. Keep them
|
||||
sealed with the transport until the native adapter split can decide.
|
||||
- `codex-api`, `codex-model-provider`, `codex-model-provider-info`,
|
||||
`codex-core::client`, and `models-manager` are no longer keeper request-path
|
||||
organs. `codex-login` is different: broad login UX can be cut, but the
|
||||
vendored Codex auth semantics should remain the anchor for subscription use.
|
||||
If a later model catalog is needed, it should be a typed Epiphany catalog
|
||||
document, not a revival of the old Codex provider stack.
|
||||
- External bearer command auth and agent identity remain inside the retained
|
||||
vendored Codex auth organ. Epiphany should expose them only if a concrete
|
||||
typed runtime/status need appears; do not clone them into a second authority.
|
||||
|
||||
## Extract Target
|
||||
|
||||
Create an outside-vendor adapter with this shape:
|
||||
|
||||
```text
|
||||
EpiphanyOpenAIAuthAdapter
|
||||
-> wrap retained Codex-compatible credential loading/refresh
|
||||
-> expose account/auth mode/model availability
|
||||
|
||||
EpiphanyOpenAIModelTransport
|
||||
-> accept typed Epiphany model-turn request
|
||||
-> call OpenAI Responses API through Codex-compatible auth
|
||||
-> emit typed stream events / receipts
|
||||
```
|
||||
|
||||
The adapter may depend on retained `codex-login` auth machinery and
|
||||
`codex-client` while the reliquary survives. It must not depend
|
||||
on `codex-app-server`, `codex_message_processor`, plugin/app/skill/marketplace
|
||||
modules, broad Codex provider/request construction, or Epiphany state
|
||||
ownership.
|
||||
|
||||
## Next Cut
|
||||
|
||||
The first `epiphany-openai-adapter` crate boundary now exists as a native typed
|
||||
surface:
|
||||
|
||||
- input: typed model-turn request plus adapter config
|
||||
- output: typed stream events and terminal usage/error receipt
|
||||
- current dependency: no Codex app-server and no Codex transport yet
|
||||
- planned internal dependency: Codex auth/model transport only
|
||||
- forbidden dependency: Codex app-server or Epiphany JSON-RPC routes
|
||||
|
||||
The first attempt to wire `codex-core` directly as a standalone path dependency
|
||||
escaped the vendored workspace lock and hit transitive ICU/temporal dependency
|
||||
skew. Do not paper over that with random version pins.
|
||||
|
||||
The first workspace-verified wrapper now exists as
|
||||
`epiphany-openai-codex-spine`. It depends on the pure typed adapter plus the
|
||||
keeper Codex auth types and projects `AuthManager` / `CodexAuth` into a typed
|
||||
`EpiphanyOpenAiAdapterStatus`. `epiphany-codex-bridge` re-exports this spine so
|
||||
the current app-server compatibility shell can compile it without making the
|
||||
pure document crate depend on Codex.
|
||||
|
||||
The transport wrapper is now also in that spine. It maps typed
|
||||
`EpiphanyOpenAiModelRequest` documents into a local serializable Responses
|
||||
request body, resolves credentials through vendored `codex-login`, chooses the
|
||||
ChatGPT/OpenAI base URL from auth mode, opens an HTTP Responses SSE stream with
|
||||
`codex-client`, and converts stream deltas/completion into typed
|
||||
`EpiphanyOpenAiStreamEvent` / `EpiphanyOpenAiModelReceipt` documents. It no
|
||||
longer imports Codex `ResponsesApiRequest`, `ResponsesClient`,
|
||||
`ResponseEvent`, provider config, model-provider, or broad app-server
|
||||
workflow. Its narrow direct imports preserve Codex-compatible auth identity
|
||||
without letting Codex own Epiphany request/state shape.
|
||||
|
||||
`schemas/cultnet/` contains publication schemas for OpenAI adapter status,
|
||||
model request, stream event, and terminal receipt. Live providers own their
|
||||
CultMesh/CultNet catalogs. The native runtime accepts the document types
|
||||
registered by the actual CultCache runtime schema; there is no standalone
|
||||
hello/catalog CLI or hand-maintained mutation-contract mirror.
|
||||
|
||||
The native runtime route is the operator and daemon edge.
|
||||
`epiphany-openai-adapter` documents
|
||||
derive CultCache `DatabaseEntry`, `epiphany-core::runtime_spine_cache`
|
||||
registers OpenAI adapter status/request/stream-event/receipt documents, and the
|
||||
outside-vendor `epiphany-openai-runtime` crate records typed OpenAI model-turn
|
||||
requests, stream events, terminal receipts, runtime sessions, jobs, and job
|
||||
results into the native runtime spine. Its `model-turn` command opens the
|
||||
Codex-backed transport through the typed spine; its `smoke` command proves the
|
||||
CultCache route without touching the network. This is the first native caller
|
||||
for the advertised OpenAI CultNet contract.
|
||||
|
||||
Heartbeat/specialist launches use the atomic
|
||||
`epiphany-core::prepare_runtime_spine_heartbeat_job` path. Initialization,
|
||||
session admission, job creation, and launch receipts are one prepared CultCache
|
||||
mutation rather than a caller-visible sequence.
|
||||
|
||||
The heartbeat launch plan is native as well:
|
||||
`epiphany-core::plan_runtime_spine_heartbeat_launch` validates the launch
|
||||
contract and active runtime-link conflicts, then returns the durable job binding
|
||||
and runtime link. Vendored `CodexThread` is still a persistence adapter for the
|
||||
compatibility route, not the owner of that state mechanics.
|
||||
|
||||
The Epiphany state-update contract and mutation law have also left vendored
|
||||
Codex. `epiphany-core::EpiphanyStateUpdate` now owns the update document used
|
||||
by the compatibility `thread/epiphany/update`, promotion, accept, and launch
|
||||
paths. `epiphany-core::epiphany_state_update_validation_errors` and
|
||||
`epiphany-core::apply_epiphany_state_update` own typed validation and mutation
|
||||
application. `codex-core` re-exports the contract only as a compatibility
|
||||
alias, and `CodexThread` now calls native state-update functions around its
|
||||
remaining revision check, persistence validation, and rollout/session writeback.
|
||||
|
||||
The credential extraction overcut has been corrected. Codex file/keyring/auto
|
||||
auth, env API key handling, ChatGPT token refresh, account metadata parsing,
|
||||
and header-client setup remain in vendored `codex-login`. The redundant
|
||||
Epiphany re-export crate is gone; the root workspace carries the Codex
|
||||
`tokio-tungstenite` / `tungstenite` patches required for standalone builds.
|
||||
|
||||
The point is ownership: Epiphany calls a model adapter; it does not live inside
|
||||
the Codex host brain.
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
# Epiphany current algorithmic map
|
||||
|
||||
Updated: 2026-08-24
|
||||
Latest committed implementation cut: `6831eb69` on `codex/epiphany-shakedown-live`
|
||||
Current worktree cut: Proposal-Modeling launch-binding subtraction; Ox17 remains paused
|
||||
Updated: 2026-08-25
|
||||
Latest committed implementation cut: `27e1dca1` on `codex/epiphany-shakedown-live`
|
||||
Current pass: historical Ox17 is checksum-sealed and retired. Idunn's active
|
||||
isolated target and Connector caller are provider-neutral Capstone 17. A
|
||||
replacement image construction and fresh run remain. `state/map.yaml` owns
|
||||
current direction. Atlas Gate 1 and production remain paused.
|
||||
|
||||
This document describes the live machine. Historical cuts, rejected paths, and
|
||||
proof chronology belong in git, `state/ledgers.msgpack`, and bounded smoke
|
||||
|
|
@ -24,6 +27,169 @@ The fixed three-Verse trust policy and public-room directory are also direct
|
|||
local Verse projections; they are not written into CultCache as counterfeit
|
||||
mutable state.
|
||||
|
||||
The shared state-model vocabulary is inhabited, not aspirational. Exact
|
||||
`a602fbdc` deletes fifteen DTO types with no external consumer: aggregate
|
||||
acceptance/runtime links, a second graph/frontier/checkpoint model, global
|
||||
retrieval and scratch state, generic job bindings, and churn telemetry. Keyed
|
||||
Mind/runtime receipts own decisions and lifecycle identity; keyed RepoModel
|
||||
documents own repository structure; scratch and retrieval inputs belong to
|
||||
sealed reasoning passes. Two callerless OpenAI-runtime mouths and eight
|
||||
definition-only type constants are also gone. The cut is 291 pure deletions;
|
||||
the state-model crate is 366 lines. It does not touch the still-unrun Atlas
|
||||
inter-swarm path.
|
||||
|
||||
Provider observability is typed consequence, not a no-op callback shelf. Exact
|
||||
`9abadfe7` removes the frame-observation DTO, sequence/preview bookkeeping, and
|
||||
observer methods from both provider transports because every live caller threw
|
||||
the observation away. Codex SSE and OpenRouter responses still lower into the
|
||||
same typed model events and receipts. A provider-specific transcript reader
|
||||
used only by a test assertion is also absent; decision audit continues to use
|
||||
sealed native/provider requests, structured terminal records, governed tool
|
||||
receipts, and Mind commit receipts rather than stream text.
|
||||
|
||||
Durable provenance is singular. Exact `2a435eb5` advances Persona memory to v2
|
||||
and removes a linked-event vector that repeated `effect_document_id` plus a
|
||||
relationship slot every producer left empty. The memory keeps the exact
|
||||
Interpreter effect document and decision context. Tool receipts advance to v1
|
||||
and remove an always-empty raw-result reference; exact result JSON or error is
|
||||
the receipt consequence. Mind/runtime writable epochs were v10/v26 at that
|
||||
cut, and old stores refuse rather than inheriting the retired shapes.
|
||||
|
||||
Routing identity lives at the owner, not in every payload. Exact `ffd91c20`
|
||||
first deleted the fixed Research role from the then-live Eyes evidence packets
|
||||
because their request and decision-context identities already owned provenance.
|
||||
It deletes the fixed Epiphany Persona agent label from each social mention because
|
||||
the Persona-only queue and immutable turn request own routing. Resident Self
|
||||
retention keeps cumulative lifecycle/envelope counts and its exact chained
|
||||
deletion digest; it no longer accepts a clock merely to persist an unread
|
||||
timestamp outside that chain. Persona social mention v2 and Resident Self state
|
||||
v3/retention head v1 remain; exact `83611f9b` later deletes the Eyes packet
|
||||
family entirely. Model Atlas is unchanged and remains unaccepted until a
|
||||
live inter-swarm exchange exercises publication, transport, verification,
|
||||
admission, wake, and withdrawal end to end.
|
||||
|
||||
Provider transport is not a second state machine. Exact `fe118e13` keeps the
|
||||
exact provider request as durable decision input, but makes provider events and
|
||||
receipts transient values. The runtime immediately lowers each into the native
|
||||
model event/receipt family that already owns tool continuation, assistant-text
|
||||
reconstruction, terminal validation, and bounded retention. The provider
|
||||
event/receipt CultCache registrations, archive requirements, schema IDs, and
|
||||
public CultNet schemas are deleted. Runtime advances to v28; the public catalog
|
||||
has 18 schemas. `epiphany-model-runtime` now points directly at its executable
|
||||
body instead of compiling through an include-only shell. Atlas is untouched.
|
||||
|
||||
Schema publication is not a shadow registry. Exact `dbed11b3` deletes the
|
||||
nonexistent runtime-event contract and the obsolete public schema for the
|
||||
private archived-session tombstone. Exact `8bb0719b` then removes every
|
||||
runtime-local identity, session, job, binding, coordinator, native model/tool,
|
||||
and state-ledger JSON mirror. Those documents remain typed CultCache state and
|
||||
are discovered from the native registration or projected by their owning
|
||||
CultMesh provider; no live CultNet crossing consumed the hand-maintained JSON,
|
||||
and several schemas contradicted their Rust owners. Those two cuts left three
|
||||
portable boundaries and removed 850 net maintained lines. Exact `87ea81db`
|
||||
then deletes the generic OpenAI-shaped schema after its Rust document ceases to
|
||||
exist. The public Epiphany catalog now contains only
|
||||
`gamecult.persona_state.v0` and `epiphany.work_organ_state.v0`. Connector owns
|
||||
the shared Codex contract; Epiphany's closed provider-request document remains
|
||||
private Mind state. Atlas remains untouched and untested end to end.
|
||||
|
||||
Dependency ownership follows source ownership. Exact `695af6c6` deletes
|
||||
thirteen manifest edges with no consumer in their owning package, plus the
|
||||
tracked marker that kept the extinct `epiphany-openai-auth-spine` directory
|
||||
alive. The release bundle no longer directly owns nine libraries used only by
|
||||
leaf crates or not used at all; four leaf/dev manifests drop their own dead
|
||||
edges. The lock graph contracts from 757 to 732 packages, including removal of
|
||||
the orphan PostgreSQL and duplicate newer crypto chains. All nine production
|
||||
entrypoints and the affected library/test targets compile individually. The
|
||||
cut removes 284 net manifest, lock, and source lines. Package-scoped cleanup of
|
||||
the five compiled packages removed 124.2 GiB of disposable historical build
|
||||
artifacts without removing the prebuilt state tool. Atlas is untouched.
|
||||
|
||||
Hands consequence receipts carry consequence, not doctrine commentary. Exact
|
||||
`65bf044f` removes fixed schema-version strings and unread contract prose from
|
||||
the patch, command, and commit documents. CultCache owns their type/schema
|
||||
identity; admission at that cut still proved the then-live intent, review,
|
||||
Substrate Gate grant, runtime job, affected paths or command, immutable receipt
|
||||
identity, and the resulting consequence. Fourteen unused shadow type constants
|
||||
and one callerless reorient display enum are also gone. Runtime advances to
|
||||
v29; old writable
|
||||
state refuses. The existing lifecycle proof carries Hands concurrently with a
|
||||
Persona admission and creates exact Verification work, while package cleanup
|
||||
removes the 678.6 MiB it generated. The cut removes 67 net maintained lines.
|
||||
Atlas remains the untouched, unaccepted collaboration slice.
|
||||
|
||||
Autonomous proposal origin has one owner. Exact `d9a196a0` folds the direction
|
||||
result, option ordinal, and worker job identities into the Proposal-Modeling
|
||||
request that already owns selection. The separate binding document, schema,
|
||||
registration, third atomic write, collision branch, and copied launch/result
|
||||
hashes are deleted. Promotion still strongly reads the immutable direction
|
||||
request/result and worker launch/result while inserting one proposal/request
|
||||
pair; launch, fulfillment, and archival validate the same request-owned chain.
|
||||
Runtime advances to v30. The full keyed lifecycle and old-store refusal pass;
|
||||
package cleanup removes 677.6 MiB. The cut removes 53 net maintained lines and
|
||||
does not touch the unrun Atlas crossing.
|
||||
|
||||
Repository-domain identity is one small typed receipt, not a second Body.
|
||||
Exact `6f5d6600` retains the canonical organizational repository name and exact
|
||||
authenticated Body hash, then deletes the copied runtime, swarm, workspace,
|
||||
fixed envelope identity/schema prose, and binding timestamp. CultCache and the
|
||||
authenticated Body route already own those facts. Bootstrap still validates
|
||||
the live Body and atomically strong-reads runtime identity plus the Body route;
|
||||
proposal promotion and fulfillment still refuse repository or Body-hash
|
||||
substitution. Runtime advances to v31. The full keyed lifecycle and old-store
|
||||
refusal pass; package cleanup removes 677.2 MiB. The cut removes 50 net
|
||||
maintained lines. Atlas remains protected until a real inter-swarm exchange
|
||||
exercises publication, trust, transport, projection, verification, admission,
|
||||
wake, brake, and re-entry together.
|
||||
|
||||
Operator objective intake carries the human assertion, not a second objective
|
||||
model. Exact `952dcd9f` keeps thread provenance, objective text, source actor
|
||||
and reference, and submission time, while deleting fixed schema prose, stored
|
||||
key identity, a digest of the already-hashed payload, and contract text. The
|
||||
singleton Mind objective remains the active state and the commit receipt still
|
||||
binds its exact operator provenance atomically. The outcome exposes only the
|
||||
projection digest, commit receipt, and changed bit. A binary test that merely
|
||||
repeated the library seed/idempotency/refusal proof is deleted; the library
|
||||
proof now requires byte-identical replay and the resident-grant provenance
|
||||
proof remains. Runtime advances to v32. The three focused proofs pass and the
|
||||
cut removes 54 net maintained lines. Package cleanup removes 18.5 GiB; the
|
||||
1.7 MiB state tool is preserved while another 961.1 MiB of regenerated build
|
||||
artifacts is removed. Atlas is untouched.
|
||||
|
||||
Repository-frontier documents carry decisions, not their own envelope labels.
|
||||
Exact `718ce9c1` removes fixed schema-version fields and contract slogans from
|
||||
thirteen durable documents across Proposal, Proposal-Modeling, Planning,
|
||||
Research, plan candidate, PlanMind, plan decision, route, Hands authority,
|
||||
Verification, Soul verdict, and post-verdict Modeling. CultCache envelope
|
||||
identity plus runtime epoch v33 owns the internal shape. Semantic identity,
|
||||
exact source versions, strong dependencies, decision context, evidence,
|
||||
disposition, and consequence remain. Sealed worker contexts keep their schema
|
||||
and contract because they are part of the exact request the model saw. The
|
||||
full keyed lifecycle, epoch refusal, provider candidate mapping, core library,
|
||||
and release-owned coordinator target pass. The cut removes 177 net maintained
|
||||
lines and leaves the unrun Atlas crossing untouched.
|
||||
|
||||
Runtime epoch identity is rooted, not repeated. Exact `a4356d1f` retains the
|
||||
runtime identity and swarm binding epoch fields, then removes fixed
|
||||
schema-version echoes from thirteen child documents covering sessions, jobs,
|
||||
execution bindings, launches, process claims, results, coordinator receipts,
|
||||
death recovery, and retained archives. Their semantic and causal payloads are
|
||||
unchanged. Runtime v34 remains the hard old-store refusal boundary. The core
|
||||
library, full keyed lifecycle, reorientation, failure audit, transcript-free
|
||||
binding audit, provider mapping, coordinator, swarm, and model-runtime targets
|
||||
pass. The cut removes 122 net maintained lines and leaves Atlas plus external
|
||||
crossings untouched.
|
||||
|
||||
The packaged model runtime has one execution authority. Exact `e60b924f`
|
||||
deletes the callerless `preflight` JSON self-certificate and registered-type
|
||||
catalog accessor; real store epoch admission and Idunn package witnesses own
|
||||
compatibility and provenance. Exact `3b991e40` deletes the uncalled standalone
|
||||
`model-turn`, `tool-followup`, and `tool-followup-turn` commands plus their
|
||||
parser/DTO/test surfaces. `run-worker` remains the sole provider execution path
|
||||
and retains internal tool continuation; `list-decisions` and `audit-decision`
|
||||
retain transcript-free operator inspection. The two cuts remove 532 net
|
||||
maintained lines. The remaining binary suite passes 8/8. Atlas is untouched.
|
||||
|
||||
CultMesh contains no JSON-derived operator snapshot, coordinator receipt,
|
||||
Hands gate, role-review event, unauthenticated Odin/Eve provider row, or daemon
|
||||
tool directory. Native runtime and keyed Mind receipts remain the owners; old
|
||||
|
|
@ -49,7 +215,7 @@ social, memory, and Body inputs without that fiction.
|
|||
|
||||
The operator surface has no unconsumed view-lens registry, legacy acceptance
|
||||
bundle builder, test-only raw-JSON result interpreter, or second CRRC
|
||||
recommendation algorithm. Typed runtime-result interpretation remains shared by
|
||||
recommendation algorithm. Typed outcome interpretation remains shared by
|
||||
current work and reorientation. The keyed status composer is the sole CRRC
|
||||
recommendation projection; its DTO does not confer routing authority.
|
||||
|
||||
|
|
@ -212,11 +378,146 @@ Body/domain binding, and canonical Modeling request; validation reads those
|
|||
facts from their owning immutable documents instead of copying them into the
|
||||
join record.
|
||||
|
||||
Proposal-Modeling launch provenance is also a narrow join. Exact `6831eb69`
|
||||
reduces its persisted binding from twelve fields to schema, request ID, job ID,
|
||||
and exact launch-document digest. The request owns proposal/runtime/thread; the
|
||||
launch owns role/binding/document; the binding key derives from the job ID.
|
||||
Fulfillment and archival read that exact key rather than scanning every binding.
|
||||
Proposal-Modeling has no second launch record. Exact `b0a4978d` deletes the
|
||||
persisted binding after its minimized form proved to own nothing. The immutable
|
||||
worker launch carries the typed request reference, job, role, binding, and sealed
|
||||
document; current-work, retry counting, fulfillment, coordinator actuation, and
|
||||
archival all read that owner directly. Family-specific semantic requests remain
|
||||
mandatory.
|
||||
|
||||
Planning and PlanMind likewise have no second launch record. Exact `b55e96ea`
|
||||
deletes both persisted binding families and derives contiguous attempts and the
|
||||
current job from the family references on immutable worker launches. A retry is
|
||||
authorized only by the exact typed failed result and its failure review; the
|
||||
launch transaction now strongly reads those documents and every prior launch.
|
||||
The semantic Planning and PlanMind requests, sealed projections, result
|
||||
validation, and two-stage adoption boundary remain family-owned.
|
||||
|
||||
Body Modeling, Imagination consideration, and Reorientation also have no
|
||||
second launch record. Exact `8dbfcf63` deletes all three persisted binding
|
||||
families. Canonical worker job identities now encode contiguous attempt order;
|
||||
the immutable worker launch carries the exact family reference, role, binding,
|
||||
and typed launch projection. Body retry CAS reads the prior launches, latest
|
||||
job, exact structured result, and matching admission refusal. Reorientation
|
||||
retry CAS reads the prior launches, latest job, typed pass failure, and exact
|
||||
terminal runtime result. Imagination retry CAS reads every prior launch. Family
|
||||
requests, sealed reasoning projections, result validation, substrate grants,
|
||||
typed failures, and Mind admission remain with their existing owners. Runtime
|
||||
writable state is v19. This cut does not touch Model Atlas or narrow the still-
|
||||
unrun inter-swarm path.
|
||||
|
||||
Verdict-driven Modeling has one writer. Exact `3ed1d564` deletes the public
|
||||
`commit_repo_frontier_modeling_request` API that production never called.
|
||||
Accepted Verification already commits its audit, Soul verdict, and canonical
|
||||
frontier-Modeling request atomically through the Mind owner; the deleted API
|
||||
reopened the store and offered a second mutation path solely for one OpenAI
|
||||
test fixture. That test now launches ordinary Body Modeling through current
|
||||
work and still proves exact sealed input, structured completion, typed result
|
||||
and job terminality, provider failure, and output-contract failure. Its
|
||||
fabricated frontier, route, Verification, and Soul tableau is gone.
|
||||
|
||||
Coordinator terminal state is not an artifact catalogue. Exact `63d2991e`
|
||||
advances the receipt to v1 and removes its unread step count, model-provider
|
||||
echo, runtime-store path, artifact and sealed-artifact lists, arbitrary
|
||||
metadata, and final-job echo. The receipt retains its terminal status/action/
|
||||
reason, time, session/thread identity, plan-or-execute mode, and the exact
|
||||
resident grant, launch, policy, argv, objective, release, manifest, and
|
||||
executable provenance continuity authenticates. Operator artifacts remain in
|
||||
the operator summary only. The published JSON schema now matches the Rust
|
||||
contract instead of rejecting the live resident fields it omitted. Runtime
|
||||
writable state is v20.
|
||||
|
||||
Runtime session and job documents are lifecycle state, not duplicate result
|
||||
records. Sessions retain only grouping identity plus active/completed state;
|
||||
jobs retain only identity plus Queued/Completed/Failed state. Exact structured
|
||||
role/reorientation outcomes own semantic decision cargo, native model receipts
|
||||
or typed pass failures own model terminality, and worker process claims own
|
||||
process death. There is no generic terminal-result document or summary-bearing
|
||||
lifecycle record.
|
||||
|
||||
Runtime physiology is created only by its real owners. Exact `cd2177e8` first
|
||||
deleted `ensure_runtime_session`, whose only consumers were fixtures and whose
|
||||
active-ID path returned a pre-existing session without comparing its payload.
|
||||
Exact `fdefb889` later deletes the remaining generic session/job writers after
|
||||
proving every caller was also a fixture. Family launch CAS, model-execution
|
||||
opening, and coordinator opening now own creation. Runtime schema is unchanged.
|
||||
|
||||
Current typed work has one assembler. Exact `0a97eef8` deletes the separate
|
||||
Body-only and unresolved-Body projectors, which reopened Body, RepoModel, and
|
||||
runtime state to reconstruct subsets already owned by `project_current_work`.
|
||||
It also deletes three unused per-job continuation wrappers that each rebuilt
|
||||
the entire projection. Live coordinator review selectors consume the one shared
|
||||
slice directly. The retained lifecycle proof confirms projection remains
|
||||
thread-free and performs no live Body scan.
|
||||
|
||||
Exact `5a047944` removes the callerless generic helper shelf rather than
|
||||
preserving API because it looks reusable: JSON ledger status, role binding and
|
||||
owner lookups, unique-string accumulation, and a successful-coordinator
|
||||
receipt wrapper. Typed owners and the exact coordinator binding validator stay
|
||||
where live callers use them. No replacement utility layer exists.
|
||||
|
||||
Exact `6ccaf937` removes three more unowned mouths. Repository Body consumers
|
||||
read the assembled typed Mind view rather than a one-field forwarding helper.
|
||||
Persona quarantine remains keyed typed state rather than gaining a second
|
||||
sorted-list projection. Substrate Gate no longer exposes a generic repo-work
|
||||
planning grant that no admission or actuator consumed; exact coordinator and
|
||||
worker grant constructors remain with their live authority paths. Nothing is
|
||||
inserted between those owners and their consumers.
|
||||
|
||||
Exact `f6a2ad7f` deletes two exported readers with no workspace consumer.
|
||||
Packaged-release authentication remains exact-ID based; its current-head
|
||||
document remains publication state, not a general query API. Idunn
|
||||
provider-health admission continues to consume and validate typed trust
|
||||
anchors supplied at its boundary; it no longer owns an unused file-loading
|
||||
convenience path. The signed admission, continuity, and substitution laws are
|
||||
unchanged.
|
||||
|
||||
Imagination consideration no longer writes a review-shaped cul-de-sac. Exact
|
||||
`be611f24` deletes the producerless review request, its durable type, schema
|
||||
constant, export, and runtime registry entry. The retained candidate remains a
|
||||
typed proposal-only outcome; a real adopted Modeling proposal follows the
|
||||
separate proposal lifecycle rather than this unread document. Runtime epoch
|
||||
v22 refuses v21 stores instead of silently carrying the retired type forward.
|
||||
|
||||
Resident Self no longer admits a receipt family that no process could produce
|
||||
or consume. Exact `3140d305` removes `ResidentSelfRuntimeReceipt`, its schema
|
||||
constant, and its registration/decoding branches. Resident Self state v2
|
||||
refuses v1 stores without mutation. The unrelated definition-only coordinator
|
||||
result-status enum is also gone; current role-board status remains the one live
|
||||
display projection. Session and worker retention continue through their actual
|
||||
runtime-spine owners.
|
||||
|
||||
Model inference no longer writes mutable adapter-status shadows. Exact
|
||||
`0091e0e1` removes both generic and OpenAI-specific status documents, their
|
||||
constructors/writers, runtime registrations, published schemas, and the test
|
||||
that only restated the synthetic OpenRouter status fields. Provider/model
|
||||
identity remains in the exact native and provider requests; credential and
|
||||
transport attachment is proved where the transport is constructed and used.
|
||||
Runtime epoch v23 refuses v22 stores. The local published catalog now contains
|
||||
twenty live or portable contracts.
|
||||
|
||||
Mind no longer contains a second, uninhabited planning ontology. Exact
|
||||
`2e3489c4` removes planning-capture, backlog-item, roadmap-stream, and
|
||||
objective-draft document families plus their 256-line generic state-model
|
||||
vocabulary. No live path could construct or admit one, so every reasoning
|
||||
projection received the same empty structure. Frontier planning requests,
|
||||
candidates, Mind review, adopted decisions, checkpoints, and consequence
|
||||
receipts remain the sole planning lifecycle. Mind/runtime epochs v9/v24 refuse
|
||||
the retired writable shape.
|
||||
|
||||
Blocked Persona turns have one durable consequence record. Exact `a6f73fc4`
|
||||
deletes the duplicate quarantine-pressure document that was written beside the
|
||||
terminal receipt but never read. The terminal receipt retains exact blocked
|
||||
crossing evidence and mention digest; keyed mentions retain `quarantined`
|
||||
status plus terminal-receipt identity. No second queue or pressure projection
|
||||
reconstructs the same decision.
|
||||
|
||||
Reorientation likewise has one decision record. Exact `fed4b857` deletes the
|
||||
entire continuity-gateway module and its unread recovery receipt, which copied
|
||||
the persisted runtime result into a companion beside the typed Mind decision.
|
||||
The sealed basis/context, exact result, strong reads, Mind decision or failure,
|
||||
and commit receipt own replay and audit. Reorientation now uses the ordinary
|
||||
Mind commit primitive. Runtime epoch v25 refuses v24 stores.
|
||||
|
||||
Bifrost owns its private feedback-signing identity. Exact `94098223` deletes
|
||||
Epiphany's host-identity executable and the private signer, persisted identity,
|
||||
|
|
@ -258,9 +559,9 @@ Epiphany-authored facsimile.
|
|||
Exact `25d4d1fa` deletes a second fictional Hands branch: the callerless route-
|
||||
relinquishment writer, its refusal receipt, its Mind receipt, and both schema
|
||||
registrations. No scheduler, coordinator, actuator, admission owner, or test
|
||||
consumed it. The live path is narrower: exact intent/review/grant authority,
|
||||
then patch/command/commit receipts, with the commit atomically creating the
|
||||
Verification request. Epiphany has no durable Hands-relinquishment lifecycle
|
||||
consumed it. That intermediate path was narrower: exact intent/review/grant
|
||||
authority, then patch/command/commit receipts, with the commit atomically
|
||||
creating the Verification request. Epiphany has no durable Hands-relinquishment lifecycle
|
||||
until a real consequence owner requires and verifies one.
|
||||
|
||||
Hands also does not record operator-authored descriptions as if they were
|
||||
|
|
@ -272,6 +573,17 @@ receipt-chain admission and Verification projection remain ready for a future
|
|||
actuator that performs the operation and emits receipts from its own observed
|
||||
effects.
|
||||
|
||||
Exact `a01c6842` removes the remaining counterfeit review. The coordinator
|
||||
created `HandsActionReview` as approved in the same function and instant as its
|
||||
intent and grant; no independent reviewer or refusal producer existed. The
|
||||
live chain is now adopted route plus Substrate Gate grant, minimal intent, and
|
||||
`RepoFrontierHandsAuthority`, followed by exact patch, command, and commit
|
||||
receipts. Receipt cargo derives job, grant, and route identity through the
|
||||
intent and authority rather than copying them. Commit admission requires one
|
||||
exact patch and one successful command receipt for the intent; a competing
|
||||
receipt refuses instead of winning a timestamp race. Runtime is v46 and the
|
||||
sealed Verification projection contract is v2.
|
||||
|
||||
The handoff from Hands to Soul has no transient aggregate. Exact `6ccc7dd2`
|
||||
deletes the 20-field receipt-chain summary and derives the Verification request
|
||||
directly from the persisted patch, command, and commit receipts plus the
|
||||
|
|
@ -285,11 +597,12 @@ producer or consumer. Exact tool definitions sealed into model requests plus
|
|||
runtime execution bindings and governed invocation receipts own the actual
|
||||
path. The catalog retains tool intent/receipt and drops to 42 schemas.
|
||||
|
||||
Continuity publishes only the document it owns. Exact `ca2d2cf2` removes packet,
|
||||
compaction-checkpoint, stale-turn-repair, and refusal contract rows that had no
|
||||
typed documents or runtime path. Reorientation still emits the exact persisted
|
||||
`ContinuityRecoveryReceipt`; compaction and handoff remain native state
|
||||
physiology rather than advertised CultNet fiction.
|
||||
Continuity owns no second reorientation archive. Exact `ca2d2cf2` removes
|
||||
packet, compaction-checkpoint, stale-turn-repair, and refusal contract rows that
|
||||
had no typed documents or runtime path. Exact `fed4b857` later removes the
|
||||
unread recovery-receipt copy. The typed reorientation Mind decision/failure,
|
||||
sealed context, exact result, and commit receipt own the live path; compaction
|
||||
and handoff remain native state physiology.
|
||||
|
||||
Soul likewise publishes only its real result. Exact `f8974d52` removes generic
|
||||
verification-request, invariant-check, regression, review, and refusal contract
|
||||
|
|
@ -297,11 +610,12 @@ rows that had no typed documents or runtime path. The keyed repo-frontier
|
|||
verification request owns the live obligation, and `SoulVerdictReceipt` owns the
|
||||
persisted audit result consumed by Modeling/Mind.
|
||||
|
||||
Eyes publishes only concrete observation evidence. Exact `201fa192` removes
|
||||
generic evidence-request, review, and refusal rows that had no typed documents
|
||||
or runtime path. Authenticated source lookup receipts prove what was inspected;
|
||||
the typed research decision becomes the exact persisted evidence packet consumed
|
||||
by current work and Modeling.
|
||||
Eyes owns one concrete admission path. Exact `201fa192` removes generic
|
||||
evidence-request, review, and refusal rows that had no typed documents or
|
||||
runtime path. Exact `83611f9b` then removes the never-persisted source-lookup
|
||||
DTO and the duplicate evidence packet. The real grant/tool receipt chain is
|
||||
authenticated in place; the structured Research decision becomes keyed Mind
|
||||
evidence/observation writes under one atomic `Eyes.frontier_research` commit.
|
||||
|
||||
Idunn deployment truth is not mirrored into invented Epiphany-local envelopes.
|
||||
Odin/Idunn owns `gamecult.idunn.deployment_manifest.v3` and signed daemon-health
|
||||
|
|
@ -346,6 +660,184 @@ Codex-derived code remains only where an OpenAI provider needs its earned
|
|||
authentication or transport. Neither owns Epiphany Mind, scheduler, route, or
|
||||
interface authority.
|
||||
|
||||
Exact `a6cf9383` first made that boundary physical in the package graph. The pure
|
||||
`epiphany-openai-runtime` library opens the exact model execution, derives the
|
||||
provider request, admits canonical native model events, emits governed tool
|
||||
intents, and records terminal physiology/audit without depending on a Codex
|
||||
crate. One shared source module at the release-bundle edge owns concrete
|
||||
Codex/OpenRouter authentication, credential reading, and network calls for the
|
||||
model and Persona entrypoints. Persona keeps its three-stage family executor;
|
||||
the service owns its concrete runner. Workers have one governed tool-capable
|
||||
route, not a tool route plus a parallel no-tools opinion.
|
||||
|
||||
The package boundary remains earned after the `a721b763` audit. It is one
|
||||
optional library target containing model/Persona orchestration, pulled only by
|
||||
the two model-backed release binaries behind `openai-runtime`. The root release
|
||||
package owns nine production binaries; folding the library into that package
|
||||
would spread model-runtime compilation or feature scaffolding across seven
|
||||
unrelated entrypoints. Package count is not improved when compile fan-out and
|
||||
ownership legibility get worse.
|
||||
|
||||
One logical state judgment consumes one pulled snapshot. Exact `7374be5e`
|
||||
makes Mind assembly, keyed RepoModel assembly, current-work projection,
|
||||
reasoning-basis construction, and Reorientation reuse the same CultCache.
|
||||
Exact `5f1bea39` extends that rule through immutable typed-fulfillment replay:
|
||||
family admission, PlanMind continuation, worker archival, and Resident Self do
|
||||
not reopen runtime state while validating the result they just selected. The
|
||||
provider runtime retains one path-based role-result CAS writer; read-only
|
||||
replay consumes an explicit `&CultCache` and cannot silently rebase model
|
||||
output against a newer store head.
|
||||
|
||||
Exact `2bcbf268` makes that projection the sole coordinator scheduling input.
|
||||
The accepted manual-regather obligation lives beside the other current work;
|
||||
operator status and Resident Self derive from the same value. Status has no
|
||||
second Mind assembly, Resident Self has no hard-coded Continue override, and
|
||||
the deleted `EpiphanyCrrcAction` enum cannot impersonate a parallel state
|
||||
machine. Reorientation decisions remain durable Mind documents; the boolean is
|
||||
only their deterministic scheduling projection.
|
||||
|
||||
Exact `125d77a2` removes the residual CRRC module. Operator review opens one
|
||||
runtime cache and reads both the job latch and exact typed Reorientation result
|
||||
from it. Pending, Completed, and Failed are the complete live vocabulary.
|
||||
Storage failure is an error, and completed-without-result is a broken invariant;
|
||||
neither is laundered into a synthetic lifecycle state.
|
||||
|
||||
Exact `dfe3757b` carries terminal failure disposition out of that same
|
||||
projection. The coordinator does not reopen the runtime job to rediscover a
|
||||
fact the projection already observed; success and failure admission owners
|
||||
still enforce their own exact strong dependencies.
|
||||
|
||||
The target boundary is independent `GameCult/CodexConnector`; exact
|
||||
`6dc80f6d266db4d82566d2434adcc55a48e8ecad` owns one typed multi-caller
|
||||
transport package and one public daemon binary without linking a Codex crate.
|
||||
With no features the library exposes only the typed provider/result contract.
|
||||
The default `client` feature adds authenticated encryption, framing, and socket
|
||||
transport. Auth, raw Responses transport, CultCache replay, and Ureq/Rustls sit
|
||||
behind the `daemon` feature.
|
||||
Each consumer internally derives its exact typed provider request. The daemon
|
||||
verifies those exact bytes, returns a digest-bound transport receipt plus typed
|
||||
events, and uses a private digest-pinned official `codex app-server` child only
|
||||
to own credential refresh. The child never receives cognitive input or output.
|
||||
|
||||
Replay is one keyed CultCache document per caller/request identity in an owned
|
||||
Redb store. `Active` is durable before provider I/O; the exact encrypted
|
||||
`Completed` response is durable before socket reply. After restart, completed
|
||||
work replays byte-identically and ambiguous active work refuses without
|
||||
consuming unrelated live capacity. RAM retains only the current process's live
|
||||
execution set.
|
||||
|
||||
Exact Epiphany `ed7357a2` completes its source-side consumer cut. The model and
|
||||
Persona entrypoints cross the shared client ABI; the complete embedded Codex
|
||||
spine, compiled Codex graph, direct auth/HTTP/SSE path, and Codex-home/readiness
|
||||
plumbing are gone. OpenRouter remains a separate direct provider edge. Durable
|
||||
model receipts retain Connector caller identity, exact native/provider digests,
|
||||
and cached-token usage without requiring a transcript. Redacted CultMesh/Odin
|
||||
readiness and one real Epiphany round trip remain open. Yggdrasil now runs the
|
||||
independent service and Idunn target for Ghostlight; Ghostlight can consume the
|
||||
ABI but cannot mutate Connector's source, package, service, credential, or
|
||||
rollback authority.
|
||||
|
||||
Exact `c37cae8b` closes the remaining request-authorship split. The consumer
|
||||
adapter turns the canonical native request directly into one closed durable
|
||||
provider-request variant before inference. For Codex, that variant contains the
|
||||
exact `CodexProviderRequest` sent across the socket. The release edge may add
|
||||
only Connector invocation physiology: caller identity, native-request digest,
|
||||
expiry, encryption, and framing. It cannot lower schemas, rename tool calls,
|
||||
choose tools, or author a second provider request. OpenRouter retains its own
|
||||
exact direct request variant. Decision contexts and runtime model execution
|
||||
persist the same provider-request bytes; runtime v47 and decision context v3
|
||||
refuse the old writable shape.
|
||||
|
||||
Exact `b83c0416` deletes the transient provider-specific request-input, tool,
|
||||
event, payload, and receipt vocabulary. CodexConnector and OpenRouter both emit
|
||||
the canonical `EpiphanyModelStreamEvent` family directly, whose completed event
|
||||
carries the canonical `EpiphanyModelReceipt`. No isomorphic normalization pass
|
||||
or provider-receipt field copy remains.
|
||||
|
||||
Exact `a721b763` deletes the `epiphany-openai-adapter` Cargo package. Pure
|
||||
provider lowering and OpenRouter response projection are modules of
|
||||
`epiphany-model-adapter`, beside the native contracts they translate. Network
|
||||
transport remains outside: OpenRouter at the release edge and Codex in the
|
||||
independent daemon.
|
||||
|
||||
Exact `8086d66f` makes packaged-release ownership physical. The pure release
|
||||
constructor and its witness/file verifier live beneath Core's existing
|
||||
`packaged_release` module, beside the CultMesh publication and authentication
|
||||
path that consumes them. The single-consumer `epiphany-release-construction`
|
||||
package, manifest, workspace target, and dependency edge are gone. The source
|
||||
body moved byte-for-byte; exact target, cache, substitution, and tamper proofs
|
||||
remain module tests under the owning Core library.
|
||||
|
||||
Exact `1e66e770` removes the three Hands receipt convenience constructors. A
|
||||
receipt is an explicit typed consequence, not an object whose provenance should
|
||||
be hidden behind a test-only factory. The retained writers are the admission
|
||||
owner: patch and command identities are immutable; commit validates the exact
|
||||
route, intent, Substrate Gate grant, patch, successful command, current frontier,
|
||||
and atomically creates the Verification obligation. The current-work lifecycle
|
||||
still proves disjoint Persona and Hands commits merge.
|
||||
|
||||
Exact `fab6a59a` deletes the producerless Idunn runtime-health projection. No
|
||||
production path called its aggregate calculator, signer, or RUDP publisher;
|
||||
only its own tests did. Idunn owns deployed daemon health outside Epiphany, and
|
||||
Epiphany's swarm derives startup permission through `resident_readiness`. A
|
||||
second dormant publisher was not redundancy; it was an unauthenticated claim
|
||||
of physiology.
|
||||
|
||||
Exact `200732e9` deletes the adjacent producerless Idunn provider-health store.
|
||||
No production process issued its query, verified a candidate through it, or
|
||||
read its monotonic admission documents; only six module-local tests did. The
|
||||
release swarm compiles without that vocabulary and continues to authenticate
|
||||
its exact packaged release, brake, process owner, and resident provider through
|
||||
`resident_readiness`. Idunn's deployment health remains an external receipt,
|
||||
not an Epiphany-owned shadow store.
|
||||
|
||||
Exact `1e4fa533` narrows packaged-release API to actual consumers. Authentication
|
||||
opens its typed witness directly; construction-private binary enumeration and
|
||||
witness I/O no longer masquerade as root APIs; validation remains visible only
|
||||
to the parent publication/authentication owner. Package, publish, inspect,
|
||||
authenticate, and exact digest operations remain the external contract.
|
||||
|
||||
Exact `e905dcd9` removes the canonical brake-surface helper as a separate API.
|
||||
The seven protected surfaces are construction data of the default brake, not a
|
||||
second callable authority. Engage, release, load, write, validation, and the
|
||||
typed brake document remain unchanged.
|
||||
|
||||
Exact `f962c0bc` removes the test-only `ResidentProviderHealth` aggregate and
|
||||
its authenticator. Resident startup has one decision path:
|
||||
`derive_resident_cognition_readiness` loads the exact provider readiness row,
|
||||
probes its process singleton, and asks `provider_authority_failures` for the
|
||||
concrete failed predicates through `provider_is_fresh`. The dead boolean
|
||||
wrapper and duplicate release/freshness/owner test are gone; exact predicate,
|
||||
clock-skew, release/process/brake, CAS, and private-state proofs remain.
|
||||
|
||||
Exact `d0a317bd` removes five one-caller review-job-ID wrappers. One coordinator
|
||||
review dispatch pulls one current-work snapshot and derives Research,
|
||||
Verification, proposal Modeling, Body Modeling, or frontier-verdict Modeling
|
||||
identity from that same state. The shared attempt projection exposes its job ID
|
||||
only for Review; every family still enters its own exact result and admission
|
||||
owner. No generic role registry or second scheduling projection exists.
|
||||
|
||||
Exact `1a9dec56` collapses the adjacent duplicated review consequences. A
|
||||
private target names the already-selected family and exact job; the coordinator
|
||||
then performs one typed result read and one operator-stop path. Automatic review
|
||||
dispatches to the existing family-specific admission function, emits the same
|
||||
family event cargo, and refreshes status once. The target is in-process control
|
||||
flow only: it owns no durable state, scheduling, callback table, or CAS law.
|
||||
|
||||
Exact `fca516f5` narrows the adjacent inspection surface. Resident-provider
|
||||
documents are validated and loaded only inside their readiness owner. Resident
|
||||
Self exposes no generic terminal-receipt reader; the consequential verifier
|
||||
reads the exact typed envelope. RepoModel basis currentness is checked through
|
||||
the cache-owned complete-view validator, with no second path-based assembler.
|
||||
|
||||
Exact `e7b66b35` makes causal request-ID construction an internal Core organ.
|
||||
The six family functions still share the one length-framed SHA-256 primitive;
|
||||
the crate root carries them only at crate visibility, not as downstream ABI.
|
||||
|
||||
Exact `9f189871` applies the same boundary to adjacent cognition helpers. The
|
||||
admitted-direction prompt is called through its owning module, supersession is
|
||||
crate-only, and Resident Self policy/death helpers are same-module private.
|
||||
|
||||
## Canonical authority map
|
||||
|
||||
| Owner | Inputs | Outputs | Invariant |
|
||||
|
|
@ -354,14 +846,14 @@ interface authority.
|
|||
| `reasoning_context.rs` Mind commit owner | invariant-owned strong reads and complete typed writes | atomic batch CAS plus `EpiphanyMindCommitReceipt` | Disjoint identities merge; same-identity or changed-strong-read conflicts refuse without partial mutation. |
|
||||
| runtime bootstrap | immutable runtime identity envelope | exact identity strong read plus keyed session/job/launch writes | Worker launch replays the original identity and existing session envelopes byte-for-byte; it cannot refresh a singleton timestamp or capability mirror and thereby serialize unrelated work. Native runtime kind and supported document types are derived for CultNet publication. |
|
||||
| concrete family admission owners | sealed decision context, exact family request/result chain, affected semantic documents | one family-specific `MindMutation` | The model cannot choose which stale state is safe to ignore. |
|
||||
| `current_work.rs` | keyed Mind view and exact runtime request/job/result/decision families | pure family scheduling and continuation projections | Events, timestamps, role lanes, and thread provenance cannot create or suppress work. |
|
||||
| `current_work.rs` | one pulled CultCache snapshot containing keyed Mind and exact runtime request/job/result/decision families | pure family scheduling and continuation projections | Mind, RepoModel, Research, Planning, Hands readiness, and launch projections derive from the same envelope set. Events, timestamps, role lanes, thread provenance, and later live reads cannot create or suppress work. |
|
||||
| coordinator policy/status | keyed Mind presence, exact current work, and the accepted continuity decision | one action/reason pair plus operator-readable role lanes | Coordinator presence is derived; no mutable coordinator head, pressure tableau, or duplicate policy wrapper exists. |
|
||||
| runtime worker attempt owner | immutable launch, exact process claim, typed result, job result, archival evidence | one terminal attempt authority | Scheduling, process liveness, and semantic admission remain distinct authorities. |
|
||||
| model-provider boundary | sealed native model request plus explicit provider configuration and injected credential | exact internally derived provider request and transport result | Provider selection cannot author a second request truth or admit Mind state. |
|
||||
| runtime worker attempt owner | sealed typed launch, exact process claim, semantic result or typed failure, job result, archival evidence | one terminal attempt authority | The launch document solely owns live family identity. The result cannot repeat routing identity; after live deletion the archive owns exact request kind/ID beside the preserved structured decision. Scheduling, process liveness, and semantic admission remain distinct authorities. |
|
||||
| model-provider boundary | sealed native model request plus explicit provider configuration | one closed durable provider-request variant and transport result | The consumer adapter owns provider lowering. OpenRouter remains a release-owned direct boundary. For Codex, the persisted request is the exact `CodexProviderRequest` crossing the lean Connector client ABI; the release edge adds only authenticated invocation physiology. The daemon owns caller admission, credential refresh, exact transport, and durable replay. No transport layer can author a second request truth or admit Mind state. |
|
||||
| OpenAI Responses schema projector | full native typed output schema | one provider-legal strict generation schema | Provider formatting preserves useful supported constraints but never replaces native decoding or Mind admission. |
|
||||
| model-pass terminal owner | sealed reasoning basis/context and typed failure class | exact transport closure plus `EpiphanyModelPassFailure` and terminal session/job result in one batch CAS | The caller cannot nominate a job/session; the context-derived binding closes role, reorient, and Persona failures without granting generic transport results decision authority. |
|
||||
| Substrate Gate | exact worker/job authority and requested operation | scoped grant or refusal | Access permission does not admit Mind state. |
|
||||
| Eyes | explicit external-evidence obligation plus governed source receipts | typed evidence packet and Mind observations | Eyes gathers outside evidence; it does not gate Modeling over the Body. |
|
||||
| Eyes | explicit external-evidence obligation plus governed grant/tool receipts | keyed Mind evidence/observations plus exact commit receipt | Eyes gathers outside evidence; it does not gate Modeling over the Body. |
|
||||
| Modeling | Body basis, keyed RepoModel view, verified consequences, and explicit proposals | typed graph/frontier mutations | Modeling processes the Body directly and owns no external-source permission. |
|
||||
| Hands | adopted route/plan plus exact capability receipts | typed consequences | A claimed intention is not a consequence. |
|
||||
| Soul | exact consequence and invariant/evidence obligations | verification audit or refusal | Work is not true merely because it ran. |
|
||||
|
|
@ -650,12 +1142,14 @@ Coordinator priority is a pure projection:
|
|||
No default Modeling job, latest lane, accepted-at comparison, runtime event, or
|
||||
generic interrupt can manufacture work.
|
||||
|
||||
There is no persisted generic runtime-event document. A runtime session or job
|
||||
owns its own lifecycle state; `EpiphanyRuntimeJobResult`,
|
||||
`EpiphanyModelPassFailure`, `EpiphanyCoordinatorRunReceipt`, and
|
||||
`EpiphanyCoordinatorDeathRecovery` own their exact terminal facts. CultNet and
|
||||
operator views derive display from those documents. No paired event may become
|
||||
a second terminality, archival, replay, or recovery authority.
|
||||
There is no persisted generic runtime-event or generic job-result document. A
|
||||
runtime session or job owns only its lifecycle latch;
|
||||
`EpiphanyModelPassFailure`, typed role/reorientation outcomes,
|
||||
`EpiphanyCoordinatorRunReceipt`, exact process claims, and
|
||||
`EpiphanyCoordinatorDeathRecovery` own their respective terminal facts.
|
||||
CultNet and operator views derive display from those documents. No paired event
|
||||
or summary record may become a second terminality, archival, replay, or
|
||||
recovery authority.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
|
|
@ -676,6 +1170,16 @@ flowchart TD
|
|||
Eyes may create evidence that creates Modeling work. Eyes acceptance never
|
||||
suppresses or authorizes ordinary Body Modeling.
|
||||
|
||||
Exact `edb5c3a3` deletes the eight optional family request-ID mirrors from the
|
||||
generic worker launch and heartbeat options, plus their terminal-result echoes,
|
||||
the copied Body basis, and the copied frontier route. Every current-work,
|
||||
admission, retry, schema, and tool path derives live family identity from the
|
||||
sealed typed launch. A successful terminal result must contain the semantic
|
||||
payload required by that context; a typed failure contains one nonempty error
|
||||
and no success cargo. Retained attempts carry request kind/ID explicitly because
|
||||
retention deletes the live launch. Runtime is v39 and the role-result contract
|
||||
is v5; no old-store reader exists.
|
||||
|
||||
Each simple unresolved model-pass family embeds one
|
||||
`EpiphanyAgentPassAttemptProjection`: continuation action plus the exact latest
|
||||
runtime job identity when one exists. Body, proposal and frontier-verdict
|
||||
|
|
@ -705,9 +1209,87 @@ These are distinct authorities and must not be collapsed into one state enum:
|
|||
- Mind owns adoption of a successful structured family result.
|
||||
- Retention owns deletion only after all live authorities are terminal.
|
||||
|
||||
The attempt aggregate centralizes exact request association and typed terminal
|
||||
classification. Archived fulfilled attempts must retain recoverable decision
|
||||
authority, not merely an ID/digest tombstone.
|
||||
Execution bindings own edges, not copied request content. Exact `ce6bff12`
|
||||
reduces a model binding to request, session, and job identity; provider, source
|
||||
worker, and reasoning basis are read from the exact native model request. A
|
||||
tool binding contains only intent, session, and job identity; its optional
|
||||
model ancestry is read from the exact typed tool intent. Validation and
|
||||
retention reload those owners. No binding timestamp participates in causality.
|
||||
Runtime writable state is v40.
|
||||
|
||||
Exact `cabdd6c3` separates coordinator incarnation audit from model-session
|
||||
admission. `EpiphanyRuntimeSession` now owns only a grouping identity and the
|
||||
active/completed latch that prevents new model/tool work after closure.
|
||||
Coordinator runs never inhabit that aggregate. Their immutable
|
||||
`EpiphanyCoordinatorRunBasis` records thread, resident launch identity,
|
||||
objective, and start time. Terminal success/failure receipts and exact process
|
||||
death recovery both compete through one deterministic
|
||||
`EpiphanyCoordinatorRunTerminality` identity; the winner and its full typed
|
||||
authority are inserted atomically while the exact basis is replayed
|
||||
byte-identically. Unrelated document commits do not participate. Timestamp
|
||||
ordering, coordinator notes, whole-store snapshots, and generic archived status
|
||||
do not own terminality.
|
||||
|
||||
Exact `dbb00c2b` finishes the model-session audit without deleting a real phase
|
||||
boundary. A model session can have terminal jobs between tool rounds, so
|
||||
binding-derived job terminality cannot say that the session has been explicitly
|
||||
closed. The live active/completed latch owns that decision. Retention later
|
||||
deletes the bindings and exact tool/model documents; the private archived
|
||||
session therefore owns the retired identity set and deletion-chain digest that
|
||||
prevents resurrection. Six echo-only fields leave the in-memory model-turn
|
||||
summary, and the one-field closure options wrapper is gone: 38 net lines.
|
||||
|
||||
Exact `a46725a1` reduces the generic runtime job to the only transition it
|
||||
actually performs. A job is inserted `Queued` and terminalizes as `Completed`
|
||||
or `Failed`; no writer ever produced `Running`, `WaitingForReview`, or
|
||||
`Cancelled`, so those states and both creation/update clocks are deleted.
|
||||
Frontier Research no longer sorts attempts by creation time. It evaluates the
|
||||
exact set: more than one live or successful authority refuses, one live attempt
|
||||
waits, one successful attempt becomes reviewable, and terminal failures require
|
||||
their exact job result before retry. Runtime writable state is v41. The next
|
||||
audit boundary is the remaining job role/session join and generic terminal-result
|
||||
cargo, not another lifecycle layer.
|
||||
|
||||
Exact `10a70afc` removes the remaining generic job ownership mirrors.
|
||||
`EpiphanyRuntimeJob` now owns only its semantic identity and the mutable
|
||||
Queued/Completed/Failed latch. The sealed outer-worker launch owns role;
|
||||
`EpiphanyRuntimeModelExecutionBinding` owns model-session membership; outer
|
||||
workers belong to the root session by construction. Session closure, retention,
|
||||
and tool admission derive those exact joins. Generic job results no longer copy
|
||||
role/session and lose their always-empty metadata map. A job snapshot requires
|
||||
zero or one exact terminal result and refuses multiplicity rather than choosing
|
||||
by completion time. Runtime writable state is v42.
|
||||
|
||||
Exact `53e869c0` removes evidence and artifact cargo from the generic terminal
|
||||
job result. Those values belong only to the structured role or reorientation
|
||||
outcome that authored them. Generic transport/process physiology retains
|
||||
verdict, terse summary and next move, completion time, and optional exact
|
||||
decision-context identity. Runtime writable state is v43.
|
||||
|
||||
Exact `8eaa96c5` removes the generic terminal document itself from structured
|
||||
worker completion. A typed role/reorientation outcome is strong-read and
|
||||
byte-identically replayed in the same CAS that terminalizes its job and process
|
||||
claim. Failed typed outcomes close as `Failed`/`TerminalFailure`; successful
|
||||
outcomes close as `Completed`/`TerminalResult`. Archives carry context plus the
|
||||
full typed role result, including typed failures. The generic result family is
|
||||
deleted. Runtime/Mind writable state is v45/v11; archived worker attempts are
|
||||
v3.
|
||||
|
||||
The sealed typed launch owns exact live request association. The terminal role
|
||||
result owns only semantic decision cargo or typed failure. The archived attempt
|
||||
owns historical request kind/ID and typed terminal classification after the
|
||||
live launch is deleted. Archived attempts retain the structured role result
|
||||
and decision context—not an ID/digest tombstone—and preserve typed failure
|
||||
cargo when the terminal process status is failure.
|
||||
|
||||
Verification receives its complete Hands consequence chain in the sealed typed
|
||||
projection: route, authority, intent, patch, command, and commit. Exact
|
||||
`24023265` deletes the second input path that exposed a governed tool for live
|
||||
runtime-store rereads of the same three receipts. The MCP schema/dispatch,
|
||||
prompt mandate, three public readers and exports, source executor store
|
||||
argument, stale Substrate Gate allowance, and advertisement assertion are gone.
|
||||
Tool observations remain for facts genuinely outside the sealed basis. The
|
||||
durable receipt chain and atomic commit-to-Verification admission are unchanged.
|
||||
|
||||
The heartbeat scheduler does not exist. Resident Self directly selects the
|
||||
oldest pending pressure when no grant, prepared launch, or active lease exists.
|
||||
|
|
@ -726,6 +1308,15 @@ the mention pending with the terminal receipt bound into its next deterministic
|
|||
request identity. Resident Self cannot launch, block, or terminalize Persona
|
||||
work.
|
||||
|
||||
Delivered speech has one local consequence owner. Exact `b3e9b229` deletes the
|
||||
standalone Persona delivery-evidence document and the conversation receipt's
|
||||
copied evidence-ID vector. Bifrost keeps the durable signed crossing request and
|
||||
receipt; the Persona social terminal folds that exact receipt into message ID,
|
||||
crossing receipt ID, and digest. Retention reloads and verifies the provider-
|
||||
owned pair and matches it against the terminal. Runtime is v37; the terminal
|
||||
and conversation execution schemas are v2. This is not an Atlas proof: live
|
||||
inter-swarm collaboration remains unrun.
|
||||
|
||||
## RepoModel projection
|
||||
|
||||
RepoModel persistence is keyed by semantic identity: identity/body binding,
|
||||
|
|
@ -764,12 +1355,22 @@ receipts, and their direct context links.
|
|||
Runtime retention may remove SSE frames, deltas, intermediate requests,
|
||||
provider events, and tool scaffolding only after the terminal context is sealed
|
||||
and the retained decision can still reach its basis/context without them.
|
||||
The v1 worker-attempt archive embeds the exact typed role result and its
|
||||
context-bound generic result family. The read-only decision audit consumes the
|
||||
same durable records before and after archival.
|
||||
The v3 worker-attempt archive embeds exact request kind/ID, decision context,
|
||||
and the full typed role outcome when one exists. The read-only decision audit
|
||||
consumes the same durable records before and after archival; it never
|
||||
reconstructs family identity from terminal-result echoes or prose summaries.
|
||||
|
||||
## Verification and open gates
|
||||
|
||||
Focused acceptance at exact `edb5c3a3` proves the complete keyed state-flow
|
||||
across Body and both frontier Modeling families, Planning/PlanMind,
|
||||
Hands/Verification, and Eyes/Research without thread-state or result-echo
|
||||
authority. Content-addressed basis/context substitution, exact provider/tool
|
||||
bytes, transcript-free audit, typed failure, disjoint merge, same-identity
|
||||
conflict, and v38 writable-store refusal all pass. Core/OpenAI libraries and the
|
||||
release-owned model binary compile natively. The cut removes 443 maintained
|
||||
lines net; it does not claim a fresh package, Atlas Gate, or live collaboration.
|
||||
|
||||
Accepted through the local `5b799b12` source boundary:
|
||||
|
||||
- core library `494/494`;
|
||||
|
|
@ -827,11 +1428,12 @@ Accepted through the local `5b799b12` source boundary:
|
|||
|
||||
Open before Model Atlas Gate 1 resumes:
|
||||
|
||||
1. let Idunn compile, test, and seal exact build-affecting source `6831eb69`;
|
||||
2. use that exact package in private fresh-store Ox17; stop after three
|
||||
provider failures total, and prove direct Body Modeling, typed
|
||||
refusal-to-retry if exercised, Hands through Verification, exact context
|
||||
audit, and restart/re-entry without public speech;
|
||||
1. construct the renamed Capstone 17 runtime package from the exact current
|
||||
Epiphany commit and deploy it with new empty private volumes;
|
||||
2. verify Connector `80de3acc` plus the bounded `1eeea5f` bridge before
|
||||
inference, reset the failure budget for that new root, and prove direct Body Modeling,
|
||||
typed refusal-to-retry if exercised, Hands through Verification, exact
|
||||
context audit, and restart/re-entry without public speech or Hands mutation;
|
||||
3. only then restart Model Atlas Gate 1 from a new
|
||||
external root.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,14 +7,13 @@ of app-server routes.
|
|||
## Objective
|
||||
|
||||
Build an inspectable native organism whose typed state, runtime, organ gates,
|
||||
memory, scheduling, interfaces, and social crossings can operate without Codex
|
||||
owning Epiphany cognition. Retain Codex-derived code only where it provides an
|
||||
earned OpenAI-compatible authentication or model-transport capability.
|
||||
memory, scheduling, interfaces, and social crossings operate without Codex
|
||||
owning Epiphany cognition. OpenAI subscription authentication and model
|
||||
transport belong to the independent CodexConnector daemon, not Epiphany's body.
|
||||
|
||||
## Current Mechanism
|
||||
|
||||
- `epiphany-state-model` owns shared typed semantic contracts; durable Mind is keyed CultCache owned by `epiphany-core`.
|
||||
- `epiphany-core` owns keyed Mind admission, runtime spine, organ gates,
|
||||
- `epiphany-core` owns the semantic contracts, keyed Mind admission, runtime spine, organ gates,
|
||||
Resident Self physiology, Persona, and CultMesh integration. Coordinator policy,
|
||||
status, and every migrated work family consume keyed Mind plus exact runtime
|
||||
receipts. Persisted `EpiphanyThreadStateEntry`, the in-memory
|
||||
|
|
@ -26,8 +25,8 @@ earned OpenAI-compatible authentication or model-transport capability.
|
|||
evidence sources until a typed reasoning consumer earns them.
|
||||
- CultCache stores typed state and receipts; CultMesh/CultNet carry typed local
|
||||
and federated projections.
|
||||
- Vendored Codex exposes no Epiphany route, DTO, thread-state field, rollout
|
||||
migration, scheduler, watcher, or bridge crate.
|
||||
- Codex implementation source is absent from Epiphany. The typed Connector
|
||||
contract is the only Codex-facing compile dependency.
|
||||
|
||||
## Invariants
|
||||
|
||||
|
|
@ -68,7 +67,8 @@ earned OpenAI-compatible authentication or model-transport capability.
|
|||
|
||||
### 0. Federate Decision-Auditable Mind
|
||||
|
||||
Status: source-complete; exact-package capstone open. The persisted thread head,
|
||||
Status: source-complete; historical Ox17 checksum-sealed and retired; renamed
|
||||
Capstone 17 image construction and fresh run active. The persisted thread head,
|
||||
global revision transaction, aggregate RepoModel, generic Mind gateway, and
|
||||
dual readers are deleted. Concrete invariant owners submit exact-envelope
|
||||
`MindMutation` plans; CultCache batch CAS merges disjoint identities and refuses
|
||||
|
|
@ -223,17 +223,93 @@ Delete or demote:
|
|||
## Immediate Next Action
|
||||
|
||||
Keep the five-day shakedown, Ox17 deployment, and Model Atlas operational Gate
|
||||
1 paused. Continue the source-subtraction and test-authority audit from exact
|
||||
`6831eb69`. The current sequence has deleted the semantic-memory/workspace-coverage
|
||||
projection system, its local daemon supervisor, and the surviving generic
|
||||
RepoModel profile, impossible lifecycle, producerless receipt/summary, and
|
||||
decorative scoring axes. Modeling consumes typed Body and keyed RepoModel state
|
||||
directly. Domains are seed-owned; model passes mutate only live keyed operations.
|
||||
Persona consumes keyed memory and social state directly. No
|
||||
Epiphany-owned Rider or Unity bridge exists: Brokkr owns Unity through typed
|
||||
Eve/CultMesh capabilities, and a future Rider daemon will own Rider. Do not
|
||||
admit a fresh package until the remaining production surfaces and tests have
|
||||
earned their authority under the same audit.
|
||||
1 paused. Exact `a721b763` leaves Epiphany with one native model event/receipt
|
||||
family and one package owning native contracts plus pure provider lowering.
|
||||
The library-only `epiphany-openai-runtime` package remains earned: its one
|
||||
optional target isolates model/Persona orchestration from seven unrelated root
|
||||
release binaries. Exact `8086d66f` deletes the single-consumer release-
|
||||
construction package and moves its pure constructor beneath Core's existing
|
||||
packaged-release owner. Exact `1e66e770` deletes the producerless Hands receipt
|
||||
constructors while retaining the consequence writers that protect the actual
|
||||
route/grant/Verification chain. Exact `fab6a59a` deletes the never-called Idunn
|
||||
runtime-health signer/aggregate/RUDP module. Exact `200732e9` deletes its
|
||||
equally isolated provider-health query/admission store. The release swarm still
|
||||
uses `resident_readiness`; Idunn's deployed health authority remains external.
|
||||
Exact `1e4fa533` then narrows packaged-release API to its real package,
|
||||
publish, inspect, authentication, and digest consumers. Exact `e905dcd9`
|
||||
inlines the single-caller canonical brake-surface list without changing brake
|
||||
authority. Exact `f962c0bc` deletes the test-only resident-provider aggregate,
|
||||
its dead boolean predicate wrapper, and one duplicate test while preserving the
|
||||
single live readiness path and its exact failed-predicate evidence. Audit the
|
||||
five one-caller current-work review-ID wrappers next: derive review identity
|
||||
from one already-owned projection if the coordinator has it, and delete the
|
||||
repeated store reads instead of introducing a generic review registry. Exact
|
||||
`d0a317bd` lands that cut. Continue through the adjacent five duplicated review
|
||||
consequence branches: select one private typed family target, execute the common
|
||||
result/stop/admit/status skeleton once, and retain family-specific admission
|
||||
without adding persisted or registry state. Exact `1a9dec56` lands that cut.
|
||||
Continue with a class audit of test-only public readers and validators; demote
|
||||
interfaces needed only by consequential Core proofs, and delete those that
|
||||
merely manufacture a second authority. Exact `fca516f5` lands that class cut:
|
||||
the terminal-receipt proof now reads the typed envelope in its test, RepoModel
|
||||
currentness uses the live cache validator, and resident readiness internals are
|
||||
private. Continue at the crate-root export boundary; deterministic causal-ID
|
||||
helpers used only inside Core should not masquerade as a public library API.
|
||||
Exact `e7b66b35` demotes that entire causal-ID surface without changing the
|
||||
digest law. Continue the same class audit through internal prompt rendering,
|
||||
supersession, and Resident Self policy/completion helpers; keep entrypoints that
|
||||
are actually consumed by release binaries or sibling packages public.
|
||||
Exact `9f189871` lands that cut and removes the dead prompt root alias. Exact
|
||||
`ed4ca58c` then deletes `RepoFrontierAdoptedPlan::effective_action` and
|
||||
`effective_command`; the operator projection and Hands consequence validation
|
||||
read the owning public fields directly. A bounded closing audit found no
|
||||
further unearned public function across `runtime_spine`, `state_model`,
|
||||
`current_work`, `resident_readiness`, and `resident_self`; every survivor has a
|
||||
production consumer, and the sole low-fanout candidate protects resident
|
||||
objective authentication. Source subtraction has reached diminishing returns.
|
||||
Construct and run the fresh provider-neutral Capstone 17 package next;
|
||||
`state/map.yaml` owns its current direction.
|
||||
|
||||
Resident startup now has one health decision. `derive_resident_cognition_readiness`
|
||||
loads the exact provider row, probes the process singleton, and obtains the
|
||||
concrete failed authority predicates through `provider_is_fresh`. No parallel
|
||||
aggregate or boolean wrapper can reinterpret those facts for another caller.
|
||||
|
||||
Coordinator family review now consumes one `EpiphanyCurrentWorkProjection`.
|
||||
Five path-based job-ID APIs are gone; the shared attempt projection exposes a
|
||||
job ID only in Review state. Research keeps its specialized lifecycle and the
|
||||
three Modeling families keep their established priority. The remaining repeated
|
||||
result-read/operator-stop/admission blocks are gone. One private target now
|
||||
selects the family, and one common pipeline reads, pauses, admits, emits, and
|
||||
refreshes. The target cannot persist, schedule, or admit anything itself.
|
||||
|
||||
Preserve exact provider-request sealing, the lean Connector client ABI,
|
||||
OpenRouter's separate temporary release edge, keyed Mind audit/CAS guarantees,
|
||||
direct Body-to-Modeling flow, Persona state, and the complete unrun Model Atlas
|
||||
slice. No Epiphany-owned Rider or Unity bridge exists: Brokkr owns Unity through
|
||||
typed Eve/CultMesh capabilities, and a future Rider daemon owns Rider.
|
||||
|
||||
The provider decision is the independent `GameCult/CodexConnector` service;
|
||||
exact `6dc80f6d266db4d82566d2434adcc55a48e8ecad` owns the v2 multi-caller contract,
|
||||
exact native/provider digest binding, typed tool and terminal receipts, the
|
||||
pinned official Codex credential child, raw Responses transport, and durable
|
||||
keyed replay. No features yields only the typed contract; default `client` adds
|
||||
authenticated framing and socket transport; `daemon` adds service authority.
|
||||
It links no Codex crate. Active replay identity is written before network
|
||||
access; the exact encrypted completion is durable before reply; restart
|
||||
ambiguity refuses rather than re-executing. Yggdrasil now runs Connector as an
|
||||
independent service and Idunn target for Ghostlight. One digest-bound Epiphany
|
||||
round trip remains unaccepted while Epiphany's selected edge is OpenRouter.
|
||||
|
||||
Epiphany pins the contract-only surface in its adapter and enables the client
|
||||
only at its release edge. Exact `87ea81db` also deletes Epiphany's obsolete
|
||||
public schema for the extinct generic provider request; the shared Codex
|
||||
contract belongs to Connector, while Epiphany's wrapper remains private Mind
|
||||
state. Ghostlight exact `8e7d980` pins the same Connector source; its live Ygg
|
||||
deployment consumes the independent service without owning it. Exact `7a125d78` removes the excluded 3,592-file
|
||||
Codex implementation tree and its obsolete keeper notes; gamecult-ops exact
|
||||
`cb63e77` repairs the independent deployment ownership map. No dual daemon or
|
||||
refresh writer is an acceptable transition state.
|
||||
|
||||
Model Atlas has component proofs but no live inter-swarm collaboration run.
|
||||
Preserve its publication, trust, transport, projection, Soul verification, Self
|
||||
|
|
@ -241,6 +317,47 @@ admission, wake, brake, and Eve path until Gate 1 exercises the complete crossin
|
|||
Streamlining may remove duplicate authors and process shells; it may not spend
|
||||
the only end-to-end collaboration path before it has carried real traffic.
|
||||
|
||||
Exact `6f5d6600` reduces the immutable repository-domain receipt to the
|
||||
canonical organizational repository name and exact authenticated Body hash.
|
||||
Runtime/swarm/workspace belong to the Body route; envelope type/key/schema
|
||||
belong to CultCache; binding time and fixed prose own nothing. The typed receipt
|
||||
still prevents deployment input from relabeling the Body after admission.
|
||||
|
||||
Exact `952dcd9f` reduces operator-objective intake to the human assertion and
|
||||
its actual provenance. CultCache owns envelope identity and schema; the
|
||||
singleton Mind objective owns active state; the exact Mind commit receipt owns
|
||||
atomic admission. One coordinator-binary test that only repeated the library
|
||||
proof is gone, while byte-idempotency, replacement refusal, and resident-grant
|
||||
provenance remain covered.
|
||||
|
||||
Exact `718ce9c1` applies the same ownership rule across the complete durable
|
||||
repository-frontier chain. Thirteen Proposal, Planning, Research, PlanMind,
|
||||
Hands, Verification, Soul, and Modeling documents no longer persist fixed
|
||||
schema/version labels or contract slogans already owned by CultCache and the
|
||||
runtime epoch. Their semantic identities, strong dependencies, exact source
|
||||
versions, decision contexts, evidence, dispositions, and consequences remain.
|
||||
Sealed worker contexts retain explicit schema and contract fields because they
|
||||
are exact model input. Runtime advances to v33. Atlas is untouched and remains
|
||||
protected until a real inter-swarm collaboration run exercises the complete
|
||||
crossing.
|
||||
|
||||
Exact `a4356d1f` roots runtime schema authority in runtime identity plus swarm
|
||||
binding and removes fixed schema-version echoes from thirteen child physiology
|
||||
documents. Sessions, jobs, execution bindings, launches, process claims,
|
||||
results, coordinator receipts, death recovery, and archives retain their exact
|
||||
causal and lifecycle payloads. Runtime advances to v34; old-store refusal and
|
||||
all affected consequence paths remain proven. No compatibility layer replaces
|
||||
the deleted fields. Atlas and external crossing contracts are unchanged.
|
||||
|
||||
Exact `e60b924f` deletes the model runtime's callerless preflight JSON
|
||||
self-certificate and registered-type catalog accessor. Exact `3b991e40`
|
||||
collapses the packaged model runtime onto admitted `run-worker` plus
|
||||
`list-decisions` and `audit-decision`. The three uncalled direct transport/tool
|
||||
CLI paths, their parser/option types, and their self-contained ingress tests are
|
||||
gone. Provider execution and tool continuation now have one production entry
|
||||
path; transcript-free decision inspection remains. The two cuts remove 532 net
|
||||
maintained lines without changing Mind/runtime schemas or Atlas.
|
||||
|
||||
Resident Self now solely imports authenticated Bifrost deliveries; exact
|
||||
`3dda58a5` deletes the standalone Persona feedback ingress and its old Starfire
|
||||
snapshot seam. Exact `56267201` deletes the three unadmitted Atlas daemon
|
||||
|
|
@ -295,17 +412,135 @@ Cut in this order:
|
|||
projection assembled from exact keyed Mind document versions. The final
|
||||
model request must render that projection; citing keyed sources while
|
||||
rendering the old thread snapshot is explicitly forbidden.
|
||||
2. **Body admission landed at `2eb95df6`; current-work projection landed at `e42788c9`:** one pure typed projection now derives Body Modeling, Eyes continuation, proposal Modeling, frontier planning, and Hands readiness from keyed Mind/runtime receipts.
|
||||
2. **Body admission landed at `2eb95df6`; current-work projection landed at `e42788c9`, consumes one exact snapshot at `7374be5e`, typed fulfillment reuses it at `5f1bea39`, coordinator scheduling becomes its sole consumer projection at `2bcbf268`, reorientation result review becomes a single-cache typed read at `125d77a2`, and terminal disposition stays in that projection at `dfe3757b`:** one pure typed projection derives Body Modeling, Eyes continuation, proposal Modeling, frontier planning, Hands readiness, accepted manual-regather obligation, and exact Reorientation success/failure disposition from one pulled keyed Mind/runtime cache. Immutable result replay, family admission, PlanMind continuation, worker archival, Resident Self completion, and operator status validate or route against that same projection rather than reopening runtime state or accepting a parallel CRRC action. Reorientation result review likewise reads its job and structured result from one runtime snapshot and exposes no synthetic backend lifecycle.
|
||||
Modeling must derive from the current Body/RepoModel obligation; Eyes only
|
||||
from explicit external-evidence obligations. Body-generation Modeling
|
||||
identity, typed Body-to-Mind admission, sealed reasoning, and atomic decision
|
||||
admission are landed. Current-work no longer reads the external Body store;
|
||||
status consumes the shared projection.
|
||||
status consumes the shared projection. Exact `0a97eef8` deletes the later
|
||||
callerless Body-only/unresolved-Body reconstruction paths and three unused
|
||||
per-job continuation wrappers, leaving `project_current_work` as the sole
|
||||
slice assembler.
|
||||
Exact `5a047944` also removes the callerless JSON ledger-status helper,
|
||||
generic role lookup helpers, unique-string wrapper, and successful-receipt
|
||||
wrapper. Live typed owners remain; no generic utility replacement is added.
|
||||
Exact `6ccaf937` removes the one-field repository Body Mind-view alias, the
|
||||
unconsumed sorted Persona-quarantine projection, and the unconsumed generic
|
||||
repo-work planning grant. Typed Mind/quarantine owners and exact live
|
||||
coordinator/worker grants remain.
|
||||
Exact `f6a2ad7f` removes the callerless packaged-release-head and Idunn
|
||||
trust-anchor file readers. Exact release authentication and typed signed
|
||||
health admission remain with their live consumers.
|
||||
Exact `be611f24` removes the producerless Imagination consideration-review
|
||||
writer and durable contract together. Candidates remain proposal-only
|
||||
outcomes; actual adopted proposal Modeling stays in its existing lifecycle.
|
||||
Runtime writable epoch is v22.
|
||||
Exact `3140d305` removes the producerless Resident Self runtime-receipt
|
||||
document and the definition-only coordinator role-result enum. Resident Self
|
||||
state v2 refuses v1 stores; actual terminal receipts and runtime retention
|
||||
remain with their live owners.
|
||||
Exact `0091e0e1` removes the unread generic/OpenAI adapter-status documents,
|
||||
their per-pass writers and construction helpers, one self-affirming test,
|
||||
two schemas, and both runtime registrations. Exact request/provider context
|
||||
and live transport construction remain. Runtime writable epoch is v23.
|
||||
Exact `2e3489c4` removes four uninhabited generic planning document families,
|
||||
their empty reasoning-projection field, and the 256-line issue-tracker DTO
|
||||
vocabulary beneath them. Actual frontier planning and adoption remain.
|
||||
Mind/runtime writable epochs are v9/v24.
|
||||
Exact `a6f73fc4` removes the write-only Persona quarantine-pressure document.
|
||||
The terminal turn receipt and receipt-bound quarantined mentions remain the
|
||||
single blocked-consequence authority.
|
||||
Exact `fed4b857` removes the unread Continuity recovery-receipt companion and
|
||||
its entire module. Exact reorientation result, typed Mind decision/failure,
|
||||
sealed context, and commit receipt remain the sole recovery audit path.
|
||||
Runtime writable epoch is v25.
|
||||
Exact `a602fbdc` removes fifteen unconsumed state-model DTO types, two
|
||||
callerless OpenAI-runtime mouths, and eight definition-only type constants.
|
||||
Exact keyed Mind/runtime receipts, RepoModel documents, pass-local reasoning,
|
||||
and the release-owned observed worker route remain. No schema epoch changes
|
||||
because none of the deleted vocabulary was registered or persisted.
|
||||
Exact `9abadfe7` removes the unconsumed provider-frame observation DTO and
|
||||
callback APIs from both transports, plus the provider-specific transcript
|
||||
reader used only by one test assertion. Typed stream events/receipts remain;
|
||||
no audit or admission path depends on frame previews or transcript text.
|
||||
Exact `2a435eb5` advances Persona memory to v2 and tool receipts to v1 while
|
||||
deleting duplicate effect-event provenance, an always-empty relationship,
|
||||
and an always-empty raw-result reference. Mind/runtime epochs are v10/v26;
|
||||
old writable stores refuse without migration.
|
||||
Exact `ffd91c20` deletes three more derived fields: the fixed Research role
|
||||
from the then-live Eyes packets, the fixed Epiphany Persona agent from queued
|
||||
mentions,
|
||||
and the unread Resident Self retention timestamp. Their owning request/
|
||||
context, Persona queue/turn request, and chained retention digest remain.
|
||||
Persona mention v2, Resident Self state v3/retention head v1, and runtime v27
|
||||
formed that hard cut; exact `83611f9b` later deletes the Eyes packet family.
|
||||
Model Atlas remains intact for its first
|
||||
live inter-swarm collaboration run; component proofs are not acceptance.
|
||||
Exact `fe118e13` makes provider events and receipts transient transport
|
||||
values and deletes their duplicate CultCache/CultNet authority. One native
|
||||
model stream/receipt family now owns continuation, reconstruction, terminal
|
||||
validation, and retention; the exact provider request remains durable audit
|
||||
input. Runtime advances to v28, the catalog drops to 18 schemas, and the
|
||||
model-runtime include shell is gone. Atlas remains unchanged and unaccepted.
|
||||
Exact `dbed11b3` and `8bb0719b` delete the remaining hand-maintained schema
|
||||
shadow registry. Runtime-local CultCache documents are discovered from the
|
||||
native registration or projected by their owning provider; they are not
|
||||
republished as stale JSON. The catalog now contains only the exact provider
|
||||
request plus the portable Persona and work-organ contracts. The cut removes
|
||||
850 net maintained lines. Atlas remains unchanged and unaccepted.
|
||||
Exact `695af6c6` deletes thirteen unowned dependency declarations and the
|
||||
final tracked marker for the extinct OpenAI auth-spine crate. The lock graph
|
||||
contracts from 757 to 732 packages; all nine production entrypoints and the
|
||||
affected library/test targets compile individually. Package-scoped cleanup
|
||||
removes 124.2 GiB of reproducible artifacts while preserving the prebuilt
|
||||
state tool. Atlas remains unchanged and unaccepted.
|
||||
Exact `65bf044f` removes redundant schema/prose cargo from the three Hands
|
||||
consequence receipts, fourteen unused shadow type constants, and one
|
||||
callerless reorient display enum. CultCache owns document type/schema
|
||||
identity; exact intent, review, grant, job, consequence, and immutable
|
||||
receipt identity remain. Runtime advances to v29. The concurrent
|
||||
Persona/Hands-to-Verification proof and old-epoch refusal pass; package
|
||||
cleanup removes 678.6 MiB. Atlas remains unchanged and unaccepted.
|
||||
Exact `d9a196a0` makes the Proposal-Modeling request the sole owner of
|
||||
autonomous proposal origin. The companion binding document, schema,
|
||||
registration, third CAS write, collision branch, and copied worker hashes
|
||||
are gone; exact direction-result, option-ordinal, and worker-job identities
|
||||
live on the request. Runtime advances to v30. The full keyed lifecycle and
|
||||
old-store refusal pass; package cleanup removes 677.6 MiB. Atlas remains
|
||||
unchanged and unaccepted.
|
||||
Exact `fdefb889` deletes the generic session/job writers after proving every
|
||||
caller was a fixture. Family launch CAS, model execution, and coordinator
|
||||
opening are the only runtime-physiology creation owners. The cut removes 96
|
||||
net maintained lines without a schema or Atlas change.
|
||||
Exact `b3e9b229` deletes Persona's standalone delivery-evidence document and
|
||||
the conversation receipt's copied evidence-ID vector. Bifrost owns the
|
||||
durable signed crossing receipt; the Persona social terminal owns the local
|
||||
delivered consequence and folds the exact message, crossing, and digest
|
||||
identities into one receipt. Retention verifies that provider-owned receipt
|
||||
directly. Runtime advances to v36 and both changed Persona documents make a
|
||||
v2 hard cut. Atlas remains untouched and operationally unaccepted.
|
||||
Exact `ce6bff12` reduces runtime execution bindings to their owned
|
||||
request-or-intent to session/job edges. Provider, worker, reasoning-basis,
|
||||
and model-request ancestry are loaded from the native request or typed tool
|
||||
intent instead of copied into bindings; non-causal binding timestamps and
|
||||
their API arguments are gone. Runtime advances to v37. The cut removes 67
|
||||
net source lines. One focused OpenAI test generated 12.713 GiB across 19,134
|
||||
files, so its provider/runtime dependency boundary is the next source-shape
|
||||
audit. Atlas remains protected for its first live inter-swarm run.
|
||||
Exact `a6cf9383` resolves that boundary: concrete Codex/OpenRouter auth,
|
||||
credential reading, and transport move to one release-entrypoint source
|
||||
module, while `epiphany-openai-runtime` retains pure exact-request opening,
|
||||
event admission, tool intent emission, and durable audit with no Codex
|
||||
dependency. Persona's concrete runner moves to the Persona service. The
|
||||
duplicate no-tools worker route, DTOs, CLI mode, and coordinator switch are
|
||||
deleted; one governed tool-capable path remains. Provider strict-shape tests
|
||||
move to the release owner. The cut removes 176 net source lines with no new
|
||||
target, process, schema, or epoch. The same runtime consequence proof now
|
||||
uses 1.869 GiB/2,942 files instead of 12.713 GiB/19,134. The release edge
|
||||
still reaches 10.473 GiB/16,483 files through `codex-login`, which is the
|
||||
next provider-edge liability. Atlas remains untouched and unaccepted.
|
||||
3. **Baseline transaction landed at `9f7b164f`; operator routing at `587c56d2`; Resident continuation and keyed acceptance at `478fb923`:** baseline Body Modeling is now thread-free from unresolved Body obligation through Launch/Wait/Review and exact Mind admission. Resident and operator callers consume the same family projection and acceptance owner. Thread ID remains immutable pass provenance only.
|
||||
4. **Proposal Modeling landed at `d1b031cb`:** its immutable request, exact launch binding, runtime attempt/result, and `Modeling.proposal_frontier` commit now own Launch/Wait/Review and admission. The aggregate launcher refuses proposal cargo; the old selector, validator, context builder, and generic-launch hint are deleted. Body and Proposal share only the exact-envelope launch CAS invariant, not a mutable family registry.
|
||||
Exact `6831eb69` later reduces that launch binding from twelve mirrored fields
|
||||
to request ID, job ID, and exact launch digest under its schema; validators
|
||||
fetch the deterministic job-derived key directly.
|
||||
4. **Proposal Modeling landed at `d1b031cb`:** its immutable request, worker launch, runtime attempt/result, and `Modeling.proposal_frontier` commit now own Launch/Wait/Review and admission. The aggregate launcher refuses proposal cargo; the old selector, validator, context builder, and generic-launch hint are deleted. Body and Proposal share only the exact-envelope launch CAS invariant, not a mutable family registry. Exact `b0a4978d` deletes the later duplicate launch-binding document entirely; the worker launch is the sole launch owner.
|
||||
5. **Continuation vocabulary landed at `e404c105` and was named truthfully at `fc5d1a4a`:** concrete pass families share one pure `EpiphanyAgentPassContinuationAction`; their semantic requests, projections, validators, and admission owners remain separate.
|
||||
6. **Dormant claim-repair authority deleted at `20dd66c8`:** no request/binding/context schema, runtime carrier, coordinator branch, result cargo, export, or model lowering survives. The independent Eyes challenge fact remains. The same pass fixed two-snapshot Body/Proposal launch derivation and stress-proved single Proposal launch 20/20.
|
||||
7. **Frontier-verdict Modeling landed at `d367e525`:** one exact Soul verdict
|
||||
|
|
@ -318,7 +553,7 @@ Cut in this order:
|
|||
8. **Frontier Research landed at `c7412998`:** the v3 request seals the exact
|
||||
frontier/dependency document closure, while the full RepoModel projection is
|
||||
audit cargo only. One deterministic attempt, terminal decision context,
|
||||
request-bound Eyes packet, and `Eyes.frontier_research` Mind mutation own
|
||||
structured Research result and `Eyes.frontier_research` Mind mutation own
|
||||
Launch/Wait/Review/admission. Stale terminal decisions remain durable, exact
|
||||
strong-state changes refuse admission byte-identically, and disjoint keyed
|
||||
writes merge. Accepted Eyes, role lanes, timestamps, generic regather, and
|
||||
|
|
@ -343,7 +578,10 @@ Cut in this order:
|
|||
planning conflicts without turning unrelated graph writes into a global
|
||||
head. Thread provenance, generic coordinator validators/constructors,
|
||||
aggregate role lanes, and accepted-at/latest-result behavior no longer own
|
||||
this family.
|
||||
this family. Exact `b55e96ea` deletes both later duplicate launch-binding
|
||||
document families. Immutable worker launches now own attempt identity and
|
||||
current jobs; exact failed results plus typed failure reviews own retry
|
||||
ancestry and are strong reads of the retry launch transaction.
|
||||
11. **Reorientation landed at `d5df53ae`:** one keyed request seals the exact
|
||||
continuity projection and source document versions; deterministic attempts
|
||||
bind an exact reasoning basis and terminal decision context. The family
|
||||
|
|
@ -352,7 +590,40 @@ Cut in this order:
|
|||
coordinator routing use the same current-work projection. Aggregate
|
||||
launch/acceptance, checkpoint/freshness recommendation, latest binding,
|
||||
global revision acceptance, and accepted-at comparison no longer own this
|
||||
family.
|
||||
family. Exact `8dbfcf63` deletes its later duplicate launch-binding document;
|
||||
the immutable worker launch owns request/job/attempt identity, while retry
|
||||
CAS strongly reads the typed failure and exact terminal runtime result.
|
||||
The same cut deletes the duplicate Body Modeling and Imagination
|
||||
consideration binding families. Canonical job identities and immutable
|
||||
family-referenced worker launches now own all three attempt sequences.
|
||||
Their semantic requests, sealed projections, result admission, substrate
|
||||
grants, typed failures, and refusal/retry invariants remain family-owned.
|
||||
Runtime writable state advances to v19. Model Atlas and the unrun
|
||||
inter-swarm collaboration path are unchanged.
|
||||
Exact `3ed1d564` then deletes the standalone public writer for the derived
|
||||
frontier-Modeling request. Accepted Verification is the sole writer and
|
||||
atomically commits its audit, Soul verdict, and request. The deleted path had
|
||||
no production caller and survived only because an OpenAI test fabricated a
|
||||
parallel Verification/Soul tableau; that test now uses ordinary Body
|
||||
Modeling while preserving provider, contract, structured-result, and job-
|
||||
terminality proofs.
|
||||
Exact `63d2991e` then reduces the coordinator terminal receipt to the
|
||||
terminal Self decision and exact resident-launch provenance continuity
|
||||
consumes. Operator artifact inventory, provider/store echoes, arbitrary
|
||||
metadata, step count, and final-job echo stay in their existing owners or
|
||||
disappear when unread. Receipt/schema version is v1 and runtime writable
|
||||
state is v20; the published schema now describes the live resident fields.
|
||||
Exact `196222d9` then makes runtime sessions/jobs minimal lifecycle records:
|
||||
session metadata and job metadata, summary, and artifact refs disappear
|
||||
because no reader consumed them and the terminal job result already owns
|
||||
consequential summary/artifact truth. Launch options no longer accept those
|
||||
echoes. Runtime writable state is v21 and the closed session/job catalog
|
||||
schemas are v1. Model Atlas remains intact for the unrun live inter-swarm
|
||||
Gate 1.
|
||||
Exact `cd2177e8` then deletes the duplicate `ensure_runtime_session` writer.
|
||||
Its two test consumers now use strict session creation; a reused active ID
|
||||
can no longer substitute a different objective, creation time, or
|
||||
coordinator note. No persisted shape changes and runtime remains v21.
|
||||
12. **Landed at `1662a012`, `07d891ba`, and `ec1431ff`:** coordinator policy now
|
||||
projects keyed current work; persisted `EpiphanyThreadStateEntry`,
|
||||
`coordinator_state_transaction`, generic coordinator services, the
|
||||
|
|
@ -587,6 +858,86 @@ Cut in this order:
|
|||
run summary loses its fixed adapter/schema catalogue and caller-supplied
|
||||
store echo. Exact authority remains the typed receipt; stdout returns only
|
||||
intent ID, receipt ID, and terminal status.
|
||||
47. **The sealed launch owns pass-family identity at `edb5c3a3`:** eight
|
||||
optional request-ID mirrors are deleted from worker launch/runtime options,
|
||||
and terminal role results lose the same family echoes plus copied Body basis
|
||||
and frontier route. Live routing decodes the typed launch; retained history
|
||||
uses archived request kind/ID beside the preserved structured decision.
|
||||
Success requires the family semantic payload and typed failure forbids
|
||||
success cargo. Runtime is v39 and role-result output is v5 with no legacy
|
||||
reader.
|
||||
48. **Coordinator incarnation and model-session authority split at
|
||||
`cabdd6c3`:** model sessions retain only identity plus active/completed
|
||||
admission state. Coordinator start provenance is an immutable typed basis;
|
||||
one deterministic terminality identity admits either the exact full run
|
||||
receipt or exact death recovery, so competing outcomes conflict while
|
||||
unrelated document inserts still merge. Runtime is v40. The generic session
|
||||
loses objective, timestamps, notes, and four unused states; the redundant
|
||||
one-field open options and whole-snapshot append helper are deleted. The next
|
||||
subtraction target is the generic runtime job aggregate, not a lifecycle
|
||||
registry.
|
||||
49. **Runtime jobs carry lifecycle, not chronology, at `a46725a1`:** every
|
||||
production job is inserted queued and terminalizes completed or failed.
|
||||
Unwritten running/review/cancelled states and creation/update clocks are
|
||||
deleted. Research derives its exact attempt set, refuses multiple live or
|
||||
successful authorities, and never selects work by timestamp order. Runtime
|
||||
is v41; the cut removes 67 maintained lines net. The remaining audit target
|
||||
is duplicated role/session identity and generic terminal-result cargo.
|
||||
50. **Runtime job ownership is derived at `10a70afc`:** the generic job is only
|
||||
job identity plus Queued/Completed/Failed status. Sealed launches own
|
||||
outer-worker role, model bindings own model-session membership, and root
|
||||
membership is structural. Jobs/results lose role/session mirrors, result
|
||||
metadata disappears, launch options lose three unused fields, and duplicate
|
||||
terminal results refuse instead of sorting by time. Runtime is v42; 73
|
||||
maintained lines are removed net. Next audit generic result cargo by terminal
|
||||
family so structured mirrors disappear without deleting the only failure
|
||||
account.
|
||||
51. **Generic job results do not own decision evidence at `53e869c0`:** evidence
|
||||
and artifact mirrors are deleted from the result contract and completion
|
||||
API. Typed role/reorientation outcomes remain their sole owners. The generic
|
||||
result retains only terminal physiology and the terse failure/display account
|
||||
needed where no structured outcome exists. Runtime is v43; 51 maintained
|
||||
lines are removed net. Next audit remaining summary, next-move, time, and
|
||||
context cargo by terminal family.
|
||||
52. **Structured decisions have one durable owner at `8eaa96c5`:** completing a
|
||||
typed role/reorientation outcome no longer persists a generic result copy.
|
||||
The exact typed envelope is a byte-identical strong dependency in the atomic
|
||||
job/process terminalization CAS. Fulfilled archives retain the context and
|
||||
full typed role result only. Runtime is v44; archived attempts are v3. The
|
||||
source delta is +7 lines for the CAS fence while one durable duplicate family
|
||||
disappears. Next determine whether the residual failure/transport result
|
||||
contract can be deleted entirely.
|
||||
53. **Generic job results are deleted at `2c3335dc`:** typed role and
|
||||
reorientation outcomes atomically close their own job/process physiology;
|
||||
native model receipts or typed model-pass failures own model terminality;
|
||||
exact process claims own process death. Reorientation failure admission and
|
||||
decision audit bind the typed failure directly. One shared outcome rule
|
||||
prevents typed failures from claiming successful process fulfillment, and
|
||||
failed archives retain the full typed failure. Runtime/Mind advance to
|
||||
v45/v11 with no compatibility reader. The cut removes 361 maintained lines
|
||||
net.
|
||||
54. **Model-session retirement is retained but stripped at `dbb00c2b`:** the
|
||||
live session latch owns explicit closure between tool rounds; the private
|
||||
archive owns retired identity collision refusal after bindings are deleted.
|
||||
Six model-turn display echoes and the one-field closure wrapper are gone:
|
||||
38 net lines. No registry or replacement aggregate is added.
|
||||
55. **Verification has one exact Hands input at `24023265`:** its sealed typed
|
||||
projection already carries the complete consequence chain. The live
|
||||
`read_hands_receipt` tool, three runtime readers, exports, tool/prompt/gate
|
||||
cargo, duplicate source-store argument, and advertisement assertion are
|
||||
deleted: 78 net lines. The consequence receipts and atomic Hands-to-
|
||||
Verification admission remain. Next map the missing production Hands
|
||||
actuator as an ABI boundary and demote only speculative public mouths.
|
||||
56. **Hands review authority is deleted at `a01c6842`:** the coordinator's
|
||||
same-function always-approved `HandsActionReview` had no independent reviewer
|
||||
or refusal producer. Adopted route, Substrate Gate grant, minimal intent,
|
||||
and frontier authority now form the complete authorization chain. Patch,
|
||||
command, and commit consequences cite the intent and derive the remaining
|
||||
identities. Commit admission requires exactly one patch and one successful
|
||||
command; ambiguous competing receipts refuse instead of being ordered by
|
||||
timestamp. Runtime advances to v46 and the sealed Verification projection
|
||||
to v2. The cut removes 381 maintained lines net; core 152/152, coordinator
|
||||
4/4, and OpenAI runtime 21/21 pass.
|
||||
|
||||
Do not preserve the aggregate for compatibility, manufacture a bootstrap
|
||||
thread, release autonomous scheduling, register topology in `gamecult-ops`,
|
||||
|
|
|
|||
901
notes/eureka-body-plane-cut.md
Normal file
901
notes/eureka-body-plane-cut.md
Normal file
|
|
@ -0,0 +1,901 @@
|
|||
# The body plane for oversize answers: cut map
|
||||
|
||||
Date: 2026-09-22. Imagination output (Opus). Nothing here is committed and no
|
||||
repository was edited. Self places and commits this file; the natural homes
|
||||
are a section of `F:\Projects\Epiphany\notes\eureka-pipeline-state-cut.md`
|
||||
(the campaign that owns the ruling) with the CultLib half copied or linked
|
||||
into `F:\Projects\CultLib\docs\`, because the cut spans the organ and
|
||||
CultMesh.
|
||||
|
||||
Status: cut map for three cuts, **BP-1** (CultLib: the content plane in the
|
||||
Rust runtime), **BP-2** (the pin move that lets Huginn see it) and **BP-3**
|
||||
(Huginn: an oversize answer is deferred to the body plane).
|
||||
|
||||
**2026-09-29 (Self):** both questions in section 8 are ruled (Q-BP1 A,
|
||||
Q-BP2 A, 2026-09-22). Selection Cut 1 merged to CultLib `main` at `dbee044`,
|
||||
so section 6's collision is gone. **BP-1 is in Hands** (Sonnet) on CultLib
|
||||
`cultmesh/rust-content-plane`, based on `268e0ef`, with two overrides to
|
||||
section 3: no committed mutation entries (the operator retired hand-written
|
||||
harnesses on 2026-09-22; each rule's test is shown red once by a scratch
|
||||
mutant instead), and verification on Yggdrasil through the stopgap rather
|
||||
than on the workstation. The Pins table below is as of 2026-09-22.
|
||||
|
||||
**BP-1 landed** (Sonnet, 2026-09-29) on `cultmesh/rust-content-plane` at
|
||||
`29c3e4c`, `11485fb` and `8a66b1d`. `cultnet-rs` went from 264 to 279 tests,
|
||||
plus 3 C# vector tests, and all 22 listed mutants went red. Size: +486 source
|
||||
lines against ~330 estimated, and +778 test lines against ~350.
|
||||
**Soul (Opus) will merge it after a fix batch, now in Hands:**
|
||||
- F1: a flood of zero-size chunks makes the client send unbounded requests;
|
||||
- F2: malformed requests are refused at decode, so a Rust server answers
|
||||
nothing where the reference answers `found:false`. **This was the map's
|
||||
defect**: section 3 ordered decode-time request validation, which
|
||||
contradicts "a failure is an answer, never silence". Refusals belong to the
|
||||
answer function;
|
||||
- F3: `BTreeMap` metadata breaks byte identity for keys that are not sorted;
|
||||
- F4: hashes are normalised only after requests have gone out;
|
||||
- F5: one error spelling differs from the reference;
|
||||
- F6: 8 surviving mutants.
|
||||
Held: byte identity in both directions beyond the fixture, verification,
|
||||
serving, validation order, and one path. The empty-body deviation (one
|
||||
zero-length chunk) is the reference's behaviour, so the spec was wrong there too.
|
||||
|
||||
**The ruling this map is written under** (operator, 2026-09-17, recorded in
|
||||
the Epiphany map at about line 368): an answer too large for the control
|
||||
plane travels over CultMesh's content/body transfer. The control plane
|
||||
carries a summary and a reference to the body. Raising the window
|
||||
(1,228,800 bytes) and bounding documents smaller were both rejected. **The
|
||||
typed oversize refusal stays as the backstop.** The ruling says the cut "spans
|
||||
the organ and CultMesh", and nothing about it was in Cut 10 except the refusal.
|
||||
|
||||
## Pins
|
||||
|
||||
| Repo | Branch | HEAD | State |
|
||||
|---|---|---|---|
|
||||
| CultLib | `main` (checked out; the operator's shell is on `main`, and the gitStatus snapshot's `cultnet/selection-cut1` is the locked worktree `.claude/worktrees/agent-af580008561975a4c`, also at `b3d9cf7`) | `b3d9cf7` | Dirty in `native/GameCult.Mesh.Quic.Native/*` and `scripts/mutate-cultmesh.mjs` from the QUIC campaign. **Not touched.** `git diff --stat a0813c6 b3d9cf7 -- packages/cultcache-rs packages/cultnet-rs packages/cultmesh-rs` is **empty**, so every Rust anchor below holds at Huginn's pin as well. |
|
||||
| CultLib selection Cut 1 | `cultnet/selection-cut1` | `b3d9cf7` + uncommitted work in the locked worktree | In Hands (another agent). The collision is in section 6. |
|
||||
| Huginn | `eureka/memory-organ` | `202e5e3` | Clean. Cut 10 and its two fix batches have landed. |
|
||||
| Epiphany | `codex/eureka-pipeline-state` | `2876a827` | Map read only. The leaf `epiphany-pipeline` is pinned by Huginn at `5cda0886` and pins `cultcache-rs` at `a0813c6`. |
|
||||
|
||||
## 0b. Model page: identity, lifecycle and authority
|
||||
|
||||
This table was written before any cut was mapped. Every persistent or
|
||||
long-lived kind the cut touches has a row, and no cell is empty.
|
||||
|
||||
| Kind | Identity (what names it) | Lifecycle (what happens to it over time) | Authority (who decides) |
|
||||
|---|---|---|---|
|
||||
| **Deferred body**: the encoded bytes of one `HuginnMindResponse` that did not fit one send | The SHA-256 of those bytes, lowercase hex: the manifest's `contentHash`. Two identical answers are one body. | Created when an answer's encoded envelope exceeds the window and the body is within the deferral bound. A second deferral of the same bytes refreshes it; it is never duplicated. Every chunk served also refreshes it (sliding expiry). It expires when it has been untouched for the TTL, and it is evicted least-recently-touched first when the retained total exceeds the budget. It is lost when the process exits, and the client asks again. It is never written to the mind, a file or any `.cc` store. | The daemon's serve module (`huginn-daemon`) decides to defer and holds the retention store. The mind decides what the answer *is* and knows nothing about deferral. |
|
||||
| **Chunk** | The SHA-256 of the chunk's bytes. Its record key is `mesh:cdn:chunk:<hash>`, which is the C# reference's spelling (`CultMeshCdnArtifactChunk.CreateRecordKey`, `CultMeshCdn.cs:120`). | It lives exactly as long as some retained body references it. A chunk shared by two bodies is stored once and stays until both are gone. | Chunking (the boundaries and the hash) belongs to `cultnet-rs` `pack_content`. Retention belongs to the daemon's store. |
|
||||
| **Reference (manifest)** | `gamecult.mesh.cdn_artifact_manifest.v1`, the reference's type, reused and not reinvented. `contentHash` identifies the body; `artifactId` and the other fields describe it and never identify it. | Transient: minted with the body, carried once inside the deferred response, and never stored by the daemon or the client. It stays valid while the body is retained. After that a chunk request answers `found: false` and the client re-asks the operation. | `cultnet-rs` owns the shape and the validation rules, with parity to `CultMeshCdn.ValidateManifestShape` (`CultMeshCdn.cs:499-526`). The daemon fills it in. |
|
||||
| **Deferred response**: the control-plane answer that stands in for an oversize one | Correlated by the operation envelope's `messageId`, exactly like every other answer. | One per request, sent once, and never deferred itself. It always fits: at the deferral bound it is about 41 KB, far under the window. | Only the daemon's serve module produces it. `Mind` never does. |
|
||||
| **Content chunk wire messages** `cultmesh.content_chunk_request.v1` / `.response.v1` | Their `schemaVersion` strings, both already defined by the reference (`CultNetSchemaMessages.cs:153,157`, classes `:1414-1446`). | Stateless: one request, one response. | The C# reference owns the spelling. `cultnet-rs` gains it with byte identity, pinned by vectors in both directions. |
|
||||
| **Vector fixtures** `contracts/cultmesh/content-vectors.cs-written.json` and `.rs-written.json` | File path plus the `label` of each vector. | Committed. Each writer regenerates its own file under `CULTNET_WRITE_VECTORS=1`. The file is expected to grow and is not pinned as final. | Each runtime owns its written file. Both runtimes' tests read both files. |
|
||||
| **Mind documents and receipts** | Unchanged: leaf keys and receipt ids. | Unchanged. **No body-plane state enters the mind.** | Unchanged: `huginn-mind` admission. |
|
||||
|
||||
Forks this page surfaced, which are sections 8's Q-BP1 and Q-BP2:
|
||||
|
||||
- **Authority over the physical path.** CultMesh's own doctrine forbids
|
||||
schema messages carrying bulk bodies and names the RUDP content path as
|
||||
legacy. That is Q-BP1.
|
||||
- **What the summary is.** The ruling says "a summary". Whether that means the
|
||||
reference's own facts or a per-operation typed summary decides whether
|
||||
`huginn-mind` grows new types. That is Q-BP2.
|
||||
|
||||
**Identity cannot be the request.** Re-deriving a body per chunk request from
|
||||
the mind would need no retention at all. But a chunk request names only a
|
||||
hash, so the daemon could not know which answer to recompute without a
|
||||
Huginn-specific request shape, and that would give up wire reuse with the
|
||||
reference. Rejected for that reason, not asked.
|
||||
|
||||
## 1. Body facts
|
||||
|
||||
Each fact is backed by a source read at the pins above or by a probe that
|
||||
ran code. The probes are in the scratchpad and nothing was committed:
|
||||
|
||||
- `bodyplane-cs/`: a net10.0 console against the built reference DLLs
|
||||
`bin/GameCult.{Networking,Mesh}/Debug/netstandard2.1/*.dll`, dated
|
||||
2026-09-16. The sources it depends on last moved at `f622ee1` (2026-08-18),
|
||||
so the DLLs match `b3d9cf7`. It emitted real reference bytes to
|
||||
`bodyplane-cs/out/`.
|
||||
- `bodyplane-rs/`: a crate against `packages/cultnet-rs` by path, release
|
||||
build. `src/main.rs` is the transport probe and `src/bin/parity.rs` the
|
||||
byte-parity probe.
|
||||
- `pinprobe/`: `cargo tree -d` over mixed CultLib git revisions.
|
||||
|
||||
### F1. Only the C# reference implements body or content transfer. The Rust client Huginn uses has no path today.
|
||||
|
||||
- **Source.** Across `packages/` there is no occurrence of
|
||||
`content_chunk_request`, `body_read_request`, `cdn_artifact_manifest`,
|
||||
`ContentChunk*` or `BodyRead*`, and no `cdn`, `chunkHash` or
|
||||
`artifact_manifest` in any `.rs`, `.ts`, `.py` or `.kt` source.
|
||||
`cultnet-rs` does not contain the word "body". What the other runtimes have
|
||||
is the *local* stream-body negotiation enum (`SharedMemory`,
|
||||
`CultCachePage`, `InlineBytes`, and so on): `cultmesh-rs/src/lib.rs:191-200`,
|
||||
`cultnet-py/.../cultmesh_contracts.py:434`, and in `cultmesh-ts` and
|
||||
`cultmesh-kotlin`. That is same-machine body mapping, not network transfer.
|
||||
- **Probe A (decode).** `cultnet_rs::decode_cultnet_message_from_slice` over
|
||||
bytes the C# reference emitted:
|
||||
- `content_chunk_request` gives *unknown variant
|
||||
`cultmesh.content_chunk_request.v1`*, because the closed enum
|
||||
(`contracts.rs:252-392`) lists 20 `cultnet.*` variants.
|
||||
- `content_chunk_response`, `body_read_response` and a real 256 KiB chunk
|
||||
response give *invalid type: byte array, expected any valid JSON value*.
|
||||
The non-raw decode path converts through `serde_json::Value`
|
||||
(`contracts.rs:404-411`), which cannot hold MessagePack `bin`. So adding
|
||||
the variants alone is not enough: both must ride the raw path that
|
||||
`document_put_raw` uses (`contracts.rs:771-854`).
|
||||
- **Probe E (on the wire).** A Rust hub sent the reference's own 262,357-byte
|
||||
chunk response raw on the `schema` channel. The transport delivered it, and
|
||||
the Rust client refused it in `receive_schema_message_once` with the same
|
||||
byte-array error.
|
||||
|
||||
**Conclusion:** no Rust runtime can request, answer, decode or verify a body
|
||||
or a content chunk today. Rust gets its first content-plane code in BP-1.
|
||||
|
||||
### F2. The reference has two planes, and only one of them fits this class of content.
|
||||
|
||||
- **The content plane.** It carries large immutable artifacts
|
||||
(`src/GameCult.Mesh/docs/content-sessions.md:1-3`). The reference is a
|
||||
`CultMeshCdnArtifactManifest` (`CultMeshCdn.cs:127-190`) listing
|
||||
content-addressed chunks (default 4 MiB, `CultMeshCdnPackOptions`
|
||||
`:44`). The chunk protocol is one bounded response per chunk
|
||||
(`content-sessions.md:44-46`). The transfer owner
|
||||
(`CultMeshContentTransferService`, `CultMeshContentTransfer.cs:115`) is the
|
||||
only owner of chunk verification, whole-body SHA-256 verification, resume
|
||||
and promotion. The preferred connector is TCP: a typed header, then raw
|
||||
bytes outside the envelope (`transport-planes.md:22-26`). It is plaintext
|
||||
and **restricted to explicit loopback development**
|
||||
(`transport-planes.md:103-106`). The RUDP content server
|
||||
(`CultMeshLegacyRudpContentServer`, `CultMeshContentSessions.cs:14-87`) is
|
||||
"basement priority and exists for compatibility and parity measurement"
|
||||
(`transport-planes.md:93-96`), and its connector is never installed by
|
||||
default (`CultMeshContentSessions.cs:97-116`, priority 10,000).
|
||||
- **The body plane proper.** It carries live, lease-bound generations of
|
||||
mutable bodies: `CultMeshNetworkBodyStore` (`CultMeshBodySessions.cs:19-219`,
|
||||
a 16 MiB default bound, 8 retained generations, HMAC capability tokens,
|
||||
expiry by lease). It is served **whole in one response**
|
||||
(`CultMeshBodyServer.HandleAsync`, `:241-272`, payload inline). There is no
|
||||
chunking.
|
||||
- **Why that decides it.** A pipeline answer is an immutable artifact, which
|
||||
is the content plane's stated class. The body plane's single response meets
|
||||
exactly the wall the ruling is routing around: probe B's whole-body send
|
||||
failed with `RUDP reliable send queue is full`. The operator's phrase
|
||||
"content/body transfer" covers both planes, and the source says which one
|
||||
this is. **This map uses the content plane's chunk protocol and manifest.**
|
||||
It is recorded as a reading of the source, not asked, because the body
|
||||
plane cannot carry this answer on any transport Huginn has.
|
||||
|
||||
### F3. What the wire carries. Decoded from real bytes and pinned at byte level.
|
||||
|
||||
| Message | Reference encoding (probe, hex prefix) | Fields in order |
|
||||
|---|---|---|
|
||||
| `cultmesh.content_chunk_request.v1` | named map `0x85`, 169 bytes for the sample | `schemaVersion`, `messageId`, `chunkHash` (64 hex), `recordKey` (may be empty), `expectedSizeBytes` (int32) |
|
||||
| `cultmesh.content_chunk_response.v1` | named map `0x87`, 179 bytes for a 4-byte payload | `schemaVersion`, `messageId`, `found`, `chunkHash`, `sizeBytes` (int32), `payload` (**MessagePack `bin`**), `error` (string, empty on success) |
|
||||
| `gamecult.mesh.cdn_artifact_manifest.v1` | **array** `0x9A` (10 integer-keyed fields), 1,097 bytes for 6 chunks | `[artifactId, kind, version, contentHash, sizeBytes(int64), mimeType, createdAtUtc, chunks[[chunkHash, offset(int64), sizeBytes(int32), recordKey]], tags[], metadata{}]` |
|
||||
| `cultmesh.body_read_response.v1` (not used) | named map `0x8B`, 11 fields | whole generation in `payload` |
|
||||
|
||||
Measured:
|
||||
|
||||
- A 256 KiB chunk response encodes to **262,357 bytes**, 213 bytes of
|
||||
overhead, so four fit in Huginn's 1,228,800-byte window.
|
||||
- The manifest for the real 1,315,551-byte case at 256 KiB chunks is 6 refs
|
||||
and 1,097 bytes.
|
||||
- **Probe parity:**
|
||||
- A Rust `rmpv` map written in the reference's key order is **byte-identical**
|
||||
to the reference's request (169 = 169) and response (179 = 179).
|
||||
- A Rust tuple-shaped manifest decodes the reference's bytes and re-encodes
|
||||
them **byte-identically**.
|
||||
- A Rust re-pack of the same body with SHA-256 at 256 KiB reproduces every
|
||||
chunk reference (hash, offset, size, record key) and the content hash.
|
||||
- `rmp_serde::to_vec_named`, the encoder Huginn uses for responses, still
|
||||
writes a tuple-shaped type as an array (`0x9A`). So the reference's array
|
||||
form survives inside Huginn's named-map payload **only if** the Rust type
|
||||
serialises as a tuple.
|
||||
|
||||
### F4. Chunked delivery works on Huginn's exact transport. Pipelining past the window does not.
|
||||
|
||||
Probes B, C and D used a hub configured exactly as Huginn's `serve::bind`
|
||||
(`serve.rs:135-143`: fragment 1200, pending 1024, one settled session) and the
|
||||
real 1,315,551-byte body, with chunks carried as base64 inside operation
|
||||
responses, the only bytes-bearing message Rust has today.
|
||||
|
||||
- **One reply carrying the whole body** (a 1,754,254-byte envelope):
|
||||
`RUDP reliable send queue is full`, returned to the sender only.
|
||||
- **Sequential request, then reply, one chunk at a time** at 256, 512 and 768
|
||||
KiB: delivered whole and SHA-equal, in 1.56 s, 1.54 s and 1.45 s on loopback
|
||||
(about 0.85 MB/s, paced by the 32-packet send window,
|
||||
`rudp.rs:34`). **Chunk size barely moves throughput.**
|
||||
- **900 KiB** as base64 is a 1,228,983-byte envelope, which exceeds the window,
|
||||
so the send fails.
|
||||
- **Six 256 KiB chunks sent without waiting:** four accepted, two refused
|
||||
immediately, and all four accepted ones were delivered. This is Cut 10's F2
|
||||
(pipelined reads) reproduced at chunk scale. The window is per session and
|
||||
counts unacknowledged packets.
|
||||
|
||||
**Consequence for the design:** the fetch is sequential, one chunk in
|
||||
flight, at 256 KiB. With `bin` payloads, as opposed to this probe's base64,
|
||||
one chunk reply is 262,357 bytes, which leaves room for a control-plane
|
||||
answer on the same session.
|
||||
|
||||
### F5. Moving one CultLib pin duplicates `cultcache-rs` even when its content is identical.
|
||||
|
||||
`pinprobe` (`cultcache-rs` at `a0813c6` beside `cultnet-rs` at `b3d9cf7`,
|
||||
whose `cultcache-rs` content is byte-identical) reports **two**
|
||||
`cultcache-rs v0.2.0` and two `cultcache-rs-derive` in `cargo tree -d`,
|
||||
because Cargo keys a git source by revision.
|
||||
|
||||
Huginn has three pins at `a0813c6`: `huginn-mind`'s `cultcache-rs`,
|
||||
`huginn-daemon`'s `cultnet-rs`, and the Epiphany leaf `epiphany-pipeline`'s
|
||||
`cultcache-rs` (at leaf `5cda0886`). All three must move together, or "one
|
||||
crate pins one revision" breaks (see `huginn-daemon/Cargo.toml` and the Cut 8
|
||||
build check `cargo tree -p huginn-mind -e normal -d` with zero duplicates).
|
||||
That is BP-2.
|
||||
|
||||
### F6. Huginn today.
|
||||
|
||||
The anchors in `huginn-daemon/src/serve.rs` at `202e5e3`:
|
||||
|
||||
- The module doc's stated limit is at `:10-17`, and the pipelined-read
|
||||
paragraphs are at `:32-44`.
|
||||
- `MAX_FRAGMENT_BYTES`, `MAX_PENDING_RELIABLE_PACKETS` and `MAX_RESPONSE_BYTES`
|
||||
are at `:124-131`, and `bind` is at `:135-143`.
|
||||
- `answer` (`:165-194`) routes `OperationRequest` and `SchemaCatalogRequest`,
|
||||
and answers every other family with `Error` (`:188-192`).
|
||||
- **`within_window` (`:217-251`) is the single owner of the size decision
|
||||
today.** It encodes the envelope, admits it at `<= MAX_RESPONSE_BYTES`, and
|
||||
otherwise answers `Refused(ResponseTooLarge { bytes, limit })`.
|
||||
- `run` (`:257-305`) reads the clock once per frame, at `:294`.
|
||||
|
||||
Elsewhere in the organ:
|
||||
|
||||
- `MindRefusal::ResponseTooLarge` is at `huginn-mind/src/refusal.rs:57-65`, and
|
||||
its doc names the daemon as owner.
|
||||
- `HuginnMindResponse` is at `huginn-mind/src/wire.rs:81-90`, and `status` is
|
||||
at `:97-102`.
|
||||
- The published schema is `schemas/cultnet/huginn.mind_response.v1.schema.json`,
|
||||
pinned byte for byte by `published_wire_schemas_match_derivation`
|
||||
(`wire.rs:210-216`).
|
||||
- The tests that pin the gate are
|
||||
`an_answer_too_large_for_one_send_is_a_typed_refusal_that_reaches_the_client`
|
||||
(`serve.rs:548-656`) and
|
||||
`the_gates_boundary_is_the_transports_boundary_byte_for_byte`
|
||||
(`serve.rs:749-777`).
|
||||
- The mutation entries D20, D20L through D20L5, D21 and D21L are in
|
||||
`tools/eureka-cut10-mutations.psd1:250-330`.
|
||||
- `huginn-mind` has **no** `cultnet-rs` dependency today. The selection
|
||||
consumer cut adds one, because `Query { instance, selection }` carries the
|
||||
substrate's type (`docs/cultnet-selection-cut.md` §12).
|
||||
- `eureka-state/src/lib.rs` is empty. Cut 13 has not been built.
|
||||
|
||||
## 2. The cut order
|
||||
|
||||
```
|
||||
[selection Cut 1, Rust commit 3 lands on CultLib main] (in Hands now, not this map)
|
||||
│
|
||||
▼
|
||||
BP-1 CultLib the content plane in cultnet-rs (wire, manifest, pack, answer, verified fetch) + vectors both ways
|
||||
│
|
||||
▼
|
||||
BP-2 Epiphany leaf + Huginn one pin move to a CultLib rev carrying selection Cut 1 and BP-1
|
||||
│
|
||||
├──► [Huginn selection consumer cut] (Epiphany map, unnumbered, not this map)
|
||||
▼
|
||||
BP-3 Huginn an oversize answer is deferred to the body plane; the refusal becomes the backstop
|
||||
│
|
||||
▼
|
||||
Cut 11 / Cut 13 (Cut 13's spec gains: resolve a Deferred answer)
|
||||
```
|
||||
|
||||
BP-3 comes **after** the Huginn selection consumer cut by default, for two
|
||||
reasons. That cut already adds `cultnet-rs` to `huginn-mind`, which BP-3's
|
||||
response variant needs. It also deletes `open_items` and `history`, so BP-3
|
||||
tests fewer read operations. If the operator orders BP-3 first, BP-3 adds
|
||||
that dependency itself (section 7, BP-3 "Adds") and nothing else changes.
|
||||
|
||||
## 3. Cut BP-1. The content plane in `cultnet-rs`
|
||||
|
||||
- **Repo and branch:** CultLib, `cultmesh/rust-content-plane`, from `main`
|
||||
**after selection Cut 1's Rust commit (its commit 3) has landed on `main`**.
|
||||
It depends on nothing else. See section 6 for why it cannot branch now.
|
||||
- **First:**
|
||||
- Record `packages/cultnet-rs` baseline tests (51 in `tests/cultnet.rs`
|
||||
and 7 in `tests/rudp_server_hub.rs` at `b3d9cf7`; recount at the actual
|
||||
base).
|
||||
- Record the target-dir path count and size.
|
||||
- Capture the reference's bytes for every vector with the C# writer
|
||||
before writing any Rust.
|
||||
- **Deletes first:** none. This cut fills an absence. No Rust code carries a
|
||||
body today, so nothing is stood beside, and there is no compensator to cut.
|
||||
This is stated rather than padded.
|
||||
- **Keeps (untouched):**
|
||||
- Every C# source file. The reference does not change: its chunk classes,
|
||||
serializer arms (`CultNetSchemaMessageSerialization.cs:79-80`), CDN pack
|
||||
and validation, and servers stay as they are.
|
||||
- `CultNetDatabaseSubscribeMessage` and its `bodyIds`/`supportedBodyTransports`.
|
||||
- `CultMeshBodyDemand.cs` and `CultMesh.cs`.
|
||||
- `contracts/cultnet/*.schema.json` and `CultNetSchemaRegistry.cs`.
|
||||
- `cultmesh-rs` (Huginn does not depend on it, and this cut does not make it).
|
||||
- `rudp.rs` (the transport carries these as ordinary schema frames).
|
||||
- **Adds:**
|
||||
|
||||
| Add | Owner | Live consumer | Protected invariant | Why an existing owner cannot serve |
|
||||
|---|---|---|---|---|
|
||||
| `contracts.rs`: `CultNetMessage::ContentChunkRequest { message_id, chunk_hash, record_key, expected_size_bytes: i32 }` and `ContentChunkResponse { message_id, found, chunk_hash, size_bytes: i32, payload: Vec<u8>, error }`, renamed to the reference's `schemaVersion`s, **both on the raw path** | `cultnet-rs` | Huginn's daemon (BP-3), `fetch_content` | **byte identity with the reference in both directions** | The closed enum cannot carry them (F1). The JSON-value path cannot carry `bin` (F1). |
|
||||
| `src/content.rs`: `CultMeshCdnArtifactManifest` and `CultMeshCdnChunkRef` (the reference's names; named Rust fields; serde `into`/`from` a private tuple mirror, so every encoder writes the reference's array, F3) | `cultnet-rs` | BP-3's deferred response, the fetch | one reference vocabulary across runtimes; the array form survives `to_vec_named` | A Huginn-local reference type would be a second vocabulary for one thing |
|
||||
| `normalize_hash` (mirrors `CultMeshCdn.NormalizeHash`, `CultMeshCdn.cs:549-560`: an optional case-insensitive `sha256:` prefix is stripped, then trimmed, then lowercased; empty is refused) | `cultnet-rs` | answer, fetch | one hash spelling | — |
|
||||
| `pack_content(artifact_id, kind, version, mime_type, created_at_utc, bytes, chunk_size) -> (CultMeshCdnArtifactManifest, Vec<CultMeshCdnChunk>)` (mirrors `CultMeshCdn.PackArtifact`, `:232-310`) | `cultnet-rs` | BP-3's store | **the same bytes and options produce the reference's manifest byte for byte** | — |
|
||||
| `validate_manifest` (mirrors `ValidateManifestShape`, `:499-526`, **including its sort by offset before the contiguity check**) and a caller-supplied `max_bytes` refused **before any fetch** | `cultnet-rs` | `fetch_content` | a hostile or corrupt manifest never makes a client allocate or ask | — |
|
||||
| `answer_content_chunk_request(request, lookup: impl Fn(&str) -> Option<&[u8]>) -> CultNetMessage` (mirrors `CultMeshLegacyRudpContentServer.HandleAsync`, `CultMeshContentSessions.cs:43-86`, **in its validation order**: message id, hash normalisation, record-key agreement with `mesh:cdn:chunk:<hash>`, lookup, size and hash of what is served; a failure is `found: false`, an empty payload, and `error` = `<ExceptionKind>: <message>` in the reference's spelling) | `cultnet-rs` | BP-3's `serve::answer` | a server never serves bytes whose hash is not the requested one, and never answers a failure as silence | Rust has no `ICultNetSchemaServer`, so a pure function is the whole server |
|
||||
| `fetch_content(manifest, max_bytes, ask: impl FnMut(CultNetMessage) -> Result<CultNetMessage>) -> Result<Vec<u8>>`: sequential, one chunk in flight, in manifest-offset order; verifies each response's correlation id, `found`, size and SHA-256 against the reference, then the whole body's size and SHA-256 against `contentHash` | `cultnet-rs` | Huginn's tests now, `eureka-state` (Cut 13) later | **one owner of verification on the receiving side**, as `CultMeshContentTransferService` is in the reference; no caller re-derives a check | The reference's transfer service is durable and file-promoting, and a C# owner does not serve Rust. This is its in-memory subset, with no resume, no failover and no files |
|
||||
| `lib.rs:21-39`: `mod content; pub use content::*;` | `cultnet-rs` | — | — | — |
|
||||
| `tests/content.rs` (new) | `cultnet-rs` | CI | the rules below | — |
|
||||
| `contracts/cultmesh/content-vectors.cs-written.json` and `content-vectors.rs-written.json`: request, response found, response not found, and manifest vectors. A **deterministic** body (`byte[i] = i % 251`, 1,315,551 bytes, the real oversize case) at 256 KiB. **The fixture must carry:** a hash given with an uppercase `SHA256:` prefix; a manifest whose chunk list is **out of offset order**, which the reference accepts; a final chunk shorter than the rest; a zero-length body (0 chunks); and a chunk response whose `chunkHash` differs from its payload's hash in the **last** hex digit only. Otherwise a loosening that compares prefixes cannot fail. | each runtime writes its own | `tests/content.rs`, the C# test below | **vectors the reference writes decode and re-encode identically in Rust, and the other way round** | — |
|
||||
| `tests/GameCult.Mesh.Tests/CultMeshContentVectorTests.cs` (**a new file**, not `NetworkingTests.cs`, which the selection cut is rewriting): writes the cs vectors under `CULTNET_WRITE_VECTORS=1`; asserts the committed cs vectors equal a fresh encode; decodes the rs-written vectors, asserts field equality and byte-identical re-encode; asserts `CultMeshCdn.PackArtifact` of the fixture body equals the fixture manifest | C# tests | CI | parity from the reference's side | — |
|
||||
| Docs: `src/GameCult.Mesh/docs/transport-planes.md` cut line (`:93-101`) and `content-sessions.md` gain one paragraph each naming the Rust runtime's content path and its transport (the wording depends on **Q-BP1**); `docs/runtime-parity-scope.md` gets one row | docs | — | describe the live system | — |
|
||||
|
||||
No new dependency: `sha2`, `serde_bytes` and `rmpv` are already in
|
||||
`cultnet-rs/Cargo.toml:14-31`. No new package, binary, transport, store
|
||||
format or JSON schema file (see section 5).
|
||||
|
||||
- **Per-file changes** in `packages/cultnet-rs/src/contracts.rs` at `b3d9cf7`.
|
||||
Re-anchor at the base, because selection Cut 1 moves this file.
|
||||
- **`:252-392`**, the enum: two variants after `OperationResponse`.
|
||||
`payload` is `#[serde(with = "serde_bytes")]`, although the raw path
|
||||
never serialises it through serde.
|
||||
- **`:398-418`**, `parse_cultnet_message`: both `schemaVersion`s join the
|
||||
raw arm at `:405`.
|
||||
- **`:420-436`**, `encode_cultnet_message_for_wire`: both join the raw arm
|
||||
at `:427`.
|
||||
- **`:458-643`**, `validate_message`: two arms.
|
||||
- Request: `messageId` non-empty; `chunkHash` non-empty after
|
||||
normalisation; `expectedSizeBytes >= 0`.
|
||||
- Response: `messageId` non-empty. `found` implies
|
||||
`payload.len() == sizeBytes` and an empty `error`. Not `found` implies
|
||||
an empty payload.
|
||||
- These are the reference's own invariants, as its constructors and
|
||||
`HandleAsync` produce them. They are not new policy.
|
||||
- **`:771-810`**, `parse_raw_cultnet_schema_message`: two arms, reading
|
||||
`bin` via `as_slice` and ints via `as_i64` range-checked to `i32`.
|
||||
- **`:812-854`**, `encode_raw_cultnet_schema_message`: two arms writing keys
|
||||
**in the reference's declaration order** (F3), with the payload as
|
||||
`rmpv::Value::Binary`.
|
||||
- **`:964-1138`**, the `gamecult.networking.v0` path: unchanged. Its `_`
|
||||
arms already refuse. A test pins that refusal.
|
||||
- **Authority map:**
|
||||
- **Owner:** `cultnet-rs::content` owns chunking, hashing, manifest shape,
|
||||
manifest validation, answering a chunk request and verifying a fetched
|
||||
body, for every Rust runtime. `contracts.rs` owns the two messages'
|
||||
spelling.
|
||||
- **Inputs:** bytes plus pack options (packing); a chunk request plus a
|
||||
lookup (answering); a manifest, a size cap and a transport closure
|
||||
(fetching).
|
||||
- **Outputs:** a manifest and chunks; a chunk response message; verified
|
||||
bytes or a typed error.
|
||||
- **Derived state:** `recordKey` is derived from the hash and is never an
|
||||
independent identity. `sizeBytes` and `contentHash` are derived from the
|
||||
bytes.
|
||||
- **Forbidden writers:** no consumer (Huginn, `eureka-state`) hashes,
|
||||
chunks, validates a manifest or checks a chunk itself. No consumer
|
||||
re-orders or retries chunks behind `fetch_content`.
|
||||
- **Shared paths:** the daemon's answer and the client's fetch both call
|
||||
`normalize_hash`. `pack_content` and `validate_manifest` share the one
|
||||
contiguity rule.
|
||||
- **Deletion line:** none. This is the first owner.
|
||||
- **Verification:**
|
||||
- **Builds** (PowerShell, `$env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'`):
|
||||
`cargo test -p cultnet-rs --tests` from `packages/cultnet-rs`; then
|
||||
`dotnet test tests/GameCult.Mesh.Tests --filter CultMeshContentVectorTests`.
|
||||
- **Tests, each with the rule it pins and the mutants that must kill it**
|
||||
(committed in `scripts/mutate-cultmesh.mjs`, CultLib's own runner, which
|
||||
owes the Eureka harness contract by name, **once the QUIC campaign has
|
||||
released that file**; see section 6):
|
||||
- `reference_vectors_encode_byte_identically` pins byte identity of the
|
||||
two messages and the manifest.
|
||||
Mutants:
|
||||
- swap two keys in `encode_raw`'s request arm (revert-shaped);
|
||||
- write `payload` as a `str` or an array of ints (loosening);
|
||||
- encode the manifest through a named-field struct, i.e. drop the tuple
|
||||
mirror (loosening, killed because `to_vec_named` then writes a map).
|
||||
- `rust_vectors_decode_in_the_reference` (C#) pins the direction the
|
||||
reference reads.
|
||||
Mutant: Rust writes `sizeBytes` as a `u64`, which MessagePack-CSharp
|
||||
rejects into `int`.
|
||||
- `a_chunk_response_carries_bin_through_the_raw_path` pins F1's failure
|
||||
mode.
|
||||
Mutant: remove the response from the raw arm at `:405`. It must fail
|
||||
with the byte-array error.
|
||||
- `normalize_hash_matches_the_reference` pins the reference's
|
||||
normalisation.
|
||||
Mutants:
|
||||
- case-sensitive prefix strip;
|
||||
- no lowercasing;
|
||||
- no trim (each killed by the fixture's `SHA256:` uppercase vector).
|
||||
- `a_manifest_out_of_offset_order_is_accepted_as_the_reference_accepts_it`
|
||||
pins parity of the sort.
|
||||
Mutant: drop the sort. It must fail, because the fixture's chunk list
|
||||
is shuffled.
|
||||
- `a_manifest_over_the_cap_is_refused_before_any_request` pins that the
|
||||
cap precedes fetching.
|
||||
Mutants:
|
||||
- check the cap after the first chunk (the test counts closure calls,
|
||||
expecting 0);
|
||||
- compare `>` instead of `>=` at the boundary.
|
||||
The cap's boundary vector is exactly `max_bytes` accepted and one byte
|
||||
over refused. **At least one mutant must be a function of the input**:
|
||||
`max_bytes * 2`. The probe sizes sit where doubling would admit them.
|
||||
- `a_chunk_whose_hash_differs_in_the_last_digit_is_refused` pins whole-hash
|
||||
comparison.
|
||||
Mutants:
|
||||
- compare the first 63 characters;
|
||||
- compare lengths;
|
||||
- compare prefixes.
|
||||
All are killed only because the fixture pair differs in the last digit.
|
||||
- `a_body_whose_chunks_verify_but_whole_hash_does_not_is_refused` pins the
|
||||
whole-body check.
|
||||
Mutant: skip the final `contentHash` comparison. The fixture substitutes
|
||||
a manifest whose chunk refs are valid but whose `contentHash` is another
|
||||
body's.
|
||||
- `a_response_for_another_message_id_is_refused` pins correlation.
|
||||
Mutant: skip the id check.
|
||||
- `answer_refuses_a_record_key_that_disagrees_with_its_hash` pins parity
|
||||
with `HandleAsync`'s third check.
|
||||
Mutant: delete the record-key check.
|
||||
- `answer_serves_found_false_with_the_reference_error_spelling` pins that a
|
||||
failure is an answer.
|
||||
Mutant: return `Error` instead.
|
||||
- `gamecult_networking_contract_refuses_content_messages` pins that the
|
||||
legacy contract does not grow them.
|
||||
Mutant: add them to its encoder.
|
||||
- A **no-op control** rewrites each target through the runner's I/O path
|
||||
and must leave everything green.
|
||||
- **Negative checks:**
|
||||
- `git diff --stat main -- src/GameCult.Networking src/GameCult.Mesh/*.cs contracts/cultnet packages/cultmesh-rs packages/cultnet-rs/src/rudp.rs`
|
||||
is empty.
|
||||
- `rg -n "untagged" packages/cultnet-rs/src/content.rs` is empty.
|
||||
- **Operator:** nothing to click. This cut's evidence is the two vector files.
|
||||
- **Subtraction estimate:** removes 0 lines. Adds about 330: `content.rs`
|
||||
about 190, `contracts.rs` about 90, `lib.rs` 2, and docs about 20. Tests add
|
||||
about 260 Rust and 90 C#, plus two vector files. There are no dependencies,
|
||||
targets or formats to remove or add. **The delta is net positive because it
|
||||
buys an explicitly ruled capability with the smallest surface that carries
|
||||
it: the reference's own wire and reference type, not a new one.**
|
||||
- **Build budget:**
|
||||
- Host and target are both the Windows workstation, debug.
|
||||
- Rust: `cultnet-rs` library and tests only, in the warm codex target dir.
|
||||
The probe's release build of the same crate by path added 945 files and
|
||||
0.32 GiB (12.31 to 12.63 GiB) cold. The expected debug delta is up to
|
||||
+1,000 paths and about +0.4 GiB. Nothing is cleaned.
|
||||
- C#: `GameCult.Mesh.Tests`, which rebuilds Networking, Mesh and Caching as
|
||||
dependencies with no source change in them.
|
||||
- No native build, no TypeScript, no Python, no workspace-wide build.
|
||||
- C: had 258 GB free.
|
||||
- The Linux target (Yggdrasil) is not exercised here. Nothing in this cut
|
||||
is platform-specific, and Cut 14 builds on Yggdrasil.
|
||||
|
||||
## 4. Cut BP-2. One pin move
|
||||
|
||||
- **Repo and branch:** Epiphany `codex/eureka-pipeline-state` and Huginn
|
||||
`eureka/memory-organ`. It depends on BP-1 **and** selection Cut 1 being on
|
||||
CultLib `main`, pushed.
|
||||
- **Why it is its own cut:** F5. Moving `cultnet-rs` alone duplicates
|
||||
`cultcache-rs`. The selection consumer cut needs the same move, so **one**
|
||||
move to one revision carrying both CultLib changes serves both Huginn cuts.
|
||||
That saves a second leaf release and a second Huginn bump. Self schedules
|
||||
it once, whichever Huginn cut runs first.
|
||||
- **Changes:**
|
||||
- Epiphany `epiphany-pipeline/Cargo.toml` `cultcache-rs` rev: from
|
||||
`a0813c6…` to the new CultLib SHA. This is one commit. The leaf's own
|
||||
tests must stay green. `cultcache-rs` content is unchanged across
|
||||
`a0813c6..b3d9cf7`, so any change after that is itself a finding.
|
||||
- Huginn: `crates/huginn-mind/Cargo.toml` `cultcache-rs` rev and
|
||||
`epiphany-pipeline` rev to the new leaf SHA, and
|
||||
`crates/huginn-daemon/Cargo.toml` `cultnet-rs` rev. This is one commit.
|
||||
- **Verification:**
|
||||
- `cargo tree -p huginn-daemon -e normal -d` shows zero `cultcache-rs` or
|
||||
`cultnet-rs` duplicates.
|
||||
- The full Huginn suite is green: 74 tests at `202e5e3`.
|
||||
- The cut8, cut9 and cut10 mutation suites rerun clean.
|
||||
- `rg "a0813c6" F:\Projects\Huginn\crates F:\Projects\Epiphany\epiphany-pipeline`
|
||||
is empty.
|
||||
- **Subtraction:** 0 or 0. Only four revision strings change.
|
||||
- **Build budget:** Huginn workspace check and tests, and `epiphany-pipeline`
|
||||
library tests. Each is a cold compile of three CultLib crates at a new
|
||||
revision, about +300 to +600 paths. The same host as before.
|
||||
|
||||
## 5. Cut BP-3. Huginn defers an oversize answer to the body plane
|
||||
|
||||
- **Repo and branch:** Huginn `eureka/memory-organ`. It depends on BP-2 and,
|
||||
by default, on the Huginn selection consumer cut (section 2). It needs
|
||||
**Q-BP2** answered, and **Q-BP1** decides one paragraph of its module
|
||||
documentation.
|
||||
- **First:** rerun
|
||||
`an_answer_too_large_for_one_send_is_a_typed_refusal_that_reaches_the_client`
|
||||
and record the two sizes it prints. At `202e5e3` they are 1,315,551 bytes
|
||||
for the wide view and 1,189,495 for the fitting one. They are the
|
||||
before-and-after witness.
|
||||
- **Deletes first:**
|
||||
- `serve.rs:217-251` `within_window` is **deleted whole**. It is the one
|
||||
owner of the size decision, and it is replaced by `deliver` (below), not
|
||||
wrapped. A gate that refuses and a second gate that defers would be two
|
||||
owners of one decision.
|
||||
- `serve.rs:10-17`, the stated limit ("This is the current bound and not a
|
||||
design target… expect the number to move"), is deleted. The number no
|
||||
longer moves, because the window stops being the organ's delivery limit.
|
||||
- In `tools/eureka-cut10-mutations.psd1:250-330`, entries D20, D20L, D20L2,
|
||||
D20L3, D20L4, D20L5, D21 and D21L are **deleted from the cut10 suite and
|
||||
re-authored in BP-3's suite** against the new spelling. Their `Old`
|
||||
anchors name `within_window`'s lines, which no longer exist, so leaving
|
||||
them would make the cut10 runner fail on anchor-not-found rather than on
|
||||
a rule.
|
||||
- **Keeps:**
|
||||
- `MAX_FRAGMENT_BYTES`, `MAX_PENDING_RELIABLE_PACKETS` and
|
||||
`MAX_RESPONSE_BYTES` (`:124-131`): the window is still what one send
|
||||
carries.
|
||||
- `MindRefusal::ResponseTooLarge { bytes, limit }` keeps its name and
|
||||
shape; **its doc is re-scoped** (below).
|
||||
- The pipelined-read paragraphs `:32-44`, unchanged and still true:
|
||||
deferral does not fix F2.
|
||||
- `encode_or_fail` (`:197-215`).
|
||||
- The `Daemon` dispatch. `daemon.rs` is untouched: **the mind and the pure
|
||||
dispatch know nothing about deferral.**
|
||||
- **Adds:**
|
||||
|
||||
| Add | Owner | Live consumer | Protected invariant | Why an existing owner cannot serve |
|
||||
|---|---|---|---|---|
|
||||
| `huginn-mind/src/wire.rs:81-90`: `HuginnMindResponse::Deferred(DeferredAnswer)` with `DeferredAnswer { manifest: CultMeshCdnArtifactManifest }` under Q-BP2 A, or plus the typed summary under B; `status()` (`:97-102`) answers `accepted` for it. The manifest's schema is embedded through a `#[schemars(schema_with)]` field attribute returning a `$ref` to its published schema, as the selection consumer cut now does for `Selection` (corrected 2026-09-22: a hand-written `impl JsonSchema` for a foreign type is `E0117`, the orphan rule). `schemas/cultnet/huginn.mind_response.v1.schema.json` is regenerated. **The schema id stays `v1`**, because no client of it exists (Cut 13 is unbuilt) | `huginn-mind` (types only) | the daemon, Cut 13 | the reference rides the response schema, so there is no second vocabulary for a client | `HuginnMindResponse` is the only response a client decodes |
|
||||
| `huginn-daemon/src/bodies.rs` (new), `DeferredBodies`: a map from chunk hash to bytes with reference counts, and a map from content hash to `(chunk hashes, bytes, last_touched)`. `retain(manifest, chunks, now)`, which is idempotent on the content hash and refreshes it; `chunk(hash, now) -> Option<&[u8]>`, which refreshes the owning bodies; `expire(now)`; eviction least-recently-touched first when the total is over budget. Constants: `DEFERRED_CHUNK_BYTES = 256 * 1024`; `MAX_DEFERRED_BODY_BYTES = 64 MiB`; `DEFERRED_BUDGET_BYTES = 256 MiB`; `DEFERRED_TTL = 60 s`, which is twice `session_timeout`, and is a field of `ServeOptions` (`:70-79`) with that default | `huginn-daemon::serve` | `deliver`, `answer` | retention is bounded in bytes and time, and a body is never evicted while its chunks are being served within the TTL | The mind must not hold transport artifacts. `cultnet-rs` owns no retention policy, and should not, because the policy is this service's memory budget |
|
||||
| `serve.rs`: `deliver(reply, message_id, operation, runtime_id, bodies, now)` replaces `within_window`. It encodes the envelope. If the envelope is `<= MAX_RESPONSE_BYTES`, the reply is sent unchanged. Otherwise it takes the **payload bytes**, the named MessagePack of the response. If they are `<= MAX_DEFERRED_BODY_BYTES`, it calls `pack_content("huginn.mind_response", "package", "", "application/vnd.gamecult.huginn.mind-response+msgpack", now, payload, DEFERRED_CHUNK_BYTES)`, then `bodies.retain`, and answers `Deferred`. Otherwise it answers `Refused(ResponseTooLarge { bytes: payload_len, limit: MAX_DEFERRED_BODY_BYTES })` | `huginn-daemon::serve` | `answer` | **the one owner of how an answer is delivered.** Three outcomes, in that order, and nothing else decides | — |
|
||||
| `serve.rs:165-194`, `answer`: a new `CultNetMessage::ContentChunkRequest` arm calls `cultnet_rs::answer_content_chunk_request(request, \|h\| bodies.chunk(h, now))`. The error text at `:188-192` names the third family. The signature gains `bodies: &mut DeferredBodies`, and `run` (`:257-305`) owns one and passes it | `huginn-daemon::serve` | the client's fetch | a chunk is answered on the requester's own session, like every other frame | — |
|
||||
| `refusal.rs:57-65`, doc only: `ResponseTooLarge` is now "the answer exceeds the largest body the organ will deliver by any plane", `limit` is `MAX_DEFERRED_BODY_BYTES`, and `bytes` is the encoded answer (the payload, not the envelope). It is still the daemon's, and still never raised by the mind | `huginn-mind` | clients | **the backstop stays, per the ruling** | — |
|
||||
|
||||
- **Authority map:**
|
||||
- **Owner:** `huginn-daemon::serve::deliver` decides, per answer, between
|
||||
delivering it whole, deferring it, or refusing it. `DeferredBodies` owns
|
||||
retention. `cultnet-rs::content` owns chunking, hashing, the manifest and
|
||||
chunk answering.
|
||||
- **Inputs:** the encoded reply, the window constants, the deferral bound,
|
||||
the store's state and the per-frame clock read (`serve.rs:294`, still the
|
||||
crate's only one).
|
||||
- **Outputs:**
|
||||
- The reply, unchanged.
|
||||
- `Deferred { manifest }` plus retained chunks.
|
||||
- `Refused(ResponseTooLarge)`.
|
||||
- `ContentChunkResponse` for each chunk request.
|
||||
- **Derived state:**
|
||||
- The manifest and the chunk hashes are derived from the payload bytes.
|
||||
- The deferred body is **cache-only** (ephemeral transport state, like
|
||||
the reference's `CultMeshNetworkBodyStore`, `CultMeshBodySessions.cs:15-18`,
|
||||
"not a CultCache document store and not world truth").
|
||||
- The mind is not a writer or a reader of any of it.
|
||||
- **Forbidden writers:**
|
||||
- `Daemon::handle` and `Mind` never produce `Deferred` and never read the
|
||||
window.
|
||||
- No second gate: `within_window` is gone, and nothing else compares
|
||||
against `MAX_RESPONSE_BYTES`.
|
||||
- The client never re-derives a check: it calls `fetch_content`.
|
||||
- No pagination or truncation behind the caller's back (the Cut 10 F1
|
||||
ruling still holds).
|
||||
- **Shared paths:** every operation's reply passes through `deliver`,
|
||||
including `Whoami` and `Admit`. There is no per-operation path to
|
||||
deferral. Direct replies and chunk replies leave through the same
|
||||
`hub.send_schema_message` at `:295`.
|
||||
- **Deletion line:** `within_window` and the stated-limit paragraph are
|
||||
deleted before `deliver` is added, and the cut10 entries that anchored
|
||||
on them are removed in the same commit.
|
||||
- **Verification:**
|
||||
- **Builds** (PowerShell, the codex target dir):
|
||||
- `cargo test -p huginn-mind --lib`;
|
||||
- `cargo test -p huginn-daemon --lib`;
|
||||
- `cargo check --workspace`;
|
||||
- `cargo tree -p huginn-daemon -e normal -d`, which must show no
|
||||
`cultcache-rs` or `cultnet-rs` duplicate.
|
||||
- **Tests, each with its rule and mutants.** They are committed in a new
|
||||
`tools/eureka-bodyplane-mutations.psd1` and run by the Eureka harness
|
||||
`C:\Users\Meta\.claude\skills\eureka\tools\eureka-mutations.ps1 -Repo F:\Projects\Huginn`,
|
||||
with a no-op control.
|
||||
- `an_oversize_answer_arrives_deferred_and_fetches_to_the_same_answer`
|
||||
rewrites `serve.rs:548-656` over a real loopback hub and a real mind.
|
||||
The wide view, at 1,315,551 bytes, comes back `Deferred`. The client
|
||||
runs `cultnet_rs::fetch_content` over the **same session**, one chunk
|
||||
in flight, and decodes the bytes as `HuginnMindResponse`. The result
|
||||
**equals** `daemon.handle` of the same request, and is `View(Some)` and
|
||||
not `Deferred`. The fitting view and `Whoami` still arrive directly.
|
||||
This pins the ruling.
|
||||
Mutants:
|
||||
- `deliver` refuses instead of deferring (revert to Cut 10's
|
||||
behaviour);
|
||||
- `deliver` defers at `MAX_RESPONSE_BYTES * 2` (a function of the input:
|
||||
the wide answer sits between 1x and 2x, so doubling lets it through
|
||||
whole and the send fails silently, and the test times out with no
|
||||
reply).
|
||||
- `the_delivery_boundary_is_the_transports_boundary_byte_for_byte`
|
||||
rewrites `serve.rs:749-777`. An envelope of exactly `MAX_RESPONSE_BYTES`
|
||||
goes direct; one byte more is `Deferred`, **not** refused. This pins
|
||||
that the threshold is the window.
|
||||
Mutants:
|
||||
- `<` for `<=` (off by one);
|
||||
- measure the payload and not the envelope (the old D20L5, re-authored);
|
||||
- `MAX_RESPONSE_BYTES - MAX_FRAGMENT_BYTES` (a function of the constant
|
||||
that a "safety margin" edit would produce).
|
||||
- `an_answer_over_the_deferral_bound_is_refused_by_name` uses a synthetic
|
||||
response sized exactly `MAX_DEFERRED_BODY_BYTES` (deferred) and one
|
||||
byte more (refused, with `bytes` = payload length and `limit` =
|
||||
`MAX_DEFERRED_BODY_BYTES`). This pins that the backstop stays and is
|
||||
measured against the deferral bound.
|
||||
Mutants:
|
||||
- `if true` (never refuse);
|
||||
- `limit: MAX_RESPONSE_BYTES` (the old limit leaking through);
|
||||
- `bytes: envelope_len` (the wrong measure);
|
||||
- `MAX_DEFERRED_BODY_BYTES * 4` (a function of the input).
|
||||
- `a_deferred_answer_is_never_itself_deferred` pins a rule written
|
||||
*inside* `deliver`: the deferred reply is always under the window, and
|
||||
a body never decodes to `Deferred`. Encode the `Deferred` reply at the
|
||||
bound (about 256 manifest refs) and assert it is `<= MAX_RESPONSE_BYTES`.
|
||||
Mutant: recurse `deliver` on the deferred reply.
|
||||
- `retention_is_idempotent_and_sliding` exercises `DeferredBodies` alone
|
||||
with an injected `now`. The same body deferred twice is stored once, and
|
||||
its budget use is counted once. A chunk served at `t = TTL - 1`
|
||||
refreshes the body, so it survives to `2 * TTL - 2`. An untouched body
|
||||
is gone at `TTL`.
|
||||
Mutants:
|
||||
- expiry from creation rather than last touch;
|
||||
- `>` rather than `>=` at the TTL boundary;
|
||||
- a TTL that is a function of the input (`ttl / 2`);
|
||||
- count bytes per retain instead of per body.
|
||||
- `eviction_is_least_recently_touched_and_bounded_in_bytes` uses three
|
||||
bodies over the budget. The one touched longest ago goes first, and a
|
||||
shared chunk survives while any owner lives.
|
||||
Mutants:
|
||||
- evict the newest;
|
||||
- evict by insertion order (killed because the fixture touches the
|
||||
oldest-inserted body last);
|
||||
- free a shared chunk with its first owner.
|
||||
- `an_evicted_body_answers_found_false_and_the_client_can_ask_again`
|
||||
pins lifecycle end to end.
|
||||
Mutant: `chunk()` returns stale bytes after eviction. That is
|
||||
impossible by construction, so the mutant used is "do not refresh on
|
||||
serve" at a TTL shorter than a fetch.
|
||||
- `the_mind_never_sees_a_deferral` is a negative test:
|
||||
`rg -n "Deferred|DeferredBodies|MAX_RESPONSE_BYTES" F:\Projects\Huginn\crates\huginn-mind\src`
|
||||
matches only the `wire.rs` variant and its schema test.
|
||||
- `published_wire_schemas_match_derivation` (`wire.rs:210-216`, kept)
|
||||
pins that the regenerated schema equals the derivation.
|
||||
- **Scenario sited at the decision:** the loopback test holds its client
|
||||
on the `Deferred` reply **before** fetching and asserts the manifest's
|
||||
`sizeBytes` equals the direct `daemon.handle` payload's length. That
|
||||
observation is at the decision, not downstream of the fetch (the
|
||||
skill's "put the observation where the rule is decided").
|
||||
- **Negative checks:**
|
||||
- `rg -n "fn within_window" F:\Projects\Huginn\crates` is empty.
|
||||
- `rg -n "sha2|Sha256" F:\Projects\Huginn\crates\huginn-daemon\src` is
|
||||
empty, because hashing belongs to `cultnet-rs`.
|
||||
- Verified to not collide: `huginn-mind`'s `sha2` is for receipts, in
|
||||
another crate.
|
||||
- **Operator:** none until Cut 14. The real-network check (WireGuard to
|
||||
Yggdrasil, an oversize read end to end) is Cut 14's runbook step and is
|
||||
recorded there.
|
||||
- **Subtraction estimate:**
|
||||
- Removes about 35 lines (`within_window` 35, the stated-limit doc 8) and
|
||||
8 mutation entries.
|
||||
- Adds about 190 lines: `bodies.rs` about 110, `deliver` about 35, the
|
||||
`answer` arm 8, the wire variant with its `schema_with` `$ref` about
|
||||
35.
|
||||
- Tests are about +220 net over the rewritten two.
|
||||
- Entries: 8 removed, about 20 added.
|
||||
- No dependency is added: `cultnet-rs` is already a daemon dependency, and
|
||||
`huginn-mind` already has it after the selection consumer cut. If BP-3
|
||||
runs first, it adds that dependency to `huginn-mind`, and that is the one
|
||||
listed cost.
|
||||
- **Build budget:**
|
||||
- `huginn-mind` and `huginn-daemon` library and tests, warm after BP-2.
|
||||
- Expected +50 to +200 paths.
|
||||
- Host and target are both the workstation. The unix arm is not exercised,
|
||||
and Cut 14 builds on Yggdrasil.
|
||||
|
||||
## 6. Interaction with CultNet selection Cut 1, and sequencing
|
||||
|
||||
Selection Cut 1 is in Hands on `cultnet/selection-cut1` in the locked worktree
|
||||
`.claude/worktrees/agent-af580008561975a4c`, based at `b3d9cf7`.
|
||||
|
||||
**What it touches, from its own map:**
|
||||
|
||||
- `CultNetDatabaseSubscribeMessage`: v1 classes beside v0. It **keeps** the
|
||||
body-plane demand fields `bodyIds` and `supportedBodyTransports`, which its
|
||||
§5 lists as not selection.
|
||||
- `CultMeshBodyDemand.cs:177-221` and `CultMesh.cs` body and snapshot
|
||||
selector construction.
|
||||
- `CultNetSchemaRegistry.cs:360-385`.
|
||||
- `CultNetSchemaMessageSerialization.cs:9-85`.
|
||||
- `NetworkingTests.cs`.
|
||||
- In Rust: `packages/cultnet-rs/src/contracts.rs:314-331`, which gains
|
||||
`SnapshotRequestV1`, `DatabaseSubscribeV1` and `SnapshotResponseRawV1`. The
|
||||
last is a **raw** message, so it edits the same `parse_raw` and
|
||||
`encode_raw` dispatch this map edits.
|
||||
- `lib.rs` re-exports, the new `selection.rs`, and the vector writer pattern
|
||||
under `CULTNET_WRITE_VECTORS=1`.
|
||||
|
||||
**Semantic interaction: none.** `bodyIds` and `supportedBodyTransports` on the
|
||||
subscribe message are demand for *live* bodies, the reference's body plane
|
||||
proper (F2). This cut uses the content plane and never touches that message,
|
||||
its schema, the body-demand code or the database subscription server and
|
||||
client. A Huginn read is an operation request and not a subscription, so the
|
||||
two cuts share no rule.
|
||||
|
||||
**Textual collision: certain, in five places:**
|
||||
|
||||
- the `CultNetMessage` enum;
|
||||
- `parse_cultnet_message`'s raw arm;
|
||||
- `encode_cultnet_message_for_wire`'s raw arm;
|
||||
- `parse_raw` and `encode_raw`;
|
||||
- `validate_message`;
|
||||
- and `lib.rs`.
|
||||
|
||||
Two Hands in one working tree is forbidden, and two branches editing one
|
||||
dispatch list is a merge that falls to whoever lands second.
|
||||
|
||||
**Sequence:**
|
||||
|
||||
1. Selection Cut 1 lands its Rust commit (its commit 3) on `main`.
|
||||
2. BP-1 branches from that `main`. The anchors in section 3 are re-taken at
|
||||
that base, and the discrepancy is reported.
|
||||
3. BP-1 lands.
|
||||
4. BP-2 moves the pins once to a revision carrying both.
|
||||
5. Then the Huginn selection consumer cut and BP-3, in that order by
|
||||
default.
|
||||
|
||||
BP-1 does **not** add JSON schema files or registry entries, so it does not
|
||||
touch `CultNetSchemaRegistry.cs` or `contracts/cultnet/`, which selection
|
||||
Cut 1 is editing. The absence of published schemas for the content messages
|
||||
predates both cuts and is a follow-up (section 7).
|
||||
|
||||
**The mutation runner:** `scripts/mutate-cultmesh.mjs` is modified in the
|
||||
working tree by the live QUIC campaign. BP-1 adds its entries only after that
|
||||
file is committed or released, and never onto someone else's uncommitted
|
||||
edit. If the QUIC campaign still holds it when BP-1 is ready, BP-1 carries
|
||||
its entries in a new `scripts/mutate-cultnet-content.mjs` under the same
|
||||
contract, and Self collapses the two later. That is a default, not a
|
||||
question.
|
||||
|
||||
## 7. Deliberately not in scope
|
||||
|
||||
- **Pipelined chunk fetch, and Cut 10's F2 (pipelined reads).** The fetch is
|
||||
sequential. F4 shows the throughput barely depends on chunk size, and
|
||||
pipelining is the known way to lose a reply silently. A window-aware sender
|
||||
is a transport change in `rudp.rs`, not this cut.
|
||||
- **Throughput.** About 0.85 MB/s on loopback (F4), set by the 32-packet
|
||||
pacing window. A 64 MiB body takes about 75 s. Recorded, not addressed.
|
||||
- **A TCP or QUIC content connector in Rust, and Rust session identity with
|
||||
Odin-signed route certificates.** These are the reference's
|
||||
`CultMeshSessionIdentityClient` and `transport-planes.md:73-86`. Huginn's
|
||||
control plane has none of that today, and the body rides the same session
|
||||
with the same (absent) trust. **Trust for both planes is Cut 14's** (D8, the
|
||||
trust boundary).
|
||||
- **Durable, resumable or file-promoted fetch** (the reference transfer
|
||||
service's resume state and `<content-hash>.body` promotion). An answer is
|
||||
re-asked, not resumed.
|
||||
- **Publishing JSON schemas for `cultmesh.content_chunk_*`.** The reference
|
||||
never published them: there is no file in `contracts/cultnet/` and no entry
|
||||
at `CultNetSchemaRegistry.cs:360-385`. Follow-up: a CultLib parity fill for
|
||||
every runtime, after selection Cut 1 has released the registry.
|
||||
- **The TypeScript, Python and Kotlin content plane.** It follows when one of
|
||||
them has a caller, the same rule the selection cut ruled.
|
||||
- **The live body plane** (`body_read_*`, `CultMeshNetworkBodyStore`, body
|
||||
demand) in Rust.
|
||||
- **Persisting deferred bodies across a daemon restart.**
|
||||
- **Per-operation summaries,** unless Q-BP2 B is ruled. Row-level summaries
|
||||
are the selection vocabulary's projections.
|
||||
- **Cut 13's client handling of `Deferred`.** Consequence for Cut 13's spec,
|
||||
to be carried into its refresh: every read tool resolves `Deferred` through
|
||||
`cultnet_rs::fetch_content` with a cap no larger than
|
||||
`MAX_DEFERRED_BODY_BYTES`, and treats `ResponseTooLarge` as "narrow the
|
||||
selection".
|
||||
- **Consequence for Cut 14's spec:** the daemon's memory budget on Yggdrasil
|
||||
grows by up to `DEFERRED_BUDGET_BYTES` (256 MiB).
|
||||
|
||||
## 8. Operator questions
|
||||
|
||||
Two real forks. Every other choice above (the content plane over the body
|
||||
plane per F2, the reference's manifest, the ephemeral store, the constants,
|
||||
the order) is a default with its reason stated. It is recorded, not asked.
|
||||
|
||||
**Q-BP1: RULED A by the operator, 2026-09-22 ("A is fine here").**
|
||||
- Chunk requests and responses travel on the session the client already
|
||||
holds, byte-identical on the wire to the reference's
|
||||
`CultMeshLegacyRudpContentServer`.
|
||||
- BP-1 amends `transport-planes.md` to name this as Rust's *explicit* content
|
||||
path, never an implicit default.
|
||||
- It lasts until Rust has an authenticated content connector, TCP+TLS or
|
||||
QUIC. That later move sits behind the same `fetch_content` owner and needs
|
||||
no change to the reference type.
|
||||
|
||||
*History, the question as asked:* **Q-BP1. Which physical path carries the body?**
|
||||
|
||||
CultMesh's own transport doctrine says "Schema messages must not carry bulk
|
||||
bodies" and that the RUDP content path is legacy, "compatibility and parity
|
||||
measurement" only (`transport-planes.md:90-96`). But the only session
|
||||
Huginn and its client share is CultNet RUDP, and the doctrine's preferred
|
||||
content plane (TCP byte stream) is plaintext and loopback-only until TLS
|
||||
(`:103-106`), while Huginn serves remote clients over WireGuard.
|
||||
|
||||
- **A. Chunk request and response on the session the client already holds.**
|
||||
This is wire-identical to the reference's `CultMeshLegacyRudpContentServer`.
|
||||
`transport-planes.md` is amended to name it as the Rust runtime's
|
||||
*explicit* content path, never an implicit default, until Rust has an
|
||||
authenticated content connector. Cost: it re-legitimises, for Rust, a path
|
||||
the C# doctrine retired, and it inherits RUDP's pacing (F4).
|
||||
- **B. Port the TCP content plane to Rust** (`cultmesh-content+tcp://`, a
|
||||
typed header and raw bytes). Faster, and outside the envelope. It is
|
||||
blocked for Huginn by the doctrine's own loopback-only gate until TLS
|
||||
exists, so it needs a TLS content connector as well: a much larger cut, and
|
||||
Huginn waits on it.
|
||||
- **C. Wait for authenticated QUIC in Rust**, which does not exist. Huginn's
|
||||
oversize answers stay refused until then.
|
||||
|
||||
**Recommended: A.** It is the only option that delivers what the ruling
|
||||
asked for on the substrate Huginn has. It reuses the reference's wire byte
|
||||
for byte, and the amendment keeps the doctrine honest rather than quietly
|
||||
contradicted. B and C are where the body moves later, behind the same
|
||||
`fetch_content` owner and without changing the wire's reference type.
|
||||
|
||||
**What depends on it:** BP-1's documentation paragraph, whether BP-1 grows a
|
||||
TCP connector (B), and whether BP-3 can land before a TLS campaign (B and C
|
||||
block it).
|
||||
|
||||
**Q-BP2: RULED A by the operator, 2026-09-22 ("I accept your
|
||||
recommendations").** The deferred answer carries the reference's own facts:
|
||||
the manifest (`sizeBytes`, `contentHash`, the chunk list) and the operation.
|
||||
It carries no per-operation summary. Row-level facts come from asking again
|
||||
with a header projection. `huginn-mind` gains no summary types. B can be added
|
||||
later on the selection projection if Cut 13's tools show the need.
|
||||
|
||||
*History, the question as asked:* **Q-BP2. What is "the summary" the control plane carries?**
|
||||
|
||||
- **A. The reference's own facts.** The manifest carries `sizeBytes`,
|
||||
`contentHash` and the chunk list, and the envelope carries the operation.
|
||||
A caller decides whether to fetch on size alone. Anything row-level (ids,
|
||||
statuses, `matched`) comes from asking again with a selection projection,
|
||||
which the selection cut already provides.
|
||||
- **B. A typed per-operation summary produced by the mind:** for a view, the
|
||||
id, kind, status and admission facts without the document; for a page,
|
||||
`matched` and row references. The caller can act without fetching at all.
|
||||
Cost: new summary types and derivations in `huginn-mind`, one per read
|
||||
operation, re-specified whenever the selection consumer cut reshapes the
|
||||
reads, plus schema growth.
|
||||
|
||||
**Recommended: A.** It keeps deferral a pure transport decision owned by one
|
||||
function that knows nothing about operations. A per-operation summary is a
|
||||
second projection vocabulary beside the one the operator just ruled into
|
||||
CultNet. If Cut 13's tools show that agents need to decide without fetching,
|
||||
B can be added then, on the selection projection rather than beside it.
|
||||
|
||||
**What depends on it:** `DeferredAnswer`'s shape, whether `huginn-mind`
|
||||
gains types, BP-3's size (B adds about 120 lines and a test per operation),
|
||||
and Cut 13's tool output.
|
||||
|
||||
## 9. What was probed, what was only read, and what is not settled
|
||||
|
||||
- **Probed by running code:** F1 (decode refusals, plus the on-wire refusal),
|
||||
F3 (real reference bytes, byte identity of both messages and the manifest
|
||||
in Rust, the Rust re-pack equal to the reference's), F4 (whole-body send
|
||||
failure; sequential delivery at three chunk sizes with SHA equality and
|
||||
timings; the 900 KiB failure; the pipelining loss), and F5 (duplicate
|
||||
`cultcache-rs` under mixed revisions).
|
||||
- **Read only, not run:**
|
||||
- C# `ValidateManifestShape` sorting and contiguity, `NormalizeHash`, and
|
||||
the legacy server's validation order. These are the rules the vectors
|
||||
will pin in BP-1.
|
||||
- The reference's TCP connector behaviour and the loopback-only gate, from
|
||||
`transport-planes.md`, not from running `CultMeshTcpContentTransport`.
|
||||
- Selection Cut 1's exact edit sites in the worktree. I read its map, not
|
||||
its uncommitted diff, and did not open the locked worktree's changes, to
|
||||
avoid reading another agent's working tree mid-edit.
|
||||
- **Not probed:**
|
||||
- Throughput over WireGuard or to Yggdrasil (loopback only).
|
||||
- The 60 s TTL against a real slow link.
|
||||
- C# decoding **Rust-written** bytes. Byte identity makes it follow, but
|
||||
BP-1's C# test is the first thing that will actually run it.
|
||||
- Whether a hostile chunk flood can starve the control plane on one
|
||||
session (the window is per session, so the likely answer is that it can
|
||||
starve only the flooding session). Recorded for Cut 14's trust pass.
|
||||
- **Substrate missing, recorded per the skill:** typed pipeline state. This
|
||||
map, its model page and its questions live in markdown because Huginn,
|
||||
which would hold them, is the thing being built.
|
||||
6464
notes/eureka-pipeline-state-cut.md
Normal file
6464
notes/eureka-pipeline-state-cut.md
Normal file
File diff suppressed because it is too large
Load diff
170
notes/eureka-pipeline-state-target.md
Normal file
170
notes/eureka-pipeline-state-target.md
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# Eureka pipeline state: target
|
||||
|
||||
Status: target (the ends). The means live in `notes/eureka-pipeline-state-cut.md`,
|
||||
written by Imagination. Operator rulings are dated 2026-09-15 and 2026-09-16.
|
||||
|
||||
**This target was rewritten on 2026-09-16** after the operator rejected the
|
||||
ownership model it had been built on. Rulings 5, 6, 10 and 13's store clauses,
|
||||
which put the store in the task's repo under a per-clone lease, are superseded
|
||||
by rulings 14-17 below. The cut map's Cut 3 sections describe the old model and
|
||||
are history, not live design.
|
||||
|
||||
## Why
|
||||
|
||||
Eureka is the Claude Code counterpart of this organism. It is a skill,
|
||||
published as `GameCult/Eureka` and installed at `~/.claude/skills/eureka`, that
|
||||
runs the same faculties (Self, Imagination, Hands, Soul, Mind Steward) over
|
||||
foundation changes. In its first campaign, the Aetheria CultCache migration, the
|
||||
pipeline worked, but its findings lived in prose: the cut map, subagent reports
|
||||
and memory files. Self re-read and re-summarised that prose at every step. Stale
|
||||
claims survived until a steward caught them. The postmortem needed a transcript
|
||||
crawl, and one operator quote survived only in a compaction summary.
|
||||
|
||||
Epiphany already has the other half: typed documents, admission and receipts.
|
||||
Her own `notes/faculty-workflow-lessons-2026-09-04.md:146-176` proposes typed
|
||||
forms of Eureka's habits (`Map`, `Spec`, `CutReport`, `Verdict`, rulings, a
|
||||
landed-names digest), but none of them are built.
|
||||
|
||||
This campaign builds them once and gives Eureka agents access to them.
|
||||
|
||||
## What the first model got wrong
|
||||
|
||||
The store was owned by a repo. Nothing owned the state, so the machinery grew to
|
||||
compensate: a per-clone writer lease, git-directory resolution, committed
|
||||
`.gitattributes` checks, and a merge tool for divergent copies. Three Soul passes
|
||||
went into propping that up, and it still resolved a store into a directory the
|
||||
operator never named.
|
||||
|
||||
Mind state also does not want version control. Git offers branches, merges and
|
||||
history rewriting; a mind wants one owner and an append-only record.
|
||||
|
||||
The operator's correction: **someone owns the state**. An instance owns its mind,
|
||||
and stewardship over repos is an assignment, not an identity. That is Epiphany's
|
||||
own design, and Eureka inherits it.
|
||||
|
||||
## Operator rulings
|
||||
|
||||
1. **Epiphany owns the schemas** for pipeline state.
|
||||
2. **Searchable state serves agents first.** Self and new agents rehydrate from
|
||||
it ("what rulings govern stores?", "what is still open?"). Soul and
|
||||
Imagination check precedent in it ("have nil map keys bitten us?"). Operator
|
||||
browsing across projects comes later.
|
||||
3. **Two campaigns.** This one covers the schemas, the memory organ, admission,
|
||||
the Eureka MCP client and a proof on one real Eureka task. The second,
|
||||
Epiphany adopting Eureka's habits, comes after the schema stops moving.
|
||||
4. **Re-pin first.** Landed: Epiphany moved from CultLib `e171eca3` to
|
||||
`a0813c6`.
|
||||
5. **Q1-Q5 (2026-09-15), of which only Q2 and Q5 survive the rewrite:** additive
|
||||
schema changes keep the epoch and a breaking bump refuses the old store
|
||||
(Q2); `schemars` is an unconditional `epiphany-core` dependency and the
|
||||
published JSON schemas are derived from the Rust types (Q5).
|
||||
6. **The operator channel is the Claude Code session.** Eureka has no Persona.
|
||||
Self is the operator surface, because spec iteration works best where the
|
||||
question and the tree share a context. A blocking question may be pushed
|
||||
through whatever notification MCP the user configures; Eureka owns no
|
||||
transport and names no provider. Answers come back in the session. Answering
|
||||
over a chat channel is a later campaign and needs identity binding first.
|
||||
7. **Eureka is published** as `GameCult/Eureka`, MIT.
|
||||
|
||||
### Ownership (2026-09-16)
|
||||
|
||||
14. **An instance owns its mind.** A store is canonical to exactly one instance.
|
||||
Identity lives in the state, not in a path. Admission refuses a write
|
||||
carrying another instance's identity, whatever the transport. Stewardship
|
||||
over repos is an assignment recorded in that mind, and one instance may
|
||||
steward several repos. Reassignment is an explicit typed hand-off recorded
|
||||
in both minds, so history stays attributable.
|
||||
15. **A service owns the state, on Yggdrasil.** The memory organ is a daemon: it
|
||||
outlives every session, owns a real resource dependency, and isolates a dead
|
||||
session from a corrupted mind. It earns its process under the GameCult
|
||||
daemon test. Minds are no longer committed to git, so there is no per-clone
|
||||
lease, no git-attribute precondition, and no divergence between clones.
|
||||
16. **The organ depends on Qdrant directly, not on voidbot.** It owns its own
|
||||
collections and indexes at admission time. voidbot keeps its read-only
|
||||
public-repo retrieval; it is not in this path. Embeddings come from the
|
||||
Ollama endpoint the map names. Epiphany's own service embeds against a
|
||||
Yggdrasil-local endpoint with the same model, and only voidbot uses
|
||||
Nightwing; follow the local precedent and name the endpoint, not the host.
|
||||
17. **Huginn is the memory organ.** The dormant `.cc`-to-Eve CLI is retired and
|
||||
generic `.cc` inspection belongs to CultCache Studio in CultLib. Huginn
|
||||
becomes the Rust service that owns instance minds, admission and the index.
|
||||
That gives Huginn a real runtime for the first time, but it closes the
|
||||
authority vacancy only for pipeline state: doctrine still names Huginn the
|
||||
Persona-state steward, and this campaign builds none of that. Recorded as a
|
||||
follow-up, not solved here.
|
||||
|
||||
### Shape decided with those rulings
|
||||
|
||||
- **Transport:** the organ speaks CultNet. `eureka-state` stays a local stdio MCP
|
||||
server that is a thin CultNet client, so the MCP surface stays swappable and
|
||||
the service protocol stays typed.
|
||||
- **Unreachable organ:** refuse loudly and let the campaign stall on that step.
|
||||
No local spool, because that reintroduces two writers.
|
||||
- **Cut 3a code:** keep the document kinds, keys, validation and the refusals
|
||||
that survive the layer. Six of the thirteen typed refusals exist only for the
|
||||
repo store and die with it. Delete the store, lease and git layers, and
|
||||
re-point admission at the organ.
|
||||
|
||||
## End state
|
||||
|
||||
- **Documents.** Epiphany defines the typed pipeline documents: campaign, target,
|
||||
question, ruling, cut spec, cut report, verdict, finding, follow-up and
|
||||
resolution, plus the three ruling 14 requires: instance, stewardship and
|
||||
hand-off. Each has a stable schema id and version, a derived key, and a live
|
||||
Eureka consumer. Schemas are derived from the Rust types and published in
|
||||
`schemas/cultnet/index.json`.
|
||||
- **Instance identity.** A mind carries the instance that owns it and the repos
|
||||
it stewards. A campaign names its repo; the mind holds campaigns across every
|
||||
repo that instance stewards.
|
||||
- **The organ (Huginn).** One service owns every mind it hosts:
|
||||
- admission, with per-document rules and typed refusals;
|
||||
- exact and filtered queries (by campaign, repo, cut, kind, status,
|
||||
supersession, time), "open items", and "rulings in force";
|
||||
- semantic search over its own Qdrant collections, indexed at admission;
|
||||
- a typed hand-off for reassigning stewardship, and an import path for
|
||||
another instance's mind. Import is not a merge: with one owner per mind,
|
||||
two copies cannot diverge, so nothing needs reconciling;
|
||||
- supervised by Idunn, with a named backup owner for its volume.
|
||||
- **Eureka.** The skill's briefs tell Self, Imagination, Hands and Soul to admit
|
||||
and query typed state instead of relaying prose. `eureka-state` is the thin
|
||||
MCP client, registered at user scope.
|
||||
- **Proof.** One real Eureka task runs end to end on typed state: rulings
|
||||
admitted, a cut spec, Hands' cut report, Soul findings, follow-ups queried
|
||||
back, and a fresh agent rehydrating from queries alone.
|
||||
|
||||
## Invariants
|
||||
|
||||
- **One owner.** Exactly one instance owns a mind, and the organ is its only
|
||||
writer. Eureka agents and the MCP client are clients; the index is a
|
||||
projection.
|
||||
- **Identity travels with the write.** An admission carries the instance, and a
|
||||
foreign instance is refused.
|
||||
- **Typed, not prose.** No JSON store and no blob fields where a typed field
|
||||
exists. JSON appears only in published schemas.
|
||||
- **Mind state is not in version control.** Campaign prose (target, cut map) stays
|
||||
in the repo; the mind does not.
|
||||
- **Supersession, not deletion.** An old ruling or finding stays queryable as
|
||||
superseded.
|
||||
- **The skill defers to the schema.** Eureka describes how to use the state; the
|
||||
schema set and admission rules are owned here.
|
||||
- **Availability is honest.** When the organ is unreachable, Eureka refuses and
|
||||
says so. It never writes a second copy.
|
||||
|
||||
## Not in scope
|
||||
|
||||
- Epiphany's own organs consuming pipeline documents: the second campaign.
|
||||
- Operator browsing surfaces (Eve/CultUI) over pipeline state.
|
||||
- Answering operator questions over a chat channel.
|
||||
- Migrating the Aetheria CultCache campaign's prose into typed documents.
|
||||
- Cleanup of `.epiphany-run/` (105 GiB) and the repo-local `target/` (39 GiB).
|
||||
- Huginn's legacy `.voidbot` Persona state, which needs its own admission
|
||||
decision.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Substrate maps (scratch, 2026-09-15): Epiphany typed state and admission; the
|
||||
MCP and client landscape.
|
||||
- Dated comparison: `~/.claude/skills/eureka/references/epiphany-comparison-2026-09-15.md`.
|
||||
- Campaign precedent: `F:\Projects\Aetheria\docs\cultcache-migration-postmortem.md`.
|
||||
- Huginn's dormancy and authority vacancy:
|
||||
`F:\Projects\gamecult-ops\docs\repo-census-2026-09\repos\Huginn.md`.
|
||||
523
notes/eureka-query-prior-art.md
Normal file
523
notes/eureka-query-prior-art.md
Normal file
|
|
@ -0,0 +1,523 @@
|
|||
# Prior art for a typed, serializable query surface over a memory organ's read side
|
||||
|
||||
Eyes pass, 2026-09-16/17. Facts with evidence pointers. No ranking, no recommendation.
|
||||
|
||||
## The thing being measured against
|
||||
|
||||
The five capabilities, abbreviated throughout as **C1..C5**:
|
||||
|
||||
- **C1** typed predicates over each kind's own fields, expressed as data
|
||||
- **C2** projection (summary or whole document)
|
||||
- **C3** ordering plus an opaque cursor
|
||||
- **C4** one hop of citation in both directions
|
||||
- **C5** count / existence without bodies
|
||||
|
||||
The portability facts, abbreviated **P1..P5**:
|
||||
|
||||
- **P1** does the query itself serialize as typed data across runtimes (MessagePack wire, constructible from TS/C#), or is it Rust-only builder calls?
|
||||
- **P2** does it derive a JSON Schema, or is the schema hand-written?
|
||||
- **P3** transitive dependency cost; anything dragging in a runtime, network client, DB driver or parser generator
|
||||
- **P4** does it assume a backing store engine?
|
||||
- **P5** licence, last release, maintenance signal, single-maintainer?
|
||||
|
||||
## The crate this lands in
|
||||
|
||||
`F:\Projects\Huginn\crates\huginn-mind`, manifest at `F:\Projects\Huginn\crates\huginn-mind\Cargo.toml:9-19`. Dependencies exactly as stated in the brief: `anyhow`, `chrono 0.4.44`, `cultcache-rs` (CultLib, rev `a0813c6e`), `epiphany-pipeline` (Epiphany, rev `5cda0886`), `rmp-serde 1`, `schemars 1`, `serde 1`, `sha2 0.10`. Dev-only: `serde_json`, `tempfile`. The manifest comment states the constraint in its own words: "No network, no socket, no index: those are the daemon's (Cut 10, Cut 11)."
|
||||
|
||||
**The baseline P3 measurement, taken directly.** `F:\Projects\Huginn\Cargo.lock` currently resolves **139 packages** for the whole three-crate workspace. A grep of the lockfile for `tokio`, `hyper`, `reqwest`, `tonic`, `async-std`, `mio`, `prost`, `pest`, `lalrpop` and `nom` returns **NONE** — there is no async runtime, no HTTP client, no gRPC stack and no parser generator anywhere in the tree today. The sibling `huginn-daemon` (`crates/huginn-daemon/Cargo.toml`) gets its socket from `cultnet-rs` and its process lifetime from `signal-hook 0.3`, synchronously. So any candidate pulling a runtime in would be introducing the first one, and the 139 is the number a transitive-dependency count should be read against.
|
||||
|
||||
---
|
||||
|
||||
# Part I — GameCult-internal candidates
|
||||
|
||||
## I-1. The incumbent: `PipelineQuery` in `huginn-mind`
|
||||
|
||||
**What it is.** A flat option-struct filter, already built, already on the wire, already schema-published. This is the surface a general one would replace.
|
||||
|
||||
**Owner.** `huginn-mind`, this crate. Landed in commit `49cc6e3` "Read a mind: views, queries, open items and history".
|
||||
|
||||
**Evidence.**
|
||||
|
||||
- `F:\Projects\Huginn\crates\huginn-mind\src\query.rs:77-97` — `PipelineQuery`: `campaign: Option<Slug>`, `repo: Option<OrgRepo>`, `cut: Option<Label>`, `kinds: Vec<PipelineKind>`, `in_force: Option<bool>`, `faculty: Option<Faculty>`, `admitted_after: Option<Short>`, `admitted_before: Option<Short>`, `limit: Option<u32>`, `semantic: Option<SemanticQuery>`. Derives `Serialize, Deserialize, JsonSchema`.
|
||||
- `query.rs:249-290` — `Reader::matches`, the whole evaluator: a hand-written `if let Some(..)` chain, one branch per field, AND-only.
|
||||
- `query.rs:30-32` — `QUERY_LIMIT_MAX = 200`.
|
||||
- `query.rs:307-311` — `ordered()`, a fixed sort by `(admitted_at, id)`; not selectable.
|
||||
- `query.rs:331-350` — `Mind::query`; `matched` is computed before truncation so a short page is not mistaken for the whole answer.
|
||||
- `query.rs:110-117` — `PipelineOpenItems`, five named buckets.
|
||||
- `query.rs:121-125` — `HistoryScope::{Subject, Repo}`.
|
||||
- `F:\Projects\Huginn\crates\huginn-mind\src\wire.rs:43-50` — `HuginnMindRequest::{Whoami, Admit, View, Query, OpenItems, History}`.
|
||||
- `wire.rs:188, 203` — round-trips through `rmp_serde::to_vec_named` in test; MessagePack is the live encoding.
|
||||
- `F:\Projects\Huginn\crates\huginn-daemon\src\envelope.rs:39, 84, 144-145` — on the live wire the payload is **`rmp_serde::to_vec_named` then base64** inside a CultNet operation envelope, dispatched by schema id (`MIND_RESPONSE_SCHEMA` vs `FAILURE_SCHEMA`). Base64 is a 4/3 size multiplier on whatever the query serializes to, which is a fact about any encoding that nests deeply.
|
||||
- `wire.rs:31-34, 210-221` — the published JSON Schemas are `include_str!`'d from `schemas/cultnet/huginn.mind_request.v1.schema.json` and pinned byte-for-byte against `schemars::schema_for!` by the test `published_wire_schemas_match_derivation`, with `HUGINN_WRITE_SCHEMAS` as the regeneration path. The request schema currently carries 59 `$defs`.
|
||||
|
||||
**Portability facts.** P1: yes, it is already typed data on a MessagePack wire. P2: yes, `schemars`, derived and pinned. P3: zero added. P4: no; it runs over `Docs::from_image(mind.envelopes())`. P5: ours.
|
||||
|
||||
**Coverage.**
|
||||
|
||||
- C1: **partial and cross-cutting only.** Every filter is a field shared across kinds or derived from admission. There is no way to express "severity = High" or "outcome = Withdrawn" — the brief's own example. `severity`, `confidence`, `origin` (`PipelineFinding`), `sequence`/`outcome` (`PipelineResolution`) are unreachable.
|
||||
- C2: **no.** `PipelineQueryPage.items` is always `Vec<PipelineDocumentView>` and a view carries the whole `PipelineDocument` (`query.rs:57-63`).
|
||||
- C3: **no cursor, fixed order.** `limit` truncates; `matched` tells you it truncated. A page is a truncation, exactly what the brief says it must stop being.
|
||||
- C4: **no, but two hops are hardcoded.** `open_items` (`query.rs:366-389`) walks `CutReport.cut_spec` and `Verdict.cut_report` by hand to derive `specs_without_report` and `reports_without_verdict`. `Reader::base` (`query.rs:237-246`) walks resolution→subject so a filter reads through a resolution.
|
||||
The derivation layer beneath it (`F:\Projects\Huginn\crates\huginn-mind\src\docs.rs`) is entirely kind-specific and entirely `pub(crate)`: `find` (`:71`), `of_kind` (`:75`), `resolutions_of` (`:97`), `assignments_of` (`:104`), `stewardships_of` (`:194`), `closing_resolution` (`:186`), `later_in_force` (`:212`), `stewardship_of` (`:225`). There is **no generic "documents citing X" and no generic "documents cited by X"** anywhere in the crate; each traversal that exists was written for one pair of kinds.
|
||||
- C5: **no.** `matched: u32` is the only count, and it is a by-product of a query that already materialized every body.
|
||||
|
||||
**Three places the incumbent reaches into key strings because there is no structured alternative** — worth recording as pressure evidence:
|
||||
|
||||
- `query.rs:169-174` `root_and_local` splits `<root>:<kind>:<local>` by position.
|
||||
- `query.rs:262` matches a cut by `format!("cut-{}.", cut.0)` prefix on the local segment.
|
||||
- `query.rs:178-195` `repo_matches` is a hand-written match over 13 document variants to answer "does this document carry this repo".
|
||||
|
||||
**The semantic hole is already declared.** `query.rs:65-72` and `331-336`: `SemanticQuery { text, top_k }` is accepted by the type and refused typed — "Cut 11 also owns which fields of each kind are text, so no substring filter lives here to become a second answer to that question."
|
||||
|
||||
## I-2. `epiphany-pipeline` — the document leaf, and how its references are shaped
|
||||
|
||||
**What it is.** The crate that owns document shape, bounds, formats and keys. Not a query surface; it is the schema C1 and C4 would have to be expressed over.
|
||||
|
||||
**Owner.** Epiphany, `F:\Projects\Epiphany\epiphany-pipeline\src\lib.rs`, 2444 lines, single file.
|
||||
|
||||
**The load-bearing fact for C4: references are not uniformly typed.**
|
||||
|
||||
- `lib.rs:400-437` — `PipelineRef { kind: PipelineKind, id: Short }` with `validate_ref()`. This is the typed reference.
|
||||
- But most citations are bare `Short`, kind implied by field name:
|
||||
- `lib.rs:349` `PipelineCutReport { cut_spec: Short, forks: Vec<Short>[8], .. }`
|
||||
- `lib.rs:356` `PipelineVerdict { cut_report: Short, .. }`
|
||||
- `lib.rs:357` `PipelineFinding { verdict: Short, .. }`
|
||||
- `lib.rs:342` `PipelineCutSpec { depends_on: Vec<Short>[8], rulings: Vec<Short>[32], questions: Vec<Short>[16] }`
|
||||
- `lib.rs:324` `VerdictClaim { findings: Vec<Short>[16], mutations: Vec<Label>[8], .. }`
|
||||
- Typed `PipelineRef` appears in only three places: `PipelineQuestion.raised_in: Option<PipelineRef>` (`lib.rs:334`), `PipelineFollowUp.source: PipelineRef` (`lib.rs:362`), `PipelineResolution.subject: PipelineRef` (`lib.rs:374`).
|
||||
- `ForeignRef` (`lib.rs:286`) is the cross-repo reference: `{ repo, commit, kind, id, payload_sha256 }` — typed, but names another repo.
|
||||
- `DocRef` (`lib.rs:287`) points at a file range, not a document.
|
||||
|
||||
There is **no generic "references out of this document" enumerator** in the leaf. `grep` finds 137 occurrences of `PipelineRef` and no `fn refs`, `fn references` or `fn cites`. A generic C4 hop must therefore either carry a per-field kind table or the leaf must grow a reference enumerator.
|
||||
|
||||
**Per-kind predicate fields C1 would need to reach** (`lib.rs:357` and around): `PipelineFinding { confidence: FindingConfidence, severity: FindingSeverity, origin: FindingOrigin, invariants: Vec<Label>[8] }`; `PipelineResolution { sequence: u32, outcome: ResolutionOutcome }` (`lib.rs:374`); `PipelineStewardship { instance, repo, sequence, assigned_on }` (`lib.rs:388`). Every bound is declared in the macro (`[8]`, `[16]`, `[64]`), so field cardinality is already known statically.
|
||||
|
||||
**Portability facts.** P1/P2: the documents already serialize and derive `JsonSchema`, same `schemars`. P3: it is already a dependency. P4: no. P5: ours.
|
||||
|
||||
**Coverage.** Owns none of C1..C5; it is the type universe they range over.
|
||||
|
||||
## I-3. CultCache (`cultcache-rs`) — no query concept at all
|
||||
|
||||
**What it is.** The store. Envelope persistence, compare-exchange, snapshot pull.
|
||||
|
||||
**Owner.** CultLib, `F:\Projects\CultLib\packages\cultcache-rs\src\lib.rs`, single file.
|
||||
|
||||
**Evidence.** The entire public surface is CAS/batch/snapshot: `compare_and_swap_entry` (`:412`), `insert_entry_if_absent` (`:437`), `compare_and_swap_batch` (`:456`), `append_if_snapshot_unchanged` (`:510`), `delete_batch_if_unchanged` (`:543`), `replace_and_append_if_snapshot_unchanged` (`:571`), `pull_all_read_only_snapshot` (`:674`), `compare_exchange` (`:728`), `register_document_type` (`:1952`), `pull_all_backing_stores` (`:2022`). There is **no** `fn query`, `fn filter`, `fn find`, `fn select` or `fn where` anywhere in the file.
|
||||
|
||||
**One correction to "no selection at all": the C# reference has two single-value index lookups, and the Rust runtime has neither.**
|
||||
|
||||
- `F:\Projects\CultLib\src\GameCult.Caching\CultCache.cs:1495-1509` — `GetByName<T>(string name)` and `GetByIndex<T>(string alias, string value)`. Both funnel through `Single<T>(...)`, so each returns **at most one document**: these are unique-index lookups, not multi-result selection, and there is no range, no set membership, no composition.
|
||||
- Consumers are all in the C# tree: `src/GameCult.Mesh/CultMesh.cs:2351, 2433, 3330`, `src/GameCult.Networking/CultNetDatabase.cs:794-797, 1105-1106`.
|
||||
- `grep` for `get_by_index` / `by_index` / `index_alias` in `packages/cultcache-rs/src/lib.rs` returns **nothing**. The runtime `huginn-mind` actually depends on has no index concept at all.
|
||||
|
||||
A repo-wide search for declared filter/predicate/selector types across `CultLib`, `Epiphany`, `Eve`, `Odin`, `Norn` and `Mimir` sources (`grep -rliE "(struct|class|interface|enum) +[A-Za-z]*(Filter|Predicate|Selector|Criterion|Criteria)"`) returns **zero hits in CultLib, Odin and Norn**; the Eve hits are all `node_modules`; Mimir's two are a signal-processing matched filter and a transport selector, unrelated.
|
||||
|
||||
**Coverage.** None of C1..C5. Read is "give me every envelope"; selection is the caller's loop. P4 is the notable one: CultCache is exactly the in-memory-image shape the brief describes, so a candidate that needs a store engine is a different kind of thing.
|
||||
|
||||
## I-4. CultNet — one selection shape on the wire, and it is two id allowlists
|
||||
|
||||
**What it is.** `CultNetMessage::SnapshotRequest`, the only CultNet message that narrows what comes back.
|
||||
|
||||
**Owner.** CultLib, `F:\Projects\CultLib\packages\cultnet-rs\src\snapshot_query.rs`.
|
||||
|
||||
**Evidence.**
|
||||
|
||||
- `snapshot_query.rs:160-163` — `CultNetRawSnapshotQuery { message_id: String, expectations: BTreeMap<(String, String), CultNetSnapshotSourceExpectation> }`.
|
||||
- `snapshot_query.rs:197-217` — `request()` lowers those expectations to `CultNetMessage::SnapshotRequest { message_id, schema_ids: Option<Vec<String>>, record_keys: Option<Vec<String>> }`. Note it lowers to the *cross product* of schema ids and record keys, then re-validates the response against the exact expectations in `accept_response` (`:220-289`).
|
||||
- `snapshot_query.rs:30-56` — `CultNetReadOnlySnapshotPolicy` is an explicit `(schema_id, record_key)` allowlist, not a predicate.
|
||||
|
||||
**Portability facts.** P1: `SnapshotRequest` crosses the wire, so yes, but it carries no predicates to serialize. P2: not `schemars`-derived here. P3: in-substrate. P4: no. P5: ours.
|
||||
|
||||
**Coverage.** C1 no (identity only, no fields). C2 no. C3 no. C4 no. C5 no. This is a fetch-by-id list, not a query.
|
||||
|
||||
## I-5. CultMesh — a query *surface*, with the query shape left to the caller
|
||||
|
||||
**What it is.** The generic plumbing for "a parameterised, watchable read". It deliberately does not define what a query is.
|
||||
|
||||
**Owner.** CultLib. C# reference `F:\Projects\CultLib\src\GameCult.Mesh\CultMeshPrimitives.cs`; TS runtime `F:\Projects\CultLib\packages\cultmesh-ts\src\index.ts`.
|
||||
|
||||
**Evidence.**
|
||||
|
||||
- `CultMeshPrimitives.cs:628` — `CultMeshQuerySurface<TParameters, TResult>`; `:706` `CultMeshBoundQuerySurface<TParameters, TResult>`. `TParameters` is entirely caller-defined. The TS mirror is `packages/cultmesh-ts/src/index.ts:~643-730`.
|
||||
- `CultMeshPrimitives.cs:876-881` — `CultMeshDocumentQueryParameters` is an **empty struct** with a single `Empty` singleton. The document-handle case has no parameters at all.
|
||||
- `CultMeshPrimitives.cs:487` `CultMeshQueryContext` and `:593` its builder — this is *routing and locality* context (verse, route hint), not selection.
|
||||
- `F:\Projects\CultLib\src\GameCult.Mesh\CultMeshDiscoveryService.cs:34-57` — `CultMeshDiscoveryQuery { EndpointId, VerseIds, AuthorityRuntimeId }`. A three-field discovery filter over Verses, not over documents.
|
||||
|
||||
**Coverage.** CultMesh supplies the *slot* a query document would sit in (`TParameters`) and nothing that fills it. None of C1..C5 are answered; the type parameter is the evidence that the question was left open.
|
||||
|
||||
## I-6. Eve DSL — binding, and an explicit refusal to own selection
|
||||
|
||||
**What it is.** A composition language for interface projection. It binds components to documents and fields.
|
||||
|
||||
**Owner.** Eve, `F:\Projects\Eve`.
|
||||
|
||||
**Evidence.**
|
||||
|
||||
- `F:\Projects\Eve\docs\surface-contract-v1.md:66-71` — the binding primitives: `var` (one live value), `collection` (event-shaped ordered values), `derive` (computed fields such as counts and latest events), `bind` (component props subscribe to a var/collection/derived field).
|
||||
- `surface-contract-v1.md:156-158` — a component names `documentId`, `schemaId`, `slotId`, `presentationKind`, `routeHint`. Then, verbatim: "Provider adapters or advertised plugins own schema-specific query selection and decoding. Renderer code must not switch on provider schema ids or call provider-specific query methods to resolve a slot."
|
||||
- Selection that does exist in Eve is presentation-only and enumerated, not general. `F:\Projects\Epiphany\epiphany-core\src\atlas\eve_surface.rs:65-93` — `AtlasEveAttentionFilter` is a five-variant enum (`NeedsAttention`, `Degraded`, `Disputed`, `Stale`, `All`) lowered to a `control.select` with a fixed option list (`eve_surface.rs:~548-574`) and an advertised command marked `presentation_only: true, domain_state_effects: "none"` (`eve_surface.rs:~1146-1180`).
|
||||
|
||||
**Coverage.** Eve binds to a subset someone else selected. `derive` covers counts (C5-adjacent) at the presentation layer only. C1..C4 are explicitly out of scope by the surface contract's own sentence.
|
||||
|
||||
## I-7. Odin — two structs named Query, neither a predicate language
|
||||
|
||||
**Owner.** Odin, `F:\Projects\Odin`.
|
||||
|
||||
**Evidence.**
|
||||
|
||||
- `F:\Projects\Odin\crates\odin-daemon\src\lib.rs:324-328` — `OdinStoreQuery { incarnation: IncarnationRef, provider_signer_identity_id: String, dependencies: Vec<IncarnationRef> }`. A by-identity read; `OdinTopologyStore::read` (`:339`) returns one `OdinStoreSnapshot`. No derives — not serializable as written.
|
||||
- `F:\Projects\Odin\crates\odin-core\src\discovery.rs:6-11` — `OdinEndpointQuery<'a> { schema: Option<&'a str>, transport_contains: Option<&'a str>, host_hint: Option<&'a str>, device_filter: Option<&'a str> }`. Four optional string filters, one of them a substring match. **Borrowed lifetimes and no serde derives**: this cannot cross a wire as written. `discover_provider_endpoints` (`:22`) loops `cache().get_all::<EveProviderAdvertisementRecord>()` and filters in Rust.
|
||||
|
||||
**Coverage.** `OdinEndpointQuery` is the same flat-option-struct shape as `PipelineQuery` (I-1), arrived at independently, over a different type. Neither covers C1..C5.
|
||||
|
||||
## I-8. VoidBot — the live Qdrant filter translation, and a second flat option struct
|
||||
|
||||
**What it is.** The only place in the substrate that builds a Qdrant filter document today. Directly relevant because Cut 11 depends on Qdrant.
|
||||
|
||||
**Owner.** VoidBot, `F:\Projects\VoidBot`.
|
||||
|
||||
**Evidence.**
|
||||
|
||||
- `F:\Projects\VoidBot\packages\shared\src\index.ts:114-124` — `RetrievalFilters { corpusKind?: "discord_history" | "repository_source" | "persona_memory"; guildId?; channelId?; authorId?; identityId?; repoName?; pathPrefix?; language?; sourceId? }`. Nine optional strings, AND-only, no ordering, no cursor.
|
||||
- `F:\Projects\VoidBot\packages\rag\src\qdrant-vector-store.ts:21` — `type QdrantFilter = Schemas["Filter"]`, i.e. the types come from the Qdrant client's own generated OpenAPI schema types, not hand-written.
|
||||
- `qdrant-vector-store.ts:543-563` — `toQdrantFilter`: builds `const must: Schemas["Condition"][]`, appends one exact match per set field, returns `must.length > 0 ? { must } : undefined`. **`should` and `must_not` are never used.** The full Qdrant boolean model is available and only the AND arm is exercised.
|
||||
- `qdrant-vector-store.ts:566-584` — `buildMatchAnyFilter(field, values)` produces a `match: { any: [...] }` condition, used only for delete-by-sourceId batches (`:119-125`).
|
||||
- `qdrant-vector-store.ts:129-136` `deleteByFilters`, `:142-162` search with `filter: toQdrantFilter(filters)`.
|
||||
- Consumers: `packages/rag/src/{file-vector-store,in-memory-vector-store,message-archive,retrieval-service,source-vector-store}.ts` all import `RetrievalFilters` — so the same nine-field struct is the filter shape for the in-memory store, the file store and Qdrant alike.
|
||||
- The MCP tool surface exposes a hand-written subset of the same fields: `packages/providers/src/local-llm-tools.ts:79-115` (`search_sources` with `repoName`, `pathPrefix`, `language`, `limit`).
|
||||
|
||||
**This is the strongest in-substrate evidence of the pressure the brief names.** Two independent flat option-structs (`PipelineQuery`, `RetrievalFilters`) plus a third (`OdinEndpointQuery`) exist because no general one does, and `RetrievalFilters` additionally flattens Qdrant's boolean model down to `must`-only on the way through.
|
||||
|
||||
## I-9. Huginn's own README and AGENTS — authority boundary
|
||||
|
||||
Recorded because it bounds where a query surface may live. `F:\Projects\Huginn\README.md:14-24` and `AGENTS.md:26-40`: Huginn owns `.cc` inspection projection and Eve DSL emission for `cultcache.huginn.inspector`; it does **not** own CultCache persistence. Note the AGENTS file still names `E:\Projects\Huginn` as project root while the live tree is `F:\Projects\Huginn` — stale, flagged, not part of this question.
|
||||
|
||||
---
|
||||
|
||||
# Part II — Prior attempts and abandonments in our own history
|
||||
|
||||
## II-1. `ThreadEpiphanyGraphQuery` — a serializable citation-hop query, built and deleted
|
||||
|
||||
**What it was.** The closest thing the substrate has ever had to C4 as data.
|
||||
|
||||
**Evidence.**
|
||||
|
||||
- Added: Epiphany commit `5c62a650` "Add Epiphany graph query surface", 2026-04-29.
|
||||
- Shape, from the generated TS at `vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyGraphQuery.ts` in that commit:
|
||||
`{ kind: ThreadEpiphanyGraphQueryKind, nodeIds?: string[], edgeIds?: string[], paths?: string[], symbols?: string[], edgeKinds?: string[], direction?: ThreadEpiphanyGraphQueryDirection | null, depth?: number | null, includeLinks?: boolean | null }`
|
||||
- `ThreadEpiphanyGraphQueryKind = "node" | "path" | "frontierNeighborhood" | "neighbors"`.
|
||||
- `ThreadEpiphanyGraphQueryDirection = "incoming" | "outgoing" | "both"` — **both directions of the hop, as data**.
|
||||
- Doc comments in the generated file: direction "Defaults to `both`"; depth "Defaults to 1 and is capped by the server"; includeLinks "Defaults to true so neighborhood queries preserve dataflow/architecture joins".
|
||||
- Schema was **derived, not hand-written**: `// This file was generated by [ts-rs]`. The same commit added `codex_app_server_protocol.schemas.json` and `.v2.schemas.json` (138 lines each) and `ClientRequest.json` (85 lines).
|
||||
- Deleted: Epiphany commit `5f6f2441` "Remove Epiphany app-server compatibility surface", 2026-07-12. Nothing named `*GraphQuery*` survives in the tree today (`find` returns nothing outside `node_modules`).
|
||||
|
||||
**What happened to it.** It was not removed for being a bad query shape. It went out with the whole app-server compatibility surface it lived in — it was a Codex protocol type in `vendor/`, carried on ts-rs and JSON Schema generation, and died with its host. Related surviving history: `2ea57376` "Add local Verse context query", `e2a8b479` "Read graph query from memory graph store", `fa222584` "Move retrieval query law to core", `0cf4b081` "Move graph query smoke to Rust", `bf433d63` "Make semantic readiness proof query-owned".
|
||||
|
||||
**Coverage it had.** C1 partial (id/kind sets, not per-kind field predicates), C2 via `includeLinks` only, C3 no cursor but a server-capped depth, **C4 yes — direction and depth as data**, C5 no.
|
||||
|
||||
## II-1b. Selective hydration, deleted from CultCache three days ago
|
||||
|
||||
**What it was.** A filter applied at the store layer so a pull could hydrate a subset rather than the whole image.
|
||||
|
||||
**Evidence.** CultLib commit `900e148d8ad6dceed4b9c14dac23d12a869cb539`, "Read only v4 directory stores; delete probes and selective hydration", 2026-09-13. Its own message: "DirectoryMessagePackBackingStore loses **HydrationFilter**, FlushStageProbe, ReadStageProbe and their stage constants, ReadPersistedGeneration, **PullSelected**, and every legacy path ... **CacheBackingStore.PullSelected** and CultPersistedRecordMetadata go with them."
|
||||
|
||||
**What happened to it.** It went out with the v1/v2/v3 store formats in a format-narrowing cut, not as a judgement on filtering. But the effect stands: as of three days before this pass, CultCache's backing-store surface has **no way to pull a subset**. `pull_all_read_only_snapshot` and `pull_all_backing_stores` are what remain. Any query surface above it reads the whole image, which is what `huginn-mind` does (`query.rs:207-208`, `Docs::from_image(mind.envelopes())` per call).
|
||||
|
||||
**Also worth recording:** every other CultLib commit matching query/filter/predicate/selector is about **schema-alias or shard-membership resolution**, not field predicates — e.g. `54543b5` "Resolve Python shard discovery filters by schema alias", `1b9140d` "Resolve C# subscription filters by schema alias", `2738f4c` "Resolve TS snapshot filters by schema alias", `7ad74cf` "Filter Python snapshots by shard membership". "Filter" in CultLib means "which records by schema and shard", never "which documents by field value".
|
||||
|
||||
## II-2. The RethinkDB lineage — what the archive actually says
|
||||
|
||||
The operator's account is **confirmed as to adoption and abandonment**, and **not confirmed as to ReQL or changefeeds specifically**.
|
||||
|
||||
**Adoption, with the stated reasons.** Discord `#development`, 2020-03-15:
|
||||
|
||||
- `688549686373515270` (00:50): "I've decided [Tympanic] is right, and abusing git for document storage is a bad strategy. I spent a couple days researching a bunch of different db solutions, I feel like I've looked at every one of them."
|
||||
- `688550094030503985` (00:52), the anchor: settled on RethinkDB for four named properties — it stores JSON, supports queries, has a realtime push feature like Firebase realtime db, and has a C# client library, "which even Firebase bafflingly lacks".
|
||||
- `688550271554682890`: "db is now live at asgard.gamecult.games:28015" (28015 is RethinkDB's client driver port).
|
||||
|
||||
**The alternative that was considered and passed over.** 2020-03-11, `#development`: Tympanic `687397982638833793` "What would be a reason to not just go straight to something like SQL? It all depends on how much data we expect to have." Metacrat `687400730948272258` "Maybe you're right about SQL though. I had to use it for my database class and didn't particularly enjoy it, but it's battle tested"; `687411066522697873` "I kinda feel like saying let's cross that bridge when we get to it, migrate to SQL if document store + git ends up being a bottleneck".
|
||||
|
||||
**Abandonment.** 2020-2021 RethinkDB was the real production database — editor tooling wrote to it (`691363946623402014`, 2020-03-22), the game would not run without it (`716084893888806983`, 2020-05-30). Then 2021-06-23, `857099401398845442`: "Pretty big change in how we're handling data here. From now on, the Rethink database is obsolete. We can still go back to it, but for now I've configured it to use JSON files when present and fall back to entries defined in a msgpack file", followed immediately by `857099432168128553` "this will make modding trivial".
|
||||
|
||||
**What that message is.** It is the CultCache origin: files plus MessagePack, chosen for moddability, replacing a server. Also `1023147757860565012` (2022-09-24) "No really take a look at rethinkdb" — still recommending it a year after dropping it.
|
||||
|
||||
**What the archive does not contain.** Searches of the indexed Discord history for ReQL, for changefeeds, and for query/filter-language design return **no hit naming ReQL or changefeeds at all**. The realtime feature is referenced only obliquely and by analogy — "a realtime push feature like Firebase realtime db". So:
|
||||
|
||||
- The inheritance the archive *documents* is: document store, JSON/schemaless records, a real client library, and realtime push. Push/watch is the dimension that visibly survives — CultMesh's `watch`/`observe` on a document handle (`CultMeshPrimitives.cs:~2010`, `packages/cultmesh-ts/src/index.ts:~886`) and `CultMeshPollingWatchOptions` (`CultMeshSnapshots.cs:~558`).
|
||||
- The inheritance the archive **does not** document is the query side. No term tree, no serialized predicate, no filter document, no selector exists anywhere in CultCache or CultNet (see I-3, I-4). CultNet's only wire selection is two id allowlists.
|
||||
- Stated plainly: on the evidence available, **the borrowing was in change notification and document-shaped storage, not in querying.** Whether that was deliberate or simply never reached is not answerable from the archive; see the open questions.
|
||||
|
||||
One adjacent scar worth recording, since it is about query complexity in our own code: `1510998723730346114` (2026-06-01), on the Aetheria codebase — a TODO cursing a LINQ query nested ten levels deep.
|
||||
|
||||
---
|
||||
|
||||
# Part III — External prior art
|
||||
|
||||
## III-1. RethinkDB ReQL wire encoding (named prior art)
|
||||
|
||||
**What it is.** A query represented not as text but as a tree of typed terms, serialized as nested arrays. Each language driver is a builder over that one wire format.
|
||||
|
||||
**Evidence.** `rethinkdb/rethinkdb`, `src/rdb_protocol/ql2.proto`, branch `next` at commit `a2cf1308b57dca2e05d744f225a082881bab3fca` (2026-03-28), 862 lines — https://github.com/rethinkdb/rethinkdb/blob/next/src/rdb_protocol/ql2.proto . Driver-authoring spec: https://rethinkdb.com/docs/writing-drivers/ . Python driver `ast.py`: https://github.com/rethinkdb/rethinkdb-python/blob/master/rethinkdb/ast.py
|
||||
|
||||
**The encoding, confirmed with one correction.**
|
||||
|
||||
- A term serializes as `[termType, [args...], {optargs}]`, and **the optargs object is omitted entirely when empty** — `ast.py:164-168`, `res = [self.term_type, self._args]; if len(self.optargs) > 0: res.append(self.optargs)`. So a term is a 2- or 3-element array, not always 3.
|
||||
- Leaf data is not wrapped; a datum is bare JSON. Documented example: `r.db("blog").table("users")` → `[15, [[14, ["blog"]], "users"]]`, where 15 = TABLE and 14 = DB.
|
||||
- Outer frame: `[QueryType, term, globalOptargs]`. `r.expr("foo")` → `[1,"foo",{}]`. `Query.QueryType`: START=1, CONTINUE=2, STOP=3, NOREPLY_WAIT=4, SERVER_INFO=5.
|
||||
- Protobuf-era messages: `Query { type=1, query(Term)=2, token=3, OBSOLETE_noreply=4, accepts_r_json=5, global_optargs(repeated AssocPair)=6 }`; `Term { type=1, datum=2, args(repeated Term)=3, optargs(repeated AssocPair)=4 }`; `Datum.DatumType` = R_NULL, R_BOOL, R_NUM, R_STR, R_ARRAY, R_OBJECT, R_JSON.
|
||||
- Framing: 8-byte LE token, 4-byte LE length, UTF-8 JSON. Response is token, length, then `{t, r, b, p, n}` = ResponseType, result, backtrace, profile, notes.
|
||||
|
||||
**Term types: closed enum, stable ids, never recycled.**
|
||||
|
||||
- `Term.TermType` holds **186 named values**; highest id is `BIT_SAR = 196`.
|
||||
- Retired ops are left as comments rather than reused: `// OBSOLETE_GROUPED_MAPREDUCE = 46;`, `// OBSOLETE_GROUPBY = 47;` (ql2.proto:452-453). Superseded ops keep their slot: `BETWEEN_DEPRECATED = 36;`. Ids are non-contiguous by design (`HTTP = 153`, `UUID = 169` sit among low-numbered ops), which is only workable because the enum is a fixed registry.
|
||||
|
||||
**Protocol versions.** `VersionDummy.Version`: V0_1 `0x3f61ba36`, V0_2 `0x723081e1`, V0_3 `0x5f75e83e`, V0_4 `0x400c2d20`, V1_0 `0x34c2bdc3`. `VersionDummy.Protocol`: PROTOBUF `0x271ffc41`, JSON `0x7e6970c7`. Originally protobuf-framed (ql2.proto:5-31: send version magic as LE32, auth-key length and key, then a length-prefixed serialized `Query` blob). The protocol-type selector arrives at **V0_3**, which is where JSON-over-socket becomes selectable. **V0_4** keeps the same handshake shape and adds parallel query execution. **V1_0** replaces the handshake with SCRAM-SHA-256 (RFC 7677) exchanged as NUL-terminated JSON objects, and protobuf is no longer a choice.
|
||||
|
||||
**P2 — was a schema published for the query document? No.** No JSON Schema or other machine-readable schema for the query document was ever published. The driver-authoring spec describes the array encoding in prose plus worked examples and points implementers at `ql2.proto` for the integer constants; third-party drivers (Go `rethinkdb-go/ql2/ql2.pb.go`, Deno, Clojure `rethinkdb-protobuf`) each vendor a copy of the `.proto` and code-generate from it. The `.proto` describes the *protobuf* framing; for the JSON protocol the term-array shape is documented prose only.
|
||||
|
||||
**What drivers owned vs. the wire.** Drivers were builders, not query engines. Driver: fluent API → term tree; 8-byte token allocation; length framing; handshake/SCRAM; JSON serialization; response demux by token; cursor and `CONTINUE` batching; pseudotype decoding on the way back (times, binary, geometry); variable-id allocation for FUNC. Server: all query semantics, type checking, arity checking, optarg validation, and backtraces. Errors return `RUNTIME_ERROR`/`COMPILE_ERROR` with a `b` backtrace of frames indexing into the term tree the driver sent; the driver renders that backtrace against its own tree (`compose()` in `ast.py`) but does **not** pre-validate. Structurally confirmed: `ast.py` is ~500 near-empty subclasses carrying only `term_type = P_TERM.X`.
|
||||
|
||||
**Function / lambda terms — the part that makes the shape unbounded.**
|
||||
|
||||
Term ids: `FUNC = 69`, `VAR = 10`, `IMPLICIT_VAR = 13`, `MAKE_ARRAY = 2`, `FUNCALL = 64`, `JAVASCRIPT = 11`.
|
||||
|
||||
- FUNC signature comment: `FUNC = 69; // ARRAY, Top -> ARRAY -> Top`. Proto text (ql2.proto:605-644), verbatim: "An anonymous function. Takes an array of numbers representing variables (see [VAR] above), and a [Term] to execute with those in scope."
|
||||
- VAR (ql2.proto:295-303), verbatim: "It's the responsibility of the client to translate from their local representation of a variable to a unique _non-negative_ integer for that variable. (We do it this way instead of letting clients provide variable names as strings to discourage variable-capturing client libraries, and because it's more efficient on the wire.)"
|
||||
- Encoding shape: `[69, [[2, [varId...]], body]]` — arg 0 is a MAKE_ARRAY of integer variable ids, arg 1 is the body term. A reference to parameter *n* is `[10, [varId]]`.
|
||||
- How a client closure is captured (`ast.py:1958-1980`): the driver reads the lambda's arity by reflection, mints a fresh globally-monotonic integer per parameter, builds `Var` placeholders, **calls the client lambda once at build time with those placeholders**, and records the term tree that the lambda's operator overloading produces. The lambda body is never inspected, compiled or transmitted; only the residue of ReQL operations survives. Host-language control flow inside the lambda runs on the client at build time and leaves no trace.
|
||||
- `r.row` is a bare `[13,[]]`. `func_wrap` (`ast.py:1951-1955`) scans a built term for an IMPLICIT_VAR and retro-wraps it as `Func(lambda x: val)`. That is why `r.row` does not work in subqueries (https://rethinkdb.com/api/python/row/) — with nesting there is no way to say which implicit scope is meant. The Java driver dropped `r.row` entirely.
|
||||
- **Implication for describing a query's shape in advance:** the term tree is a general expression tree, not a bounded predicate grammar. FUNC introduces lambda abstraction with integer binders, FUNCALL(64) applies them, BRANCH(65) is a conditional, FOR_EACH(68) iterates, and functions are first-class (HTTP's `page` optarg takes `FUNC | STRING`). Terms recurse with no depth or arity bound, and the "type system" is the doc comments (`Sequence, Function(1) -> OBJECT`), not anything enforceable. A query can be validated in advance only as far as "well-formed nested array whose head integer is a member of a 186-entry enum"; arity and type conformance are decided by the server at compile time. **The enum is closed and bounded; what can be built out of it is not.**
|
||||
- **Arbitrary client code: no.** The wire carries no client bytecode, source or serialized closure. The single escape hatch is `JAVASCRIPT = 11` — a JS string written by the developer, evaluated server-side in RethinkDB's embedded V8, default timeout 5s (https://rethinkdb.com/api/python/js/). Its second signature returns `Function(*)`, so a JS string can stand where a FUNC is expected: the one place the term grammar hands off to an opaque body.
|
||||
|
||||
**P1/P3/P4/P5.** P1: the query is pure data by construction — the drivers exist precisely because the query is the wire format, which is the property being looked for. P3: nothing to depend on; there is no reusable Rust library of the *encoding* separate from a client. P4: yes, unambiguously a front end for a database engine — the server owns every semantic. P5: Apache 2.0 (`LICENSE` at `rethinkdb@next`; GitHub reports `spdx_id: NOASSERTION` due to third-party notices). RethinkDB Inc. shut down 2016; CNCF bought the copyright and assets for $25,000, relicensed from AGPL to Apache 2.0 and contributed it to the Linux Foundation, announced 2017-02-06 (https://www.cncf.io/blog/2017/02/06/cncf-purchases-rethinkdb-source-code-contributes-linux-foundation-apache-license/); community-maintained under the `rethinkdb` org since, not a CNCF hosted project. Last server release **v2.4.4, 2023-12-11** (prior v2.4.3 2023-02-05, v2.4.2 2022-05-07). Repo not archived, 26,998 stars, 1,352 open issues, last default-branch commit `a2cf1308` 2026-03-28. Release cadence roughly one per one-to-two years.
|
||||
|
||||
**Rust drivers.** `reql` — crates.io max 0.11.2, published 2023-07-24, 96,744 all-time downloads, 886 in 90 days, MIT/Apache-2.0, repo `rethinkdb/rethinkdb-rs`, last push 2023-07-24, 213 stars, dormant ~3 years. `unreql` — a `reql` fork, max 0.2.1 published 2026-02-10, 13,437 all-time, 1,589 in 90 days, MIT, `https://github.com/vettich/un-rethinkdb-rs`; currently the only Rust driver with recent releases and its 90-day downloads now exceed `reql`'s. Also `rethinkdb-rs/thinker` (superseded, "Use the `reql` crate instead") and `mobc-reql` 0.6.4 (pool wrapper).
|
||||
|
||||
**Coverage of C1..C5.** C1 yes and then some — per-field predicates are just terms, but unbounded. C2 yes (`pluck`, `without`, `MAKE_OBJ`). C3 yes — ordering as terms, and cursors as protocol-level `CONTINUE` on a token rather than as a value in the query. C4 yes as general traversal (`eq_join`, `get_all` with an index), nothing citation-specific. C5 yes (`count`, `contains`).
|
||||
|
||||
## III-2. Qdrant's filter model
|
||||
|
||||
Recorded in detail because Cut 11 depends on Qdrant, and because VoidBot already builds these documents (I-8).
|
||||
|
||||
**REST JSON shape.** Top level `Filter`: `must`, `should`, `must_not` (each a `Condition` or an array of them), plus `min_should: {conditions: [...], min_count: uint>=1}`. Recursive — a `Condition` may itself be a `Filter`. `Condition` variants: `FieldCondition`, `IsEmptyCondition`, `IsNullCondition`, `HasIdCondition`, `HasVectorCondition`, `SliceCondition`, `NestedCondition`, `Filter`. `FieldCondition = {key, match?, range?, geo_bounding_box?, geo_radius?, geo_polygon?, values_count?, datetime_range?}`. `Match` variants: `{value}`, `{any:[...]}`, `{except:[...]}`, `{text}`, `{text_any}`, `{phrase}`, `{prefix}`. `Range = {lt,gt,gte,lte}` (floats; RFC-3339 strings for datetime). Nested keys use dot paths with array projection: `"country.cities[].population"`.
|
||||
|
||||
```json
|
||||
{
|
||||
"must": [
|
||||
{ "key": "city", "match": { "value": "London" } },
|
||||
{ "key": "price", "range": { "gte": 100.0, "lte": 450.0 } },
|
||||
{ "nested": { "key": "diet", "filter": {
|
||||
"must": [ { "key": "food", "match": { "value": "meat" } },
|
||||
{ "key": "likes", "match": { "value": true } } ] } } }
|
||||
],
|
||||
"must_not": [ { "key": "color", "match": { "any": ["red","black"] } } ],
|
||||
"should": [ { "key": "location", "geo_radius": {
|
||||
"center": { "lon": 13.403683, "lat": 52.520711 }, "radius": 1000.0 } } ],
|
||||
"min_should": { "conditions": [ { "is_empty": { "key": "reports" } } ], "min_count": 1 }
|
||||
}
|
||||
```
|
||||
|
||||
**gRPC/protobuf shape**, `qdrant/qdrant` `lib/api/src/grpc/proto/qdrant_common.proto`, Apache-2.0: `Filter { repeated Condition should=1, must=2, must_not=3; optional MinShould min_should=4 }`; `Condition` is a `oneof` of 8 arms; `FieldCondition { string key=1; Match match=2; Range range=3; ... optional bool is_empty=9, is_null=10 }`; `Match` is a `oneof` of 11 scalar-typed arms (`keyword`, `integer`, `boolean`, `text`, `keywords`, `integers`, `except_integers`, `except_keywords`, `phrase`, `text_any`, `prefix`).
|
||||
|
||||
**The two shapes are not isomorphic.** gRPC flattens `Match` into scalar-typed oneof arms; REST uses one polymorphic `value`. A single internal shape cannot be both without choosing.
|
||||
|
||||
**P2 — yes, a schema is published.** `https://github.com/qdrant/qdrant/blob/master/docs/redoc/master/openapi.json`, OpenAPI 3.0.1, 404 component schemas, every filter type present and verified (`Filter`, `Condition`, `FieldCondition`, `Match`, `Range`, `DatetimeRange`, `GeoBoundingBox`, `GeoRadius`, `GeoPolygon`, `ValuesCount`, `IsEmptyCondition`, `IsNullCondition`, `HasIdCondition`, `HasVectorCondition`, `NestedCondition`, `MinShould`, `SliceCondition`). It is **generated from `schemars` derives** on `lib/segment/src/types.rs`, where `Filter` carries `Deserialize, Serialize, JsonSchema, Validate`. Same generator we use.
|
||||
|
||||
**Structural hazard, verified.** `Condition` and `Match` are `#[serde(untagged)]` / OpenAPI `anyOf` **with no discriminator** — variants are told apart by structure alone; `Filter`'s `must`/`should`/`must_not` additionally use a one-or-array codec. Untagged serde enums force `deserialize_any`, and `rmp-serde` has an open reported failure there: msgpack-rust issue #148, untagged enums containing enums fail under rmp-serde while working under serde_json. This is a fact about copying that shape onto a MessagePack wire.
|
||||
|
||||
**`qdrant-client` (P1/P3/P4/P5).** P1: **protobuf, not serde.** `Filter` implements `prost::Message`, `Clone`, `PartialEq`, `Debug`, `Default` — **no `Serialize`/`Deserialize`**. The crate's `serde` feature is misleading: `src/serde_impl.rs` and `src/serde_deser.rs` cover payload `Value`/`Struct`/`ListValue` only, never `Filter`/`Condition`. P3: **209 transitive crates with default features, 129 with `default-features = false`** — against our current 139 for the whole workspace. Non-optional `tokio 1.52.3` and `tonic 0.14.6`; plus `reqwest 0.13.4` (via the default `download_snapshots` feature), hyper, h2, axum, tower/tower-http, rustls + ring + webpki, mio, socket2, prost. P4: yes, assumes a running Qdrant server over gRPC; not an in-memory evaluator. P5: Apache-2.0, v1.19.0 released 2026-08-04 (server v1.19.1, 2026-09-04), repo `qdrant/rust-client`, 29 contributors, commits through 2026-09-01 — org-maintained, tracks server releases, not single-maintainer.
|
||||
|
||||
**There is no types-only Qdrant crate.** `qdrant` on crates.io is a **0.0.0 placeholder** (last touched 2024-04-18). The server's `segment` crate v0.6.0 holds the real serde+schemars types but is **workspace-internal and unpublished** (`segment` on crates.io is an unrelated Meilisearch analytics fork). `qdrant_rest_client` 0.2.1 is a third-party WASM SDK from second-state.
|
||||
|
||||
**Coverage.** C1 yes over payload fields, with real boolean algebra and ranges. C2 no (projection is a separate `with_payload` parameter, not part of the filter). C3 no in-filter ordering; paging is `offset`/`limit` or `scroll` with a point-id continuation, not part of the filter document. C4 no. C5 yes (`count` endpoint takes a filter and returns a count without bodies).
|
||||
|
||||
## III-3. Baseline for reading dependency counts
|
||||
|
||||
`serde` + `schemars` + `rmp-serde` alone resolve to **22 transitive crates** (verified with `cargo generate-lockfile`, cargo 1.95.0, counting `[[package]]`). Current versions: serde 1.0.229 (MIT/Apache-2.0), schemars 1.2.2 (MIT, 2026-07-27), rmp-serde 1.3.1 (MIT, 2025-12-23). Read against the workspace's 139.
|
||||
|
||||
## III-4. Expression evaluators and scripting languages
|
||||
|
||||
**A cross-cutting fact first: none of these derive schemars anywhere in their repositories** (GitHub code search, `total_count` 0 for each). And none resolve tokio, reqwest, hyper, sqlx or lalrpop.
|
||||
|
||||
| crate | version / date | P1 | P3 transitive | P5 |
|
||||
|---|---|---|---|---|
|
||||
| `evalexpr` | 13.1.0, 2025-11-26 | **String-parsed.** `Node` AST is public, but `feature_serde` implements **`Deserialize` only, from a string** (`deserialize_str` → `build_operator_tree`). **No `Serialize` on `Node`** — one-way. | 0 default; 24 with serde+regex+rand | **AGPL-3.0-only.** 413★, 20 contributors, last commit 2025-11-26, effectively single-maintainer (ISibboI) |
|
||||
| `rhai` | 1.26.1, 2026-09-10 | **String-parsed.** `AST` has private fields, internals behind an `internals` feature. No serde on the AST. Its `serde` feature serializes `Dynamic` *values*; `metadata` emits function signatures. Neither is an AST. | 45 (48 w/ serde). futures-core/task/util, slab, pin-project-lite (async plumbing, no runtime); wasm-bindgen, js-sys | MIT/Apache-2.0. 5,682★, commit 2026-09-16, led by Stephen Chung |
|
||||
| `fasteval` | 0.2.4, **2020-01-25** | **String-parsed.** Slab-arena AST, **no serde dependency at all.** Algebraic expressions, not a general predicate language | 0 | MIT. **Dormant** — last commit 2020-09-21, 2 contributors. Forks `fasteval2` (2023-01) and `fasteval3` (2023-11) also stale |
|
||||
| `cel` (was `cel-interpreter`/`cel-parser`) | cel 0.14.5, 2026-09-07 | **String-parsed.** AST (`Ast`, `Expr`, `CallExpr`, `ComprehensionExpr`) is public but derives only `Clone, Debug, Default, PartialEq` — **no serde, not even behind a cfg_attr.** Upstream Google CEL defines protobuf `ParsedExpr`/`CheckedExpr`; **cel-rust does not implement it** | 67 — the largest here. **LOUD: `antlr4rust` 0.5.2 (ANTLR runtime) AND `nom` 7.1.3 — two parsing frameworks.** Plus futures/parking_lot, chrono, regex, `cc` + find-msvc-tools (a C compiler in the build graph), 6 windows-* crates | MIT. 677★, pushed 2026-09-16, multi-maintainer |
|
||||
| `jsonlogic-rs` | 0.5.0, 2024-12-13 | **Structured data on the wire** (JsonLogic rules are JSON trees) — **but the Rust type is `serde_json::Value`.** No typed enum with derived Serialize/Deserialize; shape checked at runtime | 34. phf + generator/macros, rand stack, deprecated proc-macro-hack, clap 2.33.1 | MIT. Last commit 2024-12-13, 56★, single-maintainer |
|
||||
| `jsonlogic` | 0.5.1, **2020-03-05** | Same: `Value` in, `Value` out | 12 | MIT. **Abandoned**, 5★, **1 contributor** |
|
||||
| `datalogic-rs` | 5.5.0, 2026-09-12 | **Structured JSON rule, not a string.** Compiles to an arena AST behind `Logic`; **`Logic` is not Serialize/Deserialize** — the serializable artifact is the input JSON | **15 — the cleanest non-trivial tree in the survey.** bumpalo, datavalue-rs, fast-float2, itoa, ryu, self_cell, smallvec, serde. No parser generator, no async, no network, no DB | Apache-2.0. 89★, 6 contributors, 0 open issues, commit 2026-09-12, effectively single-maintainer (GoPlasmatic). Ships Node, WASM, Python, Go, Java, **.NET**, PHP and C bindings plus a 1,804-case conformance suite |
|
||||
| `jmespath` | 0.5.0, 2026-01-19 | **String-parsed.** `ast::Ast` public, 18 variants, public fields, derives `Clone, Debug, Display, PartialEq` — **no serde.** Serialize in the repo is on `Variable`, the data type. **`Ast` is not `Send`/`Sync`** (holds `Rcvar`) | 22. serde, serde_json, zmij, itoa, memchr, once_cell, wasm-bindgen; build-dep `slug` → `deunicode` | MIT. 161★, 16 contributors, org-maintained, bursty |
|
||||
| `jsonpath-rust` | 1.0.11, 2026-09-09 | **String-parsed.** `JpQuery`, `Segment`, `Selector`, `Filter`, `FilterAtom` derive `Debug, Clone, PartialEq` only — **no serde, no schemars** | 24. **LOUD: `pest` 2.7 + pest_derive/generator/meta + ucd-trie — a full parser generator.** Plus the regex stack | MIT. 159★, 23 contributors, commit 2026-09-09, ~90M downloads |
|
||||
|
||||
**P4 for the whole family:** none assumes a backing store. But each is bound to a document model — jsonlogic/jsonpath-rust/jmespath to `serde_json::Value`, datalogic-rs to `DataValue`/bumpalo, evalexpr/rhai/fasteval to their own context types. None reads typed Rust structs directly.
|
||||
|
||||
**Why the family as a whole does not meet P1, stated plainly:** six of eight are string-parsed languages whose wire form is source text. Four expose a public AST; **none of the four derive serde on it.** `evalexpr` is the only one with any AST serde impl and it is Deserialize-from-string with no Serialize. Two put structured data on the wire (the JsonLogic family), but as untyped `serde_json::Value` rather than a typed tagged union. **Zero generate a JSON Schema.**
|
||||
|
||||
## III-5. Filter and predicate crates
|
||||
|
||||
**`predicates` 3.1.4** — MIT/Apache-2.0, updated 2026-02-11, ~225M downloads, assert-rs org (Nick Stevens, Ed Page), 21 contributors. P3: 13 transitive, regex optional, nothing flagged. P4: pure in-memory `fn(&T) -> bool`.
|
||||
**Why it does not fit:** P1 fails absolutely. `Predicate<T>` is a trait; combinators produce nested Rust generic types (`AndPredicate<…>`, `FnPredicate<F,T>`) or `BoxPredicate<T>`, and `FnPredicate` wraps a Rust closure. **There is no data-shaped AST and nothing crosses a wire.** A TS or C# caller cannot construct one. Nothing to derive a schema from either.
|
||||
|
||||
**`filtrex` — does not exist as a Rust crate.** `crates.io/api/v1/crates/filtrex` returns **404**. Filtrex is JavaScript (joewalnes/filtrex, fork cshaa/filtrex) plus an Elixir port (rcdilorenzo/filtrex). Recorded because it is named in the brief's family list and is otherwise easy to assume into existence.
|
||||
|
||||
**nom-based filter languages** — the published examples are small and each carries a flag. `dsq-parser` 0.2.0 (MIT/Apache-2.0, 2026-02-18, **571 downloads**, docs.rs build **failed**): parses a DSQ string into an `Expr` AST, serde non-optional so the AST is serde-shaped, but the authoring surface is a string; **flag: `nom ^8.0`**. `tellaro-query-language` 2.0.0 (2026-09-05, 3,554 downloads): **flag: a 213-line Pest grammar plus an optional `opensearch` feature that is a network client**; crates.io reports its licence as **"non-standard"**. `filter-expr` 0.2.0 (MIT, 2026-08-13, 1,324 downloads), hand-rolled parser; **flag: `async-trait` — its evaluation path is async.** Meilisearch's `filter-parser`, the most-cited real nom filter language, is **not published** (`crates.io/api/v1/crates/filter-parser` → 404); it is workspace-internal.
|
||||
|
||||
**`sea-query` 1.0.2** — MIT/Apache-2.0, 2026-08-12, ~1.8M downloads/month, SeaQL org, multi-maintainer. P3: 16 transitive, 15 direct and nearly all optional value-type integrations; **no tokio or DB driver in the core** (those live in `sea-query-binder` and the `sea-query-*-driver` crates); `#![forbid(unsafe_code)]`.
|
||||
**Why it does not fit — two independent disqualifiers.** P1: in 1.0 `SimpleExpr` is a type alias for the `Expr` enum, and the docs.rs trait-impl list for `sea_query::expr::Expr` shows `Clone`, `Debug`, many `From<T>`, `PartialEq`, `PgExpr`, `SqliteExpr` and auto traits — **no `Serialize`, no `Deserialize`**; same for `Condition`. Its `serde` feature (one of 44) covers **value** types only (bigdecimal, chrono, uuid, rust_decimal, time, ipnetwork, pgvector). P4: output is a SQL string plus bound parameters for MySQL/Postgres/SQLite; it cannot evaluate over an in-memory collection. P2: no schemars.
|
||||
|
||||
**`diesel` 2.3.13** — MIT/Apache-2.0, 2026-09-04, ~36M downloads, lead maintainer Georg Semmler (effectively one primary maintainer, very actively maintained).
|
||||
**Why it does not fit — categorically.** The DSL is compile-time typed: each fragment is a distinct generic Rust type (`Eq<column, Bound<…>>`) implementing `QueryFragment<Backend>`. **The query exists only as a monomorphized Rust type, so there is nothing to serialize**, and a TS/C# caller can never produce one. Diesel's serde involvement is row values. P3 flag: backend features pull libpq / libmysqlclient / libsqlite3, and async needs `diesel-async` → tokio. P4: hard-assumes a database engine.
|
||||
|
||||
**`sqlx` 0.9.0** — MIT/Apache-2.0, 2026-05-21, 147M downloads, LaunchBadge org.
|
||||
**Why it does not fit:** it has no query IR at all. "Compile-time checked queries without a DSL" means SQL strings validated against a live database at compile time; there is no query object, serializable or otherwise. P3 is flagged on every axis the brief names: **mandatory async runtime (tokio/async-std/smol), full PostgreSQL/MySQL/SQLite wire-protocol drivers, TLS stacks.** P4: assumes a live database at compile *and* run time.
|
||||
|
||||
## III-6. Query builders and analytic IRs with a serializable IR
|
||||
|
||||
**`polars-plan` `Expr` (polars 0.55.2)** — MIT.
|
||||
- P1: **serializable.** `polars_plan::dsl::Expr` implements `Serialize` + `Deserialize` (confirmed on the docs.rs trait-impl list for 0.51.0 — the **0.55.2 docs.rs build failed**, last successful render is 0.51.0). Python exposes it as `Expr.meta.serialize()` / `Expr.deserialize()`, `format="binary"` (default) or `"json"`.
|
||||
- Exact feature flags: on the umbrella `polars` crate, `serde` (→ polars-buffer/core/utils) and **`serde-lazy`** (→ polars-core/io/lazy/ops/time/utils — *this* is the one that reaches the expression layer). On `polars-plan` itself the feature is plain `serde`, plus `ir_serde` for the resolved IR.
|
||||
- **The wire format is explicitly unstable.** Polars documentation states serialization is not stable across versions; a LazyFrame serialized in one version may not deserialize in another. Recurrent serde-feature build breakage: issues #3431, #3875, #18697.
|
||||
- P2: **yes, real schemars.** `polars-plan` has a **`dsl-schema`** feature pulling **schemars ^1.2.1** (was ^0.8.22 at 0.51.0), plus serde_json and sha2; it auto-enables `serde` and propagates across the sub-crates. **`dsl-schema` is not exposed on the umbrella `polars` crate's 161-feature list** — it is reachable only via `polars-plan` directly.
|
||||
- P3: very large — ~22-57 MB, **~1M SLoC**. **Flag: `polars-plan` 0.55.2 deps include `tokio` and `futures`.** Plus rayon and the arrow/compute stack; `polars-io` brings object-storage/cloud network clients when enabled. No parser generator, no DB driver.
|
||||
- P4: **runs over in-memory collections natively**, no database engine required.
|
||||
- P5: 0.55.2, 2026-08-06. 120 releases, **55 of them breaking.** ~792K downloads/month, 771 dependents. Ritchie Vink plus 100+ contributors, commercially backed (Polars Inc). **`polars-plan`'s own docs.rs page states it is an internal sub-crate "not intended for external usage"** — no public API stability guarantee.
|
||||
|
||||
**`datafusion-proto` 55.1.0 (with `datafusion`'s `LogicalPlan`)** — Apache-2.0, 2026-09-11, 67 versions, quarterly-ish majors with frequent breaking changes, 5.2M downloads, ASF project with many maintainers.
|
||||
- P1: **serializable typed data via protobuf, not serde.** `LogicalPlan`/`Expr` are Rust enums without serde derives; `datafusion-proto` converts bidirectionally to generated prost messages. TS/C# callers *can* construct these — the `.proto` is the contract. Caveat: round-tripping UDFs and extension nodes requires registering codecs on the Rust side.
|
||||
- P2: the protobuf schema is the machine-readable schema (`datafusion/proto/proto/datafusion.proto` + `datafusion_common.proto`, split into `datafusion-proto-common`/`-models` at 55.x). No schemars. `serde_json` is optional and debug-path only.
|
||||
- P3: **heavy.** 22 direct deps: arrow ^59.2.0, **`object_store ^0.13.2` (a network/cloud storage client)**, prost ^0.14.1 and 16 `datafusion-*` crates. **`datafusion-execution` 55.1.0 pulls `tokio` (twice, target-gated), `tokio-util`, `async-trait`, `futures`, `dashmap`, `parquet` and `object_store` unconditionally.** The transitive graph runs to many hundreds: you cannot take `datafusion-proto` without most of a query engine.
|
||||
- P4: ships a full execution engine. It does not require an external DB (an in-memory `MemTable` works), but you embed a catalog and a scheduler and tokio.
|
||||
- `datafusion-substrait` 55.1.0 (Apache-2.0, 2026-09-11, ~2.4M downloads, ~9,085 SLoC) converts `LogicalPlan` ⇄ Substrait protobuf and **inherits the entire DataFusion tree** — tokio, object_store, arrow, parquet all present.
|
||||
|
||||
**`substrait` (substrait-rs) 0.65.0** — Apache-2.0, 2026-08-27, edition 2024, MSRV 1.88.
|
||||
- P1: **serializable typed data is the crate's entire purpose** — "Cross-Language Serialization for Relational Algebra". Ships prost-generated `substrait::proto` (`Plan`, `Rel`, `Expression`, `FunctionArgument`, …) plus `substrait::text` for YAML extensions. TS and C# construct these from the published `.proto` with standard codegen; that is designed usage, not a workaround.
|
||||
- P2: the protobuf schema **is** the machine-readable schema, published at `substrait-io/substrait` `proto/substrait/*.proto`, packaged at `substrait-io/substrait-packaging`, bindings at `substrait-io/substrait-protobuf`. The simple-extensions YAML additionally has a real **JSON Schema 2020-12** at `text/simple_extensions_schema.yaml`. Spec nuance: Substrait is defined **only** in Protobuf; the JSON you see is Protobuf-JSON output, not an official text format. The crate's `serde` feature "generates serde implementations that match the Protobuf JSON Mapping **via pbjson**" — so serde output is proto-canonical JSON, not an independent serde shape. **No schemars.**
|
||||
- P3: direct deps indexmap, prost, serde, serde_json, substrait-extensions, substrait-prost; feature-gated hex, serde_yaml, thiserror; build-deps semver, toml, prost-types. ~6-9.5 MB / **~196K SLoC**, the bulk being generated prost code. **No tokio, no network client, no DB driver, no parser generator.** The only flag is the optional `protoc`/`protox` features, which invoke a protobuf compiler at build time (`protox` is pure Rust, no external binary). Seven features, none default: embed-descriptor, extensions, parse, protoc, protox, semver, serde.
|
||||
- P4: **no backing store and no engine.** Pure IR types plus serialization; **it does not evaluate anything** — you supply the evaluator.
|
||||
- P5: 162 versions, fast cadence. **17.5M lifetime / ~892K recent downloads.** Owner Jacques Nadeau with co-owners Matthijs Brobbel, Victor Barua, YongChul Kwon, Weston Pace, 13+ contributors — a cross-vendor spec body, not single-maintainer. The repo itself is modest (92★). Still 0.x: no 1.0 stability promise on the Rust binding, and the spec versions independently.
|
||||
|
||||
## III-7. Document-store and protocol filter documents, reusable as a shape
|
||||
|
||||
**Only four of the surveyed protocols put a structured document on the wire:** MongoDB filters, Elasticsearch/OpenSearch query DSL, Firestore StructuredQuery, and Kubernetes `LabelSelector` (plus JSON Schema, structured but a different kind of predicate). OData, AIP-160, CESQL, GraphQL documents and the Kubernetes `labelSelector`/`fieldSelector` query parameters are all **strings**.
|
||||
|
||||
**Only three publish a machine-readable schema of the query shape itself:** OpenSearch, Elastic, Firestore. Kubernetes publishes one for `LabelSelector` specifically. **MongoDB — the most imitated shape of all — publishes none.**
|
||||
|
||||
### MongoDB query documents and BSON
|
||||
Structured. Keys are dot-notation field paths; values are literals (implicit `$eq`) or operator documents. `$and`/`$or`/`$nor` take arrays of filter documents; **`$not` wraps an operator document, not a full filter** — a known asymmetry. Operators: `$eq $ne $gt $gte $lt $lte $in $nin $exists $type $regex $mod $all $elemMatch $size $bitsAllSet $expr $jsonSchema $text $where`. Example: `{ "$and": [ {"status":"active"}, {"score":{"$gte":10,"$lt":100}}, {"tags":{"$in":["a","b"]}}, {"meta.region":{"$ne":"eu"}} ] }`.
|
||||
|
||||
**P2: no.** A BSON 1.1 wire spec exists (bsonspec.org/spec.html) and driver behaviour specs at github.com/mongodb/specifications, but **no published JSON Schema or IDL for the legal shape of a filter document.** Operator semantics are prose. `$jsonSchema` is a validator operator, not a schema of the filter language.
|
||||
|
||||
`bson` v3.1.0, 2025-11-20, MIT — **fully standalone, does not require the driver.** Default features are only `compat-3-0-0` (a no-op shim); **`serde` is optional**, and with it `bson::Document` implements Serialize/Deserialize. **No tokio anywhere.** Optional chrono-0_4, jiff-0_2, time-0_3, uuid-1, serde_json-1, serde_with-3, large_dates; also zero-copy `RawDocument`/`RawDocumentBuf`. MessagePack caveat: BSON has types with no MessagePack analogue (ObjectId, Decimal128, Binary subtypes, Timestamp, MinKey/MaxKey), which round-trip through serde as extended-JSON-ish maps rather than natively.
|
||||
|
||||
`mongodb` v3.9.1, 2026-09-10, Apache-2.0 — **loud: `tokio ^1.17` is a non-optional dependency** (io-util, sync, macros, net, process, rt, time, fs), plus tokio-util, futures-*, a crypto stack (hmac, md-5, sha1, sha2, pbkdf2, stringprep), base64, rand; optional rustls/openssl, reqwest, hickory-dns, mongocrypt, aws-sigv4. Notably `bson` itself is *optional* there.
|
||||
|
||||
### Elasticsearch / OpenSearch Query DSL
|
||||
Structured. `bool` with `must`/`should`/`must_not`/`filter` plus `minimum_should_match` and `boost`; leaves `term`, `terms`, `range` (gt/gte/lt/lte/format/time_zone), `match`, `match_phrase`, `exists`, `prefix`, `wildcard`, `regexp`, `ids`, `nested`, `script`. Polymorphism is "a single-key object selects the variant" — a tagged union expressed as an object with exactly one key, which is precisely why Elastic had to write a custom spec language.
|
||||
|
||||
**P2: yes, both vendors.** Elastic: `elastic/elasticsearch-specification` (Apache-2.0); source of truth is TypeScript definitions and the compiler emits `output/schema/schema.json`, **a custom JSON type-model — not JSON Schema and not OpenAPI** — covering ~500 endpoints and ~3000 types including `QueryContainer`, `BoolQuery`, `TermQuery`, `RangeQuery`. OpenSearch: `opensearch-project/opensearch-api-specification` (Apache-2.0, **OpenAPI 3.1.0**), query DSL in `spec/schemas/_common.query_dsl.yaml` (~1400 lines), `QueryContainer` with 60+ variants, plain OpenAPI/JSON-Schema-flavoured.
|
||||
|
||||
Rust: the official `elasticsearch` 9.1.0-alpha.1 (2025-08-08, Apache-2.0) is **a full HTTP client, reqwest + tokio**, and its bodies are `serde_json::Value` anyway — no typed DSL. Types-only alternatives exist: `elasticsearch-dsl` 0.4.13 (~2023, MIT/Apache-2.0, explicitly independent of the official client, no HTTP, serde types, **no runtime deps**) and `opensearch-dsl` (a fork). Both stale-ish and community-maintained.
|
||||
|
||||
### Firestore StructuredQuery (protobuf)
|
||||
Structured, protobuf, `google/firestore/v1/query.proto` in googleapis/googleapis, Apache-2.0. `StructuredQuery { select, from[], where: Filter, order_by[], start_at, end_at, offset, limit, find_nearest }`. `Filter` is a **oneof of exactly three**: `composite_filter`, `field_filter`, `unary_filter`. `CompositeFilter { op: AND|OR, filters: repeated Filter }` — recursion lives here and only here. `FieldFilter { field: FieldReference, op, value: Value }` with ops `LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL, EQUAL, NOT_EQUAL, ARRAY_CONTAINS, IN, ARRAY_CONTAINS_ANY, NOT_IN`. `UnaryFilter { field, op }` with `IS_NAN, IS_NULL, IS_NOT_NAN, IS_NOT_NULL`. `FieldReference { field_path }`, dot-delimited. **There is no NOT combinator** — negation exists only as `NOT_EQUAL`/`NOT_IN`/`IS_NOT_*`.
|
||||
|
||||
JSON mapping is standard protobuf JSON (lowerCamelCase, enums as string names):
|
||||
`{"where":{"compositeFilter":{"op":"AND","filters":[{"fieldFilter":{"field":{"fieldPath":"status"},"op":"EQUAL","value":{"stringValue":"active"}}},{"fieldFilter":{"field":{"fieldPath":"score"},"op":"GREATER_THAN_OR_EQUAL","value":{"integerValue":"10"}}},{"unaryFilter":{"field":{"fieldPath":"deletedAt"},"op":"IS_NULL"}}]}}}`
|
||||
|
||||
Rust: **no first-party Google crate.** Generated bindings (`googleapis-tonic-google-firestore-v1`, `gcloud-sdk`, `firestore`) — **loud: tonic ⇒ hyper + tokio + prost; `gcloud-sdk` adds an HTTP and auth stack.** The runtime-free path is taking the Apache-2.0 `.proto` and running prost yourself, which is build-time codegen and gives protobuf encoding rather than MessagePack.
|
||||
|
||||
### Kubernetes label selectors and fieldSelector — two distinct surfaces
|
||||
`labelSelector` **as a query parameter on LIST/WATCH is a string**: `environment=production,tier!=frontend`, `environment in (production,qa)`, `partition`, `!partition`. Comma means AND. **No OR at all.** The `LabelSelector` **API object** embedded in Deployment/Service specs is **structured**: `{matchLabels:{k:v}, matchExpressions:[{key, operator: In|NotIn|Exists|DoesNotExist, values:[...]}]}`, all terms AND, **no OR, no nesting, no recursion.** `fieldSelector` is **string only** (`metadata.name=foo,status.phase!=Running`) with no structured equivalent and per-resource, mostly undocumented selectable fields.
|
||||
|
||||
**P2: yes for `LabelSelector`** — `io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector` and `.LabelSelectorRequirement` in the Kubernetes OpenAPI (kubernetes/kubernetes `api/openapi-spec`, served live at `/openapi/v3`). `k8s-openapi` v0.28.0, 2026-06-15, Apache-2.0, generated from that spec, serde-derived, **no async runtime, types only** (the runtime hazard lives in `kube`, which pulls tokio/hyper/tower). Requires a `v1_xx` feature flag and is a very large crate if you want two structs.
|
||||
|
||||
**Partial fit, stated plainly:** the smallest well-schematized structured predicate in the survey and proof the pattern works — but flat and conjunction-only.
|
||||
|
||||
### JSON:API filter
|
||||
**Unspecified.** Spec v1.1 reserves the `filter` query-parameter family and states it is agnostic about the strategies servers support. No operators, no grammar, no nesting rules, no document shape. jsonapi.org publishes a JSON Schema for the *response* document, not for filters. No Rust crate.
|
||||
**Why it does not fit:** it is a naming convention for a URL query parameter, not a predicate language. The only transferable idea is `filter[field]=value`, a string/URL encoding that cannot carry typed values or nesting.
|
||||
|
||||
### OData `$filter`
|
||||
**String expression:** `$filter=Price gt 20 and (contains(Name,'milk') or Category/Name eq 'Dairy')`. Operators `eq ne gt ge lt le and or not in has`, arithmetic, functions, lambda `any`/`all`, `/` traversal. **A machine-readable grammar is published — as ABNF, not as a data schema:** OASIS OData 4.01 ABNF Construction Rules (docs.oasis-open.org/odata/odata/v4.01/cs01/abnf/odata-abnf-construction-rules.txt, source at oasis-tcs/odata-abnf). CSDL JSON schematizes the *entity model*, not the filter (OASIS Standard 4.01, 2020-05-11; validating schemas at oasis-tcs/odata-csdl-schemas). So the data model has a JSON schema and the predicate language does not. No mainstream typed-AST Rust crate exists; **any usable path means an ABNF/PEG parser generator and you own the AST and the MessagePack mapping.**
|
||||
**Why it does not fit:** string-first by design. Serializing it as typed data means inventing the AST OASIS deliberately never published.
|
||||
|
||||
### GraphQL
|
||||
**The query is a string** — a GraphQL document parsed server-side. Filters inside it are arguments typed by **input object types** (`input UserFilter { and: [UserFilter!], or: [UserFilter!], age: IntFilter }`), which is a genuinely reusable predicate shape — but values reach the server either as literals in the query string or as JSON `variables`, so only the variables half is structured data. Spec: GraphQL October 2021 (spec.graphql.org/October2021/). **P2: yes for the type system** — SDL plus standardized introspection (`__schema`, `__type`, `__InputValue`), so clients can discover filter input types at runtime. **No standard schema for filter semantics**: there is no canonical `IntFilter`/`and`/`or` vocabulary, and Hasura, Postgraphile, Prisma and Dgraph each invent their own operator names. Rust: async-graphql, juniper, graphql-parser — **flag: a parser, and for servers an async runtime.**
|
||||
**Partial fit:** the input-object pattern is directly stealable; the transport is not.
|
||||
|
||||
### JSON Schema as a predicate language
|
||||
Draft **2020-12** is current (json-schema.org/draft/2020-12); IETF standardization is ongoing (`draft-ietf-jsonschema-json-schema-03`, Aug 2026). The ecosystem is fragmented across draft-07 / 2019-09 / 2020-12. A schema is itself JSON, and it is **self-describing** — meta-schema at json-schema.org/draft/2020-12/schema.
|
||||
**Can express:** full boolean algebra including **`not`** — real negation, which both MongoDB and Firestore lack — plus type checks, numeric ranges, string constraints (`pattern`, `minLength`, `format`), `enum`, `const`, array constraints (`contains`, `minContains`, `prefixItems`, `uniqueItems`), object constraints, `if`/`then`/`else`, and `$ref`/`$dynamicRef` recursion.
|
||||
**Cannot express:** comparison between two fields of the same document (no `a.x > a.y`); any reference to an external value or parameter — every bound is a literal baked into the document; sorting, projection, aggregation or joins; arithmetic; dot-path addressing in a predicate sense — a deep-field range check requires nested `properties` mirroring the document rather than `"a.b.c": {gte: 5}`. Parameterizing a query means rewriting the schema document.
|
||||
Rust: `jsonschema` (MIT, active) — **flag: a regex engine, fancy-regex, URL resolution, and optionally `reqwest` for remote `$ref` fetching.** `schemars` generates schemas from Rust types but is not an evaluator.
|
||||
|
||||
### CloudEvents SQL (CESQL)
|
||||
**String expression**, SQL-like: `subject LIKE '%mySubject%' AND myext = 'value' AND (type = 'a' OR type = 'b')`, evaluating to boolean/integer/string. **v1.0 approved 2024-06-13**, github.com/cloudevents/spec/blob/main/cesql/spec.md, Apache-2.0 (CNCF), SDKs for Go and Java. **No document schema** — the spec defines an ANTLR-based grammar, not a serializable AST — and it operates over the flat CloudEvents attribute set, so there are no nested paths. **No Rust crate**; `cloudevents-sdk` has no CESQL module. **Flag: porting means a parser generator.**
|
||||
|
||||
### Cross-cutting structural facts for this family
|
||||
- **Recursion and negation matrix:** MongoDB has `$and`/`$or`/`$nor` plus operator-scoped `$not`; Elasticsearch's `bool` has `must_not` and nests freely; **Firestore has AND/OR recursion but no NOT combinator**; Kubernetes `LabelSelector` has neither recursion nor OR; JSON Schema has full boolean algebra including `not`.
|
||||
- **Tagged-union encoding differs sharply.** MongoDB and Elasticsearch use "object with a single magic key" (`{"$gt":5}`, `{"term":{…}}`) — cheap in JavaScript, painful in C# and for MessagePack codegen, and the same untagged-discrimination problem Qdrant has (III-2). Firestore's protobuf `oneof` with named enum operators and Substrait's proto messages are the explicitly-tagged counterexamples in the set, and both map cleanly to TypeScript discriminated unions and C# sealed hierarchies.
|
||||
- **Runtime-free Rust type sources that were found:** `bson` 3.1.0 + serde (MIT, no tokio, standalone); `k8s-openapi` 0.28.0 (Apache-2.0, types-only, large); prost-generating from Firestore's `query.proto` yourself (Apache-2.0 proto, build-time codegen); `elasticsearch-dsl` 0.4.13 (MIT/Apache, no HTTP, stale). **Carrying runtimes:** `mongodb` (mandatory tokio), `elasticsearch` (reqwest + tokio), `kube` (tokio/hyper/tower), and every OData/AIP-160/CESQL path (parser generators).
|
||||
|
||||
## III-8. Cursor and pagination prior art as data
|
||||
|
||||
**Google AIP-158** (https://google.aip.dev/158). Request: `int32 page_size` (optional; the API may choose a default and return fewer) and `string page_token`. Response: repeated results as the **first** field, `string next_page_token` (empty means end), optional `int32 total_size`. Opacity, verbatim: page tokens **must** be opaque (but URL-safe) strings and **must not** be user-parseable. **No encoding is mandated.** The AIP *suggests* an internal protobuf message, serialized and base64-encoded, for non-sensitive tokens — and warns that base64-encoding an otherwise-transparent token is **not** sufficient obfuscation. A server-side handle is not required: tokens may carry state, but scope is strict — they must be limited to indicating where to continue and **must not provide any form of authorization**, which is re-checked every call. Expiry is permitted, ~3 days named as reasonable if the server stores data behind the token. No cryptographic guidance.
|
||||
|
||||
**AIP-159** covers the `-` wildcard for cross-collection reads and **contains nothing about page tokens.** Only tangentially relevant: cross-parent requests should avoid `order_by` because ordering is ambiguous.
|
||||
|
||||
**Relay GraphQL Cursor Connections** (https://relay.dev/graphql/connections.htm). `Connection { edges: [Edge], pageInfo: PageInfo! }`; `Edge { node, cursor }` where `node` may be Scalar/Enum/Object/Interface/Union or Non-Null thereof and **must not be a list**, and `cursor` is a String or a custom scalar that serializes as a String; `PageInfo { hasPreviousPage: Boolean!, hasNextPage: Boolean!, startCursor: String, endCursor: String }`. Arguments `first`/`after` and `last`/`before`. Verbatim: the cursor field's result "should be considered opaque by the client, but will be passed back to the server." `startCursor`/`endCursor` must correspond to the first and last nodes and **can be null when there are no results**. **The spec mandates no encoding — base64 appears nowhere in it.** Base64 of `type:id` or of an offset is ecosystem convention (graphql-js, graphql-ruby, Relay tooling), not spec text. Neither the HTML nor the markdown source carries a printed version or date; the spec's own example uses the placeholder `after: "opaqueCursor"`.
|
||||
|
||||
**Rust crates offering an opaque-cursor type.** The exact names `cursor-pagination`, `page-token`, `seek-pagination` and `opaque-cursor` return **zero crates.io results**.
|
||||
|
||||
- **`use-cursor` 0.0.1** (crates.io/crates/use-cursor, repo RustUse/use-api). Types `OpaqueCursor`, `AfterCursor`, `BeforeCursor`, `NextCursor`, `PreviousCursor`, `CursorDirection`. P1: **a string newtype, not typed data** — `OpaqueCursor::new("…")` / `as_str()`, **no serde.** P2: no. P3: **zero dependencies.** P4: store-agnostic, and it does no encoding or decoding at all — a validated string wrapper plus direction slots. P5: MIT/Apache-2.0, a single release 2026-05-25, **132 downloads**, single maintainer, Rust 1.95 / edition 2024, ~170 LoC. One release, no track record.
|
||||
- **`api-paginate` 0.1.1** (crates.io/crates/api-paginate). `PaginationParams`, `PaginatedResponse`, `CursorPagination { cursor: Option<String>, limit: u32 }`. P1: **the envelope is serde-typed; the cursor itself is `Option<String>`**, with no encode/decode helpers. P2: no schemars; OpenAPI via an optional `utoipa ^4`. P3: serde plus optional utoipa; dev-only criterion/proptest; **no runtime, network, DB or parser deps.** P4: store-agnostic. P5: MIT/Apache-2.0, 0.1.0 2026-09-01 and 0.1.1 2026-09-12, **27 total downloads**, single maintainer, ~196 LoC. Two weeks old.
|
||||
- **`paginator-utils` / `paginator-rs` 0.4.0** (crates.io/crates/paginator-rs). Keyset cursors, "URL-safe Base64 and validated on decode", `next_cursor`/`prev_cursor` derived from boundary rows. P1: string cursors over base64; the surrounding page structs are serde. P2: no schemars (a `paginator-zod` sibling generates Zod/TS schemas, 32 downloads). P3: utils deps are base64 ^0.22, serde, serde_json — clean, **no async runtime in core** — but the family fans out into `paginator-axum`/`-actix`/`-rocket`/`-sqlx`/`-sea-orm`/`-surrealdb`, which pull web servers and DB drivers. P4: core is store-agnostic. P5: MIT, 0.4.0 2026-09-07, 10 releases since 2025-05-07, ~16.8K downloads, **single maintainer**.
|
||||
- **`bomboni_request` 0.5.0** — the only AIP-158-shaped page-token implementation found. `PlainPageTokenBuilder` plus base64, AES-GCM-encrypted and RSA-encrypted variants; tokens are typed and serde-serializable. P2: no schemars. P3: **loud.** Non-optional aes-gcm 0.11, rsa 0.9, blake2, base64ct, **prost 0.14**, **pest + pest_derive 2.8 (a parser generator)**, regex, itertools, serde, thiserror, time, and four internal `bomboni_*` crates; optional **tonic 0.14**, mysql_common, postgres-types, wasm-bindgen. This is a filter/query-language framework, not a cursor primitive. P5: crates.io reports the licence as **"Non-standard"**; 0.5.0 2026-07-20, 65 versions since 2023-11, 106K downloads, single maintainer.
|
||||
- Inspected and **not** opaque-cursor primitives: `page-turner` 1.0.0 (an abstraction over *calling* paginated APIs as a stream; futures 0.3, tokio dev-only; defines no cursor type); `mongodb-cursor-pagination` 0.3.2 (**pulls the MongoDB driver**, stale since 2023); `paginate` 1.1.11 (offset math, 2022); `page-hunter` 0.7.0 (offset model); `rdb-pagination`, `pg_filters`, `diesel_filter`, `diesel_pagination`, `sqlx-data-params`, `clickhouse-filters` (all SQL/ORM-bound); `api-bones` 6.12.0 (a whole REST-envelope framework).
|
||||
|
||||
**Stated plainly: no general-purpose, standalone, typed opaque-cursor crate exists in Rust.** The four obvious names are unpublished; the only crate whose stated purpose is modelling an opaque cursor (`use-cursor`) is a zero-dep **string newtype** at 0.0.1 with no serde that explicitly declines to assume a serialization; `api-paginate` types only the envelope; and everything with real download numbers is framework- or database-bound or is offset arithmetic. **Nothing found offers schemars or JSON Schema for a cursor type** — the closest schema stories are `utoipa` (api-paginate) and `paginator-zod`.
|
||||
|
||||
**Framework-bound cursors worth recording.**
|
||||
- **async-graphql 7.2.1** (2026-09-12, MIT/Apache-2.0): `trait CursorType { type Error: Display; fn decode_cursor(s:&str)->Result<Self,Self::Error>; fn encode_cursor(&self)->String; }` (not object-safe), implemented for primitive ints/floats, bool, char, String, Uuid, `DateTime<Utc>`, Timestamp and ID — plus **`OpaqueCursor<T>` where `T: Serialize + DeserializeOwned`**, "encode/decode the value to base64". **This is the one place in the entire survey where a cursor is a typed serde value with a defined wire encoding, and it is framework-bound.**
|
||||
- **sea-orm** has two mechanisms: `Paginator` (`.paginate(db, page_size)`) is offset/limit page numbers with no token; `Cursor` (`.cursor_by(col)`, PR SeaQL/sea-orm#822) is true keyset with `.after()/.before()/.first()/.last()`, where **the cursor value is the raw typed column value** — no serialization, no base64, nothing wire-shaped.
|
||||
- **diesel** has **no built-in pagination.** The canonical approach is the "Extending Diesel" guide: hand-roll `Paginated<T>` implementing `QueryFragment`, emitting `SELECT *, COUNT(*) OVER () FROM (subquery) LIMIT … OFFSET …`. Offset-based, Postgres-specific for the window count, no cursor type. Third-party helpers (`diesel_pagination` 2.0.0, `diesel_filter` 2.0.0, `length_aware_paginator`) reproduce the same offset pattern.
|
||||
|
||||
## III-9. Two cross-cutting facts that touch the MessagePack constraint
|
||||
|
||||
1. **Nothing surveyed targets MessagePack natively.** Serde-based candidates (polars `Expr`) would go through `rmp-serde`. Protobuf-based candidates (substrait, datafusion-proto, Firestore, qdrant-client) have protobuf-binary and protobuf-JSON as their defined encodings; MessagePack means encoding the proto-derived structure yourself, or going proto → pbjson → serde → rmp, which drops the protobuf wire contract.
|
||||
2. **Untagged-union discrimination is the recurring hazard.** Qdrant (`Condition`, `Match`), MongoDB (`{"$gt":5}`) and Elasticsearch (`{"term":{…}}`) all discriminate variants by structure or by a single magic key rather than an explicit tag. `#[serde(untagged)]` forces `deserialize_any`, and `rmp-serde` has an open reported failure there — msgpack-rust issue #148, untagged enums whose variants contain enums fail under rmp-serde while succeeding under serde_json. Firestore's protobuf `oneof`, Substrait's proto messages and ReQL's leading integer term id are the explicitly-tagged counterexamples in the set.
|
||||
|
||||
**Sources for Part III-2 through III-9:** [qdrant filtering](https://qdrant.tech/documentation/concepts/filtering/) · [qdrant openapi.json](https://github.com/qdrant/qdrant/blob/master/docs/redoc/master/openapi.json) · [qdrant_common.proto](https://raw.githubusercontent.com/qdrant/qdrant/master/lib/api/src/grpc/proto/qdrant_common.proto) · [qdrant-client Filter](https://docs.rs/qdrant-client/latest/qdrant_client/qdrant/struct.Filter.html) · [lib.rs qdrant-client](https://lib.rs/crates/qdrant-client) · [msgpack-rust #148](https://github.com/3Hren/msgpack-rust/issues/148) · [serde enum representations](https://serde.rs/enum-representations.html) · [evalexpr](https://github.com/ISibboI/evalexpr) · [rhai](https://docs.rs/rhai/1.26.1/rhai/) · [fasteval](https://github.com/likebike/fasteval) · [cel](https://docs.rs/cel/0.14.5/cel/) · [datalogic-rs](https://github.com/GoPlasmatic/datalogic-rs) · [jmespath.rs](https://github.com/jmespath/jmespath.rs) · [jsonpath-rust](https://github.com/besok/jsonpath-rust) · [predicates](https://lib.rs/crates/predicates) · [sea-query Expr](https://docs.rs/sea-query/1.0.2/sea_query/expr/enum.Expr.html) · [polars-plan features](https://docs.rs/crate/polars-plan/latest/features) · [substrait-rs](https://docs.rs/substrait/latest/substrait/) · [simple_extensions_schema.yaml](https://github.com/substrait-io/substrait/blob/main/text/simple_extensions_schema.yaml) · [datafusion-proto deps](https://deps.rs/crate/datafusion-proto/55.1.0) · [bson](https://crates.io/crates/bson) · [elasticsearch-specification](https://github.com/elastic/elasticsearch-specification) · [opensearch query_dsl.yaml](https://github.com/opensearch-project/opensearch-api-specification/blob/main/spec/schemas/_common.query_dsl.yaml) · [Firestore query.proto](https://github.com/googleapis/googleapis/blob/master/google/firestore/v1/query.proto) · [AIP-160](https://google.aip.dev/160) · [AIP-158](https://google.aip.dev/158) · [Relay Connections](https://relay.dev/graphql/connections.htm) · [k8s-openapi](https://crates.io/crates/k8s-openapi) · [CESQL](https://github.com/cloudevents/spec/blob/main/cesql/spec.md) · [async-graphql CursorType](https://docs.rs/async-graphql/latest/async_graphql/types/connection/trait.CursorType.html) · [use-cursor](https://crates.io/crates/use-cursor) · [sea_orm::Cursor](https://docs.rs/sea-orm/latest/sea_orm/struct.Cursor.html)
|
||||
|
||||
---
|
||||
|
||||
# Questions this pass could not answer
|
||||
|
||||
1. **Was the query side of RethinkDB deliberately not carried into CultCache, or simply never reached?** The archive names four adoption reasons and none of them is ReQL; ReQL and changefeeds are never mentioned by name in the indexed Discord history. Nothing found says "we chose not to have queries". **To answer:** ask the operator directly. The archive cannot settle intent, only what was said.
|
||||
|
||||
2. **Does the 2020-2021 Aetheria-Economy tree contain the C# ReQL driver usage, and what query shapes were actually written against it?** That would show which ReQL constructs were ever exercised here, which is different from what ReQL offered. **To answer:** check out `GameCult/Aetheria-Economy` around commits from 2020-03 to 2021-06 and grep for the C# driver's term builders. Not indexed by voidbot under a name I could confirm.
|
||||
|
||||
3. **Which of the thirteen document kinds actually need per-kind predicates, and on which fields?** I recorded what the types expose (`severity`, `confidence`, `origin`, `sequence`, `outcome`, `repo`). I did not find a record of what callers have asked for and been unable to express. **To answer:** search the Eureka campaign's own findings and follow-ups for read-side requests, or ask the operator for the driving query list.
|
||||
|
||||
4. **What exactly does Cut 11 intend to own?** `query.rs:65-72` says Cut 11 owns "which fields of each kind are text" and the index. Whether Cut 11 also intends to own structured filter pushdown into Qdrant — which would make the structured-filter shape and Qdrant's filter shape the same question — is not written anywhere I found. The Huginn repo contains no `docs/` or `notes/` directory and no file mentioning "Cut 11" outside `query.rs`'s comments. **To answer:** locate the campaign's target document and cut map (the `PipelineCampaign.target_doc` DocRef would name it) or ask the operator.
|
||||
|
||||
5. **Is there a stored mind image to measure against?** Every portability claim about paging and counting assumes a document population size. I found no fixture corpus outside `fixtures.rs`. **To answer:** run the daemon against a real mind store and count documents per kind, or read `crates/huginn-mind/src/fixtures.rs` as the only current sample and treat it as synthetic.
|
||||
|
||||
6. **Does `epiphany-pipeline` want to grow a reference enumerator, and is that Epiphany's call or Huginn's?** C4 needs one; the leaf has none and references are split between typed `PipelineRef` and bare `Short`. Changing the leaf is a cross-repo change at a pinned rev (`5cda0886`). **To answer:** an operator ruling on which repo owns the hop table.
|
||||
|
||||
7. **Whether `unreql` or any other crate exposes the ReQL term-tree encoding *without* a client.** The survey found drivers, not an encoding library. **To answer:** read `unreql`'s crate structure for a types-only module, or accept that no such split exists.
|
||||
|
||||
8. **Does `rmp-serde` actually fail on the specific untagged shape we would write, or only on the nested-enum case in msgpack-rust #148?** The hazard is recorded (III-2, III-9) but not reproduced against our own types. **To answer:** a ten-line test — define a two-variant `#[serde(untagged)]` enum whose variants contain enums, round-trip it through `rmp_serde::to_vec_named`, and see. This is cheap and would settle whether untagged encoding is available to us at all.
|
||||
|
||||
9. **What does `qdrant-client` actually resolve to with `default-features = false` *inside our lockfile*?** 209/129 are the survey's numbers taken in isolation; the overlap with our existing 139 is unmeasured, and the marginal cost is what matters. **To answer:** add it to `huginn-daemon` on a scratch branch, `cargo generate-lockfile`, diff the `[[package]]` count, discard.
|
||||
|
||||
10. **Can the Qdrant filter shape be obtained as types without the client?** The survey establishes there is no published types-only crate and that `segment` (which has the serde+schemars derives) is workspace-internal. Whether generating Rust types from Qdrant's published OpenAPI, or from `qdrant_common.proto` via prost, produces something usable without tonic was not tested. **To answer:** run a schema-to-Rust generator over `openapi.json` and count what comes out.
|
||||
|
||||
11. **Is the polars `Expr` serialization instability a blocker in practice, and does `dsl-schema` actually emit a usable schema?** Both facts are from documentation and feature lists; neither was exercised. The 0.55.2 docs.rs build failure means the current API surface was not directly inspected. **To answer:** build `polars-plan` with `dsl-schema`, emit the schema, and check whether a serialized `Expr` survives a patch-version bump.
|
||||
|
||||
12. **Does Substrait's expression subset cover per-kind field predicates over our document types without a relational schema underneath it?** Substrait is relational algebra; our image is not tables. Whether `Expression` is usable detached from `Rel`/`ReadRel` was not established. **To answer:** attempt to construct a bare `Expression` tree with `substrait` 0.65.0 and see what it requires.
|
||||
|
||||
13. **How large does a serialized query actually get under each shape, through base64?** The daemon base64s its MessagePack payload (I-1), so encoding verbosity costs 4/3 on top. No candidate was measured. **To answer:** encode the same worked predicate — "open findings of High severity on this campaign" — in each candidate shape and compare byte counts.
|
||||
1245
notes/eureka-read-side-cut.md
Normal file
1245
notes/eureka-read-side-cut.md
Normal file
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue