Note: Versions 0.3.26 – 0.3.55 were released as git tags without changelog entries. Changelog resumes at 0.3.56 below.
-
Mac↔Windows peer connections over LAN.
BonjourDiscoverynow publishes an explicithostfield with a normalized mDNS-valid hostname (.localsuffix). On Windows,os.hostname()returns a bare NetBIOS name (e.g.xmesh-hp) with no domain suffix;bonjour-servicepreviously 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 returnedNo 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 thehostfield being unset.)Fix is two-part:
config.loadOrCreateIdentity()normalizesidentity.hostnamevia the newnormalizeMdnsHostname()helper — bare names get.localappended, FQDNs and already-.localnames pass through. Existing identities with bare hostnames are auto-migrated on next load.BonjourDiscovery._startBonjourFallback()passes the normalizedidentity.hostnameas thehostfield tobonjour.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 thoughos.hostname()happens to produce.local-suffixed output on macOS.
-
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-turnsym_recalloverhead. 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-channelv0.3.0,@sym-bot/melotune-pluginv0.1.7.
- Remix CMB key self-reference on first-observation (MMP §14).
Pairs with the
@sym-bot/core0.3.36 fix. When neural SVAF admits an incoming CMB,_processNeuralSVAFnow mints a fresh remix key viaremixKey(fusedFields, incomingKey, this._node.name)and overwrites bothfusedEntry.cmb.keyandfusedEntry.keybefore remix-store. Previously the receiver preserved the sender's CMB key on the stored remix, producinglineage.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/core0.3.36.
@sym-bot/coredep bumped to^0.3.36forremixKey+ heuristic-SVAF fix.
- MMP §5.8 mesh group membership.
SymNodeacceptsopts.group(default"default") andopts.discoveryServiceType(default"_sym._tcp"); both are propagated intoBonjourDiscoveryfor LAN-layer isolation. The handshake frame version is bumped0.2.2→0.2.3and carries the optionalgroupfield per §5.2. Matches thesym-swiftSymNode(discoveryServiceType:)parameter so Node and Swift implementations align. - MMP §4.4.4 targeted CMB send.
SymNode.remember(fields, opts)now acceptsopts.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()exposespeerId(full nodeId) alongside the truncatediddisplay form, so external callers can resolve a peer by name to a full peerId without reaching into internal_peersstate.tests/remember-targeted.test.jscovering broadcast regression, targeted send to connected peer, targeted send to disconnected peer, andpeers().peerIdexposure.
frame-handler.jsmoved 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/coreretains 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.
- Bump
@sym-bot/coredependency to^0.3.35.
- Bump
@sym-bot/coreto 0.3.33. Migrates@xenova/transformers→@huggingface/transformers@^4.0.1. Eliminates deprecatedprebuild-installand the EBUSY DLL lock on Windows.
- Clear socket timeout after TCP connect.
_connectToPeerset a 10-secondsocket.setTimeoutas 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.
- 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 immediatediscovery.reconnect()when delivery returns 0 peers, instead of waiting for the next 15s timer tick.
- LAN reconnect timer. Discovered peers are cached. Every 15 seconds,
peer-foundis re-emitted for cached peers not currently connected. Handles TCP drops without requiring a process restart.
- Removed leader-election gate from bonjour discovery. Both sides
now emit
peer-foundand 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.
- Prefer IPv4 in bonjour-service discovery.
service.addressesfrom bonjour-service can include IPv6 link-local (fe80::...) which requires a scope ID for TCP. Now picks the first IPv4 address.
- Cross-platform LAN discovery: use
bonjour-serviceinstead of nativedns-sdbinary. The macOSdns-sd -Lresolve 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. Thebonjour-servicenpm package uses multicast for both browse AND resolve, which works cross-platform. Verified Mac↔Windows on the same wifi (2026-04-09). Thedns-sdbinary code path remains inlib/discovery.jsas dead code for reference but is no longer called.
windowsHide: trueadded 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 inlib/platform.js(resolvePython× 2,resolveClaudeCLI,findProcessByPort× 2,findProcessByName× 2,safeExecdefaults) and 3 inlib/discovery.js(dns-sd -Rregister,dns-sd -Bbrowse,dns-sd -Lresolve).lib/llm-cli.jsalready had it. No-op on macOS/Linux. Catalogued by claude-code-win during the 2026-04-09 cross-machine round-trip session.
- Identity lockfile prevents two SymNode processes from claiming the
same nodeId on the same host.
~/.sym/nodes/<name>/lock.pidis acquired in the constructor and released instop(). Cross-process duplicates throwEIDENTITYLOCK; same-PID re-acquisition (tests, hot-reload) is allowed; stale locks (dead PID) are reclaimed automatically. Catches thesym-daemon+ MCP server collision that silently broke real-time push on Windows. SeecliHostMode-vs-MCPbug from 2026-04-09 round-trip test. node.send()now returns the actual delivered count. Previously returned undefined; sym-mesh-channel had to readpeers().lengthseparately, which could disagree with reality (peers in_peerswith broken transports)._broadcastToPeers()now wraps eachtransport.send()in try/catch and counts successes. Backwards compatible — existing callers ignoring the return value continue to work.
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.
- Excluded
*.bak,*.swp,.DS_Storefrom published tarball via.npmignore. 0.3.68 accidentally shipped local backup files. Same code as 0.3.68; deprecate 0.3.68.
RelayConnectionno longer silently reconnects on close code 4004 ("Replaced by new connection"). Logs FATAL, sets a hard-stop flag, fires the newidentity-collisionevent, and exits the close handler. Breaks the duplicate-identity ping-pong loop. Seed6a17f6.
identity-collisionevent onSymNode—{ nodeId, name, code }. Optional listener; default behavior is loud-log + stop reconnecting. Hosts wanting hard-exit semantics should listen and callprocess.exit()themselves.
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.
sym observe --standalone— daemon-less one-shot CMB emission. Spins up a freshSymNodeinside the CLI process, reads relay credentials from~/.sym/relay.env, emits one CMB, and disconnects. Works even whensym-daemonis not running — the daemon becomes an optimisation, not a requirement. Auto-enabled as a graceful fallback whenever the daemon is down, so existingsym observecommands no longer fail with "sym-daemon is not running."sym observe --name <id>— set the mesh identity for standalone-mode emissions. Defaults tosym-cli. Identity is stable across invocations via the cachedSymIdentitykeypair in~/.sym/nodes/<name>/, so repeated calls with the same name resolve to the samenodeId. Claude Code users should pass--name claude-code-mac(orclaude-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 IPCrememberhandler 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.
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.
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.
state-syncframe 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; nostate-syncbroadcast.updateContext(text)updates the local CfC only; nostate-syncbroadcast.- The per-handshake
state-syncsend is removed. Handshake exchanges identity, version, and lifecycle role only; cognitive bootstrap happens via the anchor CMB exchange that follows. - Coordinated with
@sym-bot/core0.3.32, which silently drops inboundstate-syncframes at the frame-handler with a deprecation log. - The wire format is preserved (the
state-syncframe type is still parseable for backward compatibility with v0.2.0 / v0.2.1 peers); only the send paths are removed.
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).
sym-daemondefault node name is now platform-scoped (sym-daemon-mac/sym-daemon-win/sym-daemon-linux) instead of the hardcodedsym-daemon-win. The hardcoded fallback caused Mac daemons to identify assym-daemon-win, leading to identity collisions and stale~/.sym/nodes/directories on cross-platform development machines.
- Bumped
@sym-bot/coreto^0.3.31to restorecmb-acceptedevent emission incliHostMode. Without this bump,bin/sym-daemon.js'scmb-acceptedlistener never fires undercliHostMode, silently disablingsym subIPC subscribers and the daemon→hosted-agent fanout path.
sym-daemonnow usescliHostMode: true(renamed fromrelayMode). Daemon no longer stores forwarded CMBs — eliminates ~5x duplication on multi-agent hosts.sym recallis 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).
- High-quality CMBs were silently buried, never promoted to the Review Board, because of two compounding bugs:
lib/llm-reason.jsappended a hardcoded "Return a JSON object with 7 CAT7 fields" suffix to every prompt, with no mention of_meta. This overrode any_meta.founderActioninstructions 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_metatag explicitly:_meta:{founderAction, urgency, reason}and instructs the model to set it per role rules.lib/mesh-agent.jsdetectFounderAction(the fallback when_metais missing) only scannedintent + issueand only matched a hedge-paraphrase vocabulary list (prioritize,monitor,competitive, etc.). Disciplined extraction prompts produce CMBs with concrete verbs likefetch,flag,draft,endorser,arxiv,cite,respond,submit— none of which were in the list, so high-quality CMBs failed to promote. Now scansintent + 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.
- Windows terminal popups in CLI provider (
lib/llm-cli.js) —clauderesolved to a.cmdshim that opened visible cmd.exe windows on every spawn. Now usesplatform.resolveClaudeCLI()to get the node binary +cli.jspath directly, pluswindowsHide: trueon the spawn options. - Per-agent env vars not loaded at module-load time (
lib/mesh-agent.js) — agents readSYM_RESEARCH_PROVIDER,SYM_COO_MODEL, etc. at top-of-file constants before theMeshAgentconstructor runs. Added a top-levelloadRelayEnv()IIFE that loads~/.sym/relay.envwhenmesh-agent.jsis first required, so per-agent env overrides resolve correctly.
- Claude Code CLI provider (
provider: 'cli'). Spawnsclaude -p --output-format jsonas 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-loadsCLAUDE.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(defaultbypassPermissions),maxBudget(passed as--max-budget-usd),timeoutMs. Selectable viaprovider: 'cli'per call orSYM_LLM_PROVIDER=cliglobally. Seelib/llm-cli.js. - This is the path for the existing
addDirsparameter that HTTP providers were silently ignoring — agents that already passedaddDirs: [...]get directory access for free as soon as they switch provider.
lib/llm-reason.jsgetProviderConfigandinvokeupdated to dispatchcli/anthropic/openai. CLI provider skips the API-key check (uses local Claude Code auth) and skipswithRetry(subprocess errors aren't typically transient).
MemoryStore._cmbKeynow delegates to@sym-bot/corecmbKey()instead of re-implementing the SHA256-truncate logic. Eliminates duplicated CMB key code that had drifted multiple times. Single source of truth lives insym-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/coredependency bumped to^0.3.29to pick up the newcmbKeyexport and the FNV-1a context encoder fix. The encoder fix restores cross-SDK n-gram embedding parity withsym-core-swiftfor the first time — see@sym-bot/core0.3.29 changelog for the wire-impact details.
- CMB content key algorithm: MD5 → SHA256 (truncated to 32 hex chars).
MemoryStore._cmbKey()now usescrypto.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/corecmb-encoder.js) is now back in sync. Wire-breaking with respect to dedup against pre-0.3.56 stored CMBs. Coordinated with@sym-bot/core0.3.28 andsym-core-swift0.3.6. @sym-bot/coredependency bumped to^0.3.28.
- Removed accidental self-dependency
@sym-bot/sym: ^0.3.43frompackage.jsondependencies. The package now declares only its real runtime deps (@sym-bot/core,bonjour-service,ws).
- 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.
- Catchup via mesh broadcast. Daemon broadcasts
"catchup"message to all peers.MeshAgentlistens for it and triggers immediate domain poll. Replaces the old hosted-agent-only catchup path.
- Handshake:
versionandextensionsfields per MMP v0.2.1 Section 5.2. Handshake now sendsversion: "0.2.1"andextensions: []. - 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.
- 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.
- MeshAgent: every agent is a standalone peer node (MMP v0.2.1). Removed hosted/daemon mode. Every
MeshAgentcreates its ownSymNodewith 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.
- 119 tests (was 100). MeshAgent test updated for standalone-only constructor.
- 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)--jsonflag forsym status,sym peers,sym metrics— structured output for programmatic consumers
- Remix guard:
remember()with parents now resetshasNewDomainData. 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.
- IPC socket moved from
/tmp/sym.sockto~/.sym/daemon.sockper spec Section 4.5.SYM_SOCKETenv var still overrides.
- 100 tests (was 83). Added: remix guard reset, MeshAgent validation, CLI --json, socket path.