Threadnote uses Effect 4 for infrastructure, orchestration, lifecycle, and protocol boundaries. Pure memory algorithms, formatting, and React state remain plain TypeScript where an Effect service or typed error channel would not add value.
The package currently pins effect, @effect/platform-node, @effect/vitest, and
@effect/ai-openai-compat to the same exact Effect 4 beta. Threadnote intentionally uses effect/unstable/*; beta
upgrades are explicit and must pass the protocol and bundle gates below.
src/effect/command.tsowns interruptible child processes, typed command failures, timeouts, output limits, and git environment isolation.src/effect/http.tsowns HTTP status/text/JSON requests and typed transport/status failures.src/effect/time.tsowns polling and retry schedules. Tests useTestClock, so they do not wait in real time.src/effect/openviking.tsapplies typed transient failures and scheduled retries to OpenViking resource removal.src/effect/file_lock.tsprovides Effect-native, token-owned local critical sections for bounded candidate and feedback stores, including retry and stale-lock recovery.src/effect/runtime.tsonly assembles the application layer. It does not own aManagedRuntimeor expose execution helpers.- Each executable builds one Effect, provides the application layer once, and calls
NodeRuntime.runMainonce. Library code never callsrunPromiseor folds an Effect into a Promise; Effect-native workflows compose upward to that entry boundary. - Promise-based filesystem and compatibility helpers are lifted with
Effect.tryPromiseat composition points. The manager's Node HTTP callback submits request Effects to a scopedFiberSetruntime created by its entry Effect, so in-flight requests are interrupted when the manager scope closes. - Application Promise lifting is centralized in
src/effect/errors.ts; production modules use itsfromPromiseandfromSyncadapters with an operation label rather than declaring local copies. The CLI has no generic legacy-Promise command bridge: every command composes an Effect-returning workflow. - Application output uses Effect's
Consoleservice.src/effect/console.tscarries the current service across the remaining Promise compatibility boundary and captures scoped output without mutating process-global handlers. - Graph manifest reads, seeding, MCP installation, hook installation, lifecycle commands, recall, memory migration, pack I/O, and command execution compose through the application layer. Pure graph parsing, edge resolution, and Markdown rendering remain plain TypeScript.
- Recall feedback, candidate-review persistence, audit writes, session-closeout orchestration, and MCP candidate application compose as Effects using the shared filesystem and clock services. Deterministic BM25/ranking, confidence, explanation, candidate comparison, and memory document transforms remain pure TypeScript.
src/effect/share.tsis the Effect-facing adapter for the transaction-oriented sharing implementation. This keeps the CLI error channel and runtime boundary uniform while the lower-level rollback callbacks remain Promise-based.- Long-lived servers and Effect-owned temporary directories use
Scope/acquireRelease, so interruption closes servers and removes staged files.
Threadnote is an ESM package. The checked-in .cjs bin files are deliberately tiny CommonJS launchers that dynamically
import the ESM bundles. This avoids TypeScript's CommonJS-to-ESM require() mismatch while retaining npm bin
compatibility.
The CLI entry normalizes dash-prefixed string values and strings containing multiple = characters before invoking the
Effect CLI parser. This preserves Commander-era argument behavior while Effect 4 is in beta; the integration suite
protects the compatibility shim so it can be reevaluated on an upgrade.
MCP tool inputs use Effect Schema as the single source for handler types, runtime decoding, constraints, descriptions, and emitted JSON Schema. Empty input structs are normalized to an explicit JSON Schema object because Effect 4 beta currently represents an empty struct as an object-or-array union.
The stdio integration tests connect with the official MCP client and protect:
- tool names and core/full toolsets;
- server instructions and context-byte budget;
- JSON Schema fields and numeric constraints;
- runtime rejection of invalid inputs;
- OpenViking forwarding and share behavior.
Threadnote can use @effect/ai-openai-compat for structured consolidation drafts and bounded recall query expansion.
It is intentionally opt-in. After an applicable package update, Threadnote offers to install the recommended local
model; declining dismisses that offer without enabling or downloading anything. The same setup is available directly:
threadnote local-ai install
threadnote local-ai model switch
threadnote local-ai status
threadnote enrich-memories # preview personal and shared-memory backfill
threadnote enrich-memories --apply --install-local-aiWith no --model, local-ai install downloads the recommended pinned official Gemma 4 E4B Q4_0 GGUF file (4.59 GB).
It is currently the only verified model. Each download is verified by exact size and SHA-256, stored under
THREADNOTE_HOME/threadnote/models, and retained when another verified model is selected. Threadnote writes the
active model to a versioned THREADNOTE_HOME/threadnote/local-ai.json. Pass --model-path <path> to verify and reuse
an existing GGUF copy for the selected model instead.
The service binds only to 127.0.0.1:1934. Installation also creates a mode-0600 access token. Health checks use a
challenge-response proof before Threadnote sends any prompt, and the OpenAI-compatible endpoints require that token.
The full enrichment command processes each eligible personal memory and configured shared durable memory locally, then
stores up to eight compact keywords: headers without changing its URI, body, timestamp, or lifecycle metadata. It
prioritizes active memories before historical personal records. Shared skills, repository guidance, and other
non-memory files are excluded. Already-enriched memories are skipped, so an interrupted run can be resumed. Use
--force to regenerate keywords and --limit <count> for a bounded trial. A large corpus can take a long time; the
command prints [n/N] progress, generated keywords, failures, and a final summary instead of working silently. Shared
updates use the repository and memory locks but stay as local Git changes until the user runs the printed
threadnote share sync --team <team> command. A model response that cannot produce usable structured keywords leaves
that memory unchanged and does not fail the backfill; provider availability, timeouts, filesystem, parsing,
concurrent-edit, and storage failures remain retryable errors.
The opt-in post-update action is introduced for 3.0.0 and is also advertised during its prerelease cycle. If the
model is absent, accepting the enrichment action installs it first. New active personal memories written through the
CLI or Threadnote MCP are enriched automatically when the configured provider is loopback-local; storage still
succeeds unchanged if enrichment is unavailable or fails. Emergency pre-compact snapshots are never delayed for
enrichment.
Effect owns the service lifecycle: configuration, download orchestration, the lifecycle lock, detached process and PID
record, health checks, logging, lazy startup, and safe stop/uninstall behavior. The bundled Python file is only a thin
OpenAI-compatible HTTP adapter around the llama_cpp package already installed with OpenViking; it does not decide
when or how the process runs. Configured local AI follows threadnote start, stop, repair, and uninstall, and a
weak recall can lazily start a stopped service. A bounded readiness wait fails open, so model startup cannot prevent the
deterministic recall result from returning.
Stop verifies the authenticated launch ID, PID, model, and model path immediately before signaling the process. If a stale record or unhealthy endpoint cannot prove ownership, Threadnote refuses to signal that PID and leaves manual inspection to the user.
Use the dedicated commands for explicit control:
threadnote local-ai disable # stops and disables local AI; preserves its model and configuration
threadnote local-ai enable # enables the installed model; the next weak recall can start it lazily
threadnote local-ai start
threadnote local-ai stop
threadnote local-ai status
threadnote local-ai model switch # interactively select an installed model and press Enter
threadnote local-ai uninstall # preserves the managed model
threadnote local-ai uninstall --erase-model # also removes the managed modelstart and stop control only the current service process. enable and disable persist whether Threadnote may use
the installed provider for recall, enrichment, and consolidation. The generic switching flow is retained for future
verified models: it safely stops the authenticated current service, changes the active model, and restarts it when
local AI is enabled. If no verified managed model is present, the switch command exits successfully and prints the
installation command.
For recall, deterministic retrieval and ranking always run first. High-confidence results never call the model. For weaker results, an explicit loopback model first reranks at most 24 query-ranked local index candidates using bounded, scrubbed topic, title, and index excerpts. Selected topics become grounded rewrites: medium-confidence results evaluate one and low-confidence or no-answer results evaluate at most two, one search scope at a time. Mandatory OpenViking hits remain in deterministic ranking but do not crowd this grounded candidate window. Rewrites are schema-validated, deduplicated, cached by query/project/provider fingerprint, and limited to 512 characters.
Expanded candidates are merged with the original candidates and exact matches, then reranked by the same deterministic ranker. The loopback model receives at most 24 final candidate IDs and may keep at most 8 directly relevant IDs; it cannot add candidates or change their deterministic order. Threadnote retains the first two non-lexical deterministic anchors so a small model cannot suppress the strongest ranked evidence. An empty confident selection produces no answer, while a timeout, malformed response, or unknown-ID-only response preserves the deterministic result. Each model stage uses deterministic sampling, times out after five seconds, and fails open. Remote endpoints never receive candidate summaries or run either filtering stage.
Remote query expanders receive only the original query and inferred project. Explicit loopback endpoints additionally receive a bounded, scrubbed shortlist of local topic/identifier names with six-term index excerpts; rewrites that do not contain an exact shortlist term are discarded. This grounding data never goes to a non-loopback endpoint.
During recall, the local model does not extract additional memory candidates or mutate canonical memories. Candidate
extraction keeps its existing policy. Explicit enrichment only adds retrieval keywords to personal and configured
shared durable memories; older Threadnote versions ignore those unknown repeated headers and continue reading the
document body. Explicit manager consolidation may use the same provider to generate an Effect Schema-validated
{ "draft": string } object, but generating a draft never deletes source memories; cleanup remains a separate
user-approved operation.
Environment variables remain an advanced override for Ollama, LM Studio, hosted APIs, or another OpenAI-compatible chat-completions endpoint:
export THREADNOTE_EFFECT_AI=1
export THREADNOTE_EFFECT_AI_MODEL=<openai-compatible-model>
export THREADNOTE_EFFECT_AI_API_KEY=<key> # optional when the endpoint does not require one
export THREADNOTE_EFFECT_AI_API_URL=<base-url> # optional for the default OpenAI endpointAn explicit environment provider takes precedence over persisted local AI. Set THREADNOTE_EFFECT_AI=0 to disable the
persisted provider for one process.
Use an instruction-tuned model with at least a 2K context window. Small models can satisfy the JSON schema while still choosing superficial topic matches; the expander fails open, but recall quality depends on the local model.
Keep every Effect package on the same exact beta, then run:
npm run lint
npm run prettier:check
npm run typecheck
npm run build
npm run check:bundle-size
npm test
npm run pack:dry-runTreat a change to unstable MCP/AI behavior as a compatibility migration: update the adapter and parity tests together. Do not bypass schema, tool-list, instruction, lifecycle, or bundle-size failures merely to accept a beta upgrade.
The dedicated distribution suite uses the built CommonJS launchers, a real pinned OpenViking CLI/server, a random local port, and a suite-scoped temporary datastore. It exercises CLI memory and share writes, manager APIs and shutdown, native OpenViking MCP plus Threadnote stdio MCP, optional Effect AI consolidation, and an installed npm tarball. Global teardown stops the server and recursively removes the temporary datastore.
npm run test:e2e:install-openviking # explicit opt-in; installs the version pinned in src/constants.ts with uv
npm run test:e2e:local-binsThe E2E command refuses an OpenViking CLI/server version that differs from the Threadnote pin. CI runs the installer and
suite in a dedicated job, so updating the pin automatically validates the new OpenViking version across the shipped
entrypoints. If uv rejects a malformed prebuilt llama-cpp-python wheel, the installer retries from source with the same
bounded-parallelism and macOS Metal settings as Threadnote's production installer.