v1.4.0
The silent-failure release
Every fix here addresses the same shape of bug: a mechanism that reported success, or reported nothing at all, while doing nothing. An identity ledger whose entries could never leave proposed. LLM calls that spent their whole token budget on hidden reasoning and returned an empty string. Two separate guards against exactly that, both inert — one matching a tag format the provider no longer emits, the other a soft switch the model no longer honours, sitting under a comment asserting it worked. Consolidation monitoring that read a timestamp nothing ever wrote, so a store with 3,131 processed observations was indistinguishable from one that had never consolidated at all.
None of these announced themselves, and each was found by checking a mechanism against reality rather than reading its description. That is the same principle the new service supervision applies at runtime: a service is up when its endpoint answers, not when its process exists.
Added
- Service supervision (
src/services/) —fozikionow manages the two daemons the default install depends on: ollama and the bundled NLI cross-encoder.up,down,status, andservice <start|stop|restart|status|logs>spawn them detached, capture output to~/.fozikio/logs/, track PIDs in~/.fozikio/run/, and wait on an HTTP probe before reporting ready.up --watchsupervises continuously: exponential backoff, a circuit breaker after five failed restarts, and a notification hook (FOZIKIO_NOTIFY_URL/FOZIKIO_NOTIFY_CMD) so repeated restarts escalate rather than being hidden. A service is up when its endpoint answers, not when its process exists — a live process that has stopped responding reportsdegraded. That is the failure this exists to catch: ollama dies silently, every semantic tool then fails while ops, threads and journal keep working, and the outage reads as "cortex is fine" until a query comes back empty. Services started outside fozikio are reported as such and are never killed. fozikio doctor— checks runtime, config, store, NLI wiring and both services, and prints a concrete remedy for each failure rather than only naming it. Exits 1 on any error, so it works as a cron gate. Catches the silent case wherenli.enabledis set but no URL is reachable, which degrades adjudication to the LLM invisibly.fozikio update— compares the installed version against the registry. Reports only; it never installs.- Interactive shell — a bare
fozikioon a TTY opens a session: a prompt with tab completion over the command tree, history, and a filterable palette on an empty enter. Commands run in-process and errors are contained, so a bad command reports and returns to the prompt instead of ending the session.serveandnliare refused inside it with a pointer to the supervised equivalent, because both block forever. Falls back to help when piped. fozikio dashboard— the live service and memory view, with keys to start, stop, restart and tail logs. Reachable from the shell or directly.- Noun-verb command tree —
fozikio memory <health|vitals|report|anomalies|wander|maintain|digest>andfozikio service <verb>. The previous flat spellings (fozikio health, …) remain permanent hidden aliases: not deprecated, no warning, because cron jobs and published docs invoke them directly. evolution_resolve— identity evolutions can finally leaveproposed.evolvewrotestatus: 'proposed'and nothing in the engine could ever write another value, yetevolution_listacceptedapplied,rejected, andrevertedas filters and returned anapplied_atfield. Three of four documented statuses were unreachable through the tool surface, so the ledger only ever grew: adopting a change in practice left the record permanently claiming it was still pending. (Resolved records can exist — a live store was found holding 39applied— but every one carried anapplied_atidentical to the millisecond, the signature of a one-off bulk backfill written outside the tools, not something an agent could do during a session.) The new tool transitions a proposal with an optional note, stampsapplied_aton apply, recordsprevious_status, and refuses to revert an evolution that was never applied — a revert with no corresponding apply reads as history that never happened.applied_atsurvives a later revert as the record of when the change had been in force.evolution_listnow also returnsresolved_atandnote, so a resolution is visible rather than merely stored. Brings the tool count to 60.
Changed
- CLI internals rebuilt on a shared framework (
src/cli/) — oneutil.parseArgs-based parser replaces the ten hand-rolledparseArgs()implementations the commands each carried, so unknown flags now fail loudly instead of being silently ignored. Colour, symbols, tables, box-drawing, spinners, prompts and menus are shared, TTY-aware, and honourNO_COLOR/FORCE_COLOR/--no-color. Help is generated from the same tree the router walks, so the two can no longer drift. No new runtime dependencies;enginesstays at>=20. - Colour is emitted directly rather than through
util.styleText— styleText runs its own TTY check againstprocess.stdoutand silently returns text unstyled when that check fails, overriding an explicit--colororFORCE_COLORwhenever output was piped. Detection now lives in one place. This also sidestepsstyleTextnot existing before Node 20.12, which is below the declared engine floor. - NLI provisioning helpers moved to
src/services/nli-env.tsso the registry, doctor and supervisor can use them without importing a command module.bin/nli-cmd.tsre-exports them;fozikio nliis unchanged. - Memory
nameis now a real label, not a raw text truncation. All three creation paths (goal_set, high-salienceobservepromotion, and the dreamcreatephase) previously derivednameby slicing the definition —goal_setdid a raw mid-wordslice(0, 60)with no word boundary and no ellipsis, so names rendered as broken fragments (…verifiable tr), and the paths named identical-length memories inconsistently. A newengines/naming.tscentralises naming:deriveName(text, llm)mints a genuine short concept label via the LLM at creation time (the intended behaviour), falling back toderiveNameHeuristic(text)— first-sentence preference, word-boundary truncation, ellipsis on elision — whenever the LLM is unavailable, errors, or returns nothing usable. Adds the versionedlabel-conceptprompt.
Fixed
- CI ran
tsconly, on ubuntu only. The vitest suite was configured but never executed by the merge gate, and nothing ever ran on Windows — where detached spawning,taskkilland PID handling all differ. Adds aTestjob acrossubuntu-latestandwindows-latest. The existingType Checkjob keeps its exact name so required-status-check rules continue to match. - Every Ollama LLM call was returning empty or truncated output on the default model.
qwen3:14bis a reasoning model, and its reasoning tokens are drawn from the samenum_predictbudget as the answer. Measured directly: atnum_predict: 300, thinking consumed all 300 tokens,responsecame back empty withdone_reason: length, in 17.3s. The identical call withthink: falseanswered completely in 50 tokens and 3.1s. Every engine call site is bounded —naming.tscaps at 32 tokens,social-draftat 100,reflectat 300 — so all were silently degraded.deriveName()in particular could never have succeeded on Ollama; it fell through to its heuristic fallback on every call while appearing to work. BothgenerateandgenerateJSONnow send the nativethink: false. - The two existing defences were both inert.
stripThinking()matches inline<think>...</think>blocks, but current Ollama returns reasoning in a separatethinkingfield that never matches — it stripped nothing while the answer was already gone.generateJSONprefixed prompts with/no_think, the legacy qwen3 soft switch, measured identical to sending nothing (300 tokens intothinking, emptyresponse) — so JSON calls were parsing an empty string while the comment above them claimed they were protected. That prefix also leaked a literal/no_thinkline into the prompt for every model that does not implement it.stripThinking()is retained for models that genuinely inline their reasoning, with its actual scope documented. dreamnever recorded that it ran, so consolidation monitoring could never fire.sleep_pressureandconsolidation_statusboth answer "when did consolidation last happen" by reading adream_statedoc and falling back to aconsolidation_historycollection — and nothing in the engine wrote either one. Both tools therefore returnedlast_dream_at_iso: nullandhours_since_dream: nullpermanently, no matter how many timesdreamhad actually run, which inverts the meaning of the signal: a store that had consolidated thousands of observations was indistinguishable from one that had never consolidated at all.consolidation_statusalso derives itslast_dreamsummary andquality_trendfrom the same collection, so both were always empty. Verified against a live store: 3,131 of 3,137 observations processed, and thesystemandconsolidation_historycollections holding zero rows.dreamnow appends a history entry with the per-phase counts, totals, duration, integration rate and failure count, written after the cycle so a failed run leaves no false entry.consolidation_qualityis deliberately left absent rather than filled with a stand-in, sincedreamConsolidatedoes not compute one and both readers already treat it as nullable.