Skip to content

Latest commit

 

History

History
1045 lines (754 loc) · 69.1 KB

File metadata and controls

1045 lines (754 loc) · 69.1 KB

Changelog

Note: Versions 0.3.26 – 0.3.55 were released as git tags without changelog entries. Changelog resumes at 0.3.56 below.

0.12.1 (2026-08-18)

Fixed

  • A rooted deployment keeps its identity, keys, stores and lock inside its own root. Setting SYM_STATE_DIR already moved a node's memory; it did not move the tree that holds each node's identity, keypair and single-writer lock, which stayed at ~/.sym/nodes for every deployment on the machine. Two independent deployments on one host — each with its own SYM_STATE_DIR — therefore shared one identity tree, and the second to start was refused the lock the first held, while appearing healthy in every other respect. The identity tree, the daemon socket and the logs now follow SYM_STATE_DIR too. Nothing moves for a deployment that sets nothing: ~/.sym remains the default, in the same place, with the same contents.

0.12.0 (2026-08-17)

Added

  • The runtime is now self-sufficient — everything a node needs to think is in this package. Semantic category encoding and SVAF evaluation, which previously lived in the separate @sym-bot/core package, are part of the open runtime. A node can create, sign, exchange, verify, evaluate, admit and store records with no additional engine installed, which is what makes this package a complete, independently usable implementation of MMP 2.0 rather than the open half of a pair.

  • Every release is gated on two stock nodes. Before publishing, the packed tarball is installed into an empty directory and two plain nodes are driven through the entire path — create, sign, exchange, verify, evaluate, admit, store with lineage — and then one is restarted and the record must still be there, with the signed record of why it was admitted. The gate runs against the artifact you install, not the source tree, because those are not the same thing.

Changed

  • Nothing existing behaves differently. The absorbed code is additive: where both lineages had an implementation, this package keeps its own — measured, not assumed, by the signing and interoperability conformance suites, which decide any disagreement. Existing nodes upgrade with no change to the wire, to record addresses, or to stored data.

0.11.3 (2026-08-13)

Added

  • Reads the published MMP v2.0 record signature suite. A record signed under mmp-sig-v2.0 now verifies against the published preimage, so a v2.0 record from an independent implementation interoperates with this one. Signature verification, the end-to-end category encryption, the handshake proof-of- possession, and the session key schedule all match the published v2.0 spec byte-for-byte. Records signed under the previous suite continue to verify unchanged — this release reads v2.0; it still emits the previous suite, so nothing on the wire changes for peers on earlier versions. Emission moves to v2.0 in a later release, once v2.0 readers are widely deployed.

  • Verification receipt for verified-record consumers. After this node verifies a v2.0 record it can emit a compact receipt bound to the exact record bytes, so a downstream consumer can admit the record without re-verifying it and detect any mutation between verification and use.

Changed

  • Lineage is walked from verified local records only. A record's provenance is resolved by traversing parents this node has actually stored, never a sender-supplied ancestor list — a non-conforming peer can no longer inject an apparent root. Admission freshness is derived from the signed timestamp of a verified record rather than a transport field.

0.10.2 (2026-08-04)

Fixed

  • The delivery inbox survives a session restart. Communication is addressed to the NODE, and a new session relinks to it — including what was delivered while no session was attached. The inbox was process memory only, so a restart silently wiped the delivery feed while every sender believed it had delivered (observed live: four gate requests, and separately five broadcasts, vanished into restarted peers that showed as live on Bonjour throughout). Ring, sequence and drain cursor now persist together in the node directory: messages without the cursor would replay what was already drained; the cursor without the messages would silently skip the backlog. A missing or corrupt inbox file starts fresh, exactly like the old behaviour.

0.10.1 (2026-08-01)

  • Pinned to @sym-bot/core 0.7.0. Shadow samples are now persisted per node rather than reduced to counts in a log line — the receive path writes one row per admission, carrying the receiving node and per-field values. No behaviour change to admission.

0.10.0 (2026-08-01)

  • Pinned to @sym-bot/core 0.6.0. Adds an observe-only admission diagnostic on the receive path: computed alongside the five-valued band on every admission, logged beside it, and deciding nothing. No peer can observe it — it never enters the signed attestation payload — so nothing on the mesh can come to depend on its behaviour.
  • feat: svafRedundancyThreshold node option. Every other SVAF threshold has been settable for a long time; the redundancy floor was not settable anywhere, and it is the whole of the redundancy cut. Deliberately carries no default here — unset, core applies its own, so the value keeps exactly one home across the two packages.
  • ⚠ Note on that option: while C is derived from the five-valued band, C's floor is the acting gate's floor. Setting svafRedundancyThreshold therefore changes what the node admits, not just what the shadow records. Treat it as a production gating change until C becomes an independent cut.

0.9.0 (2026-08-01)

Entry written retroactively on 2026-08-01. This version and 0.8.0 were published without a changelog entry or a git tag; both tags were created after the fact at their own release commits.

  • Pinned to @sym-bot/core 0.5.0 — the boundary record model. Two-section records, content-only addressing, per-field keys, and signatures that verify against the author's key rather than the delivering peer's.
  • Every consumer read migrated. Authorship is resolved from the signed author field; an unattributed block is skipped rather than published under an invented name.
  • Pre-boundary history stays readable — older blocks carried as unverified-legacy, not refused.

0.8.0 (2026-07-31)

Entry written retroactively on 2026-08-01 — see the note above.

  • cmb-only cutover. v1 key derivation throughout; pinned to @sym-bot/core 0.4.0.

0.7.30 (2026-07-07)

  • fix (cross-peer grounding): remember() with parents now mints the REMIX-scheme cmb1- key (§8.2.1 role dispatch). Previously a lineage-bearing authored CMB carried a root-scheme key, failed the receiver's content re-verification, and was hard-rejected as forged — agent-authored grounding CMBs silently never landed on any peer. Two-node regression test included.
  • feat (sym emit + sym/emit): MMP Class 1 Emitter (§17.1) — one-shot, signed CAT7 emission to a remote mesh node with the emitter's own persistent identity. No daemon, no store, no identity lock. CLI: sym emit --server <host:port> [--group] [--name] [--to] [--parents] '{...}'; programmatic: require('sym').emitemitOnce() / connect(). LAN TCP; relay emission lands with one-shot E2E (§18.2.1). Real-TCP e2e tests.
  • deps: @sym-bot/core ^0.3.48 (tether attestations, recomputeKey export, retroactive-audit evaluation, conformance vectors + schemas).

0.7.27 — 2026-07-01

Fixed

  • Stop a cross-node echo/replay storm — own-only anchors + reload-durable dedup. Two in-memory safeguards that a plugin reload or process restart wiped, plus a missing origin filter, let already-seen CMBs re-circulate across a multi-node mesh:
    • Origin filter on anchor exchange (node.js). On peer connect a node sent its 5 most-recent store entries as SVAF anchors with no origin check, so it re-forwarded CMBs it had merely received from other peers — the A→B→C→A amplifier. Anchors are now the node's own emissions only (peerId == null); a peer learns each node's state from that node directly, so own-origin anchors suffice.
    • Reload-durable receive dedup (frame-handler.js). The receive-path dedup cache (_seenCmbKeys) was in-memory only, so a reload made every already-processed CMB look new again. It now persists to a dotfile beside the store (TTL-pruned on load, throttled write, best-effort — any FS error degrades to in-memory), so reloads and version skew are non-fatal.
    • Verified: own-only anchors forward 0 peer CMBs; the dedup cache rehydrates across a simulated reload; expired keys prune on load. Suite 258/258.

0.7.26 — 2026-06-30

Added

  • Nodes self-report their memory stats over the mesh. A node is sovereign over its store, so an observer on another machine can never read it — it could only see a node's broadcasts, never what it admitted. Each node now EMITS its own tally to the roster as a lightweight node-stats frame (metadata, NOT a CAT7 CMB — it never enters a cognition stream or SVAF): emitted = CMBs it authored (store local count), admitted = CMBs it accepted from peers (store peer count), memory = total. Gossiped on start and every statsInterval (default 15s). frame-handler ingests a peer's node-stats and the node re-emits it as a node-stats event for hosts (e.g. the Mesh Edge observer) to render. Self-reported and unsigned — a convenience metric, not stored or treated as authority. Lets any observer show real emitted/admitted counts for every node, including cross-machine agents whose stores are unreadable locally. 3 tests; suite 258/258.

0.7.25 — 2026-06-29

Added

  • Earned-authority-weighted attestation aggregation (EA6). node.aggregateAttestations(cmbKey) folds every attestation about a CMB into a single roster verdict weighted by who actually holds rank. Each attestation is signature-gated against the attester's key from the roster registry (unverifiable ones are excluded as evidence, never weighted), then its attester's role is resolved at its own attestation time (role-at-time) from the rooted grant chain; weight is 2^rank (participant 1, validator 2, anchor 4). Overall and per-CAT7-field verdicts become weighted tallies with a deterministic dominant + confidence (dominant's share of total weight); over-claims (claimed role ≠ resolved) are down-weighted to the resolved rank and surfaced in mismatches. So an anchor's admit outweighs a participant's and a node asserting unearned authority cannot inflate consensus. With no anchor pinned, every attester resolves to participant (uniform weight 1) and this reduces to an unweighted tally — it sharpens the moment authority is activated. 2 tests; suite 255/255.

0.7.24 — 2026-06-29

Added

  • Roster key registry (EA5) — verify signatures from peers you never directly met. A nodeId is a uuidv7, independent of its Ed25519 key, so until now a node could only verify CMBs/attestations/grants from direct handshake peers; a relayed frame from a non-adjacent node failed as unknown-key (invisible on a fully connected LAN, but it capped the protocol at direct connectivity).
    • lib/roster-keys.jsRosterKeyRegistry pins nodeId→publicKey by source precedence (anchor > handshake > grant-vouched). A weaker-or-equal source can never repoint a stronger binding; equal-strength key conflicts are refused and recorded as evidence; duck-types Map get/set so it drops in where the raw key map was used. Persisted append-only.
    • Keys ride the rooted authority chain. A grant now binds the grantee's key into its signed payload (granteeKey, @sym-bot/core 0.3.45), so a node that never handshook the grantee learns its key from a rooted grant — tamper-evidently, because swapping the key breaks the grantor's signature (the relayer never vouches). The store pins a grantee key only when the grant is role-effective (grantor actually held the rank), so an unrooted or over-reaching grant vouches for nothing and cannot poison the registry.
    • Node wiring — handshake pins at handshake strength (_pinPeerKey, writing both the legacy CMB map and the registry); attestation / checkpoint / witness verification now resolves keys via _identityKey (registry first, covering relayed + persisted bindings); grantRole binds the grantee's known key. 9 tests; suite 253/253.

0.7.23 — 2026-06-29

Added

  • Earned authority — the lifecycle role a node claims is now earned and verifiable, not self-asserted (MMP §6.5). A node's validator/anchor authority flows only along signed role-grant chains that terminate at a pinned, non-earnable anchor (the founder root); over-reaching, unrooted, and cyclic grants confer nothing (Douceur — authority must bottom out at a pinned root).
    • lib/role-grant-store.jsRoleGrantStore holds signed role-grant / role-revoke records and resolves "what role did node N hold at time T" by walking the chain. Signatures verify on ingest against the grantor's announced key (anchor key pinned); whether the grantor actually held the rank is a resolve-time, role-at-time property, so an unentitled grant is stored but inert. Persisted append-only, reloaded on construction.
    • lib/node.js — the node pins an anchor (opts.anchor or SYM_FOUNDER_ANCHOR="nodeId:publicKey"), holds the grant store, and stamps its resolved role into attestations + witnesses (so verifyAttestationRole checks the stamp against the chain). grantRole / revokeRole sign + gossip a grant; resolveRole(nodeId, at) exposes chain resolution. With no anchor configured the node falls back to the static lifecycleRole (backward compatible).
    • lib/frame-handler.js — ingests role-grant / role-revoke frames (store verifies, relay-once).
    • §6.5 enforcementMemoryStore.validateCMB now requires the caller's resolved role to rank validator-or-above; canonizeCMB requires anchor. node.validateCMB / node.canonizeCMB resolve this node's earned role and let the store enforce, so a participant can advance no CMB's lifecycle.
    • 17 tests (10 store + 7 node). Earned authority is dormant until an anchor is pinned — until then every node uses its static role, unchanged.

0.7.22 — 2026-06-29

Added

  • Admission Attestation audit trail is now durable across restarts. The attestation index persists every record append-only under the node dir (attestations.jsonl / checkpoints.jsonl / witnesses.jsonl) and reloads it on startup, so the cross-mesh audit trail — attestations, Merkle checkpoints, and witness countersignatures — no longer evaporates from memory when a node restarts. Append-only matches the compliance model (never rewrite); a corrupt line is skipped, never fatal; persistence failures never break gating. On reload the node restores its per-attester chain cursor (seq/head) from its own reloaded chain, so seq stays monotonic and prev keeps linking across the restart boundary — otherwise a restart would reset the chain to genesis and read as a false omission. The guarantee is now: tamper-evident + omission-evident to the last witnessed checkpoint, and durable across restarts.

0.7.21 — 2026-06-29

Added

  • Admission Attestation gossip + cross-mesh audit trail with omission-evidence (Phases D1–D3; requires @sym-bot/core ^0.3.43).
    • D1 — index + every gate attested. lib/attestation-store.js: a per-node index keyed by gated-CMB (the audit trail for a CMB) and by attester chain (seq). The gate now attests reject and redundant too (a refusal is the compliance-critical event), so the per-attester chain covers every gating event. verifyChain flags seq gaps and prev breaks — local omission-detection.
    • D2 — roster gossip. Every attestation is gossiped on a dedicated attestation frame to roster peers (same-group by mDNS isolation). On receipt the node verifies the attester's original Ed25519 signature against its authenticated identity key (a relay never vouches), rate-limits per (of,by) against a flood, records, and relays once (epidemic spread). Forged/invalid dropped.
    • D3 — checkpoints + witnessing. The node periodically commits a signed Merkle checkpoint over its attestation chain (merkleRoot of the ordered signatures to seq N); roster peers verify and countersign it (witness). reconcileChain(by) recomputes the root over the held chain vs the witnessed commitment — so dropping any attestation ≤ N after it is witnessed makes the root diverge. Cross-node omission-evidence.
    • Guarantee: tamper-evident + omission-evident to the last witnessed checkpoint, not real-time completeness. Remaining (D4): roster key registry for relayed-attester verification + role-at-time, witness-quorum tuning, durable persistence.

0.7.20 — 2026-06-29

Added

  • Admission Attestations persisted on the gated remix (Phase C; requires @sym-bot/core ^0.3.42). When the SVAF gate ADMITS a CMB, the node signs an Admission Attestation and attaches it to the stored remix (cmb.admission) as the durable, attributable audit record: { of, by, at, roster, method, verdict, fields, role, seq, prev }, signed with the node's Ed25519 identity key. of binds the original gated CMB key; the per-field fields come from the gate's verdict (heuristic) or field_drifts mapped through computeFieldVerdicts (neural). A per-attester hash-chain (seq monotonic, prev = sha256 of the previous signature) makes a dropped attestation a detectable gap (omission-evidence backbone; in-memory for now). role is the node's claimed lifecycle role — consumers verify it against the rooted role-grant chain, never the stamp. The attestation is a CMB-envelope sibling, so it does not affect cmbKey or any existing signature. (Reject/redundant attestation, the queryable index, and mesh-wide gossip are the next phase.)

0.7.19 — 2026-06-29

Fixed

  • Opaque payload survives the SVAF-admit remix path. A directed CMB's payload (the substrate-level data riding alongside CAT7 — e.g. the LLM request/response primitive) was dropped whenever the receiver SVAF-admitted it: the fused remix is rebuilt from CAT7 fields and the heuristic fusion returns a fresh cmb with no payload, so the payload vanished before reaching the inbox. The same CMB rejected-but-directed surfaced the raw message and kept its payload — so payload delivery silently depended on the receiver's per-node SVAF drift. That was the root of the cross-device "payload arrives on some peers, not others" asymmetry (a receiver that admits drops it; one that rejects keeps it) — not an OS or transport difference. _preserveIncomingPayload re-attaches the incoming payload onto the fused remix on both the neural and heuristic store paths; payload rides alongside CAT7 and is never part of the cmbKey hash, so an admitted llm-request/llm-response remix correctly carries its substrate data. Regression-tested in tests/frame-handler-payload.test.js (unit) and tests/integration/e2e-payload-receive.js (two-node e2e). Completes the cross-device payload fix begun in 0.7.18 (which covered only the inbox pull path).

0.7.18 — 2026-06-28

Fixed

  • Inbox pull path preserves the opaque payload. A CMB's payload is a sibling of cmb.fields, but _pushInbox copied only fields — so any CMB pulled via node.inbox() (the sym_receive / sym_fetch path) silently lost its payload, while the channel-push path (reading entry.cmb.payload) kept it. Structured agent-to-agent data now survives cross-device directed delivery on both paths. Regression-tested in tests/inbox.test.js.

0.7.17 — 2026-06-27

Notes

  • Reconciliation release: 0.7.16 (store rename + migration + role/EIP renames) was published off a stale main; 0.7.17 is the same content rebased onto origin, now also including #31 (lib-level resolveAvailableName) and #33 (ws 8.21.0). No new changes beyond the merge.

0.7.16 — 2026-06-27

Changed

  • Per-node CMB store dir renamed meshmem/cmbs/ (it stores CMBs). A fresh node self-migrates its own dir on construct; migrateStores() (exported) bulk-renames every NON-live node at sym/mesh-channel install (live nodes are skipped — they self-migrate on restart). Readers read cmbs/ only — clean break, no fallback. Very old memories/ stores still migrate via the field-mapped path.

0.7.15 — 2026-06-27

Changed

  • CLI sym observesym publish (+ the bundled sym skill), matching the MCP tool rename to canonical EIP verbs. Publishing emits a projection of the agent's state; the cognitive mechanism terms stay in the spec. Clean break — no alias.

0.7.14 — 2026-06-27

Changed

  • Lifecycle role observer renamed to participant (MMP normalization). Frees the "observ-" stem for the projection/observation distinction (an emitted CMB is a projection; an admitted one a peer's observation). participant → validator → anchor; handshake lifecycleRole default is now participant. Clean break — no backward-compat alias.

0.7.13 — 2026-06-27

Fixed

  • Loopback registry self-cleans on abrupt exit. A node writes ~/.sym/loopback/<nodeId>.json on start and stop() unlinks it on graceful teardown — but a process that exits or is killed without calling stop() (test runs that just finish, Ctrl-C) left the registration behind as a stale "group" until a pid-liveness check filtered it out. Now a one-shot sync unlink is registered on process.on('exit') so the common case self-cleans; the listener is removed again in stop(). (SIGKILL still can't be caught — that residue is what the pid-liveness check in readers is for.)

0.7.12 — 2026-06-25

Added

  • IPC remember carries lineage parents. The daemon IPC remember now forwards opts.parents (each { key }) through to node.remember, so a CMB emitted via the IPC client (e.g. mesh-edge's emitCMB) can declare its source as a remix edge (MMP §14) rather than bare fields.

  • Adaptive integration timescale plumbing for SVAF (the liquid substrate). New node options svafAdaptiveTimescale / svafMinFreshnessSeconds / svafReactivity / svafChangeWeights / svafRecentWindow, a bounded ring of recent SVAF verdicts, and call-site plumbing passing recentDecisions + the adaptive config into @sym-bot/core's processHeuristicSVAF (requires @sym-bot/core ^0.3.39). When enabled, the SVAF memory-decay timescale shortens after recent guarded/rejected verdicts and lengthens when stable — the content gate driving the temporal timescale, instead of a fixed-gain integrator. Off by default. The decision log now records effectiveTau + changeSignal per heuristic admission.

0.7.9

Added

  • Ingestion flag on surfaced CMBs (MMP §9.2.2). Because directed delivery and SVAF memory admission are decoupled, a surfaced CMB now declares whether the receiver ingested it (remixed into memory with lineage → remixed: true) or only delivered it (surfaced to the agent but not stored → remixed: false, the directed-but-SVAF-rejected case), alongside the SVAF decision. Consumers check remixed to know whether a directed request is recallable from memory later or transient. Covered by four cases in tests/inbound-cmb-surfacing.test.js.

0.7.8

Fixed

  • Directed (peer-bound) CMBs now surface unconditionally (MMP §4.4.4 / §9.2.2). A CMB sent to a specific recipient (sym_send to=X) is a request between two agents and must reach the receiving agent regardless of the SVAF verdict — previously every inbound CMB (directed or broadcast) ran through the group-autonomous SVAF surfacing gate, so a directed coordination CMB scored low (redundant/foreign) by SVAF was silently dropped. Now the send path marks the wire frame with to + directed, and the receiver surfaces a directed CMB exactly once: on SVAF admit via the existing store path, on SVAF reject/redundant via a dedicated delivery path. SVAF governs memory admission only for directed CMBs, never delivery. Group-bound broadcasts (sym_observe, no to) stay fully SVAF-gated for surfacing — unchanged.

0.7.7

Fixed

  • Inbound-CMB receive fix — record dedup key after surfacing, not before. The receive-path dedup recorded a CMB's content-hash key as "seen" before it had actually surfaced, so a first pass that surfaced nothing (an SVAF reject with neutral mood) still poisoned the key — the same CMB re-arriving on the next reconnect was deduped and silently dropped, leaving the node receive-blind. The key is now recorded only after the CMB genuinely surfaces, preserving the anti-replay-storm guarantee without swallowing undelivered CMBs.

0.7.6

Added

  • SVAF decision log. Every SVAF evaluation — aligned / guarded / redundant / rejected — is now persisted and emitted, not just the admitted CMBs. A node's autonomous, per-field admission — including the rejections the memory store never keeps — is observable: node.decisions({ limit, since, decision, source }), a live svaf-decision event, and an append-only, capped log at ~/.sym/nodes/<name>/decisions/log.jsonl. Each record carries the per-field fieldDrifts + gateValues, the evaluated CMB key, and a short focus label — never the rejected payload (local-first, label-only). Capped (default 2000; SYM_DECISION_LOG_CAP); opt out with SYM_DECISION_LOG=0. Additive + backward compatible — meshmem and all existing readers are unchanged.

0.7.5

Fixed

  • Mesh replay-storm receive-path dedup (#32). Received CMBs are deduped by content-hash, so already-seen CMBs are no longer re-surfaced or re-broadcast. Stops the loop where nodes re-dumped their stored history on every (re)connection, flooding the mesh with stale CMBs.

0.7.4

Added

  • Same-host loopback discovery — co-resident SymNodes now mesh with no network interface. mDNS multicasts over an interface, so with Wi-Fi off two nodes on one host couldn't discover each other even though each already listens on a TCP port and could connect over 127.0.0.1. BonjourDiscovery now runs a filesystem-registry path alongside Bonjour: each node advertises its loopback endpoint to ~/.sym/loopback/<nodeId>.json ({nodeId, name, port, pid, serviceType, ts}) on a 5s heartbeat, scans for live (pid-alive + ts < 30s), same-serviceType peers, and emits the existing peer-found event over 127.0.0.1. No transport/protocol change — the connection + handshake path is identical to Bonjour. Group isolation by serviceType (MMP §5.8); the lower nodeId dials (mirrors the Bonjour tie-break) so the pair connects exactly once; quick catch-up scans (300/1200/3000ms) mesh near-simultaneous starts in ~1s.

Notes

  • Additive + backward-compatible: activates on next node start — no config, no forced restart, Bonjour path byte-for-byte unchanged. Both nodes must run ≥0.7.4 for same-host loopback meshing (each side must register and scan).

0.7.3

Changed

  • sym groups now lists groups cross-platform (including Windows) via a discovery beacon, replacing the dns-sd shell-out (which doesn't exist on Windows, and which bonjour-service can't substitute for because it doesn't answer the DNS-SD meta-query). Every running daemon now advertises its group on a shared _symgroups._tcp service (group name in TXT) via the bundled pure-JS bonjour-service; sym groups browses that beacon and lists the live groups with their nodes. Discovery-only — comms stay isolated on each group's own _<group>._tcp. Group names may be anonymous (opaque codes), so the LAN listing need not reveal a group's purpose.

Notes

  • Groups are open-join in this release (anyone who knows the name can join). Invite-gated private groups (admin-set, join-by-invite, LAN handshake gating) are the planned fast-follow.

0.7.2

Fixed (Windows — COO bisected it on re-test)

  • sym ask still crashed on Windows in 0.7.1 — the 0.7.1 broadcast-socket fix was necessary but not the culprit. COO bisected the crash to the synthesis path: process.exit(0) was firing while the LLM call's handles (a spawned claude subprocess's stdio pipes, or a fetch socket) were still closing, tripping the libuv UV_HANDLE_CLOSING assertion (0xC0000409). Fix: sym ask no longer force-exits — it sets the exit code and lets the event loop drain naturally (so closing handles finish cleanly), with a deferred unref'd fallback that force-exits only if an idle keep-alive handle lingers (by which point nothing is mid-close). Verified prompt clean exit (≈130ms, no provider).

0.7.1

Fixed (Windows — found by COO's cross-machine test on a real Windows box)

  • sym ask crashed on exit on Windows with a libuv assertion (!(handle->flags & UV_HANDLE_CLOSING), win/async.c, exit 0xC0000409). The best-effort broadcast socket was closed with socket.end(), which leaves the named-pipe handle mid-close; when the command then process.exits, Windows aborts. Now socket.destroy() tears the handle down fully (and the timeout is cleared) before exit. Mac/Linux unaffected either way.
  • sym groups errored with spawn dns-sd ENOENT on Windows (Apple Bonjour's dns-sd isn't installed). It now degrades gracefully — a clear message that LAN group enumeration needs the tool, while noting the node still meshes via the bundled pure-JS bonjour-service and you can sym join <name> directly. Also fixed a timer-ordering bug in the discovery path.

(Cross-machine mesh, install, and daemon lifecycle all PASSED on Windows in 0.7.0 — these two were the only issues.)

0.7.0

Added

  • Mesh group commands (MMP §5.8). sym start --group <name> joins a group at launch (+ --relay-url / --relay-token for WAN); sym join <name> switches into one, sym leave returns to the default mesh, sym groups discovers groups live on the LAN, sym group shows the current one. A group is the "group chat" boundary — only nodes in the same group discover each other and exchange CMBs. lib/groups.js is the single source of truth for the group↔serviceType mapping (default → _sym._tcp, <kebab> → _<group>._tcp), matching sym-mesh-channel (the Claude MCP node) and sym-swift so CLI, app, and Claude peers meet in the same group. The daemon resolves the group from SYM_GROUP env → persisted ~/.sym/group → default (the file is the cross-platform source of truth across launchd/spawn restarts) and logs it on startup.

Fixed

  • Windows portability. sym stop no longer shells out to pgrep (POSIX-only, absent on Windows): the daemon's pid is tracked in ~/.sym/daemon.pid and stopped via process.kill (pgrep kept only as a Linux fallback). isDaemonRunning now checks pid-file liveness on Windows instead of always returning true. The group-switch restart uses a portable Atomics.wait sleep instead of execSync('sleep'). macOS (launchd) path unchanged.

Docs

  • README (EN + ZH) reframed around the node × reach × scope model: the daemon is the polyglot, real-time node (any language joins via shell-out + sym listen), not optional; a new Groups section; Privacy reconciled with the relay (local stays local; remote forwards E2E-encrypted bodies through your own authenticated relay). The agent SKILL teaches the group commands.

0.6.0

Added

  • sym ask "<question>" — ask the whole mesh one question and get one synthesized answer. Broadcasts the question to the mesh (best-effort; live agents can contribute and it's logged with lineage), gathers the contributions every peer has fused into shared memory (~/.sym/nodes/*/meshmem, ranked by keyword overlap with the question, falling back to most-recent for context), and synthesizes a single answer with the configured LLM provider — each point cited to the agent that supplied it. With no provider configured it prints the ranked raw contributions and their sources instead of erroring, so it always returns what the mesh knows. Flag: --raw (skip synthesis, show contributions). This is the headline experience: ask the mesh directly, instead of asking one agent and getting one perspective.
  • complete(opts) + hasProvider(opts) exported from lib/llm-reason. complete() is a free-form sibling of invoke() — same Anthropic / OpenAI-compatible / Claude-CLI providers, returns raw text instead of extracting CAT7 (throws code: 'NO_PROVIDER' when no key / CLI provider is configured). hasProvider() reports whether a provider is configured without making a network call. Used by sym ask; available to any caller needing plain LLM completion over the mesh's provider config.

Changed

  • Skill teaches sym ask. The SYM agent skill gains an "Asking the mesh a question" section so agents query the whole mesh when a question spans other agents' domains. The .agents/ and .claude/ skill copies — which had drifted — are reconciled to one canonical source (.agents/), with the .claude/-only "Real-time listener" section ported in so nothing is lost.
  • README refocused on a single capability — collective intelligence: ask the mesh, answer as one mind. Defines "the mesh" in plain language up front, answers What / Why / How in the first screen, headlines sym ask, and moves the heavy config / drift-math inline reference to spec pointers.

Tests

  • 6 offline tests for sym ask (gather + relevance ranking, empty-mesh, no-provider fallback, usage) plus the llm-reason synthesis exports. No paid API in CI. Full suite 162 passing.

0.5.8

Added

  • opts.payload on SymNode.remember(fields, opts) — optional opaque payload attached to the CMB alongside CAT7 fields. Rides the wire frame (the existing peer.transport.send({ type: 'cmb', timestamp, cmb }) path serializes the whole cmb object, so payload propagates automatically) and the local store. NOT part of cmbKey — CAT7 fields alone remain the content-addressed identity, preserving cross-SDK CMB dedup with sym-core-swift. Substrate-level protocols (LLM request/response primitive, sym.day ATTACH-DATABASE) carry data beyond CAT7 through this slot without violating MMP §8 semantics by smuggling JSON through motivation or other CAT7 fields. Senders ensure CAT7 fields differ when payloads differ (e.g. unique request_id in focus) to avoid store-side dedup collisions on the CAT7 hash. Backward-compat: when opts.payload is omitted, CMB serialization is identical to v0.5.7.

Compatibility

  • Old peers receiving payload-bearing CMBs ignore the unknown field (existing cmb-accepted handlers only read entry.cmb.fields / entry.content). Forward-compat.
  • New peers receiving non-payload CMBs from old peers see cmb.payload === undefined. Backward-compat.
  • cmbKey() algorithm unchanged → CMB identity remains stable across the v0.5.7 / v0.5.8 boundary.

0.5.7

Added

  • Origin-aware retention. MemoryStore.compactByOrigin(localFreshnessMs, peerFreshnessMs) lets callers move self-authored CMBs (peerId == null) and peer-received CMBs to cold tier on independent freshness thresholds. Useful when the agent's own lineage chains carry more retrospective value than peer chatter — apps configure local > peer freshness so their own emissions survive longer.
  • SymNode constructor opts localRetentionSeconds + peerRetentionSeconds. Optional overrides of the uniform retentionSeconds. When omitted, both fall through to retentionSeconds (back-compat preserved). When set, _runRetentionPurge (run on start + hourly) uses the new origin-aware path and logs both values when they differ.

Compatibility

  • MemoryStore.compact(freshnessMs) is now a back-compat shim that calls compactByOrigin(freshnessMs, freshnessMs). No behavioral change for callers that don't explicitly opt into origin-aware retention.
  • Existing apps configured with only retentionSeconds see no behavioral change. The new opts are purely additive.

Tests

  • 2 new tests in tests/memory-store.test.js: shim equivalence (back-compat) + origin discrimination (peer compacts past peer cutoff while self stays hot under local cutoff). 18/18 pass.

0.5.6

Fixed

  • Apply 1s stale-prior threshold to _createPeer path. v0.5.5 lowered the threshold to 1s in the inbound-connection handler but left the _createPeer path on the old _heartbeatInterval (10s). When both sides of a peer pair dialled each other in rapid succession, the inbound handler accepted with the 1s rule but the merged _createPeer re-evaluated with 10s and could pick the opposite winner. Mac-side and Node-side then kept different connections, each killing the other's choice — visible in field testing as a continuous ~6s join → disconnect cycle even after v0.5.5. Aligned both sites on 1s.

0.5.5

Fixed

  • Stale-prior threshold lowered from 10s to 1s. v0.5.3+v0.5.4 introduced lastSeen-aware stale detection in the inbound-connection and _createPeer dedup paths, with the threshold tied to _heartbeatInterval (default 10s). Field testing showed this was too lenient: when a peer process was killed and quickly relaunched, the old run had typically sent a CMB seconds before death, so lastSeen was still within the 10s window. The dedup logic then rejected the legitimate redial as a same-direction-duplicate, producing connection ready → immediate disconnect with no handshake-complete on the dialing side.

    Lowered to a hardcoded 1s threshold in both dedup paths. Sub-second TCP-retry races during initial handshake still keep prior (the case the same-direction-duplicate rule was designed for); peer restarts with ≥1s between kill and re-dial now recover within the application layer instead of being blocked until OS keepalive reaps the underlying socket (~100s).

0.5.4

Fixed

  • Replacement transports never received a handshake; remote rejected the next heartbeat-ping as a protocol violation. Companion fix to v0.5.3. When the dual-dial dedup or stale-prior swap path in _createPeer replaced an existing transport, the new transport was registered in existingPeer.transports but no handshake was sent on it — _addPeer (which sends the handshake) is only called for brand-new peers, not transport replacements. The remote (sym-swift) saw the new connection reach .ready, sent its own handshake, and waited for ours. Ours never arrived. ~10 seconds later the heartbeat tick fired ping on every transport, the remote saw ping as the first frame, and disconnected with [SYM] session: expected handshake, got ping — protocol violation. Net result: a flap loop where every reconnect was killed within 10s by the protocol-violation trip-wire.

    Fix: extracted handshake-build into _buildHandshake() helper; the existing-peer branch in _createPeer now sends the handshake on every newly-registered transport. Idempotent — if the remote already sent its handshake, it processes both fine.

    Verified end-to-end on Mac Catalyst MeloMove ↔ claude-code-mac (Node) on the same Mac. Connection stays stable, peers persist in the UI, CMBs flow continuously without the periodic 10s drop.

0.5.3

Fixed

  • Same-host loopback peers stayed permanently rejected after one peer restarted. Companion fix to 0.5.2's same-host dedup. v0.5.2's stale-prior check looked only at the transport's _closed flag — set when transport.close() had been called explicitly. But the common dead-but-ESTABLISHED case (peer process killed; OS doesn't deliver FIN to the survivor before macOS keepalive reaps it) leaves _closed=false forever. On loopback this is a hard block — macOS default TCP_KEEPALIVE is 7200 seconds (2 hours) before the first probe. The survivor sees the dead socket as alive, and the dedup logic against this zombie entry rejects every redial from the restarted peer.

    On Wi-Fi the same logical bug is much less visible — mobile TCP routes are noisy (route flaps, ARP timeouts, AP transitions) and keepalive idle defaults are short, so stale sockets die in seconds. On loopback there's zero noise; the dead socket sits in ESTABLISHED indefinitely.

    Observed: Mac Catalyst MeloMove ↔ claude-code-mac (Node) on the same Mac. Each Mac MeloMove rebuild → claude-code-mac retains a dead ESTABLISHED socket → new Mac MeloMove's redial is rejected for 2h. iPhone ↔ claude-code-mac on Wi-Fi recovers within seconds because Wi-Fi noise reaps stale sockets quickly.

    Three-part fix:

    1. TcpTransport enables TCP keepalive on the socketsocket.setKeepAlive(true, 1000). 1-second initial idle delay before OS keepalive probes start, then OS-default probe cadence. macOS detects dead remote in ~10s instead of ~2h.

    2. inbound-connection handler and _createPeer now treat stale-by- lastSeen as stale. A prior peer entry whose lastSeen is older than _heartbeatInterval (default 10s) is now considered stale regardless of the _closed flag. The remote re-dialling is itself strong evidence its prior is dead — a healthy peer wouldn't dial again. Closes the dead prior explicitly so its close handler runs and removes the dict entry before the new transport is registered.

    3. Identity-aware close handlers. When a stale prior is closed and replaced, its eventual close handler must NOT clobber the new transport entry. Both close handlers in _createPeer now guard with transports.get(source) === transport before mutating the transports dict. Prevents a late-firing close from a swapped-out prior tearing down its replacement.

    Affects all peers running on the same host as another sym instance, and any peer-restart scenario where the peer's TCP socket on the survivor side stays in ESTABLISHED state past the OS-level FIN.

0.5.2

Fixed

  • Same-host Bonjour peers permanently rejected each other. When two @sym-bot/sym (or sym-swift) processes ran on the same host and Bonjour-discovered each other, neither could maintain a peer relationship. The inbound-connection handler and _createPeer short-circuited the moment a same-source transport key was present in peer.transports, regardless of whether that prior was actually alive or what direction it was in. Three real failure modes collapsed into the same bug:

    1. Stale prior — the previous transport's _closed flag was set but its close handler hadn't fired yet. Apple's Network framework doesn't always deliver FIN promptly when a peer process exits abruptly, leaving a dead entry in the transports map. Any reconnect attempt was permanently rejected until the OS reaped the dead entry.
    2. Same-direction duplicate — listener fires newConnectionHandler twice for the same advertised service (TCP retry, multipath race, repeated Bonjour resolution). Silently replacing the established healthy inbound with the duplicate tore down the wire pair on the remote side and triggered peer-left storms.
    3. Dual-dial collision — both peers Bonjour-discovered each other within ~50ms and both initiated outbound TCP. Each side held one outbound + one inbound for the same nodeId. The unconditional reject killed one side's view of the connection, leaving asymmetric peer state.

    Observed in the field on macOS: a Mac Catalyst app (sym-swift) and a Node CLI (@sym-bot/sym) on the same Mac would never maintain a peer relationship — the Node side silently rejected the Catalyst side's inbound dial via transport.close(); return;. Cross-host LAN peers worked because the timing windows differ.

    Fix is two-part, applied in both inbound-connection handler and _createPeer:

    1. Stale-aware dedup — short-circuit only when the prior transport is alive (!_closed). A stale _closed=true entry is treated as no prior; the new connection replaces it.
    2. Direction-aware dedup with deterministic tie-break — for a live prior:
      • Same-direction duplicate (both inbound or both outbound) → keep prior, drop new (no wire-pair teardown on the remote).
      • Dual-dial collision (different directions) → nodeId-based tie-break. The lower nodeId acts as client and keeps its outbound; the higher keeps the matching inbound. Both peers independently compute the same physical-socket winner without exchanging coordination frames.

    Mirrors the equivalent fix shipped in @sym-bot/sym-swift v0.3.79 + v0.3.80 so cross-runtime peers (sym-swift ↔ sym Node) now agree on the same dedup convention.

    Affects all peers running on the same host with another sym instance, and any deployment where Bonjour discovery races finish within ~50ms of each other.

0.5.1

Fixed

  • Mac↔Windows peer connections over LAN. BonjourDiscovery now publishes an explicit host field with a normalized mDNS-valid hostname (.local suffix). On Windows, os.hostname() returns a bare NetBIOS name (e.g. xmesh-hp) with no domain suffix; bonjour-service previously advertised that verbatim as the SRV target. macOS mDNSResponder only resolves the .local. TLD, so the Mac could discover the Windows peer via mDNS browse but failed to open an outbound TCP connection — hostname resolution returned No Such Record. CMBs sent to Windows peers never arrived; no replies ever came back. (Same class of bug as the 0.3.72 cross-platform resolve fix; regression path was the host field being unset.)

    Fix is two-part:

    1. config.loadOrCreateIdentity() normalizes identity.hostname via the new normalizeMdnsHostname() helper — bare names get .local appended, FQDNs and already-.local names pass through. Existing identities with bare hostnames are auto-migrated on next load.
    2. BonjourDiscovery._startBonjourFallback() passes the normalized identity.hostname as the host field to bonjour.publish() so the SRV target matches.

    Affects all peers; Windows nodes must upgrade (their advertisement was broken). Mac nodes benefit from the explicit host: field for determinism even though os.hostname() happens to produce .local-suffixed output on macOS.

0.5.0

Added

  • node.buildStartupPrimer({ maxCount, maxAgeMs }) — reconstitute an agent's remix memory as a human-readable primer, suitable for injection into the LLM context at session start. Operationalises MMP §4.2 O2 (rejoin-without-replay). A fresh agent session wakes with its prior cognitive state already loaded — zero first-turn sym_recall overhead. Returns { text, count, dropped, totalInStore }. Defaults: maxCount=20, maxAgeMs=86_400_000 (24h). Recency window applied first, then count cap. Empty store yields an empty primer.

    Intended use — call as the final step of plugin initialisation:

    const node = new SymNode({ name, ... });
    await node.start();
    // ... transport, tool surface, subscriptions ...
    const primer = node.buildStartupPrimer();
    mcpServer.instructions += '\n\n' + primer.text;

    Inherits to every plugin that depends on @sym-bot/sym. Consumers: @sym-bot/mesh-channel v0.3.0, @sym-bot/melotune-plugin v0.1.7.

0.3.82

Fixed

  • Remix CMB key self-reference on first-observation (MMP §14). Pairs with the @sym-bot/core 0.3.36 fix. When neural SVAF admits an incoming CMB, _processNeuralSVAF now mints a fresh remix key via remixKey(fusedFields, incomingKey, this._node.name) and overwrites both fusedEntry.cmb.key and fusedEntry.key before remix-store. Previously the receiver preserved the sender's CMB key on the stored remix, producing lineage.parents=[remix.key] — a self-edge that broke DAG traversal. Fix guarantees remix key ≠ parent key by construction while keeping idempotent dedup for retries from the same sender to the same receiver. The heuristic SVAF path is fixed in @sym-bot/core 0.3.36.

Changed

  • @sym-bot/core dep bumped to ^0.3.36 for remixKey + heuristic-SVAF fix.

0.3.81

Added

  • MMP §5.8 mesh group membership. SymNode accepts opts.group (default "default") and opts.discoveryServiceType (default "_sym._tcp"); both are propagated into BonjourDiscovery for LAN-layer isolation. The handshake frame version is bumped 0.2.20.2.3 and carries the optional group field per §5.2. Matches the sym-swift SymNode(discoveryServiceType:) parameter so Node and Swift implementations align.
  • MMP §4.4.4 targeted CMB send. SymNode.remember(fields, opts) now accepts opts.to (full peerId). When set, the CMB frame is emitted only to that connected peer; when omitted, behaviour is unchanged (broadcast to all peers). The local store write runs in both cases — lineage and §14.7 remix-guard invariants are enforced identically.
  • peers() exposes peerId (full nodeId) alongside the truncated id display form, so external callers can resolve a peer by name to a full peerId without reaching into internal _peers state.
  • tests/remember-targeted.test.js covering broadcast regression, targeted send to connected peer, targeted send to disconnected peer, and peers().peerId exposure.

0.3.80

Added

  • frame-handler.js moved from @sym-bot/core. FrameHandler is protocol plumbing — frame routing, store writes, event emission — and belongs in the protocol/node package. Imports now resolve to the local copy; @sym-bot/core retains a backward-compat re-export.
  • Echo loop prevention (MMP Section 14). _handleMemoryShare() now checks whether incoming CMB lineage parents exist as local keys in the memory store. If so, the CMB is a derivative of our own broadcast and is silently dropped — preventing ping-pong between same-app peers.
  • MemoryStore.hasLocalKey(key) — returns true if a CMB key exists in local (non-peer) entries. Used by the echo loop guard.

Changed

  • Bump @sym-bot/core dependency to ^0.3.35.

0.3.78

Changed

  • Bump @sym-bot/core to 0.3.33. Migrates @xenova/transformers@huggingface/transformers@^4.0.1. Eliminates deprecated prebuild-install and the EBUSY DLL lock on Windows.

0.3.77

Fixed

  • Clear socket timeout after TCP connect. _connectToPeer set a 10-second socket.setTimeout as a connect timeout but never cleared it after success. The timeout kept firing on the CONNECTED socket, killing any LAN connection idle for >10 seconds. Connections now stay open indefinitely after establishment.

0.3.76

Fixed

  • Fresh mDNS re-browse on reconnect timer. The 15s reconnect timer now restarts the bonjour-service browser (fresh mDNS query) instead of retrying stale cached addresses/ports.
  • On-demand reconnect on send failure. node.send() triggers an immediate discovery.reconnect() when delivery returns 0 peers, instead of waiting for the next 15s timer tick.

0.3.75

Added

  • LAN reconnect timer. Discovered peers are cached. Every 15 seconds, peer-found is re-emitted for cached peers not currently connected. Handles TCP drops without requiring a process restart.

0.3.74

Fixed

  • Removed leader-election gate from bonjour discovery. Both sides now emit peer-found and attempt to connect. The old gate (only the lower nodeId initiates) was fragile: stale bonjour cache on the initiator side → no connection, because the other side was gated.

0.3.73

Fixed

  • Prefer IPv4 in bonjour-service discovery. service.addresses from bonjour-service can include IPv6 link-local (fe80::...) which requires a scope ID for TCP. Now picks the first IPv4 address.

0.3.72

Fixed

  • Cross-platform LAN discovery: use bonjour-service instead of native dns-sd binary. The macOS dns-sd -L resolve step uses unicast DNS-SD queries that fail to resolve services advertised by Windows' Bonjour implementation. Browse (multicast) works, but resolve (unicast) returns empty — so Mac discovers Windows peers but can't get their port, and the TCP connection never happens. The bonjour-service npm package uses multicast for both browse AND resolve, which works cross-platform. Verified Mac↔Windows on the same wifi (2026-04-09). The dns-sd binary code path remains in lib/discovery.js as dead code for reference but is no longer called.

0.3.71

Fixed

  • windowsHide: true added to all 10 child_process spawn sites so Windows agents (especially the four Centro pm2 agents) no longer flood the desktop with cmd.exe popup windows on every git query, python resolution, port lookup, etc. Sites: 7 in lib/platform.js (resolvePython × 2, resolveClaudeCLI, findProcessByPort × 2, findProcessByName × 2, safeExec defaults) and 3 in lib/discovery.js (dns-sd -R register, dns-sd -B browse, dns-sd -L resolve). lib/llm-cli.js already had it. No-op on macOS/Linux. Catalogued by claude-code-win during the 2026-04-09 cross-machine round-trip session.

0.3.70

Fixed

  • Identity lockfile prevents two SymNode processes from claiming the same nodeId on the same host. ~/.sym/nodes/<name>/lock.pid is acquired in the constructor and released in stop(). Cross-process duplicates throw EIDENTITYLOCK; same-PID re-acquisition (tests, hot-reload) is allowed; stale locks (dead PID) are reclaimed automatically. Catches the sym-daemon + MCP server collision that silently broke real-time push on Windows. See cliHostMode-vs-MCP bug from 2026-04-09 round-trip test.
  • node.send() now returns the actual delivered count. Previously returned undefined; sym-mesh-channel had to read peers().length separately, which could disagree with reality (peers in _peers with broken transports). _broadcastToPeers() now wraps each transport.send() in try/catch and counts successes. Backwards compatible — existing callers ignoring the return value continue to work.

Migration

SymNode now acquires a lockfile on construction. Hosts MUST wire SIGTERM/SIGINT to call node.stop() so the lockfile is cleaned up — otherwise stale locks accumulate (they're auto-reclaimed on next startup, but cleaner shutdown is better). sym-mesh-channel v0.1.3+ already does this.

If two of your processes legitimately need different identities, set SYM_NODE_NAME to distinct values per process. If they're fighting for the same identity by mistake (e.g. inherited shell env), the lockfile error message will tell you which PID holds the existing claim.

0.3.69

Fixed

  • Excluded *.bak, *.swp, .DS_Store from published tarball via .npmignore. 0.3.68 accidentally shipped local backup files. Same code as 0.3.68; deprecate 0.3.68.

0.3.68

Fixed

  • RelayConnection no longer silently reconnects on close code 4004 ("Replaced by new connection"). Logs FATAL, sets a hard-stop flag, fires the new identity-collision event, and exits the close handler. Breaks the duplicate-identity ping-pong loop. See d6a17f6.

Added

  • identity-collision event on SymNode{ nodeId, name, code }. Optional listener; default behavior is loud-log + stop reconnecting. Hosts wanting hard-exit semantics should listen and call process.exit() themselves.

Why

Two processes holding the same nodeId would enter a 1s ping-pong loop on the relay, flooding peer-left/peer-joined events. MMP principle: identity is bound to a keypair, so two simultaneous holders is an error condition — refuse loudly instead of silently retrying.

0.3.67

Added

  • sym observe --standalone — daemon-less one-shot CMB emission. Spins up a fresh SymNode inside the CLI process, reads relay credentials from ~/.sym/relay.env, emits one CMB, and disconnects. Works even when sym-daemon is not running — the daemon becomes an optimisation, not a requirement. Auto-enabled as a graceful fallback whenever the daemon is down, so existing sym observe commands no longer fail with "sym-daemon is not running."
  • sym observe --name <id> — set the mesh identity for standalone-mode emissions. Defaults to sym-cli. Identity is stable across invocations via the cached SymIdentity keypair in ~/.sym/nodes/<name>/, so repeated calls with the same name resolve to the same nodeId. Claude Code users should pass --name claude-code-mac (or claude-code-win / claude-code-linux) so their CMBs are attributable on the mesh grid.
  • sym observe --parents <key1,key2> — comma-separated parent CMB keys for remix lineage. Using this flag implies --standalone (the daemon IPC remember handler does not accept lineage parents). Makes it trivial to emit resolution CMBs that close upstream tickets on the Review Board via the SVAF lineage graph.

Why

Before this release, sym observe required sym-daemon to be running — any user who had stopped the daemon (or never started it) hit a hard failure. This was the main friction for Claude Code sessions that want to participate in the mesh as real peers without running a persistent background daemon. The daemon-less path makes the entire mesh emission surface usable out of the box after npm install -g @sym-bot/sym.

Migration

No breaking changes. Existing sym observe '<json>' calls continue to work unchanged when the daemon is running (same IPC fast path). When the daemon is down, the CLI now falls back to standalone mode instead of failing.

0.3.66

Changed (MMP v0.2.2 spec conformance)

  • state-sync frame is now deprecated. CfC hidden states never cross the wire under SVAF (Xu, 2026, Symbolic-Vector Attention Fusion for Collective Intelligence, arXiv:2604.03955, §3.4). Cognitive coupling propagates as CMBs at SVAF Layer 4 only; the per-agent CfC at Layer 6 stays private to each agent.
  • _reencodeAndBroadcast() updates the local CfC only; no state-sync broadcast.
  • updateContext(text) updates the local CfC only; no state-sync broadcast.
  • The per-handshake state-sync send is removed. Handshake exchanges identity, version, and lifecycle role only; cognitive bootstrap happens via the anchor CMB exchange that follows.
  • Coordinated with @sym-bot/core 0.3.32, which silently drops inbound state-sync frames at the frame-handler with a deprecation log.
  • The wire format is preserved (the state-sync frame type is still parseable for backward compatibility with v0.2.0 / v0.2.1 peers); only the send paths are removed.

Migration

If you previously listened for coupling-decision events driven by state-sync, switch to events emitted by the CMB pipeline (memoryReceived, cmbAccepted) and read (valence, arousal) from cmb.fields.mood. The mood field is delivered across domain boundaries even when SVAF rejects the rest of the CMB (MMP §9.3 protocol guarantee R5).

0.3.65

Fixed

  • sym-daemon default node name is now platform-scoped (sym-daemon-mac / sym-daemon-win / sym-daemon-linux) instead of the hardcoded sym-daemon-win. The hardcoded fallback caused Mac daemons to identify as sym-daemon-win, leading to identity collisions and stale ~/.sym/nodes/ directories on cross-platform development machines.

0.3.64

Fixed

  • Bumped @sym-bot/core to ^0.3.31 to restore cmb-accepted event emission in cliHostMode. Without this bump, bin/sym-daemon.js's cmb-accepted listener never fires under cliHostMode, silently disabling sym sub IPC subscribers and the daemon→hosted-agent fanout path.

0.3.63

Changed (BREAKING)

  • sym-daemon now uses cliHostMode: true (renamed from relayMode). Daemon no longer stores forwarded CMBs — eliminates ~5x duplication on multi-agent hosts.
  • sym recall is now federated: scans ~/.sym/nodes/*/meshmem/ directly, deduped by CMB key, sorted by recency. Works without the daemon. New --node <name> flag scopes the scan.
  • Requires @sym-bot/core@^0.3.31 (originally shipped against 0.3.30; 0.3.30 had a regression — see sym-core CHANGELOG).

0.3.61

Fixed

  • High-quality CMBs were silently buried, never promoted to the Review Board, because of two compounding bugs:
    1. lib/llm-reason.js appended a hardcoded "Return a JSON object with 7 CAT7 fields" suffix to every prompt, with no mention of _meta. This overrode any _meta.founderAction instructions the agent's role definition (SKILL.md) tried to convey, so the model emitted just the 7 fields and the founderAction signal was lost. Suffix now requests the optional _meta tag explicitly: _meta:{founderAction, urgency, reason} and instructs the model to set it per role rules.
    2. lib/mesh-agent.js detectFounderAction (the fallback when _meta is missing) only scanned intent + issue and only matched a hedge-paraphrase vocabulary list (prioritize, monitor, competitive, etc.). Disciplined extraction prompts produce CMBs with concrete verbs like fetch, flag, draft, endorser, arxiv, cite, respond, submit — none of which were in the list, so high-quality CMBs failed to promote. Now scans intent + issue + commitment + mood.text (wider field surface) and the keyword list is expanded with concrete-action verbs, research vocabulary, and stakes-signalling affect words.

Verified: a real research-win arxiv CMB ("Fetch full PDF today. Check author list for endorser candidates...") now correctly promotes via the keyword fallback path. Going forward, disciplined agents using the _meta schema in their prompt suffix will set founderAction explicitly and bypass the keyword fallback entirely.

0.3.59

Fixed

  • Windows terminal popups in CLI provider (lib/llm-cli.js) — claude resolved to a .cmd shim that opened visible cmd.exe windows on every spawn. Now uses platform.resolveClaudeCLI() to get the node binary + cli.js path directly, plus windowsHide: true on the spawn options.
  • Per-agent env vars not loaded at module-load time (lib/mesh-agent.js) — agents read SYM_RESEARCH_PROVIDER, SYM_COO_MODEL, etc. at top-of-file constants before the MeshAgent constructor runs. Added a top-level loadRelayEnv() IIFE that loads ~/.sym/relay.env when mesh-agent.js is first required, so per-agent env overrides resolve correctly.

0.3.58

Added

  • Claude Code CLI provider (provider: 'cli'). Spawns claude -p --output-format json as a subprocess instead of hitting an HTTP API. Gives every agent the full Claude Code tool surface — Read, Write, Bash, Grep, WebFetch, Skill, etc. — and auto-loads CLAUDE.md / .claude/settings.json / project skills from the agent's working directory. Uses local Claude Code auth (no API key needed). Per-call options: model (opus/sonnet/haiku alias or full id), addDirs, allowedTools, permissionMode (default bypassPermissions), maxBudget (passed as --max-budget-usd), timeoutMs. Selectable via provider: 'cli' per call or SYM_LLM_PROVIDER=cli globally. See lib/llm-cli.js.
  • This is the path for the existing addDirs parameter that HTTP providers were silently ignoring — agents that already passed addDirs: [...] get directory access for free as soon as they switch provider.

Changed

  • lib/llm-reason.js getProviderConfig and invoke updated to dispatch cli / anthropic / openai. CLI provider skips the API-key check (uses local Claude Code auth) and skips withRetry (subprocess errors aren't typically transient).

0.3.57

Changed

  • MemoryStore._cmbKey now delegates to @sym-bot/core cmbKey() instead of re-implementing the SHA256-truncate logic. Eliminates duplicated CMB key code that had drifted multiple times. Single source of truth lives in sym-core/lib/cmb-encoder.js. The raw-content fallback path remains a direct SHA256 (distinct input space, cannot collide with the field-keyed path).
  • @sym-bot/core dependency bumped to ^0.3.29 to pick up the new cmbKey export and the FNV-1a context encoder fix. The encoder fix restores cross-SDK n-gram embedding parity with sym-core-swift for the first time — see @sym-bot/core 0.3.29 changelog for the wire-impact details.

0.3.56

Changed

  • CMB content key algorithm: MD5 → SHA256 (truncated to 32 hex chars). MemoryStore._cmbKey() now uses crypto.createHash('sha256').digest('hex').slice(0, 32) for both the field-text and raw-content code paths. This duplicated CMB-key logic (separate from @sym-bot/core cmb-encoder.js) is now back in sync. Wire-breaking with respect to dedup against pre-0.3.56 stored CMBs. Coordinated with @sym-bot/core 0.3.28 and sym-core-swift 0.3.6.
  • @sym-bot/core dependency bumped to ^0.3.28.

Fixed

  • Removed accidental self-dependency @sym-bot/sym: ^0.3.43 from package.json dependencies. The package now declares only its real runtime deps (@sym-bot/core, bonjour-service, ws).

0.3.25

Changed

  • sym-core 0.2.0 — semantic encoder for SVAF evaluation. Paraphrase similarity: 0.31 (n-gram) → 0.69 (semantic). Per-field evaluation quality bounded by encoder quality, not model capacity.

0.3.24

Added

  • Catchup via mesh broadcast. Daemon broadcasts "catchup" message to all peers. MeshAgent listens for it and triggers immediate domain poll. Replaces the old hosted-agent-only catchup path.

0.3.23

Added

  • Handshake: version and extensions fields per MMP v0.2.1 Section 5.2. Handshake now sends version: "0.2.1" and extensions: [].
  • Error frame support per MMP v0.2.1 Section 7.2. sendError(peerId, code, message, detail) sends protocol-level error frames. Codes 1xxx close connection; 2xxx informational.

Parity

  • 100% feature parity with sym-swift (Swift SDK). Both SDKs implement all 10 frame types, handshake with version/extensions/e2ePublicKey, error frames, multi-transport per peer, SVAF per-field evaluation, MD5 content-addressable CMB keys, lineage, remix guard, and metrics.

0.3.22

Changed

  • MeshAgent: every agent is a standalone peer node (MMP v0.2.1). Removed hosted/daemon mode. Every MeshAgent creates its own SymNode with own identity, transport, coupling engine, and memory store. Coupling is per-node — agents that share another node's identity cannot have independent SVAF weights.
  • sym recall --json — new flag returns full entry objects (source, peerId, CMB fields, lineage) as JSON. Enables sym.day to get real source data from daemon memory.

Tests

  • 119 tests (was 100). MeshAgent test updated for standalone-only constructor.

0.3.21

Added

  • MeshAgent — protocol-level agent lifecycle class. Agents provide fetchDomain(), reason(), remix(). Protocol handles event-driven remix, canRemix() gate, fingerprint dedup, lineage, silence. No LLM code in SDK.
  • sym metrics — new CLI command exposing protocol-level metrics (CMBs, peers, LLM cost, uptime)
  • --json flag for sym status, sym peers, sym metrics — structured output for programmatic consumers

Fixed

  • Remix guard: remember() with parents now resets hasNewDomainData. Previously a remix counted as new domain data, allowing infinite remix chains from a single observation.
  • Startup race: relay disconnect handlers directly deleted peers, bypassing multi-transport failover (Section 4.6/5.5). Now only closes the relay transport — Bonjour survives.
  • Undefined variables in _handleRelayPeerLeft (peers, peer) — leftover from refactoring.

Changed

  • IPC socket moved from /tmp/sym.sock to ~/.sym/daemon.sock per spec Section 4.5. SYM_SOCKET env var still overrides.

Tests

  • 100 tests (was 83). Added: remix guard reset, MeshAgent validation, CLI --json, socket path.

0.11.1 (2026-08-10)

⚠ Wire changes — every node must move together

The CAT7 container is categories. A record is { categories, metadata }. Nodes on 0.11.0 and earlier do not read this container name.

The LAN rendezvous is _symrooms._tcp. A node on this version and one on 0.11.0 cannot discover each other, in either direction, with no error to explain the silence. The word there is not a concept, it is an address.

The persisted room moved filename. The retired file is not read — one name, no fallback — but its presence is now announced: the daemon names the file, the room it held, and the command that restores it. Without that, an upgraded node silently starts in default, loses every peer, and says nothing.

Changed — group is now room, with one meaning

Identifiers, the SYM_ROOM environment variable, the persisted state file, the module files, and the beacon. The audience of a record has been room since the two-section record; the discovery group is the same concept and now carries the same word.

Changed — @sym-bot/core 0.8.1 → 0.9.3

Adopts the renamed core surface (computeCategoryVerdicts, categoryKeyV1, encryptCategories/decryptCategories, encodeCategory, categoryWeights, categoryDrifts, categoryVerdicts, categoryParents) and core's fallback removals. Drift arithmetic changes for stores whose anchors carry no confidence — core no longer invents one.

Removed — grandfathering of pre-boundary records

A pre-boundary record carried its audience under the retired name, so its audience can no longer be established, and core refuses it rather than treating an absent room as a broadcast. A node holding genuine pre-boundary history will now see it refused rather than surfaced. Refused is still not forged: such a record does not touch the forgery counter, because an operator watching that counter at cutover must not see ordinary history in it.