Changelog
Two layers:
- Releases (below) — version-anchored freeze records. Each
## [x.y.z]is the read-only snapshot of what shipped at that git tag: mature features frozen in, plus what was deliberately held back. Append a new section per tag; never rewrite a released one. - Architecture milestones (further down) — newest-first narrative of contract/architecture changes. Deeper context; not version-keyed.
Versioning rules live in AGENT.md §3. Freeze criteria in AGENT.md §5.
Releases
[0.2.0-rc.3] — 2026-07-10
Git tag v0.2.0-rc.3, branch release/v0.2.0. Engine 0.2.0rc3.
Third release candidate of the v0.2 line. Bug-fix-only delta over rc.2:
fixes two related sources of tx_log / IO noise from the inbound pipeline.
Fixed since rc.2:
- Store no-op suppression for identical writes —
Store.set_field,set_links,tag,untag,sync_identity,update_node,unset_field, andcreate_linknow short-circuit when the requested write would not change the persisted state. No database mutation, notx_logrow, noupdated_atbump, and no reactive event is emitted. This stops theobsidian-mirror/agent-sessionconnectors from generating millions of redundanttx_logrows during periodic full-scan polls (observed: 87 GBdata.dbfrom 49.9 Mtx_logrows in ~5 days). - State-machine self-transitions still emit
state-transitioned— the engine’s(transition …)primitive detects when the new state equals the current one and records the transition attempt directly intotx_logvia the Store’s publicaudit_eventAPI. The Store layer stays field-name-agnostic (no__state__carve-out inset_field); the engine owns the “this is a state transition” semantics and the reactive bus picks the audit event up through its existingstore.on_commitsubscription unchanged. SCMrecheck_quarter_after_*actions rely on this — without it, the reactiveclose_quarter_and_draft_nextcascade would never re-fire after a sprint closes or a relevant Thought is added. obsidian-markdownconnector skips unchanged files by mtime — the connector now persists a{path: mtime}baseline on theIngestConnectionnode (上次扫描mtime). Between pulls it only reads files whose mtime has changed. Combined with Store no-op suppression, this reduces the steady-state cost of a periodic vault scan from “parse every file every tick” to “stat every file every tick”.
Held back / unchanged from rc.2: 成熟功能表(CLAUDE.md §5),实验功能 隐藏(CLAUDE.md §7)。
Gates: pytest 1210 passed / 15 skipped / 1 xfailed, ruff clean, iso
runex init + runex ontology load + runex doctor 三步通。
[0.2.0-rc.2] — 2026-07-10
Git tag v0.2.0-rc.2, branch release/v0.2.0. Engine 0.2.0rc2.
Second release candidate of the v0.2 line. Replaces rc.1; the rc.1 → rc.2 delta is small and bug-fix-only.
Fixed since rc.1:
- install.sh:
while IFS= read丢最后一行 —extensions/CORE_SKILLS原文件没有 trailing newline,bash 的while read会默默丢掉最后一行 结果RUNEX_SKILLS永远是空。修法:- CORE_SKILLS 加 trailing newline
- install.sh 改用
while read ... || [ -n "$line" ]防御性写法
- ontology: validate supertag refs on SCM load —
load_ontology_file现在校验每个 machine 的:supertag是否已注册;未注册抛LoaderError而非等到 dispatch 时才崩。dispatch也把ValueError归类为unregistered_supertag(PR #d18ac24,Windows 端报的 50+ 测试错误的 根因) - build 脚本单源 version —
scripts/runex/{build.sh,build.ps1}现在从src/runex/__init__.py读__version__,按 Nuitka 4-part 格式转换(0.2.0rc2→0.2.0.2)。runex --version与CFBundleShortVersionString同步,不会再因为 hardcoded 错位 - help URL 更新 —
runex.0ma.ai→0ma.ai(文档站改路径) __init__.py简化 — 删importlib.metadata.versionfallback (wheel metadata 是 stale 的,曾导致0.3.0.dev1残留覆盖 source)
Held back / unchanged from rc.1: 成熟功能表(CLAUDE.md §5),实验功能 隐藏(CLAUDE.md §7)。
Gates: pytest 1201 passed / 15 skipped / 1 xfailed, ruff clean, iso
runex init + runex ontology load + runex doctor 三步通。
[0.2.0-rc.1] — 2026-07-09
Git tag v0.2.0-rc.1, branch release/v0.2.0. Engine 0.2.0rc1.
First release candidate of the v0.2 line. Superseded by rc.2 — has the trailing-newline bug fixed in rc.2. Not the source of truth; historical record.
[0.2.0-canary.1] — 2026-07-09
Git tag v0.2.0-canary.1, branch dev. Engine 0.2.0.dev1.
First canary of the v0.2 line. Aggregated everything from v0.2.0-alpha forward: Windows build, obsidian-mirror Recipe, agent-session / obsidian-assistant / autoagency packs, runex skill 重设计, ADR-0018 引擎最小底座。Used for internal testing only; bugs expected.
[0.2.0-alpha] — 2026-06-26
First frozen tag of the v0.2 milestone (git tag v0.2.0-alpha, branched from the
Phase-6-hardened commit). The contents below accurately describe this tag.
Not released as a user-facing version. Stable for CI and architecture review; no installer published. Next: Phase 9 (agent-mesh, ADR-0012) + real Agent Provider.
[0.2.0] — 2026-06-25
Proactive Agent Runtime. Ontology state can now durably dispatch slow Agent/tool work, survive restart, request human approval, and evolve its own rules — all without weakening the synchronous deterministic ReactiveBus. Slow, durable work lives permanently outside the bus (ADR-0010); every new durable concept is an ordinary node, no new L1 table.
Shipped (Phase 1–6, all frozen mature):
- Obsidian ↔ Ontology human mirror (Phase 1). Runnable Connector / Recipe /
IngestJob seam; the
obsidian-markdownConnector + no-I/Oontology-frontmatterRecipe; safe Frontmatter/body patch with checksum baseline +Conflictnode instead of silent overwrite; cursor-based Store observation that schedules projection (ADR-0008/0011). - Durable work protocol (Phase 2).
WorkItemas a Supertag + Machine + Actions — idempotentsignalkey, attempts, availability, claim owner, lease, retry/backoff, and expired-claim recovery. A killed worker restarts and finishes exactly once at the business-effect level. - Worker host + Agent contract (Phase 3).
runex ontology listencomposes ingest + durable work + projection in one supervised loop with graceful shutdown and a persisted cursor. The Agent contract: bounded envelope, fresh reference-context load, manifest-validatedCommandBatchoutput, provenance, and a deterministicFakeAgentfor CI (no model in tests). - Proposal & human approval (Phase 4). Generic
Proposallifecycle (pending_review → approved/rejected/revision_requested/expired/cancelled). Approval is a later graph write (a Frontmatter/CLI field), never a waiting thread; approving idempotently upserts the real object by natural key, rejecting creates none. - First proactive reflex arc (Phase 5). Obsidian Thought → WorkItem →
Agent extraction → Task Proposal → Obsidian approval → real Task → Obsidian
projection, proven from a clean
RUNEX_HOMEwith a crash injected during work (recovers exactly once), no echo loop, complete provenance. Markdown sink now renderstype:so machine-born nodes round-trip back in. - Safe natural-language rule evolution (Phase 6).
BusinessRuledraft → compiled → verified → approved → deployed (→ rolled_back). A Compiler Agent only produces a.scmdraft; the verifier statically checks it against the live manifest + a machine-readable DSL primitive catalog (runex ontology primitives), writing structured diagnostics; an approved rule is the only thing that loads into production. No Agent has direct deploy authority.
New CLI: ontology listen, ontology proposals / proposal-approve /
proposal-reject, ontology rules / rule-approve, ontology primitives.
Held back / not in this release:
- A real-Agent dogfood is an operator-run manual gate; CI uses
FakeAgent/FakeCompilerthroughout. - Phase 7–8 (Assertion / Conflict / Current-Truth; Artifact & external collaboration) remain post-v0.2 candidates.
- Legacy non-Recipe preset adapters (
obsidian-thought,wechat-session,media-snapshot, nocodb) are no longer on the evolution path; the stable ingest path is theobsidian-markdownConnector +ontology-frontmatterRecipe.packs/media.scm/packs/session.scm/intel-research.scmstay dev-only fixtures.
Gates: full pytest green (682 pass, 2 skip), ontology check --strict exit 0,
ReactiveBus unchanged (no async/threading).
[0.1.1] — 2026-06-24
清理 + 文档发布(PATCH,无新功能、无 schema 变更)。
- 撤出误进 wheel 的实验 pack:
intel-research.scm从src/runex/skill/ ontology/packs/移到tests/fixtures/,不再随包分发(0.1.0 曾误打进 wheel)。media.scm/session.scm同处 fixtures,均开发中。 runex ingest标记为实验功能:docstring 注明 adapter / 参数 / 命令行 入口未来可能变更或移除,勿在稳定流程依赖。- 安装文档改进:README +
references/installation.md首推”交给 agent 代装”,手动路径主推uv tool install(自动 bootstrap Python)。 - 修正
skill-collection-roadmap.md与代码现实矛盾(拆分已落地)。 - changelog 引入版本号锚定的冻结层;AGENT.md §7 固化该流程。
Held back 不变:见 [0.1.0]。
[0.1.0] — 2026-06-24
First distributable release. Frozen mature surface:
| 模块 | 内容 |
|---|---|
| core-memory.scm | 17 通用类型 · 10 机器 · 36 action |
| packs/knowledge.scm | Atom / Topic / HowTo / MOC |
| packs/time.scm | Thread(跨周期工作线,closure-aware scheduling) |
| packs/wiki-links.scm | body [[..]] 自建 mentions / 提炼自 边 |
| skills | capture · agenda · sprint-planning · distill · topic · moc |
| ref facade | 所有 ref 字段 CLI 可写(名称 → typed edge,ADR-0009) |
| runex init / skill install | 一键建库 + 加载 ontology + 装 skill |
| host cron | close_sprint / close_goal(scheduling 闭环) |
| kernel | derived-node 删除传播 · guard 自省 · dry-run preview · blocking-kernel lint |
Held back (experimental, dev-only — not in this freeze): runex ingest
入口及其 adapter(obsidian-thought / wechat-session / media-snapshot);
packs/media.scm、packs/session.scm、intel-research.scm(均在
tests/fixtures/,开发中)。下个版本发布前再确认去留。
Architecture milestones
Newest first. Each entry covers what changed at the contract and architecture level; implementation detail lives in the commit history.
2026-08-05 — personal-assistant pack:闭环行动 + 会话导入合并 + Pi 支持
New pack. extensions/packs/personal-assistant/ 把「无需 Obsidian 的闭环行动
体验」(know-me / capture-now / plan-today / advance-work / review-week +
onboard + unified-ontology-compile)与「历史会话导入」合为一个包;原
agent-session pack 并入其中(连接器仍在引擎层)。
What changed.
- 新增
PiSessionsupertag(extends LLMSession)与pi-sessionConnectorpi-session-mirrorRecipe(读取~/.pi/agent/sessions/<编码cwd>/*.jsonl, 目录编码与 Claude Code 一致);session-onboard 从 3 平台扩到 4 平台 (Claude Code / Codex CLI / OpenCode / Pi)。
- 会话类型在图中可被 know-me / advance-work 感知:「认识下我」先读最近会话, 「推进X」先跨会话检索未完成事项(跨 Agent 续接)。
- 安装器在 runex home 注入
AGENTS.md(CLAUDE.md软链)+ 部署 8 个项目级 skill + 按序加载 memory → knowledge → session → personal-core。 - 移除
extensions/packs/agent-session/(其连接器claude/codex/opencode _session_connector与三个 mirror Recipe 保留在引擎层,由 personal-assistant 的 session.scm 提供服务)。
边界。 新版 GPT 应用(含 Codex 功能)的会话在云端、无本地文件,无法导入;
Codex 仅指 CLI 版(~/.codex/sessions)。
Contract change. Until now runex was published as a pip install runex
/ uv tool install runex Python wheel. The engine binary that downstream
products shell out to was resolved through that Python-package install.
As of this entry, runex ships exclusively as a Nuitka-compiled onefile
Mach-O inside runex-macos-arm64.zip — src/runex/ is build-time
source-of-truth, no longer a Python-installable artifact.
What changed.
scripts/runex/build.shandscripts/runex/build-release.shproducedist/runex-macos-arm64/{runex, install.sh, README.txt}and pack them intodist/runex-macos-arm64.zip. The Mach-O is self-contained (Python runtime + system SCMs + SQLite DDL baked in via--include-data-dir=).scripts/runex/install.shsymlinks the binary into~/.local/bin/runex;--uninstallremoves the symlink only.- The
[tool.hatch.build.targets.wheel]block inpyproject.tomlis annotated as a dev-only convenience: it still resolves a Python module for in-tree tests, but production consumers take the binary from the release zip. autoagency(runexfab/build-release.sh) now acceptsRUNEX_BINARY_ZIPto pull the engine Mach-O from a release zip instead of the legacysrc/runex/mirror.
Why now. Concrete pain: integrating two agents across pip install
uv tool installwas a constant footgun (the wrongrunexPATH entry got picked up; the source-treesrc/runex/import shadowed the binary; CI tests couldn’t tell which one they were exercising). The onefile binary has a single concrete location, no Python on the host, and trivial install symmetry — every consumer sees the same~/.local/bin/runex.
Platforms. Only macos-arm64 is built today. Cross-platform packaging
is a future addition; the build script has the platform check at the
top — adding linux-x86_64 is a matter of an analogous build-linux.sh
that pipes through the same Nuitka invocation.
Migration path. Operators with an existing uv tool install runex
can uv tool uninstall runex then ./install.sh from the unzipped
runex-macos-arm64/. The CLI surface (runex commands) is unchanged —
same command names, same --help output, same options. Anything
sub-command set changed is recorded in the v0.2.0-alpha → next tag
release notes.
Held back / not in this entry.
src/runex/skill/references/content stays as-is for now; the move tobundled/continues under separate doc-side rewrites (see the 2026-07-05 bundled/ refactor entry).- autoagency-side runex mirror (
src/runex/) is git-ignored but kept on disk — see.gitignoreentry for the next-commit removal. The binary is now authoritative; the source mirror is only for archaeology and diff inspection. - RunEX skill bundling for the engine binary itself (
runex skill installfinding the SKILLs from inside the zip) is a follow-up — currentrunex skill installstill walks the source tree.
2026-06-30 — Thread-local Store isolation for multi-threaded access
The existing single-writer Store model was thread-unsafe: a multi-threaded context (e.g., a worker host running ingest and work concurrently) could interleave writes, corrupt the transaction log, or observe each other’s uncommitted state.
What changed. Store gains an isolated() context manager that clones the
underlying SQLite connection per thread. Each thread gets its own ThreadLocalStore
— a private connection with independent transaction state and cursor. Event
producers emit through the original connection’s on_commit hook, so the
reactive bus still sees a single serialised event stream.
Invariant preserved. The ReactiveBus remains single-threaded and
deterministic within each effect scope. The multi-threaded boundary is
ingest-side only: parallel Connector reads, not parallel reactive dispatch.
ADR-0014’s effect_scope() contract (one bus, one thread per cascade) is
unchanged; the thread-local layer applies only to the Store→L2 boundary, not
to L3 dispatch.
Gates: pytest -q green; no change to effect_scope() or reactive ordering.
2026-06-29 — ADR-0014: Manual-effect scope, cascade deferral, and ADR-0015 dual-entry implicit scoping
A bug surfaced where a manual action’s effect block could trigger mid-effect reactive cascades before the effect finished, causing guards to see inconsistent intermediate state. This violated the principle that a reactive rule sees a consistent final snapshot.
What changed (ADR-0014, Accepted). Reactive cascade is now deferred until
the manual effect block completes. The bus flushes accumulated events in one
batch at the end of effect_scope(), guaranteeing that any guard sees the
complete final __state__* and field values of the triggering mutation. Three
mechanisms deliver this:
- M1 — Deferred cascade.
bus.effect_scope()collects store events during thewith:block and dispatches matched actions only on exit. - M2 — Forced LWW dedup. Multiple writes to the same literal field within
one scope are reduced to the last value before reactive dispatch, eliminating
self-churn from sequential
set-fieldcalls mid-effect. - M3 — Net-zero short-circuit. If the final field value equals the
pre-scope value, no
field-setevent is emitted at all — preventing no-op cascades from restarting.
ADR-0015 (Implicit Effect Scope — Dual-Entry Design) extends ADR-0014 with
the effect_scope() context-manager API and two entry paths:
- Scheme A (primary):
Engine.dispatchopenseffect_scope()at the call boundary — every manual dispatch is implicitly scoped without explicitwith:in the DSL. - Scheme B (secondary): The DSL evaluator opens
effect_scope()on any bare(begin …)block — covers code executed outsidedispatch(e.g., kernel callbacks, test harnesses).
M5 enforces single-effect transactional atomicity: if any step in the effect
block raises, the entire scope rolls back via store SAVEPOINT/ROLLBACK.
M6 nests correctly — events from enclosing scopes survive even if an inner
scope rolls back.
ADR-0015 is referenced but the ADR file is pending; the implemented behaviour is documented in ADR-0014’s “M5/M6 + Dual-Entry Design” sections.
Gates: pytest -q green; guard introspection traces unchanged; M3 net-zero
verified by CascadeSpy assertions; M5 rollback verified with
store.atomic(); effect_scope() is the only public entry to the reactive
flush machinery.
2026-06-29 — ADR-0013: Vault schema distribution from manifest (Proposed)
ADR-0013 (Proposed) addresses a gap left by ADR-0011: the human-sovereign
Obsidian mirror has no machine-readable contract for frontmatter shapes.
When an agent creates a note, it has no structural guarantee about required
fields, types, or valid ref targets for a given supertag.
The proposed direction: manifest() as the single schema authority —
agents and skills read frontmatter shapes from it; runex vault schema
generates Obsidian templates and .base (Bases) files from the live
ontology; no second source of schema truth exists.
Not yet implemented; the ADR is in Proposed state. A companion PRD
(tasks/prd-vault-registry-distribution.md) details the opinionated
default vault path (~/.runex/runex-vault/) and the template/Base
generation surface.
2026-06 — Documentation information architecture: one source of truth per concern
The planning docs had drifted into overlap: docs/roadmap.md duplicated the
phase breakdown that lives in tasks/, mirrored changelog.md (it carried
large amounts of done material), and absorbed realized-architecture essays
that belong with the invariants. “Who is SoT?” had no clear answer.
The assignment now is explicit:
tasks/is the planning SoT —prd-*.md(requirements + acceptance, with requirement IDs) andtasks-*.md(the execution checklist dev is driven from). Promoted to a first-class entry in thedocs/map; a newtasks/README.mdstates the convention and distinguishes the active milestone pair from the historical v0.1-era coverage PRDs.changelog.mdis the history SoT (this file).architecture.mdis the current-state SoT. It absorbed roadmap’s realized “channels as extensions” thesis: the capability/channel seam’s executable criterion (thetest_capability_seam.pygrep gate), a new “Hot-discovery and the trust boundary” subsection, strengthened Invariant 2 (“no channel names in core”), and a new Invariant 12 (the extension-directory trust principal).roadmap.mdis now future-only (~150 lines, down from ~560): the shipped v0.2.0 summary, the post-v0.2 Phase 7/8/9 direction, and open architectural positions. It no longer restates phase detail, done-tracking, or invariants; it points at the SoT for each.
No code behavior changed; stale docs/roadmap.md pointers in source/tests were
repointed to architecture.md, and one obsolete “guard introspection is not yet
diagnosable” note was corrected (it shipped).
2026-06 — Derived-node lifecycle: source deletions now propagate to the Store
FileWatcher previously handled two file events: create (new node) and modify
(re-ingest → update). The third event — deletion — was silently ignored, leaving
stale ghost nodes in the Store whenever a source file was removed.
What changed. FileWatcher now runs a deletion sweep on every poll tick.
After scanning the current file set, it compares against the previously-ingested
snapshot. For each file that has disappeared, it looks up Store nodes whose
来源路径 field matches the path and deletes them. The lookup is implemented via
store.find_nodes_by_field_value(field_name, value), a new cross-supertag field
query that does not require knowing the supertag name.
The invariant this establishes. Derived nodes — those ingested from an external source file — now have a complete and symmetric lifecycle: they are created when their source appears, updated when their source changes, and deleted when their source disappears. The Store reflects the external source’s existence state without any manual reconciliation step.
Native nodes are structurally untouched. Nodes created inside runex
(via create-node DSL effect, direct CLI, or reactive synthesis) carry no
来源路径 field and are therefore unreachable by the deletion sweep. The
two-mode contract — derived nodes are a disposable cache, native nodes live
in the Store permanently — is now enforced by lifecycle behaviour, not just
convention.
Opt-out. FileWatcher(…, source_path_field=None) disables deletion
propagation for adapters that do not write a path field (or where ghost
retention is explicitly desired). The default remains "来源路径".
2026-05 — Dogfooding round: guard introspection, dry-run preview, blocking-kernel lint
Three usability gaps surfaced during real SCM scenario dogfood and closed.
Guard expression introspection. When dispatch() or the reactive bus
rejects an action because its guard returned false, the result now carries a
structured guard_trace list. Each entry names the failing sub-expression, the
value it produced at runtime, and a note (e.g. "always-false literal" or the
evaluated value of (field "client")). evaluate_guard_trace re-walks the
guard AST after the fact — safe because guards are pure — and pinpoints the
sub-expression responsible. Before: guard_rejected gave the action name and
nothing else; an agent had to know engine internals to root-cause a silent
rejection. After: the trace is machine-readable and surfaces directly in
dispatch() Result.data and in the reactive_guard_rejected event payload.
Dry-run preview — ontology preview. runex ontology preview <action> <node-id> evaluates the full guard+effect+reactive cascade and returns the
event list, but commits nothing. Implementation: Store._txn was changed from
BEGIN/COMMIT to SAVEPOINT _sp / RELEASE (outer behavior identical, now
nestable); facade.preview() wraps dispatch in a SAVEPOINT _preview / ROLLBACK TO SAVEPOINT _preview, capturing cascade events before the rollback.
Result carries data.dry_run=true, data.ran, the full events list, and
guard_trace on rejection. The failure mode that motivated this: agents ran
ontology trace (which commits) as a preview step, consuming state transitions;
when the user later said “execute”, the engine returned IllegalTransitionError
because the states were already consumed.
Blocking-kernel lint — register_kernel(..., blocking=True) + ontology check. The architectural decision: do not make the reactive bus async.
Async dispatch introduces thread-local hazards (_depth counter is mutable
instance state) and does not fix the real problem — non-idempotent writes
(create-node) inside an action effect would race on concurrent bus wakeups.
The root cause is action design, not bus threading. What shipped instead:
register_kernel(name, fn, *, blocking=False)— opt-in flag marks kernels that perform heavy I/O (subprocess, network, ASR).engine.blocking_kernels: set[str]— maintained alongsideengine.kernels.list_kernels()andmanifest()return[{"name": k, "blocking": bool}]dicts (previously plain name strings — agents can now read the flag before authoring an action).analyze_blocking_kernels()— static AST walk over every action’seffect_steps, flagging(call-kernel "NAME" ...)calls whereNAMEis inblocking_kernels. Returns structured{warnings, clean}.ontology checknow runs both conflict analysis and blocking-kernel lint, printingWARNINGlines with the Signal-Then-Work pointer.--strictexits 1 on any blocking-kernel warning (in addition to the existingerror-on-conflictgate).extension-authoring.mddocuments the Signal-Then-Work pattern: action effect writes a status field only (fast, atomic); external daemon detects pending status and runs the blocking kernel; a second reactive action reacts to the result field.
The thought_auto_transcribe action in runex-product is the canonical
example of what the lint now catches. Its redesign into three-phase shape
(detect → daemon runs ASR → analyze) is tracked as a product-side task.
2026-05 — ADR directory added for durable architecture decisions
The docs now include a dedicated ADR directory:
adr/README.md.
The first accepted records capture decisions that had become important enough to stop re-arguing informally:
adr-0001-agent-native-ontology-runtime.mdadr-0002-capability-vs-channel-boundary.mdadr-0003-machine-first-contract.mdadr-0004-canonicalitem-bidirectional-boundary.mdadr-0005-reactive-semantics-over-imperative-orchestration.mdadr-0006-runtime-loadable-ontology-over-python-registered-logic.mdadr-0007-python-as-host-and-extension-interface.md
These ADRs complement, rather than replace:
architecture.mdfor the current structureroadmap.mdfor future directionwhitepaper.mdfor the long-form category thesis
2026-05 — White paper added for the technical vision
The repo now carries a long-form white paper:
whitepaper.md.
It frames runex not as “just a protocol” or “just an app”, but as an agent-native ontology runtime: a shared operating substrate for agents, human-facing shells, and external systems. The paper is problem-led rather than product-led: why current screen-centric apps, tool-calling agents, and workflow graphs leave a missing semantic runtime layer; why ontology, eventfulness, and runtime-malleable rules matter; and why channels, typed boundaries, and machine-first contracts are central to the category.
2026-05 — Repository-local Obsidian + NocoDB e2e workflow landed
The end-to-end validation loop is now repository-owned rather than an oral procedure.
What changed. The repo now carries a sanitized but structurally real
Obsidian vault fixture, a disposable local NocoDB environment, a
bootstrap that creates the 自媒体作品快照 / 作品快照 schema and seed
rows, and one formal suite proving both source-read and writeback paths.
A single script — scripts/run-local-e2e.sh —
starts the service, bootstraps it, loads the generated env, and runs the
suite.
Why this matters. The capability/channel architecture is no longer validated only by unit tests, stub-at-seam tests, or one-off production dogfood. The repository now contains a repeatable, safer-than-production business-loop check for:
Obsidian Thought -> ingest- reactive bilibili detection
Bookmarkcreation- NocoDB writeback through the real sink/source adapters
What is proven. OrbStack headless was verified as a working local Docker-compatible runtime for this environment, and the formal suite now passes against the local NocoDB service.
2026-05 — Extension authoring contract documented
The capability/channel architecture now has its Python-side authoring
guide. extension-authoring.md documents the
runtime contract for:
DataSourceSpecsource extensionsSinkSpecsink extensionsKERNELSextension modules
It makes the hot-discovery shape operational for extension authors: directory layout, discovery semantics, capability injection, spec-vs- configured-sink distinction, and the expected testing layers from unit tests through e2e ontology wiring.
2026-05 — Channel extension hot-discovery completed
The last open P1 from the capability/channel architecture landed: the registry is now hot-pluggable in both directions, not just for kernels.
What changed. At startup, runex now scans
~/.runex/extensions/sources/*.py and .../sinks/*.py in addition to
.../kernels/*.py. A module-level SPEC: DataSourceSpec or
SPEC: SinkSpec is merged into the lazy registry automatically, so a new
channel can appear in manifest() and CLI registry lookups without editing
the core module list.
Why the implementation moved. Discovery helpers were extracted to a
top-level runex.extensions module. The registry can depend on this
without importing the ontology package and re-entering facade startup, so
the extension path stays cycle-safe.
What is proven. tests/test_extension_discovery.py now covers all three
extension buckets: missing dirs are a silent no-op; kernel discovery still
works; source/sink extensions appear in manifest().data.datasources /
manifest().data.sinks once dropped into the extension dir.
2026-05 — Convergence determinism (4 gaps closed)
Four correctness gaps in the reactive bus were identified and closed behind regression tests. All are observable through the typed event log.
Gap 1 — Ordering enforced at dispatch, not at load. priority ASC, name ASC is now applied inside _on_commit at the point a matched group is
formed, not only when actions are loaded. Reloading actions in a different
order no longer changes dispatch outcome.
Gap 2 — error-on-conflict fails closed on opaque writes. An
error-on-conflict action whose derived field target is not a literal (kernel
result, variable — anything static analysis cannot resolve) is now blocked
(opaque_blocked) rather than passed through. The guarantee it asserted
cannot be proven, so refusal is correct.
Gap 3 — Cross-event same-field conflicts surfaced statically. Two actions
on different triggers that write the same field never share a dispatch plan.
analyze_latent_conflicts() / manifest().latent_conflicts /
runex ontology check now run a whole-ontology static pass and classify
findings as blocking (an error-on-conflict field is co-written elsewhere)
or advisory. Catch it at design time; the append-only tx_log cannot
un-write a prior cascade hop.
Gap 4 — Lint gate is real. ruff check now runs in the test suite
(tests/test_lint_gate.py). Pre-existing violations were fixed. The
datasource capability registry (adapters/registry.py) landed as part of
this milestone: adapters self-describe via SPEC: DataSourceSpec,
manifest().data.datasources exposes the registry as structured data.
2026-05 — Contract leap (Phases 0–5)
Rebuilt the L4 interface as a machine-first contract. Previously the facade returned heterogeneous shapes; CLI output was Rich prose; failure was sometimes a raised exception and sometimes a returned dict depending on which operation was called.
Result envelope. Every operation — load, dispatch, manifest,
events, analyze_conflicts — now returns {ok, data, error, events, cursor}. ok:false carries a stable error.code (branch on this; never
regex message). The CLI --json flag emits this envelope verbatim; exit
code mirrors ok.
Closed Event taxonomy + actor provenance invariant. event.kind is drawn
from a closed set, readable from manifest().data.event_kinds. Actor field
carries one invariant: reactive:<action> ⟺ that action’s effect committed;
system:reactive ⟺ a decision event (guard rejected, blocked conflict,
illegal transition). Nothing is swallowed.
manifest() as single introspection surface. One call returns the entire
introspectable surface: supertags, machines, actions, kernels, datasources,
event_kinds, and known latent_conflicts. An agent reads what is possible from
this one structured call before acting — never from documentation or source.
Event stream with cursor. events(since, limit, kinds) is
cross-process-safe and resumable. cursor is the scanned-window tail — it
advances even when a kind filter matches nothing, so a follower on a rare kind
never wedges. runex ontology events --follow is a safe cross-process
tail.
Datasource registry. Adapters self-describe via SPEC: DataSourceSpec.
manifest().data.datasources exposes the registry; DataSourceSpec.build(ctx)
validates required params before construction with a structured error naming
what is missing.
analyze_conflicts() pre-flight. Returns {field_conflicts, opaque_guarded_actions, blocking_count, clean}. runex ontology check --strict exits 1 on blocking_count > 0 — drop it in CI as a design gate.
Policy-A migration gate. upsert_supertag now enforces: additive schema
changes always accepted at runtime; destructive changes refused when nodes
exist; byte-identical re-apply is a true no-op (no event emitted, no churn on
repeated load).
2026-05 — Data-driven ontology layer
The foundational architectural shift: state machines and actions became declarative data, not Python code.
Before. State machines were Python @register Action subclasses. Adding
a new lifecycle required a code edit, restart, and deploy. Agents could not
synthesize behavior at runtime. Cascade behavior was imperative call-site
coupling.
After. A machine is a node tagged MachineDefinition; an action is a
node tagged ActionDefinition. Both are written in a small Scheme-like DSL
(s-expressions) stored as longtext fields. A reactive bus subscribes to store
mutations and auto-dispatches matching actions.
Shipped in 8 phases: DSL kernel (parser + evaluator) → internal supertags +
store_io → Engine + Kernel registry → Bookmark machine as data (first
dogfood) → Thought machine + cross-node writes → retire old Python framework
→ ReactiveBus + Store.on_commit → wiki-link extraction as a reactive action
→ Ontology facade + CLI + agent guide → pipeline + reactive ingest e2e.
Outcome. A single upsert_node_from_item() call produces a
fully-processed graph — tagged, fielded, wiki-linked, triaged, zettelized —
with zero orchestration code in the pipeline. Behavior changes by editing
.scm files; no Python touched, no restart required.
Migration path. from runex.ontology import register, run_by_name is
gone; use Engine.dispatch or Ontology.dispatch. Machines and wiki
extraction are now .scm in config/ontology/. The old Python framework
classes are deleted. store.resolve_wiki_links() and
store.sync_all_wiki_links() are gone; wiki extraction is reactive and
automatic once wiki_links.scm is loaded.