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 |
3756 changed files with 26154 additions and 876245 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
|
||||
5500
Cargo.lock
generated
5500
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
39
Cargo.toml
39
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,12 +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"
|
||||
required-features = ["openai-runtime"]
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-model-connector"
|
||||
path = "epiphany-openai-runtime/src/bin/epiphany-model-connector.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
|
||||
|
|
|
|||
23
README.md
23
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,19 +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
|
||||
- an independently supervised, loopback-only
|
||||
[model provider connector](docs/model-provider-connector.md) can lend that
|
||||
transport to an admitted GameCult service without lending Epiphany's Mind or
|
||||
credentials
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -25,9 +25,6 @@ outside the working notes.
|
|||
- [Fork implementation plan](../notes/epiphany-fork-implementation-plan.md)
|
||||
records the current architecture and cut lines.
|
||||
- [Project map](../state/map.yaml) is the canonical slow machine map.
|
||||
- [Model provider connector](./model-provider-connector.md) records the typed
|
||||
loopback boundary that lends Epiphany's Codex transport without lending her
|
||||
Mind, credentials, or swarm runtime.
|
||||
|
||||
For historical GameCult-level investor context, the sibling site dossier lives
|
||||
at `F:\Projects\gamecult-site\docs\gamecult_integrated_dossier.tex`. It is a
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
# Epiphany Model Provider Connector
|
||||
|
||||
## Objective
|
||||
|
||||
Let another GameCult service use Epiphany's admitted Codex subscription
|
||||
transport without acquiring Epiphany's Mind, credentials, scheduler, swarm
|
||||
runtime, or authority. The first consumer is Ghostlight Dungeon.
|
||||
|
||||
The connector is an independently supervised loopback service. Its daemon is
|
||||
earned by credential isolation, request-concurrency ownership, and failure
|
||||
isolation from both the consumer and the Epiphany swarm.
|
||||
|
||||
## Authority map
|
||||
|
||||
- **Owner:** `epiphany-model-connector` owns Codex authentication custody,
|
||||
provider transport, physical request concurrency, transport replay handling,
|
||||
and a redacted capability/status projection.
|
||||
- **Inputs:** one encrypted, expiring
|
||||
`epiphany.model_connector_invocation.v1` carried in a bounded CultNet
|
||||
`DocumentPutRaw` frame. The admitted caller and model are configured by the
|
||||
operator.
|
||||
- **Outputs:** one encrypted `epiphany.model_connector_result.v1` containing
|
||||
ordered provider events and the terminal model receipt, or a standard
|
||||
CultNet refusal. Odin receives only
|
||||
`epiphany.model_connector_status.v1`.
|
||||
- **Derived state:** connector readiness and capacity are discovery state. They
|
||||
are not provider credentials, inference permission, campaign state, or
|
||||
Epiphany Mind state.
|
||||
- **Forbidden writers:** the caller cannot select an unadmitted model, attach
|
||||
tools, request reasoning summaries, chain provider response state, inspect
|
||||
credentials, or mutate Epiphany. The connector cannot interpret model output
|
||||
as application state or commit application mutations.
|
||||
- **Shared path:** every accepted consumer request uses the same bounded frame,
|
||||
authenticated envelope, caller/model checks, semaphore, Codex transport, and
|
||||
terminal receipt validation.
|
||||
- **Cut line:** the connector calls the existing Codex transport directly. It
|
||||
does not start Epiphany's runtime, open her CultCache Mind, emit heartbeat
|
||||
state, or participate in swarm scheduling.
|
||||
|
||||
## Wire flow
|
||||
|
||||
```text
|
||||
consumer-owned typed stage
|
||||
-> provider-neutral model request
|
||||
-> MessagePack connector invocation
|
||||
-> AES-GCM authenticated envelope
|
||||
-> bounded TCP-framed CultNet on loopback
|
||||
-> Epiphany Codex transport
|
||||
-> ordered text/provider receipt events
|
||||
-> encrypted typed connector result
|
||||
-> consumer validation and local interpretation
|
||||
-> consumer-owned commit authority
|
||||
```
|
||||
|
||||
The outer CultNet message ID, document key, encrypted envelope request ID,
|
||||
invocation request ID, model request ID, every stream-event request ID, and
|
||||
terminal receipt request ID must agree. Requests carry a short expiry and an
|
||||
exact payload digest. A repeated request ID with identical bytes receives the
|
||||
cached terminal response for that connector lifetime; a conflicting or
|
||||
concurrently active duplicate is rejected. Application idempotency and commit
|
||||
authority remain with the consumer.
|
||||
|
||||
The frame decoder rejects a declared payload larger than its advertised limit
|
||||
before allocating the payload. This is a CultNet transport invariant rather
|
||||
than a connector-specific workaround.
|
||||
|
||||
## Security and privacy
|
||||
|
||||
- The listener must bind to loopback. It is never routed through nginx or a
|
||||
public firewall.
|
||||
- A service-private shared key authenticates and encrypts every invocation and
|
||||
result. It is stored outside source and release directories.
|
||||
- The Codex credential stays in the connector's service-readable credential
|
||||
directory. It is never copied into Ghostlight, arguments, logs, CultMesh, or
|
||||
Odin.
|
||||
- Tools, provider-side state chaining, reasoning summaries, reasoning deltas,
|
||||
and tool-call events are refused.
|
||||
- Prompts and results are not persisted by the connector. The consumer owns
|
||||
its own permitted stage receipts.
|
||||
|
||||
## Cache and token efficiency
|
||||
|
||||
The request contract carries `prompt_cache_key` and `max_output_tokens` through
|
||||
the existing model spine. Consumers construct the cache key from stable stage
|
||||
and output-contract identity, not volatile world state. Provider receipts
|
||||
report prompt, completion, reasoning, and cached-input token counts when the
|
||||
provider supplies them. Epiphany neither fabricates cache hits nor interprets
|
||||
their application value.
|
||||
|
||||
## Deployment boundary
|
||||
|
||||
Idunn manages this connector as a separate immutable artifact and service. An
|
||||
Epiphany deployment brake may prevent changing Epiphany's swarm release; it
|
||||
does not make the connector part of that release and must not wake the swarm.
|
||||
The connector's own deployment and continuity remain explicit.
|
||||
|
||||
The default endpoint is `127.0.0.1:4103`, maximum payload is one MiB, and
|
||||
physical concurrency is eight. On Yggdrasil, the Odin publication endpoint is
|
||||
`10.77.0.1:17871`; local `127.0.0.1:17871` belongs to Idunn.
|
||||
|
||||
## Verification
|
||||
|
||||
Focused tests prove:
|
||||
|
||||
- encrypted round trips and exact request correlation;
|
||||
- wrong caller, model, expiry, replay, tool, and reasoning refusals;
|
||||
- no reasoning or tool material crosses the accepted result boundary;
|
||||
- connector status contains no credential or application state;
|
||||
- oversized TCP-framed payloads are refused before allocation;
|
||||
- provider cache-token telemetry survives the Codex spine;
|
||||
- a consumer transport can use the real framed/encrypted boundary without
|
||||
learning connector credentials or Epiphany state.
|
||||
|
||||
The build surface is one new production binary in the existing OpenAI runtime
|
||||
crate. It reuses the existing CultNet, CultMesh, Codex login, and model-spine
|
||||
dependencies. It does not add a second model client or a second orchestration
|
||||
runtime.
|
||||
|
|
@ -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" }
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -574,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,
|
||||
|
|
@ -359,17 +359,6 @@ mod platform {
|
|||
use super::*;
|
||||
use chrono::{DateTime, SecondsFormat, Utc};
|
||||
|
||||
fn is_missing_error(error: &anyhow::Error) -> bool {
|
||||
error.chain().any(|cause| {
|
||||
cause
|
||||
.downcast_ref::<std::io::Error>()
|
||||
.is_some_and(|error| {
|
||||
error.kind() == std::io::ErrorKind::NotFound
|
||||
|| error.raw_os_error() == Some(libc::ESRCH)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn stat(process_id: u32) -> Result<(u64, char)> {
|
||||
let raw = std::fs::read_to_string(format!("/proc/{process_id}/stat"))
|
||||
.with_context(|| format!("read /proc/{process_id}/stat"))?;
|
||||
|
|
@ -386,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 {
|
||||
|
|
@ -557,21 +554,6 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn absent_linux_pid_is_missing_not_indeterminate() {
|
||||
let identity = ProcessInstanceIdentity {
|
||||
process_id: u32::MAX,
|
||||
creation_token: 1,
|
||||
created_at_rfc3339: None,
|
||||
executable_path: PathBuf::from("/definitely/absent"),
|
||||
};
|
||||
assert_eq!(
|
||||
observe_process_instance(&identity),
|
||||
ProcessInstanceObservation::Missing
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_exit_is_observed_for_the_exact_instance() {
|
||||
let mut child = if cfg!(windows) {
|
||||
|
|
|
|||
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,298 +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";
|
||||
pub const MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID: &str = "epiphany.model_connector_envelope.v1";
|
||||
pub const MODEL_CONNECTOR_INVOCATION_SCHEMA_ID: &str = "epiphany.model_connector_invocation.v1";
|
||||
pub const MODEL_CONNECTOR_RESULT_SCHEMA_ID: &str = "epiphany.model_connector_result.v1";
|
||||
pub const MODEL_CONNECTOR_STATUS_SCHEMA_ID: &str = "epiphany.model_connector_status.v1";
|
||||
|
||||
#[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>,
|
||||
#[cultcache(key = 16, default)]
|
||||
pub max_output_tokens: Option<u32>,
|
||||
#[cultcache(key = 17, default)]
|
||||
pub prompt_cache_key: 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,
|
||||
max_output_tokens: None,
|
||||
prompt_cache_key: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypted CultNet cargo. The correlation fields are deliberately repeated
|
||||
/// inside the authenticated plaintext and must match after decryption.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EpiphanyModelConnectorEnvelope {
|
||||
pub schema_id: String,
|
||||
pub request_id: String,
|
||||
pub message_kind: String,
|
||||
pub nonce: Vec<u8>,
|
||||
pub ciphertext: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.model_connector_invocation.v1",
|
||||
schema = "EpiphanyModelConnectorInvocation"
|
||||
)]
|
||||
pub struct EpiphanyModelConnectorInvocation {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub caller_runtime_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub expires_at_unix_ms: u64,
|
||||
#[cultcache(key = 4)]
|
||||
pub request: EpiphanyModelRequest,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.model_connector_result.v1",
|
||||
schema = "EpiphanyModelConnectorResult"
|
||||
)]
|
||||
pub struct EpiphanyModelConnectorResult {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub request_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub accepted: bool,
|
||||
#[cultcache(key = 3, default)]
|
||||
pub events: Vec<EpiphanyModelStreamEvent>,
|
||||
#[cultcache(key = 4, default)]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.model_connector_status.v1",
|
||||
schema = "EpiphanyModelConnectorStatus"
|
||||
)]
|
||||
pub struct EpiphanyModelConnectorStatus {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub provider_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub capability_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub request_schema_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub result_schema_id: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub envelope_schema_id: String,
|
||||
#[cultcache(key = 6)]
|
||||
pub transport_protocol: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub host: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub port: u16,
|
||||
#[cultcache(key = 9)]
|
||||
pub max_payload_bytes: u32,
|
||||
#[cultcache(key = 10)]
|
||||
pub max_parallel_requests: u32,
|
||||
#[cultcache(key = 11)]
|
||||
pub model: String,
|
||||
#[cultcache(key = 12)]
|
||||
pub ready: bool,
|
||||
#[cultcache(key = 13)]
|
||||
pub updated_at_utc: String,
|
||||
}
|
||||
|
||||
#[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>,
|
||||
#[cultcache(key = 9, 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,
|
||||
cached_input_tokens: 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,322 +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,
|
||||
#[cultcache(key = 15, default)]
|
||||
pub max_output_tokens: Option<u32>,
|
||||
#[cultcache(key = 16, default)]
|
||||
pub prompt_cache_key: Option<String>,
|
||||
}
|
||||
|
||||
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,
|
||||
max_output_tokens: None,
|
||||
prompt_cache_key: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
},
|
||||
max_output_tokens: request.max_output_tokens,
|
||||
prompt_cache_key: request.prompt_cache_key.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
#[cultcache(key = 8, default)]
|
||||
pub cached_input_tokens: Option<u64>,
|
||||
}
|
||||
|
||||
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,
|
||||
cached_input_tokens: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn native_provider_identity_selects_one_exact_wire_dialect() {
|
||||
let mut openai = EpiphanyModelRequest::new(
|
||||
"openai-request",
|
||||
"conversation",
|
||||
"openai-codex",
|
||||
"gpt-test",
|
||||
"decide",
|
||||
);
|
||||
openai.max_output_tokens = Some(512);
|
||||
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.max_output_tokens, Some(512));
|
||||
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,22 +13,16 @@ path = "src/lib.rs"
|
|||
[dependencies]
|
||||
anyhow = "1"
|
||||
chrono = "0.4.44"
|
||||
cultcache-rs = { path = "../vendor/cultcache-rs" }
|
||||
cultmesh-rs = { path = "../vendor/cultmesh-rs" }
|
||||
cultnet-rs = { path = "../vendor/cultnet-rs" }
|
||||
codex-login = { path = "../vendor/codex/codex-rs/login" }
|
||||
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"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] }
|
||||
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,7 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use epiphany_openai_runtime::{EpiphanyModelConnectorOptions, serve_model_connector};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
serve_model_connector(EpiphanyModelConnectorOptions::from_env_args()?).await
|
||||
}
|
||||
|
|
@ -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,996 +0,0 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use cultmesh_rs::{CultMesh, CultMeshNodeOptions, CultMeshRudpDocumentPublishOptions};
|
||||
use cultnet_rs::{
|
||||
CultNetClientSecurityOptions, CultNetMessage, CultNetRawDocumentRecord,
|
||||
CultNetRawPayloadEncoding, CultNetSecret, CultNetWireContract, TcpFramedTransportConnection,
|
||||
TcpFramedTransportProfileOptions, create_tcp_framed_transport_profile,
|
||||
decode_cultnet_message_from_slice, encode_cultnet_message_to_vec,
|
||||
};
|
||||
use epiphany_model_adapter::{
|
||||
EpiphanyModelConnectorEnvelope, EpiphanyModelConnectorInvocation, EpiphanyModelConnectorResult,
|
||||
EpiphanyModelConnectorStatus, EpiphanyModelRequest, EpiphanyModelStreamEvent,
|
||||
EpiphanyModelStreamPayload, MODEL_ADAPTER_EVENT_SCHEMA_ID, MODEL_ADAPTER_REQUEST_SCHEMA_ID,
|
||||
MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID, MODEL_CONNECTOR_INVOCATION_SCHEMA_ID,
|
||||
MODEL_CONNECTOR_RESULT_SCHEMA_ID, MODEL_CONNECTOR_STATUS_SCHEMA_ID,
|
||||
};
|
||||
use epiphany_openai_codex_spine::{EpiphanyCodexOpenAiTransport, auth_manager};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
|
||||
const CONNECTOR_PROVIDER_ID: &str = "epiphany.codex-model";
|
||||
const CONNECTOR_CAPABILITY_ID: &str = "model.generate.structured";
|
||||
const CONNECTOR_STATUS_KEY: &str = "epiphany.codex-model";
|
||||
const CONNECTOR_RUNTIME_ID: &str = "epiphany-model-connector";
|
||||
const CONNECTOR_REQUEST_KIND: &str = "model_request";
|
||||
const CONNECTOR_RESULT_KIND: &str = "model_result";
|
||||
const CONNECTOR_MAX_EXPIRY_SKEW_MS: u64 = 5 * 60 * 1_000;
|
||||
const CONNECTOR_MAX_OUTPUT_TOKENS: u32 = 32_768;
|
||||
|
||||
cultmesh_rs::cultmesh_documents!(ModelConnectorDocuments {
|
||||
EpiphanyModelConnectorStatus => MODEL_CONNECTOR_STATUS_SCHEMA_ID,
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EpiphanyModelConnectorOptions {
|
||||
pub bind: SocketAddr,
|
||||
pub state_path: PathBuf,
|
||||
pub codex_home: PathBuf,
|
||||
pub connection_key_path: PathBuf,
|
||||
pub allowed_caller_runtime_id: String,
|
||||
pub model: String,
|
||||
pub max_parallel_requests: u32,
|
||||
pub max_payload_bytes: u32,
|
||||
pub odin_endpoint: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl EpiphanyModelConnectorOptions {
|
||||
pub fn from_env_args() -> Result<Self> {
|
||||
let mut options = Self {
|
||||
bind: "127.0.0.1:4103".parse()?,
|
||||
state_path: PathBuf::from("state/epiphany-model-connector.cc"),
|
||||
codex_home: crate::default_codex_home()?,
|
||||
connection_key_path: PathBuf::new(),
|
||||
allowed_caller_runtime_id: "ghostlight-dungeon-yggdrasil".to_string(),
|
||||
model: "gpt-5.4".to_string(),
|
||||
max_parallel_requests: 8,
|
||||
max_payload_bytes: 1_048_576,
|
||||
odin_endpoint: Some("127.0.0.1:17871".parse()?),
|
||||
};
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--bind" => options.bind = next_arg(&mut args, "--bind")?.parse()?,
|
||||
"--state" => options.state_path = PathBuf::from(next_arg(&mut args, "--state")?),
|
||||
"--codex-home" => {
|
||||
options.codex_home = PathBuf::from(next_arg(&mut args, "--codex-home")?)
|
||||
}
|
||||
"--connection-key-file" => {
|
||||
options.connection_key_path =
|
||||
PathBuf::from(next_arg(&mut args, "--connection-key-file")?)
|
||||
}
|
||||
"--allowed-caller" => {
|
||||
options.allowed_caller_runtime_id = next_arg(&mut args, "--allowed-caller")?
|
||||
}
|
||||
"--model" => options.model = next_arg(&mut args, "--model")?,
|
||||
"--max-parallel" => {
|
||||
options.max_parallel_requests =
|
||||
next_arg(&mut args, "--max-parallel")?.parse()?
|
||||
}
|
||||
"--max-payload-bytes" => {
|
||||
options.max_payload_bytes =
|
||||
next_arg(&mut args, "--max-payload-bytes")?.parse()?
|
||||
}
|
||||
"--odin" => {
|
||||
let endpoint = next_arg(&mut args, "--odin")?;
|
||||
options.odin_endpoint = if endpoint == "none" {
|
||||
None
|
||||
} else {
|
||||
Some(endpoint.parse()?)
|
||||
};
|
||||
}
|
||||
other => bail!("unknown epiphany-model-connector argument {other:?}"),
|
||||
}
|
||||
}
|
||||
options.validate()?;
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<()> {
|
||||
if !self.bind.ip().is_loopback() {
|
||||
bail!("model connector must bind to a loopback address")
|
||||
}
|
||||
if self.connection_key_path.as_os_str().is_empty() {
|
||||
bail!("--connection-key-file is required")
|
||||
}
|
||||
if self.allowed_caller_runtime_id.trim().is_empty() {
|
||||
bail!("--allowed-caller must be non-empty")
|
||||
}
|
||||
if self.model.trim().is_empty() {
|
||||
bail!("--model must be non-empty")
|
||||
}
|
||||
if self.max_parallel_requests == 0 {
|
||||
bail!("--max-parallel must be greater than zero")
|
||||
}
|
||||
if self.max_payload_bytes < 4_096 {
|
||||
bail!("--max-payload-bytes must be at least 4096")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn next_arg(args: &mut impl Iterator<Item = String>, name: &str) -> Result<String> {
|
||||
args.next().with_context(|| format!("missing {name} value"))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ReplayCache {
|
||||
active: HashSet<String>,
|
||||
completed: HashMap<String, CachedResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CachedResponse {
|
||||
request_digest: [u8; 32],
|
||||
expires_at_unix_ms: u64,
|
||||
document: CultNetRawDocumentRecord,
|
||||
}
|
||||
|
||||
pub async fn serve_model_connector(options: EpiphanyModelConnectorOptions) -> Result<()> {
|
||||
options.validate()?;
|
||||
let connection_key = read_secret_file(&options.connection_key_path)?;
|
||||
let security = CultNetClientSecurityOptions::new(connection_key)?;
|
||||
let listener = TcpListener::bind(options.bind)
|
||||
.await
|
||||
.with_context(|| format!("failed to bind model connector at {}", options.bind))?;
|
||||
let local_addr = listener.local_addr()?;
|
||||
let auth = auth_manager(options.codex_home.clone());
|
||||
if auth.auth().await.is_none() {
|
||||
bail!(
|
||||
"Codex subscription authentication is unavailable at {}",
|
||||
options.codex_home.display()
|
||||
)
|
||||
}
|
||||
publish_connector_status(&options, local_addr)?;
|
||||
|
||||
let profile = create_tcp_framed_transport_profile(
|
||||
CONNECTOR_RUNTIME_ID,
|
||||
TcpFramedTransportProfileOptions {
|
||||
host: Some(local_addr.ip().to_string()),
|
||||
port: Some(local_addr.port()),
|
||||
max_payload_bytes: Some(options.max_payload_bytes),
|
||||
..TcpFramedTransportProfileOptions::default()
|
||||
},
|
||||
);
|
||||
let permits = Arc::new(Semaphore::new(options.max_parallel_requests as usize));
|
||||
let replay = Arc::new(Mutex::new(ReplayCache::default()));
|
||||
eprintln!(
|
||||
"{CONNECTOR_RUNTIME_ID} ready at {local_addr} for {} with {} concurrent calls",
|
||||
options.allowed_caller_runtime_id, options.max_parallel_requests
|
||||
);
|
||||
|
||||
loop {
|
||||
let (stream, peer) = listener.accept().await?;
|
||||
if !peer.ip().is_loopback() {
|
||||
continue;
|
||||
}
|
||||
let stream = stream.into_std()?;
|
||||
stream.set_nonblocking(false)?;
|
||||
let profile = profile.clone();
|
||||
let security = security.clone();
|
||||
let permits = permits.clone();
|
||||
let replay = replay.clone();
|
||||
let auth = auth.clone();
|
||||
let allowed_caller = options.allowed_caller_runtime_id.clone();
|
||||
let model = options.model.clone();
|
||||
tokio::spawn(async move {
|
||||
let _permit = match permits.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if let Err(error) = serve_one_connection(
|
||||
stream,
|
||||
profile,
|
||||
security,
|
||||
replay,
|
||||
auth,
|
||||
&allowed_caller,
|
||||
&model,
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("model connector request refused: {error:#}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve_one_connection(
|
||||
stream: TcpStream,
|
||||
profile: cultnet_rs::CultNetTransportProfile,
|
||||
security: CultNetClientSecurityOptions,
|
||||
replay: Arc<Mutex<ReplayCache>>,
|
||||
auth: Arc<codex_login::AuthManager>,
|
||||
allowed_caller: &str,
|
||||
model: &str,
|
||||
) -> Result<()> {
|
||||
let (connection, request_document) = tokio::task::spawn_blocking(move || {
|
||||
let mut connection = TcpFramedTransportConnection::new(stream, profile);
|
||||
let frame = connection.receive()?;
|
||||
let message = decode_cultnet_message_from_slice(
|
||||
&frame.payload,
|
||||
CultNetWireContract::CultNetSchemaV0,
|
||||
)?;
|
||||
let document = request_document_from_message(message)?;
|
||||
Ok::<_, anyhow::Error>((connection, document))
|
||||
})
|
||||
.await??;
|
||||
|
||||
let outer_request_id = request_document.record_key.clone();
|
||||
let request_digest: [u8; 32] = Sha256::digest(&request_document.payload).into();
|
||||
match claim_request(&replay, &outer_request_id, request_digest).await {
|
||||
Ok(Some(cached)) => return send_document(connection, cached).await,
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
eprintln!("model connector replay refusal for {outer_request_id}: {error:#}");
|
||||
return send_error(connection, "model connector rejected the replay").await;
|
||||
}
|
||||
}
|
||||
|
||||
let outcome =
|
||||
process_request_document(&request_document, &security, auth, allowed_caller, model).await;
|
||||
let (response, expires_at) = match outcome {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
release_request(&replay, &outer_request_id).await;
|
||||
eprintln!("model connector pre-execution refusal for {outer_request_id}: {error:#}");
|
||||
return send_error(connection, "model connector rejected the invocation")
|
||||
.await
|
||||
.with_context(|| error.to_string());
|
||||
}
|
||||
};
|
||||
complete_request(
|
||||
&replay,
|
||||
outer_request_id,
|
||||
request_digest,
|
||||
expires_at,
|
||||
response.clone(),
|
||||
)
|
||||
.await;
|
||||
send_document(connection, response).await
|
||||
}
|
||||
|
||||
fn request_document_from_message(message: CultNetMessage) -> Result<CultNetRawDocumentRecord> {
|
||||
let CultNetMessage::DocumentPutRaw {
|
||||
message_id,
|
||||
document,
|
||||
} = message
|
||||
else {
|
||||
bail!("model connector accepts only CultNet DocumentPutRaw invocations")
|
||||
};
|
||||
if message_id != document.record_key {
|
||||
bail!("model connector message id does not match the document key")
|
||||
}
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
async fn claim_request(
|
||||
cache: &Mutex<ReplayCache>,
|
||||
request_id: &str,
|
||||
request_digest: [u8; 32],
|
||||
) -> Result<Option<CultNetRawDocumentRecord>> {
|
||||
let now = unix_ms()?;
|
||||
let mut cache = cache.lock().await;
|
||||
cache
|
||||
.completed
|
||||
.retain(|_, response| response.expires_at_unix_ms >= now);
|
||||
if let Some(response) = cache.completed.get(request_id) {
|
||||
if response.request_digest != request_digest {
|
||||
bail!("request id was reused with different encrypted cargo")
|
||||
}
|
||||
return Ok(Some(response.document.clone()));
|
||||
}
|
||||
if !cache.active.insert(request_id.to_string()) {
|
||||
bail!("request is already in flight")
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn release_request(cache: &Mutex<ReplayCache>, request_id: &str) {
|
||||
cache.lock().await.active.remove(request_id);
|
||||
}
|
||||
|
||||
async fn complete_request(
|
||||
cache: &Mutex<ReplayCache>,
|
||||
request_id: String,
|
||||
request_digest: [u8; 32],
|
||||
expires_at_unix_ms: u64,
|
||||
document: CultNetRawDocumentRecord,
|
||||
) {
|
||||
let mut cache = cache.lock().await;
|
||||
cache.active.remove(&request_id);
|
||||
cache.completed.insert(
|
||||
request_id,
|
||||
CachedResponse {
|
||||
request_digest,
|
||||
expires_at_unix_ms,
|
||||
document,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async fn process_request_document(
|
||||
document: &CultNetRawDocumentRecord,
|
||||
security: &CultNetClientSecurityOptions,
|
||||
auth: Arc<codex_login::AuthManager>,
|
||||
allowed_caller: &str,
|
||||
allowed_model: &str,
|
||||
) -> Result<(CultNetRawDocumentRecord, u64)> {
|
||||
if document.schema_id != MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID {
|
||||
bail!("unexpected request envelope schema")
|
||||
}
|
||||
let envelope: EpiphanyModelConnectorEnvelope =
|
||||
rmp_serde::from_slice(&document.payload).context("request envelope is malformed")?;
|
||||
if envelope.schema_id != MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID
|
||||
|| envelope.message_kind != CONNECTOR_REQUEST_KIND
|
||||
|| envelope.request_id != document.record_key
|
||||
{
|
||||
bail!("request envelope identity is inconsistent")
|
||||
}
|
||||
let plaintext = CultNetSecret::decrypt_bytes(&envelope.ciphertext, &envelope.nonce, security)
|
||||
.context("request envelope authentication failed")?;
|
||||
let invocation: EpiphanyModelConnectorInvocation =
|
||||
rmp_serde::from_slice(&plaintext).context("request invocation is malformed")?;
|
||||
let validation = validate_invocation(
|
||||
&invocation,
|
||||
&envelope.request_id,
|
||||
allowed_caller,
|
||||
allowed_model,
|
||||
unix_ms()?,
|
||||
);
|
||||
let result = match validation {
|
||||
Ok(()) => execute_invocation(invocation.clone(), auth).await,
|
||||
Err(error) => EpiphanyModelConnectorResult {
|
||||
schema_id: MODEL_CONNECTOR_RESULT_SCHEMA_ID.to_string(),
|
||||
request_id: invocation.request_id.clone(),
|
||||
accepted: false,
|
||||
events: Vec::new(),
|
||||
error: Some(error.to_string()),
|
||||
},
|
||||
};
|
||||
let response_envelope = encrypt_result(&result, security)?;
|
||||
let response_payload = rmp_serde::to_vec_named(&response_envelope)?;
|
||||
Ok((
|
||||
CultNetRawDocumentRecord {
|
||||
schema_id: MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID.to_string(),
|
||||
record_key: invocation.request_id,
|
||||
stored_at: now_utc(),
|
||||
payload_encoding: CultNetRawPayloadEncoding::Messagepack,
|
||||
payload: response_payload,
|
||||
source_runtime_id: Some(CONNECTOR_RUNTIME_ID.to_string()),
|
||||
source_agent_id: None,
|
||||
source_role: Some("model-provider-connector".to_string()),
|
||||
tags: Some(vec![CONNECTOR_CAPABILITY_ID.to_string()]),
|
||||
},
|
||||
invocation.expires_at_unix_ms,
|
||||
))
|
||||
}
|
||||
|
||||
fn validate_invocation(
|
||||
invocation: &EpiphanyModelConnectorInvocation,
|
||||
outer_request_id: &str,
|
||||
allowed_caller: &str,
|
||||
allowed_model: &str,
|
||||
now_unix_ms: u64,
|
||||
) -> Result<()> {
|
||||
if invocation.schema_id != MODEL_CONNECTOR_INVOCATION_SCHEMA_ID
|
||||
|| invocation.request.schema_id != MODEL_ADAPTER_REQUEST_SCHEMA_ID
|
||||
{
|
||||
bail!("unexpected model invocation schema")
|
||||
}
|
||||
if invocation.request_id != outer_request_id
|
||||
|| invocation.request.request_id != invocation.request_id
|
||||
{
|
||||
bail!("model invocation substituted request identity")
|
||||
}
|
||||
if invocation.caller_runtime_id != allowed_caller {
|
||||
bail!("caller runtime is not admitted")
|
||||
}
|
||||
if invocation.expires_at_unix_ms < now_unix_ms
|
||||
|| invocation.expires_at_unix_ms > now_unix_ms + CONNECTOR_MAX_EXPIRY_SKEW_MS
|
||||
{
|
||||
bail!("model invocation is expired or exceeds the expiry horizon")
|
||||
}
|
||||
if invocation.request.provider != "openai-codex" {
|
||||
bail!("connector admits only the openai-codex provider")
|
||||
}
|
||||
if invocation.request.model != allowed_model {
|
||||
bail!("requested model is not admitted")
|
||||
}
|
||||
if invocation.request.instructions.trim().is_empty() {
|
||||
bail!("model instructions must be non-empty")
|
||||
}
|
||||
if !invocation.request.tools.is_empty() {
|
||||
bail!("shared model connector does not admit tool execution")
|
||||
}
|
||||
if invocation.request.previous_response_id.is_some() {
|
||||
bail!("shared model connector admits stateless requests only")
|
||||
}
|
||||
if invocation.request.reasoning_summary.is_some() {
|
||||
bail!("shared model connector does not expose reasoning summaries")
|
||||
}
|
||||
if matches!(invocation.request.max_output_tokens, Some(0))
|
||||
|| invocation
|
||||
.request
|
||||
.max_output_tokens
|
||||
.is_some_and(|value| value > CONNECTOR_MAX_OUTPUT_TOKENS)
|
||||
{
|
||||
bail!("max_output_tokens exceeds the connector bound")
|
||||
}
|
||||
if invocation
|
||||
.request
|
||||
.prompt_cache_key
|
||||
.as_deref()
|
||||
.is_some_and(|value| value.trim().is_empty() || value.len() > 128)
|
||||
{
|
||||
bail!("prompt_cache_key must contain at most 128 non-blank bytes")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_invocation(
|
||||
invocation: EpiphanyModelConnectorInvocation,
|
||||
auth: Arc<codex_login::AuthManager>,
|
||||
) -> EpiphanyModelConnectorResult {
|
||||
let provider_request = epiphany_openai_adapter::request_from_native(&invocation.request);
|
||||
let transport = EpiphanyCodexOpenAiTransport::openai(auth);
|
||||
let events = match transport.collect_model_events(provider_request).await {
|
||||
Ok(events) => sanitize_model_events(
|
||||
&invocation.request,
|
||||
events
|
||||
.iter()
|
||||
.map(|event| crate::model_event_from_openai_event("openai-codex", event))
|
||||
.collect(),
|
||||
),
|
||||
Err(error) => {
|
||||
eprintln!(
|
||||
"model connector provider failure for {}: {error:#}",
|
||||
invocation.request_id
|
||||
);
|
||||
vec![EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id: invocation.request_id.clone(),
|
||||
provider: "openai-codex".to_string(),
|
||||
sequence: 0,
|
||||
payload: EpiphanyModelStreamPayload::Failed {
|
||||
message: "model provider transport failed".to_string(),
|
||||
},
|
||||
}]
|
||||
}
|
||||
};
|
||||
EpiphanyModelConnectorResult {
|
||||
schema_id: MODEL_CONNECTOR_RESULT_SCHEMA_ID.to_string(),
|
||||
request_id: invocation.request_id,
|
||||
accepted: true,
|
||||
events,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_model_events(
|
||||
request: &EpiphanyModelRequest,
|
||||
events: Vec<EpiphanyModelStreamEvent>,
|
||||
) -> Vec<EpiphanyModelStreamEvent> {
|
||||
let mut sanitized = Vec::new();
|
||||
for event in events {
|
||||
if event.request_id != request.request_id || event.provider != request.provider {
|
||||
return vec![connector_failed_event(
|
||||
&request.request_id,
|
||||
"model provider substituted event identity",
|
||||
)];
|
||||
}
|
||||
match event.payload {
|
||||
EpiphanyModelStreamPayload::ReasoningDelta { .. } => continue,
|
||||
EpiphanyModelStreamPayload::ToolCall { .. } => {
|
||||
return vec![connector_failed_event(
|
||||
&request.request_id,
|
||||
"model provider returned an inadmissible tool call",
|
||||
)];
|
||||
}
|
||||
EpiphanyModelStreamPayload::Completed { receipt } => {
|
||||
if let Some(limit) = request.max_output_tokens
|
||||
&& !receipt_proves_public_output_bound(&receipt, limit)
|
||||
{
|
||||
return vec![connector_failed_event(
|
||||
&request.request_id,
|
||||
"model provider did not prove the requested output-token bound",
|
||||
)];
|
||||
}
|
||||
sanitized.push(EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id: request.request_id.clone(),
|
||||
provider: request.provider.clone(),
|
||||
sequence: sanitized.len() as u64,
|
||||
payload: EpiphanyModelStreamPayload::Completed { receipt },
|
||||
});
|
||||
}
|
||||
payload => sanitized.push(EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id: request.request_id.clone(),
|
||||
provider: request.provider.clone(),
|
||||
sequence: sanitized.len() as u64,
|
||||
payload,
|
||||
}),
|
||||
}
|
||||
}
|
||||
if sanitized.is_empty() {
|
||||
sanitized.push(connector_failed_event(
|
||||
&request.request_id,
|
||||
"model provider returned no public events",
|
||||
));
|
||||
}
|
||||
sanitized
|
||||
}
|
||||
|
||||
fn receipt_proves_public_output_bound(
|
||||
receipt: &epiphany_model_adapter::EpiphanyModelReceipt,
|
||||
limit: u32,
|
||||
) -> bool {
|
||||
let Some(total_output_tokens) = receipt.output_tokens else {
|
||||
return false;
|
||||
};
|
||||
let public_output_tokens = match receipt.reasoning_output_tokens {
|
||||
Some(reasoning_tokens) if reasoning_tokens <= total_output_tokens => {
|
||||
total_output_tokens - reasoning_tokens
|
||||
}
|
||||
Some(_) => return false,
|
||||
None => total_output_tokens,
|
||||
};
|
||||
public_output_tokens <= u64::from(limit)
|
||||
}
|
||||
|
||||
fn connector_failed_event(request_id: &str, message: &str) -> EpiphanyModelStreamEvent {
|
||||
EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id: request_id.to_string(),
|
||||
provider: "openai-codex".to_string(),
|
||||
sequence: 0,
|
||||
payload: EpiphanyModelStreamPayload::Failed {
|
||||
message: message.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_invocation(
|
||||
invocation: &EpiphanyModelConnectorInvocation,
|
||||
security: &CultNetClientSecurityOptions,
|
||||
) -> Result<EpiphanyModelConnectorEnvelope> {
|
||||
let nonce = CultNetSecret::new_nonce();
|
||||
let plaintext = rmp_serde::to_vec_named(invocation)?;
|
||||
Ok(EpiphanyModelConnectorEnvelope {
|
||||
schema_id: MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID.to_string(),
|
||||
request_id: invocation.request_id.clone(),
|
||||
message_kind: CONNECTOR_REQUEST_KIND.to_string(),
|
||||
ciphertext: CultNetSecret::encrypt_bytes(&plaintext, &nonce, security)?,
|
||||
nonce: nonce.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
fn encrypt_result(
|
||||
result: &EpiphanyModelConnectorResult,
|
||||
security: &CultNetClientSecurityOptions,
|
||||
) -> Result<EpiphanyModelConnectorEnvelope> {
|
||||
let nonce = CultNetSecret::new_nonce();
|
||||
let plaintext = rmp_serde::to_vec_named(result)?;
|
||||
Ok(EpiphanyModelConnectorEnvelope {
|
||||
schema_id: MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID.to_string(),
|
||||
request_id: result.request_id.clone(),
|
||||
message_kind: CONNECTOR_RESULT_KIND.to_string(),
|
||||
ciphertext: CultNetSecret::encrypt_bytes(&plaintext, &nonce, security)?,
|
||||
nonce: nonce.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decrypt_result(
|
||||
envelope: &EpiphanyModelConnectorEnvelope,
|
||||
security: &CultNetClientSecurityOptions,
|
||||
) -> Result<EpiphanyModelConnectorResult> {
|
||||
if envelope.schema_id != MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID
|
||||
|| envelope.message_kind != CONNECTOR_RESULT_KIND
|
||||
{
|
||||
bail!("unexpected connector result envelope")
|
||||
}
|
||||
let plaintext = CultNetSecret::decrypt_bytes(&envelope.ciphertext, &envelope.nonce, security)?;
|
||||
let result: EpiphanyModelConnectorResult = rmp_serde::from_slice(&plaintext)?;
|
||||
if result.schema_id != MODEL_CONNECTOR_RESULT_SCHEMA_ID
|
||||
|| result.request_id != envelope.request_id
|
||||
{
|
||||
bail!("connector result substituted request identity")
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn send_document(
|
||||
connection: TcpFramedTransportConnection<TcpStream>,
|
||||
document: CultNetRawDocumentRecord,
|
||||
) -> Result<()> {
|
||||
let message = CultNetMessage::SnapshotResponseRaw {
|
||||
message_id: document.record_key.clone(),
|
||||
documents: vec![document],
|
||||
};
|
||||
let payload = encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0)?;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut connection = connection;
|
||||
connection.send("schema", &payload)
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_error(
|
||||
connection: TcpFramedTransportConnection<TcpStream>,
|
||||
message: &str,
|
||||
) -> Result<()> {
|
||||
let payload = encode_cultnet_message_to_vec(
|
||||
&CultNetMessage::Error {
|
||||
error: message.to_string(),
|
||||
},
|
||||
CultNetWireContract::CultNetSchemaV0,
|
||||
)?;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut connection = connection;
|
||||
connection.send("schema", &payload)
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn publish_connector_status(
|
||||
options: &EpiphanyModelConnectorOptions,
|
||||
bind: SocketAddr,
|
||||
) -> Result<()> {
|
||||
if let Some(parent) = options.state_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let status = EpiphanyModelConnectorStatus {
|
||||
schema_id: MODEL_CONNECTOR_STATUS_SCHEMA_ID.to_string(),
|
||||
provider_id: CONNECTOR_PROVIDER_ID.to_string(),
|
||||
capability_id: CONNECTOR_CAPABILITY_ID.to_string(),
|
||||
request_schema_id: MODEL_CONNECTOR_INVOCATION_SCHEMA_ID.to_string(),
|
||||
result_schema_id: MODEL_CONNECTOR_RESULT_SCHEMA_ID.to_string(),
|
||||
envelope_schema_id: MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID.to_string(),
|
||||
transport_protocol: "cultnet.tcp_framed.v0".to_string(),
|
||||
host: bind.ip().to_string(),
|
||||
port: bind.port(),
|
||||
max_payload_bytes: options.max_payload_bytes,
|
||||
max_parallel_requests: options.max_parallel_requests,
|
||||
model: options.model.clone(),
|
||||
ready: true,
|
||||
updated_at_utc: now_utc(),
|
||||
};
|
||||
let mut node = CultMesh::create_node(
|
||||
&options.state_path,
|
||||
ModelConnectorDocuments,
|
||||
CultMeshNodeOptions {
|
||||
runtime_id: CONNECTOR_RUNTIME_ID.to_string(),
|
||||
pull_on_start: true,
|
||||
},
|
||||
)?;
|
||||
node.put(CONNECTOR_STATUS_KEY, &status)?;
|
||||
node.flush()?;
|
||||
if let Some(target) = options.odin_endpoint {
|
||||
if let Err(error) = node.publish_document_to_rudp_catalog(
|
||||
CONNECTOR_STATUS_KEY,
|
||||
&status,
|
||||
CultMeshRudpDocumentPublishOptions::odin(target, CONNECTOR_RUNTIME_ID),
|
||||
) {
|
||||
eprintln!("model connector Odin publication failed: {error:#}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_secret_file(path: &PathBuf) -> Result<String> {
|
||||
let raw = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read connector key {}", path.display()))?;
|
||||
let key = raw.trim();
|
||||
if key.is_empty() || raw.trim_matches(['\r', '\n']) != key {
|
||||
bail!("connector key file is empty or contains surrounding whitespace")
|
||||
}
|
||||
Ok(key.to_string())
|
||||
}
|
||||
|
||||
fn unix_ms() -> Result<u64> {
|
||||
Ok(SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.context("system clock predates Unix epoch")?
|
||||
.as_millis()
|
||||
.try_into()
|
||||
.context("system clock does not fit u64 milliseconds")?)
|
||||
}
|
||||
|
||||
fn now_utc() -> String {
|
||||
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use epiphany_model_adapter::EpiphanyModelReceipt;
|
||||
|
||||
fn invocation(now: u64) -> EpiphanyModelConnectorInvocation {
|
||||
let mut request = EpiphanyModelRequest::new(
|
||||
"request-1",
|
||||
"conversation-1",
|
||||
"openai-codex",
|
||||
"gpt-5.4",
|
||||
"Return one bounded answer.",
|
||||
);
|
||||
request.max_output_tokens = Some(512);
|
||||
EpiphanyModelConnectorInvocation {
|
||||
schema_id: MODEL_CONNECTOR_INVOCATION_SCHEMA_ID.to_string(),
|
||||
request_id: request.request_id.clone(),
|
||||
caller_runtime_id: "ghostlight-dungeon-yggdrasil".to_string(),
|
||||
expires_at_unix_ms: now + 60_000,
|
||||
request,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_connector_cargo_round_trips_without_exposing_plaintext() -> Result<()> {
|
||||
let security = CultNetClientSecurityOptions::new("not-a-production-secret")?;
|
||||
let invocation = invocation(1_000);
|
||||
let envelope = encrypt_invocation(&invocation, &security)?;
|
||||
assert!(
|
||||
!envelope
|
||||
.ciphertext
|
||||
.windows(10)
|
||||
.any(|part| part == b"conversation")
|
||||
);
|
||||
let plaintext =
|
||||
CultNetSecret::decrypt_bytes(&envelope.ciphertext, &envelope.nonce, &security)?;
|
||||
let decoded: EpiphanyModelConnectorInvocation = rmp_serde::from_slice(&plaintext)?;
|
||||
assert_eq!(decoded, invocation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connector_rejects_wrong_caller_expiry_tools_and_output_overrun() {
|
||||
let now = 1_000;
|
||||
let mut value = invocation(now);
|
||||
assert!(
|
||||
validate_invocation(
|
||||
&value,
|
||||
"request-1",
|
||||
"ghostlight-dungeon-yggdrasil",
|
||||
"gpt-5.4",
|
||||
now,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
value.caller_runtime_id = "foreign-runtime".to_string();
|
||||
assert!(
|
||||
validate_invocation(
|
||||
&value,
|
||||
"request-1",
|
||||
"ghostlight-dungeon-yggdrasil",
|
||||
"gpt-5.4",
|
||||
now,
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
value = invocation(now);
|
||||
value.expires_at_unix_ms = now - 1;
|
||||
assert!(
|
||||
validate_invocation(
|
||||
&value,
|
||||
"request-1",
|
||||
"ghostlight-dungeon-yggdrasil",
|
||||
"gpt-5.4",
|
||||
now,
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
value = invocation(now);
|
||||
value.request.max_output_tokens = Some(CONNECTOR_MAX_OUTPUT_TOKENS + 1);
|
||||
assert!(
|
||||
validate_invocation(
|
||||
&value,
|
||||
"request-1",
|
||||
"ghostlight-dungeon-yggdrasil",
|
||||
"gpt-5.4",
|
||||
now,
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
value = invocation(now);
|
||||
value.request.model = "another-model".to_string();
|
||||
assert!(
|
||||
validate_invocation(
|
||||
&value,
|
||||
"request-1",
|
||||
"ghostlight-dungeon-yggdrasil",
|
||||
"gpt-5.4",
|
||||
now,
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
value = invocation(now);
|
||||
value
|
||||
.request
|
||||
.tools
|
||||
.push(epiphany_model_adapter::EpiphanyModelToolDefinition {
|
||||
name: "forbidden".to_string(),
|
||||
description: "must not execute".to_string(),
|
||||
parameters_json: "{}".to_string(),
|
||||
});
|
||||
assert!(
|
||||
validate_invocation(
|
||||
&value,
|
||||
"request-1",
|
||||
"ghostlight-dungeon-yggdrasil",
|
||||
"gpt-5.4",
|
||||
now,
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
value = invocation(now);
|
||||
value.request.previous_response_id = Some("provider-state".to_string());
|
||||
assert!(
|
||||
validate_invocation(
|
||||
&value,
|
||||
"request-1",
|
||||
"ghostlight-dungeon-yggdrasil",
|
||||
"gpt-5.4",
|
||||
now,
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
value = invocation(now);
|
||||
value.request.reasoning_summary = Some("brief".to_string());
|
||||
assert!(
|
||||
validate_invocation(
|
||||
&value,
|
||||
"request-1",
|
||||
"ghostlight-dungeon-yggdrasil",
|
||||
"gpt-5.4",
|
||||
now,
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connector_refuses_outer_message_and_document_identity_split() {
|
||||
let document = CultNetRawDocumentRecord {
|
||||
schema_id: MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID.to_string(),
|
||||
record_key: "request-1".to_string(),
|
||||
stored_at: now_utc(),
|
||||
payload_encoding: CultNetRawPayloadEncoding::Messagepack,
|
||||
payload: Vec::new(),
|
||||
source_runtime_id: None,
|
||||
source_agent_id: None,
|
||||
source_role: None,
|
||||
tags: None,
|
||||
};
|
||||
assert!(
|
||||
request_document_from_message(CultNetMessage::DocumentPutRaw {
|
||||
message_id: "request-2".to_string(),
|
||||
document,
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_is_removed_and_public_events_are_renumbered() {
|
||||
let invocation = invocation(1_000);
|
||||
let mut receipt = EpiphanyModelReceipt::new("request-1", "openai-codex", "gpt-5.4");
|
||||
receipt.output_tokens = Some(10);
|
||||
let events = vec![
|
||||
EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id: "request-1".to_string(),
|
||||
provider: "openai-codex".to_string(),
|
||||
sequence: 0,
|
||||
payload: EpiphanyModelStreamPayload::ReasoningDelta {
|
||||
text: "private".to_string(),
|
||||
},
|
||||
},
|
||||
EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id: "request-1".to_string(),
|
||||
provider: "openai-codex".to_string(),
|
||||
sequence: 1,
|
||||
payload: EpiphanyModelStreamPayload::TextDelta {
|
||||
text: "public".to_string(),
|
||||
},
|
||||
},
|
||||
EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id: "request-1".to_string(),
|
||||
provider: "openai-codex".to_string(),
|
||||
sequence: 2,
|
||||
payload: EpiphanyModelStreamPayload::Completed { receipt },
|
||||
},
|
||||
];
|
||||
let events = sanitize_model_events(&invocation.request, events);
|
||||
assert_eq!(events.len(), 2);
|
||||
assert_eq!(events[0].sequence, 0);
|
||||
assert_eq!(events[1].sequence, 1);
|
||||
assert!(matches!(
|
||||
events[0].payload,
|
||||
EpiphanyModelStreamPayload::TextDelta { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
events[1].payload,
|
||||
EpiphanyModelStreamPayload::Completed { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connector_refuses_unproven_or_excessive_provider_output() {
|
||||
let invocation = invocation(1_000);
|
||||
let event = |output_tokens| EpiphanyModelStreamEvent {
|
||||
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
|
||||
request_id: "request-1".to_string(),
|
||||
provider: "openai-codex".to_string(),
|
||||
sequence: 0,
|
||||
payload: EpiphanyModelStreamPayload::Completed {
|
||||
receipt: {
|
||||
let mut receipt =
|
||||
EpiphanyModelReceipt::new("request-1", "openai-codex", "gpt-5.4");
|
||||
receipt.output_tokens = output_tokens;
|
||||
receipt
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
for output_tokens in [None, Some(513)] {
|
||||
let events = sanitize_model_events(&invocation.request, vec![event(output_tokens)]);
|
||||
assert!(matches!(
|
||||
events.as_slice(),
|
||||
[EpiphanyModelStreamEvent {
|
||||
payload: EpiphanyModelStreamPayload::Failed { .. },
|
||||
..
|
||||
}]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connector_bounds_public_output_without_exposing_hidden_reasoning() {
|
||||
let mut receipt = EpiphanyModelReceipt::new("request-1", "openai-codex", "gpt-5.4");
|
||||
receipt.output_tokens = Some(700);
|
||||
receipt.reasoning_output_tokens = Some(300);
|
||||
assert!(receipt_proves_public_output_bound(&receipt, 512));
|
||||
|
||||
receipt.reasoning_output_tokens = None;
|
||||
assert!(!receipt_proves_public_output_bound(&receipt, 512));
|
||||
|
||||
receipt.reasoning_output_tokens = Some(701);
|
||||
assert!(!receipt_proves_public_output_bound(&receipt, 512));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_result_refuses_outer_identity_substitution() -> Result<()> {
|
||||
let security = CultNetClientSecurityOptions::new("not-a-production-secret")?;
|
||||
let result = EpiphanyModelConnectorResult {
|
||||
schema_id: MODEL_CONNECTOR_RESULT_SCHEMA_ID.to_string(),
|
||||
request_id: "request-1".to_string(),
|
||||
accepted: true,
|
||||
events: Vec::new(),
|
||||
error: None,
|
||||
};
|
||||
let mut envelope = encrypt_result(&result, &security)?;
|
||||
envelope.request_id = "request-2".to_string();
|
||||
assert!(decrypt_result(&envelope, &security).is_err());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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,15 +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. |
|
||||
| model-provider connector | one configured caller's encrypted expiring native request over bounded loopback CultNet | ordered public model events, terminal receipt, and redacted CultMesh capability status | A consumer may borrow Codex transport and physical concurrency without borrowing Epiphany's Mind, credentials, swarm lifecycle, or state-admission authority. |
|
||||
| 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. |
|
||||
|
|
@ -651,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
|
||||
|
|
@ -677,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
|
||||
|
|
@ -706,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.
|
||||
|
|
@ -727,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,
|
||||
|
|
@ -765,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`;
|
||||
|
|
@ -828,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.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue