| … | … | @@ -1,4 +1,4 @@ |
| 1 | # hyper-atomspace bridge — M0 + M1 | |
| 1 | # hyper-atomspace bridge — M0 · M1 · M3 | |
| 2 | 2 | |
| 3 | 3 | Milestones of the HyperStorageNode (spec: `../../spec/hyperstoragenode.md`; |
| 4 | 4 | `atomspace` org issue #4). A Bare daemon holds the op-log; a C++ client speaks the |
| … | … | @@ -14,10 +14,14 @@ real opencog s-expr codec over a UNIX-domain socket. An AtomSpace is a |
| 14 | 14 | (barrier) ──barrier──▶ ok |
| 15 | 15 | (replay) ──load─────▶ ( get(0..len) ) |
| 16 | 16 | (checkout @K) ──loadat K─▶ ( get(0..K) ) ← cheap fork |
| 17 | (provenance) ──seqof X──▶ <birth seq of X> ← causal lineage | |
| 17 | 18 | Sexpr::decode_atom ◀──────────── |
| 18 | 19 | gold check: src AtomSpace ≡ dst AtomSpace |
| 19 | 20 | ``` |
| 20 | 21 | |
| 22 | All three ActiveGraph properties (arXiv:2605.21997) now hold over the log: | |
| 23 | **deterministic replay**, **cheap forking**, **complete lineage**. | |
| 24 | ||
| 21 | 25 | ## Milestones proven here (`./run.sh` runs all three) |
| 22 | 26 | - **M0 — bridge + codec round-trip.** C++ `AF_UNIX` client round-trips atoms |
| 23 | 27 | through the Bare daemon; payload is the real `libsexpr` codec (encode is a |
| … | … | @@ -28,6 +32,13 @@ real opencog s-expr codec over a UNIX-domain socket. An AtomSpace is a |
| 28 | 32 | - **M1 #2 — cheap fork.** `loadat K` is a read-only **checkout at seq K**: read |
| 29 | 33 | fewer blocks, get an earlier, strictly-smaller graph, no reprocessing. `[fork]` |
| 30 | 34 | phase → `GOLD #2` (full=8, checkout@3=3, subset ✓, strict ✓). |
| 35 | - **M3 #3 — complete lineage.** For a high-level atom `X = | |
| 36 | Evaluation(lives-in, List(whale, water))`, trace its causal history through the | |
| 37 | log via `seqof`: the exact ops that produced it — `whale@3 water@4 lives-in@5 | |
| 38 | List@6 Eval@7` — every dependency logged **before** X (append-only, outgoing-first), | |
| 39 | and the causally-unrelated `cat@0 animal@1 Inheritance@2` correctly **excluded**. | |
| 40 | `[lineage]` phase → `GOLD #3`. The daemon answers `seqof` (log position); the C++ | |
| 41 | side composes the causal walk — structure is the projection's job, not the log's. | |
| 31 | 42 | |
| 32 | 43 | ## Files |
| 33 | 44 | | File | Role | |
| … | … | @@ -42,7 +53,8 @@ real opencog s-expr codec over a UNIX-domain socket. An AtomSpace is a |
| 42 | 53 | # [full] live round-trip src=8 dst=8 PASS |
| 43 | 54 | # [verify] durable replay src=8 dst=8 GOLD #1 PASS |
| 44 | 55 | # [fork] full=8 checkout@3=3 subset=1 strict=1 GOLD #2 PASS |
| 45 | # M1 ALL PASS | |
| 56 | # [lineage] X=(EvaluationLink …) @seq 7 … found=1 deps<X=1 excl-unrelated=1 chain=1 GOLD #3 PASS | |
| 57 | # M0-M3 ALL PASS | |
| 46 | 58 | ``` |
| 47 | 59 | Requires: `bare` (v1.26+), `g++` (**C++20** — opencog headers use `std::atomic::wait`), |
| 48 | 60 | opencog under `/usr/local` (`libatomspace`/`libsexpr`/`libatombase`/`libatom_types`). |
| … | … | @@ -55,16 +67,19 @@ opencog under `/usr/local` (`libatomspace`/`libsexpr`/`libatombase`/`libatom_typ |
| 55 | 67 | | `store <sexpr>` | — | `core.append(sexpr)` (fire-and-forget) | |
| 56 | 68 | | `load` | `(<sexpr>…)\n` | replay whole log | |
| 57 | 69 | | `loadat <K>` | `(<sexpr>…)\n` | replay prefix 0..K (checkout / cheap fork) | |
| 70 | | `seqof <sexpr>` | `<seq>\n` | log position this op entered at, or `-1` (provenance) | | |
| 58 | 71 | | `clear` | `ok\n` | `core.truncate(0)` | |
| 59 | 72 | | `barrier` | `ok\n` | sync point (all prior stores applied) | |
| 60 | 73 | | `count` | `<len>\n` | `core.length` | |
| 61 | 74 | |
| 62 | 75 | ## Deferred (next) |
| 63 | - **Values** — structure only; per *event-source the graph, checkpoint the Values* | |
| 64 | (`../../spec/activegraph-synthesis.md`). | |
| 65 | 76 | - **`HyperStorageNode : public StorageNode` + guile module** — packaging, not new |
| 66 | capability: the data path is proven and `HyperChannel` is the transplant unit. | |
| 67 | Needs atom_types codegen + PersistSCM + CMake (a loadable `.so`). | |
| 68 | - **Autobase multi-writer** (M4) — two daemons, one linearized log = two agents. | |
| 69 | - **Branch-fork with divergent writes** — M1 `loadat` is a read-only checkout; | |
| 70 | true forking-with-writes rides Autobase. | |
| 77 | capability: all three properties are proven and `HyperChannel` is the transplant | |
| 78 | unit. Needs atom_types codegen + PersistSCM + CMake (a loadable `.so`) so opencog | |
| 79 | can `(cog-open (HyperStorageNode "hyper://…"))` directly. | |
| 80 | - **Values** — structure only; per *event-source the graph, checkpoint the Values* | |
| 81 | (`../../spec/activegraph-synthesis.md`). The GPU slice-table (Linas Step 4). | |
| 82 | - **Autobase multi-writer** (M4) — two daemons, one linearized log = two agents; | |
| 83 | the device-as-agent step. `seqof`'s linear scan also wants a real seq index here. | |
| 84 | - **Branch-fork with divergent writes** — `loadat` is a read-only checkout; true | |
| 85 | forking-with-writes rides Autobase. |
| … | … | @@ -11,6 +11,8 @@ |
| 11 | 11 | // store <sexpr>\n append <sexpr> to the log. NO reply (fire-and-forget). |
| 12 | 12 | // load\n reply: (<sexpr0>…<sexpr_{len-1}>)\n — whole log. |
| 13 | 13 | // loadat <K>\n reply: (<sexpr0>…<sexpr_{K-1}>)\n — prefix/checkout @K. |
| 14 | // seqof <sexpr>\n reply: <seq>\n — log position this op entered at, or -1. | |
| 15 | // (log-level provenance; the C++ side composes causal lineage.) | |
| 14 | 16 | // clear\n truncate the log to 0. reply: ok\n |
| 15 | 17 | // barrier\n sync point. reply: ok\n |
| 16 | 18 | // count\n reply: <len>\n |
| … | … | @@ -61,6 +63,17 @@ async function handleLine (line, sock) { |
| 61 | 63 | case 'loadat': |
| 62 | 64 | sock.write(await replay(parseInt(rest, 10) || 0)) |
| 63 | 65 | return |
| 66 | case 'seqof': { | |
| 67 | // Exact-match scan for the op that entered the log as `rest`. The log is | |
| 68 | // append-only so the first (only) match is the birth position. A seq index | |
| 69 | // is a later optimisation; M3's log is small. | |
| 70 | let found = -1 | |
| 71 | for (let i = 0; i < core.length; i++) { | |
| 72 | if (await core.get(i) === rest) { found = i; break } | |
| 73 | } | |
| 74 | sock.write(String(found) + '\n') | |
| 75 | return | |
| 76 | } | |
| 64 | 77 | case 'clear': |
| 65 | 78 | await core.truncate(0) |
| 66 | 79 | sock.write('ok\n') |
| … | … | @@ -15,12 +15,18 @@ |
| 15 | 15 | * same Hypercore dir -> proves durable deterministic replay, M1 #1) |
| 16 | 16 | * fork clear + store + load(full) + loadat(K) + prefix check |
| 17 | 17 | * (checkout at seq K = cheap fork, M1 #2) |
| 18 | * lineage clear + store; then for a high-level atom X, trace its | |
| 19 | * causal history through the log via seqof and check it is the | |
| 20 | * exact set of ops that produced X, ordered, excluding | |
| 21 | * causally-unrelated atoms (complete lineage, M3 #3) | |
| 18 | 22 | * |
| 19 | 23 | * SPDX-License-Identifier: AGPL-3.0-or-later |
| 20 | 24 | */ |
| 21 | 25 | |
| 22 | 26 | #include <algorithm> |
| 27 | #include <functional> | |
| 23 | 28 | #include <iostream> |
| 29 | #include <set> | |
| 24 | 30 | #include <string> |
| 25 | 31 | #include <vector> |
| 26 | 32 | |
| … | … | @@ -103,6 +109,11 @@ public: |
| 103 | 109 | send_line("loadat " + std::to_string(k) + "\n"); |
| 104 | 110 | return recv_line(); |
| 105 | 111 | } |
| 112 | int seqof(const std::string& sexpr) | |
| 113 | { | |
| 114 | send_line("seqof " + sexpr + "\n"); | |
| 115 | return std::stoi(recv_line()); | |
| 116 | } | |
| 106 | 117 | }; |
| 107 | 118 | |
| 108 | 119 | // --------------------------------------------------------------------------- |
| … | … | @@ -211,6 +222,64 @@ int main(int argc, char** argv) |
| 211 | 222 | return ok ? 0 : 1; |
| 212 | 223 | } |
| 213 | 224 | |
| 225 | if (mode == "lineage") | |
| 226 | { | |
| 227 | ch.clear(); | |
| 228 | for (const Handle& h : order) ch.store(Sexpr::encode_atom(h)); // creation order | |
| 229 | ch.barrier(); | |
| 230 | ||
| 231 | // X = the high-level fact: Evaluation(lives-in, List(whale, water)). | |
| 232 | const Handle& X = order[7]; | |
| 233 | ||
| 234 | // Causal closure: X + its transitive outgoing atoms (its dependencies). | |
| 235 | std::set<Handle> closure; | |
| 236 | std::function<void(const Handle&)> dfs = [&](const Handle& h) { | |
| 237 | if (closure.count(h)) return; | |
| 238 | if (h->is_link()) | |
| 239 | for (const Handle& o : h->getOutgoingSet()) dfs(o); | |
| 240 | closure.insert(h); | |
| 241 | }; | |
| 242 | dfs(X); | |
| 243 | ||
| 244 | int seqX = ch.seqof(Sexpr::encode_atom(X)); | |
| 245 | ||
| 246 | // Each dependency has a real log position, entered BEFORE X (append-only, | |
| 247 | // outgoing-first). Order the lineage by seq to reconstruct the chain. | |
| 248 | std::vector<std::pair<int, std::string>> chain; | |
| 249 | bool all_found = true, deps_before_X = true; | |
| 250 | for (const Handle& h : closure) | |
| 251 | { | |
| 252 | int s = ch.seqof(Sexpr::encode_atom(h)); | |
| 253 | if (s < 0) all_found = false; | |
| 254 | if (h != X && !(s < seqX)) deps_before_X = false; | |
| 255 | chain.push_back({s, Sexpr::encode_atom(h)}); | |
| 256 | } | |
| 257 | std::sort(chain.begin(), chain.end()); | |
| 258 | ||
| 259 | // Causally-unrelated atoms (cat, animal, Inheritance) must NOT appear. | |
| 260 | bool excludes_unrelated = | |
| 261 | !closure.count(order[0]) && !closure.count(order[1]) && | |
| 262 | !closure.count(order[2]); | |
| 263 | ||
| 264 | // Expected lineage, by seq: whale@3 water@4 lives-in@5 List@6 Eval@7. | |
| 265 | std::vector<std::pair<int, std::string>> expect; | |
| 266 | for (int i : {3, 4, 5, 6, 7}) | |
| 267 | expect.push_back({i, Sexpr::encode_atom(order[i])}); | |
| 268 | bool chain_ok = (chain == expect); | |
| 269 | ||
| 270 | std::cout << "[lineage] X=" << Sexpr::encode_atom(X) << " @seq " << seqX << "\n"; | |
| 271 | for (const auto& pr : chain) | |
| 272 | std::cout << " seq " << pr.first << " " << pr.second << "\n"; | |
| 273 | ||
| 274 | bool ok = all_found && deps_before_X && excludes_unrelated && chain_ok && | |
| 275 | (int) closure.size() == 5 && seqX == 7; | |
| 276 | std::cout << " found=" << all_found << " deps<X=" << deps_before_X | |
| 277 | << " excl-unrelated=" << excludes_unrelated | |
| 278 | << " chain=" << chain_ok | |
| 279 | << (ok ? " GOLD #3 PASS\n" : " GOLD #3 FAIL\n"); | |
| 280 | return ok ? 0 : 1; | |
| 281 | } | |
| 282 | ||
| 214 | 283 | // mode == full : live append + replay round-trip. |
| 215 | 284 | ch.clear(); |
| 216 | 285 | for (const Handle& h : order) ch.store(Sexpr::encode_atom(h)); |
| … | … | @@ -62,4 +62,9 @@ start_daemon "$STORE2" |
| 62 | 62 | ./m0_roundtrip "$SOCK" fork |
| 63 | 63 | stop_daemon |
| 64 | 64 | |
| 65 | echo "M1 ALL PASS" | |
| 65 | # Phase 4 — fresh store, causal lineage of a high-level atom (GOLD #3) | |
| 66 | start_daemon "$WORK/store3" | |
| 67 | ./m0_roundtrip "$SOCK" lineage | |
| 68 | stop_daemon | |
| 69 | ||
| 70 | echo "M0-M3 ALL PASS" |