Skip to content

v1.4.0

Choose a tag to compare

@github-actions github-actions released this 30 Jul 23:52
· 55 commits to master since this release
6b04f66

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/) — fozikio now manages the two daemons the default install depends on: ollama and the bundled NLI cross-encoder. up, down, status, and service <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 --watch supervises 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 reports degraded. 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 where nli.enabled is 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 fozikio on 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. serve and nli are 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 treefozikio memory <health|vitals|report|anomalies|wander|maintain|digest> and fozikio 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 leave proposed. evolve wrote status: 'proposed' and nothing in the engine could ever write another value, yet evolution_list accepted applied, rejected, and reverted as filters and returned an applied_at field. 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 39 applied — but every one carried an applied_at identical 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, stamps applied_at on apply, records previous_status, and refuses to revert an evolution that was never applied — a revert with no corresponding apply reads as history that never happened. applied_at survives a later revert as the record of when the change had been in force. evolution_list now also returns resolved_at and note, 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/) — one util.parseArgs-based parser replaces the ten hand-rolled parseArgs() 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 honour NO_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; engines stays at >=20.
  • Colour is emitted directly rather than through util.styleText — styleText runs its own TTY check against process.stdout and silently returns text unstyled when that check fails, overriding an explicit --color or FORCE_COLOR whenever output was piped. Detection now lives in one place. This also sidesteps styleText not existing before Node 20.12, which is below the declared engine floor.
  • NLI provisioning helpers moved to src/services/nli-env.ts so the registry, doctor and supervisor can use them without importing a command module. bin/nli-cmd.ts re-exports them; fozikio nli is unchanged.
  • Memory name is now a real label, not a raw text truncation. All three creation paths (goal_set, high-salience observe promotion, and the dream create phase) previously derived name by slicing the definition — goal_set did a raw mid-word slice(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 new engines/naming.ts centralises naming: deriveName(text, llm) mints a genuine short concept label via the LLM at creation time (the intended behaviour), falling back to deriveNameHeuristic(text) — first-sentence preference, word-boundary truncation, ellipsis on elision — whenever the LLM is unavailable, errors, or returns nothing usable. Adds the versioned label-concept prompt.

Fixed

  • CI ran tsc only, on ubuntu only. The vitest suite was configured but never executed by the merge gate, and nothing ever ran on Windows — where detached spawning, taskkill and PID handling all differ. Adds a Test job across ubuntu-latest and windows-latest. The existing Type Check job 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:14b is a reasoning model, and its reasoning tokens are drawn from the same num_predict budget as the answer. Measured directly: at num_predict: 300, thinking consumed all 300 tokens, response came back empty with done_reason: length, in 17.3s. The identical call with think: false answered completely in 50 tokens and 3.1s. Every engine call site is bounded — naming.ts caps at 32 tokens, social-draft at 100, reflect at 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. Both generate and generateJSON now send the native think: false.
  • The two existing defences were both inert. stripThinking() matches inline <think>...</think> blocks, but current Ollama returns reasoning in a separate thinking field that never matches — it stripped nothing while the answer was already gone. generateJSON prefixed prompts with /no_think, the legacy qwen3 soft switch, measured identical to sending nothing (300 tokens into thinking, empty response) — so JSON calls were parsing an empty string while the comment above them claimed they were protected. That prefix also leaked a literal /no_think line 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.
  • dream never recorded that it ran, so consolidation monitoring could never fire. sleep_pressure and consolidation_status both answer "when did consolidation last happen" by reading a dream_state doc and falling back to a consolidation_history collection — and nothing in the engine wrote either one. Both tools therefore returned last_dream_at_iso: null and hours_since_dream: null permanently, no matter how many times dream had 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_status also derives its last_dream summary and quality_trend from the same collection, so both were always empty. Verified against a live store: 3,131 of 3,137 observations processed, and the system and consolidation_history collections holding zero rows. dream now 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_quality is deliberately left absent rather than filled with a stand-in, since dreamConsolidate does not compute one and both readers already treat it as nullable.