Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

timeranger2 + treedb, in 30 minutes

This is the crash course on Yuneta’s persistence layer. At the end you know the difference between timeranger2 (the append-only time-series log) and treedb (the graph database on top), how schemas are declared, how nodes link to each other, and which rules will ruin your day if you ignore them.

Conceptual frame. This document describes the information plane of Yuneta’s typed-graph model. The behavior plane is in GOBJ.md. The claim that both planes share one set of primitives — topic/gclass, node/gobj, hook/subscription — is laid out in The Typed-Graph Model. Read that first if you want to know why treedb and gobj look so similar before diving into either one.

Companion to GOBJ.md. Sibling to YUNO_LIFECYCLE.md (which uses these topics to store realms, yunos, binaries and configurations), YUNO_AUTH.md (which uses them for users, roles and audit), and REALMS.md (the realm hooks lifecycle).


1. Mental model

                ┌───────────────────────────────────────┐
                │     your gclass calls gobj_*node()    │
                └───────────────────┬───────────────────┘
                                    │
                                    ▼
                ┌───────────────────────────────────────┐
                │            c_treedb / c_node          │   gobj wrappers
                │  (graph operations, in-memory hooks)  │
                └───────────────────┬───────────────────┘
                                    │
                                    ▼
                ┌───────────────────────────────────────┐
                │            tr_treedb.c                │   graph layer
                │  topics, nodes, hooks, fkeys, schema  │
                └───────────────────┬───────────────────┘
                                    │
                                    ▼
                ┌───────────────────────────────────────┐
                │            timeranger2.c              │   append-only log
                │  per-key files + md2 binary index     │
                └───────────────────┬───────────────────┘
                                    │
                                    ▼
                              filesystem
                       (one directory per topic,
                        one subdir per key,
                        one .json + .md2 per day)

Two distinct things:

LayerWhat it is
timeranger2An append-only time-series log with a key index. Stores records keyed by a primary key, time-partitioned, with a 32-byte binary metadata index for fast lookup by rowid, time, or pkey. Knows nothing about graphs.
treedbA graph database that uses timeranger2 as its persistent store. Adds the notion of topics with schemas, typed columns, hooks (parent→children in-memory pointers), and fkeys (child→parent persistent references).

If you want raw time-series, you go straight to timeranger2. If you want a graph of typed nodes, you use treedb. The agent uses treedb for everything: realms, yunos, binaries, configurations, users and roles. The logcenter yuno uses raw timeranger2 to write records.


2. timeranger2

2.1 The on-disk layout

For each opened database (a top-level directory):

timeranger2 on-disk layout: a database holds timeranger2.json and per-topic files. Records live under keys/<key>/<date>.json, paired with a 32-byte <date>.md2 index. The disks/<rt_id>/ tree holds hardlinks back into the keys/ files, which is how non-master and cross-yuno readers see the same data.

The same layout in text:

<database>/
  __timeranger2__.json                ← metadata + master lock
  <topic_1>/
    topic_desc.json                   ← {topic_name, pkey, tkey, system_flag}
    topic_cols.json                   ← persisted cols schema  ⚠ versioning trap
    topic_var.json                    ← user-mutable per-topic flags
    keys/
      <key_value_a>/
        2026-05-22.json               ← appended JSON records, one per line
        2026-05-22.md2                ← 32-byte binary index, one per record
        2026-05-23.json
        2026-05-23.md2
        …
      <key_value_b>/
        …
    disks/                            ← non-master / cross-yuno hardlink slots
      <rt_id>/
        <key_value_a>/                ← hardlinks to the keys/ files
        <key_value_b>/
        …
  <topic_2>/
    …

Path-building lives in kernel/c/timeranger2/src/timeranger2.c. The data filename mask is "%Y-%m-%d" by default — each appended record lands in the file whose mask matches its __t__. Big topics naturally rotate every day.

2.2 Records and the md2 index

Each .md2 file is an array of fixed 32-byte records in big-endian order. The struct (timeranger2.c, in-memory shape md2_record_ex_t at timeranger2.h):

A md2 record is 32 bytes: four uint64 fields t, tm, offset, size. The .md2 file is an array of these, indexed by rowid. A lookup multiplies rowid by 32, seeks the .md2, reads offset and size, then seeks the paired .json. O(1).
typedef struct {
    uint64_t __t__;         // storage timestamp + high-16-bit user flags
    uint64_t __tm__;        // creation timestamp + high-16-bit system flags
    uint64_t __offset__;    // byte offset of the record in the paired .json
    uint64_t __size__;      // byte size of the record
    // (in memory only:)
    uint16_t system_flag;
    uint16_t user_flag;
    uint64_t rowid;
} md2_record_ex_t;

The high 16 bits of __t__ and __tm__ are reserved for flags. Macros at timeranger2.c extract and pack them. Lookup by rowid is O(1) — multiply by 32, seek the .md2, read offset+size, seek the .json. Lookup by time range is O(N) over .md2 records, which is still fast, because each record is 32 bytes.

2.3 g_rowid vs i_rowid — the rule

Two rowids per record, both maintained only by timeranger2:

NameMeaning
g_rowidGlobal rowid for that key — cumulative across all files, never reset
i_rowidRowid within the current .md2 file — (offset / sizeof(md2_record_t)) + 1

tranger2_append_record (timeranger2.c:2332) computes both and returns them in md_record_ex->rowid (timeranger2.c). Callers never set them. For topics with sf_rowid_key, timeranger2 also asserts g_rowid == i_rowid (timeranger2.c) — a mismatch is a data-corruption indicator.

If you write test fixtures and you fill g_rowid by hand, stop. That is the work of the framework.

2.4 __t__ vs __tm__

Both timestamps, but semantically distinct:

FieldWhat it meansWhen it is set
__t__When timeranger2 wrote the record to diskAt append time. Defaults to “now”.
__tm__When the underlying event happened (from the record’s tkey field)Caller-controlled via tkey config.

__t__ partitions files. __tm__ is the event-time for your queries. For records that are events as they happen, the two are usually identical (within milliseconds). For batch imports of historical data the two diverge — __tm__ is the original event, __t__ is “now I imported it”.

2.5 Topic declaration

When you create a topic you provide a topic_desc_t (timeranger2.h):

typedef struct {
    const char       *topic_name;
    const char       *pkey;          // primary-key field name
    const system_flag2_t system_flag;
    const char       *tkey;          // time-key field name
    const json_desc_t *jn_cols;       // column schema
    const json_desc_t *jn_topic_ext;
} topic_desc_t;

system_flag bits (timeranger2.h):

FlagMeaning
sf_string_keypkey is a string. Directory names use it verbatim.
sf_int_keypkey is a uint64. Directory names zero-padded.
sf_rowid_keypkey is auto-generated rowid. g_rowid == i_rowid enforced.
sf_t_ms__t__ in milliseconds (default: seconds).
sf_tm_ms__tm__ in milliseconds.
sf_zip_record.json records are zlib-compressed.
sf_cipher_record.json records are encrypted.

Persisted in topic_desc.json at create time (timeranger2.c) and loaded on open.

2.6 Public API in 12 calls

timeranger2.h. Grouped by purpose:

// lifecycle
json_t *tranger2_startup    (hgobj, json_t *jn_tranger, yev_loop_h);
int     tranger2_stop       (json_t *tranger);
int     tranger2_shutdown   (json_t *tranger);
json_t *tranger2_create_topic(json_t *tranger, const char *topic_name,
                              const char *pkey, const char *tkey,
                              json_t *jn_topic_ext, system_flag2_t system_flag,
                              json_t *jn_cols, json_t *jn_var);
json_t *tranger2_open_topic  (json_t *tranger, const char *topic_name, BOOL verbose);
int     tranger2_close_topic (json_t *tranger, const char *topic_name);

// append
int     tranger2_append_record(json_t *tranger, const char *topic_name,
                               uint64_t __t__, uint16_t user_flag,
                               md2_record_ex_t *md_record_ex, json_t *jn_record);

// read
json_t *tranger2_open_iterator    (json_t *tranger, const char *topic_name, const char *key,
                                   json_t *match_cond, tranger2_load_record_callback_t,
                                   const char *iterator_id, hgobj creator, json_t *data, json_t *extra);
json_t *tranger2_iterator_get_page(json_t *tranger, json_t *iterator,
                                   uint64_t from_rowid, int limit, BOOL backward);
int     tranger2_close_iterator   (json_t *tranger, json_t *iterator);

// realtime
json_t *tranger2_open_rt_mem (…);   // master-side realtime (writes pushed via callback)
json_t *tranger2_open_rt_disk(…);   // non-master realtime (watches hardlinks)

tranger2_open_rt_disk is the workhorse for cross-yuno reads — see §4.5.

2.6b The two time axes (t and tm)

Every record carries two timestamps, and they are independent:

AxisMeaningIts source
tPersistence time — when the record was appendedthe __t__ argument of tranger2_append_record (now, if 0)
tmMessage time — when the event it carries happenedthe record’s tkey field (usually tm), set by the producer

They diverge whenever data is backfilled or a device uploads a buffer late. Both are in the topic’s unit: seconds, or milliseconds when the topic sets sf_t_ms / sf_tm_ms (read system_flag from the topic desc — over the wire, topics expanded=1).

The match_cond of an iterator takes a range on each axis (from_t and to_t, from_tm and to_tm), the from_rowid and to_rowid pair, and the user_flag conditions, and ANDs them. Every condition is honored per record: a filtered paging iterator builds its row index when it opens, so tranger2_iterator_size(), pages and the pages themselves count only matching records — and get_page’s from_rowid is then a position among the matching rows, not a global rowid. An unfiltered iterator builds no index (its open stays cheap regardless of key size) and its positions are the global rowids.

list-keys reports, per key, records plus the key’s span on both axes (fr_t/to_t, fr_tm/to_tm), read from the topic’s in-memory cache totals — so a client can bound a time picker to the content of the key, and it reads no record.

Note (in the md2 record, times carry flags). On disk the 16 high bits of __t__ hold the user_flag and those of __tm__ the system_flag. Always read them through get_time_t() or get_time_tm(). The raw field gives you a timestamp that still contains the flags.

2.7 Master / non-master

tranger2_startup (timeranger2.c:330) attempts an exclusive lock on __timeranger2__.json. Whoever gets it is the master:

The lock is held for the lifetime of the process. If a master crashes without releasing, the OS releases the flock on exit and the next yuno that opens the database becomes master.

2.8 Snapshots

The current timeranger2 API does not expose a snapshot primitive named tranger2_*_snap* — those calls live one layer up at the treedb level (§3.7). The closest underlying mechanism is the disks/<rt_id>/ hardlink trick that gives non-masters a consistent view at the point the directory was wired.

2.9 The delete-record story

Two granularities, both implemented in v7 as of 2026-05-26.

Propagation to subscribers (2026-05-26)

tranger2_delete_key() now notifies every subscriber tracking the deleted key. Two paths:

Register with:

tranger2_set_rt_key_deleted_callback(handle, cb, user_data);

…on any handle returned by tranger2_open_rt_mem, tranger2_open_rt_disk or tranger2_open_iterator. Pre-2026-05-26 followers that polled their cache on a timer can drop the timer.

Memory: project_tranger2_delete_record_deferred.

2.10 Durability

tranger2_append_record performs the write but does not fsync (timeranger2.c). Durability is whatever the OS gives you — on EXT4 with the default journal, that is “data on disk within the journal commit interval, usually 5 s”. If you need stronger guarantees, add an explicit fsync in the wrapping code, but understand the throughput cost.


3. treedb

3.1 The graph model

A treedb sits inside a tranger. Topics become entity types, nodes become records keyed by id, hooks are in-memory pointers from parent nodes to their children, fkeys are persistent references from child nodes to their parent. Schema is JSON.

The schemas already documented in this repo’s docs cover the canonical examples:

Read those for the operational shape. This section explains how the schema works.

3.2 Topic schema JSON

A real, minimal example (yuno_agent schema, paraphrased):

{
  "id":            "yunos",
  "schema_version": 1,
  "topic_version":  19,
  "pkey":          "id",
  "pkey2s":         "yuno_release",
  "tkey":          "",
  "system_flag":   "sf_string_key",
  "cols": {
    "id":         { "type": "string", "flag": ["persistent", "required"] },
    "realm_id":   { "type": "string", "flag": ["fkey"],
                    "fkey": { "realms": "yunos" } },
    "yuno_role":  { "type": "string", "flag": ["persistent", "required"] },
    "configurations": { "type": "object", "flag": ["hook"],
                        "hook": { "configurations": "yunos" } }
  }
}

Six things to notice:

  1. pkey — column name that serves as the primary key. Maps to topic_desc_t.pkey.

  2. pkey2s — optional secondary key (composite). Allows multiple records per primary key, for example several versions of a binary. treedb_get_instance(), treedb_list_instances() and the agent’s instances command query them. Invariant (since dbf532ec9): the pkey2 secondary index shares the SAME node object as the primary index, and treedb_save_node() points it again on every runtime save. Before that correction it held a separate object that only the disk-load filled. A runtime update-node was therefore invisible through list_instances until the next reload. That was the bug behind list-binaries, which showed a stale binary immediately after update-binary.

  3. schema_version and topic_version — these are different. Schema is the overall layout. Topic is per-topic. Raise topic_version every time you change cols — §3.5.

  4. cols declares typed columns. Type + flag list (next section).

  5. fkey field on the child points at (parent topic, hook name). Persisted.

  6. hook field on the parent points at (child topic, child fkey name). Rebuilt in-memory at load time.

3.3 Column types and flags

Column types live in the JSON spec, parsed by tr_treedb.c. Common ones: string, integer, boolean, real, array, object, blob, enum, wild. Plus semantic decorations: email, url, password, time.

Flags (parsed by kw_has_word throughout tr_treedb.c):

FlagEffect
persistentWritten through to timeranger2 on save.
requiredCannot be null at creation.
notnullCannot be null ever.
hookParent → children link. In-memory only (rebuilt on load from children’s fkeys).
fkeyChild → parent reference. Persisted. Encoded as topic^parent_id^hook_name.
pkeyMarks the primary-key column.
pkey2Marks a secondary key.
tkeyMarks the time-key column.
passwordTreated as opaque secret on inspection.
email/url/enum/wildSemantic types, mostly informational.
inheritInherits a value from a related node.

Absence of persistent + absence of hook/fkey means volatile — in-memory only.

3.4 The __md_treedb__ metadata block

Every loaded node carries a metadata sidecar (tr_treedb.c, attached at tr_treedb.c):

"__md_treedb__": {
    "treedb_name": "treedb_yuneta_agent",
    "topic_name":  "yunos",
    "g_rowid":     14,
    "i_rowid":     14,
    "t":           1737499200,
    "tm":          1737499200,
    "tag":         0,
    "pure_node":   true
}

A node that appears in multiple places in a JSON dump (once under the topic’s id index, once nested inside its parent’s hook) carries the same __md_treedb__ everywhere. Same record, multiple views.

3.5 The topic_cols.json versioning trap

Memory feedback_treedb_schema_versioning:

Any cols change needs a higher topic_version. If you do not raise it, the persisted topic_cols.json continues to mask the new schema. Delete store/ when you reproduce the problem.

What happens: treedb_open_db() (tr_treedb.c:485) reads the persisted topic_cols.json and compares its topic_version against the schema in code. If they match, the persisted file wins. If you edited the schema in code but forgot to bump topic_version, your running yuno sees the old schema and silently ignores any new columns you added.

The fix:

  1. Bump topic_version in the schema JSON every time you change cols.

  2. While debugging schema problems, wipe the topic’s directory in store/ to force a clean load.

3.6 Node CRUD: the public API

Two layers — treedb_* (the low-level graph API) and gobj_*node (the gobj wrappers most user code uses).

Low-level (tr_treedb.h):

json_t *treedb_create_node(json_t *tranger, const char *treedb_name,
                           const char *topic_name, json_t *kw);
json_t *treedb_update_node(json_t *tranger, json_t *node, json_t *kw, BOOL save);
int     treedb_delete_node(json_t *tranger, json_t *node, json_t *jn_options);
json_t *treedb_get_node   (json_t *tranger, const char *treedb_name,
                           const char *topic_name, const char *id);
json_t *treedb_list_nodes (json_t *tranger, const char *treedb_name,
                           const char *topic_name, json_t *jn_filter,
                           BOOL (*match_fn)(json_t *node, json_t *jn_filter));

// links (graph operations)
int     treedb_link_nodes  (json_t *tranger, const char *hook_name,
                            json_t *parent_node, json_t *child_node);
int     treedb_unlink_nodes(json_t *tranger, const char *hook_name,
                            json_t *parent_node, json_t *child_node);

gobj-level wrappers (gobj.h):

json_t *gobj_create_node(hgobj, const char *topic, json_t *kw, json_t *opt, hgobj src);
json_t *gobj_update_node(hgobj, const char *topic, json_t *kw, json_t *opt, hgobj src);
int     gobj_delete_node(hgobj, const char *topic, json_t *kw, json_t *opt, hgobj src);
json_t *gobj_list_nodes (hgobj, const char *topic, json_t *filter, json_t *opt, hgobj src);
int     gobj_link_nodes  (hgobj, const char *hook,
                          const char *parent_topic, json_t *parent_rec,
                          const char *child_topic,  json_t *child_rec, hgobj src);
int     gobj_unlink_nodes(hgobj, const char *hook,
                          const char *parent_topic, json_t *parent_rec,
                          const char *child_topic,  json_t *child_rec, hgobj src);

Most production code calls gobj_*node. Those functions route to the right treedb from the priv of the gobj, and they integrate the authzs and the traces.

CLAUDE.md hard rule, reproduced verbatim from tr_treedb.c:

PUBLIC int treedb_link_nodes(...) {
    _link_nodes(gobj, tranger, hook_name, parent_node, child_node, FALSE);
    /*---Save persistent: Only children are saved---*/
    return treedb_save_node(tranger, child_node);   // ← only child
}

PUBLIC int treedb_unlink_nodes(...) {
    _unlink_nodes(gobj, tranger, hook_name, parent_node, child_node, FALSE);
    /*---Save persistent: Only children are saved---*/
    return treedb_save_node(tranger, child_node);   // ← only child
}

The rule: link/unlink writes the child to disk, never the parent. Why: the persistent reference lives on the child (the fkey field). The parent’s hook field is in-memory and gets rebuilt on the next load by scanning all children for fkey == parent.id.

Two consequences:

  1. After treedb_link_nodes, the child’s g_rowid advances by 1 (one new record appended). The parent’s g_rowid does not change.

  2. If you write tooling that snapshots state by reading rowids, the parent’s rowid is a bad signal of “has anything happened to this node’s relationships” — you have to look at the children too.

3.8 Cross-yuno reads: the rt_by_disk pattern

When a non-master yuno needs to read another yuno’s store, it opens the master’s database in read-only mode and registers an rt_by_disk watcher. The master, on every change, writes hardlinks into disks/<rt_id>/ for that subscriber. The subscriber’s filesystem watcher fires, and it re-reads the hardlinks.

Memory feedback_cross_yuno_via_store_not_command: in wattyzer (and by extension other multi-yuno SPAs), cross-yuno queries from the SPA go through db_history_wz reading B+ yunos’ stores non-master via this pattern. cmd_command_yuno does not work for B+ yunos, because they do not publish their service through __top_side__. The store path is the correct one.

Code: tranger2_open_rt_disk at timeranger2.h. The mechanism is purely filesystem-mediated — no socket between the master and the watchers.

3.9 Snapshots (treedb-level)

Snapshots tag a point in time across the treedb. APIs at tr_treedb.h:

int     treedb_shoot_snap   (json_t *tranger, const char *treedb_name,
                             const char *snap_name, const char *description);
int     treedb_activate_snap(json_t *tranger, const char *treedb_name,
                             const char *snap_name);
json_t *treedb_list_snaps   (json_t *tranger, const char *treedb_name,
                             json_t *jn_filter);

gobj_list_snaps(gobj, filter, src) is the gobj-level wrapper.

Snapshots are how the agent picks which binary version to run when multiple are stored — see YUNO_LIFECYCLE.md §4.3. The binary resolver tries the active snapshot first (gobj_list_snaps, c_agent.c). If that fails, it does a direct (role, role_version) lookup.

3.10 Immutable nodes and non-deletable topics

Some records must never be deleted by CRUD (the seed root role and yuneta user — see YUNO_AUTH.md §4.2), and some topics must never be dropped (the __system__ treedb’s structural topics, and every treedb’s __snaps__ / __graphs__). The protection is metadata, never a data column — it does not touch the user schema and never bumps topic_version. Design write-up: DESIGN-immutable-topics-records.md.

Record level rides a free md2 system_flag bit, sf_immutable_record (0x0800, inherited band) — the same metadata channel as the snapshot tag, persisted on disk and decoded on every load:

Topic level rides system_topic: true in the topic’s topic_var.json (additive, no topic_version bump). Declare it in the schema next to topic_version, or pass system_topic=TRUE to treedb_create_topic(). treedb_delete_topic() (and tranger2_delete_topic() as a backstop) refuse it. A system topic’s records stay deletable — only the topic is frozen.

Out of scope on purpose: delete-treedb / a whole-store rm -rf. This protects against CRUD/control-plane deletion, not against an operator wiping the realm — “only a full store wipe removes them”. Regression coverage: tests/c/tr_treedb_immutable.


4. Sharp edges

4.1 g_rowid and i_rowid are read-only to user code

(§2.3, §3.4.) Never set them in test fixtures, code that calls treedb_create_node, or anywhere else. timeranger2 computes them and shows them in __md_treedb__ for inspection only.

(§3.7.) If you read g_rowid on the parent after a link operation and it did not change, that is correct. Read the g_rowid of the child instead.

4.3 Schema changes need a higher topic_version

(§3.5.) A stale topic_cols.json overrides new code, and it gives no message. The trap is worse because the yuno still works. For treedb the new columns do not exist. Always raise the version.

4.4 Master-only writes

(§2.7.) tranger2_append_record does nothing on a non-master and returns -1. If you write in a yuno that is the non-master, you have a deployment bug: two yunos opened the same store.

4.5 timeranger2 is append-only — with two scoped deletes

(§2.9.) Nothing ever rewrites the .json data log itself. Appends go to the end, and nothing else changes. What is mutable is the .md2 index, and two delete primitives operate on it:

Both are master-only and irrecoverable. The append-only contract still holds at the data-log level — only the index is mutated.

4.6 No fsync after append

(§2.10.) Durability is what the OS gives you. For audit logs where a crash window of a few seconds is unacceptable, add an explicit fsync — but understand the throughput cost.

4.7 Do not open the same store twice in the same process

tranger2_startup caches by path. Two starts of the same path return the same tranger handle, but two distinct yunos in the same process trying to coexist on the same store is unsupported.

4.8 The deprecated range_ports/last_port columns on realms

(See REALMS.md §7.1.) Same class of trap as §3.5: columns that the schema still declares but the runtime ignores. Reading them returns stale data. Trust the agent’s own attrs, not the schema column.

4.9 Multiple node occurrences in dumps share one g_rowid

A node listed under topic.id_index[id] and also nested inside a parent’s hook array is the same record. They share the __md_treedb__.g_rowid. Do not count it twice when you compute stats from a dump.

4.10 Hooks rebuild on load — only fkeys persist

(§3.7.) In the database on disk you find the fkeys of the children but not the hooks of the parents. Hooks are in-memory pointers only, and treedb builds them again when it scans the children. This is why a corrupt fkey on a child makes the hook of its parent look short. Read the child first.

4.11 No raw malloc / free for treedb-allocated json_t

CLAUDE.md hard rule. gbmem_* everywhere. Jansson is routed through gbmem_*, so all json_* APIs are safe. Never free() a json_t yourself.

4.12 Do not cache a json_t * from treedb_get_node across a

restart

The pointer is valid for the life of the loaded tranger. After a tranger2_stop and tranger2_startup cycle the pointer is stale. If you keep references across stops, the framework does not detect it. Your crash does.

C_NODE publishes EV_TREEDB_NODE_LINKED / EV_TREEDB_NODE_UNLINKED only when its with_link_events attr is set (SDF_RD, default false). Two things bite here:


5. Recipes

5.1 Browse a topic from the CLI

yutils/c/ylist/ ships ylist for this. Without it, raw find + jq:

# every record in the realms topic (date-partitioned)
cat /yuneta/store/agent/treedb_yuneta_agent/realms/keys/*/*.json | jq .

# specific node
cat /yuneta/store/agent/treedb_yuneta_agent/yunos/keys/<id>/*.json | jq .

For machine-friendly access, prefer ycommand against the agent (list-yunos, list-realms, list-binaries, list-configs) — those go through the treedb’s in-memory state and apply schema correctly.

5.2 Add a new column to an existing topic

   "cols": {
       …
+      "my_new_field": { "type": "string", "flag": ["persistent"] }
   },
-  "topic_version": 19
+  "topic_version": 20

Without the topic_version bump the field will be silently ignored on load. With it, treedb migrates: every existing node gets the column with its default value on first save.

For a hot rollout in which you cannot restart the yunos:

  1. Update the schema file in source. Bump topic_version.

  2. Build + redeploy (see YUNO_LIFECYCLE.md §6.2).

  3. Verify the new field shows up:

    ycommand -c 'command-yuno id=<yuno> service=__yuno__ command=list-nodes topic=<topic>'

In C, inside an action or command handler:

json_t *node = gobj_create_node(
    gobj,
    "users",
    json_pack("{s:s, s:b}", "id", "alice", "disabled", 0),
    NULL,
    src
);

json_t *parent = gobj_get_node(gobj, "roles",
    json_pack("{s:s}", "id", "operator"), NULL, src);

gobj_link_nodes(gobj, "users",
    "roles", parent,
    "users", node,
    src);

// note: only `node` has been saved (the child with the fkey).
// `parent` is unchanged on disk.

5.4 Inspect snapshots

ycommand -c 'command-yuno id=<yuno> service=__yuno__ command=list-snaps'

Snapshots are global to a treedb. You see one entry per “tag”.

5.5 Recover from a botched schema change

# 1. stop the yuno that owns the store
ycommand -c 'kill-yuno id=<yuno>'

# 2. wipe the topic's data (do NOT do this in production — this is
#    for fresh-checkout / dev-loop recovery)
sudo rm -rf /yuneta/store/<realm>/<yuno>/treedb_<name>/<topic>/

# 3. restart — the topic is recreated from the schema
ycommand -c 'run-yuno id=<yuno>'

For production, do this against a backup. Never rm -rf a live store.

5.6 Read another yuno’s topic non-master (rt_by_disk)

Pseudocode in a different yuno that does NOT own the store:

json_t *tranger = tranger2_startup(gobj, json_pack(
    "{s:s, s:b}",
    "path",     "/yuneta/store/<other_yuno>",
    "master",   false
), yev_loop);

tranger2_open_rt_disk(
    tranger,
    "events",
    "*",                    // every key
    NULL,                   // no extra filter
    my_on_record_callback,
    "my_unique_rt_id",      // mandatory unique id
    gobj,
    NULL
);

The master will detect your disks/my_unique_rt_id/ directory and start writing hardlinks there on every change. Your callback fires as soon as the kernel notifies the filesystem watcher. No socket between the two yunos — pure inode plumbing.


6. Code pointers

WhatWhere
timeranger2 public APIkernel/c/timeranger2/src/timeranger2.h (747 lines)
timeranger2 runtimekernel/c/timeranger2/src/timeranger2.c (~7.8k lines)
md2_record_t (32-byte index)timeranger2.c
md2_record_ex_t (in-memory)timeranger2.h
system_flag2_t (sf_string_key, sf_int_key, …)timeranger2.h
Master / non-master locktimeranger2.c
tranger2_append_recordtimeranger2.c (g_rowid set at 2667, i_rowid at 2634)
tranger2_open_rt_disk (cross-yuno reads)timeranger2.h
TRACE_FS sitestimeranger2.c (multiple)
treedb public APIkernel/c/timeranger2/src/tr_treedb.h (617 lines)
treedb runtimekernel/c/timeranger2/src/tr_treedb.c (~8.9k lines)
__md_treedb__ buildertr_treedb.c
Topic schema loader (topic_cols.json)tr_treedb.c
topic_version matchingtr_treedb.c
treedb_link_nodes / treedb_unlink_nodestr_treedb.c (saves child only)
treedb_create/update/delete/get/list_node[s]tr_treedb.h, tr_treedb.c
Snapshot APItr_treedb.h
gobj wrappers (gobj_*node)gobj.h
gobj_list_snapsgobj.h
Canonical schemasyunos/c/yuno_agent/src/treedb_schema_yuneta_agent.c, kernel/c/root-linux/src/treedb_schema_authzs.c
Treedb gclass (gobj wrapper)kernel/c/root-linux/src/c_treedb.c, c_node.c