Mímir Agent Application#
Purpose of this document#
This is the canonical guide to the Mímir application as it exists now: how a request enters the service, how the Mastra agent is assembled, what it can retrieve, what it remembers, what it cannot see, what each tool does, how answers reach Slack and the portal, how the service is tested and deployed, which capabilities have been proven in production, and which parts remain roadmap rather than runtime.
The document deliberately separates five states that older architecture notes sometimes mix:
| State | Meaning |
|---|---|
| Live | The path is registered in the production application and available when its binding is healthy. |
| Production-proven | A live control exercised the deployed path and left a durable receipt. |
| Implemented, not primary | Code exists but the live entry point does not currently route through it. |
| Open risk | The current implementation works but has a known security, completeness, or operating limitation. |
| Planned | A ratified spec or open GitHub issue describes it; it is not current behavior. |
Source code is the authority for runtime behavior. GitHub issues and production receipts establish work state and proof. Specifications establish intended future architecture. Historical docs are useful context but do not override current code.
1. Product definition#
Mímir is Viska Capital's internal research agent. Its primary interface is Slack; it also exposes a server-sent-events chat API for the Viska portal. It is not designed as a general chatbot. Its core job is to turn Viska's research corpus, fund positioning, strategy data, and selected live-market sources into a concise investment view.
The live agent is intentionally opinionated:
- lead with a position rather than a neutral summary;
- ground figures, ratings, and attributions in retrieved evidence;
- check the live fund book and strategy before recommending an asset;
- compare stale research prices with current market data before using them in advice;
- use Viska's internal corpus first;
- use web search only when explicitly requested or clearly outside corpus coverage;
- disclose uncertainty instead of inventing a total, citation, link, or source;
- keep answers dense and useful in Slack.
Mímir's current strongest workflow is:
LIST → select up to three documents → DIVE into complete bodies → connect the evidence to positions, strategy, and market data → optionally generate a report.
2. System boundary#
2.1 What this repository owns#
This repository is the Mastra application layer. It owns:
- the Node.js service;
- Slack Socket Mode integration;
- the
/api/chatportal interface; - the system prompt and model selection;
- tool definitions and application-side data access;
- answer streaming and Slack rendering;
- conversation memory integration;
- App Home and interactive actions;
- query, feedback, and error telemetry;
- Railway deployment for the
mimirandmimir-stagingservices.
It does not own the upstream research ingestion pipeline or database schema. Ingestion and normalization are owned by ViskaN8N; schema, grants, RPCs, and RLS are owned by ViskaDB. GitHub and credential bindings are operated under the Viska fleet ownership model.
2.2 External systems#
| System | Role |
|---|---|
| Slack | Primary client surface through Bolt Socket Mode; mentions, DMs, threads, buttons, commands, and App Home. |
| Railway | Hosts production and staging Node services; production tracks main. |
| Supabase/Postgres | Research corpus, structured parcels and bodies, fund/strategy surfaces, user settings, query logs, feedback, and Mastra persistence. |
| OpenAI / configured Mastra model provider | Main answer model; OpenAI also supplies embeddings and OpenAI-only secondary synthesis surfaces. |
| Cohere | Optional reranking for vector/hybrid corpus results. |
| n8n | Research ingestion, report workflows, Dropbox link resolution, and selected operational webhooks. |
| Market APIs | Market candles, crypto, derivatives, DeFi, sentiment, and optional web/recent-community research. |
| Miðeind and BÍN | Icelandic grammar, translation, and morphology. |
2.3 What the runtime agent cannot access#
The deployed Mímir model has no tool for:
- the Viska Engineering Wiki or Knowledge Wiki;
- GitHub issues, pull requests, or project boards;
- Railway administration;
- credential stores or secret values;
- arbitrary shell commands or repository files;
- arbitrary SQL;
- unrestricted internet browsing.
The coding seat uses the Viska Wiki to maintain the application, but that is a separate agent environment. The client-facing runtime does not inherit the coding seat's memory or tools.
3. Runtime topology#
Slack app_mention / DM / participating thread
│
▼
Slack Bolt Socket Mode
│
▼
handlers/mention-mastra.js
│
auth + date + language + request context
│
▼
lib/mastra-agent.js
static system prompt + model
32 registered tool IDs
│
┌────────────┼──────────────┐
▼ ▼ ▼
structured DB vector corpus external/n8n
retrieval retrieval services
└────────────┼──────────────┘
▼
lib/mastra-stream.js
│
rotating Slack stream messages
│
▼
source links + action buttonsThe same mimirAgent also serves the portal:
viska.gg edge → POST /api/chat → handlers/chat-http.js
→ SSE text / sources / actions / meta / done
→ same agent, tools, model, and Mastra memory3.1 Process startup#
src/index.js starts two services in parallel:
- Fastify, exposing health and application endpoints.
- Slack Bolt, using an outbound Socket Mode WebSocket.
If Slack credentials are absent, Fastify still starts. This supports local development and health checks, but it also means /health proves process liveness only; it does not prove Slack, corpus, or memory readiness.
At boot:
mastra-instance.jsregisters the agent and observability;mastra-storage.jsinitializes one shared PostgresStore connection pool;- startup checks report missing bindings and basic connectivity;
- Railway checks
GET /health; - Slack Socket Mode connects if its three Slack credentials exist.
3.2 Slack ingress#
The live Slack handler accepts:
- an
app_mentionin the configured channel; - any direct message;
- a reply in an allowed-channel thread where the bot already participated.
The handler:
- strips the Slack mention;
- checks the
viska_chat_usersallowlist; - adds today's date and language guidance;
- creates a request-local Mastra
RequestContext; - posts one temporary progress line;
- invokes the agent with
maxSteps: 5; - streams answer text into Slack;
- enriches eligible broker sources with Dropbox links;
- adds deep-dive, report, follow-up, and feedback actions;
- records query telemetry and semantic conversation copies;
- removes the progress line.
The progress mechanism updates one status message rather than flooding a thread with one message per tool call.
3.3 Portal ingress#
POST /api/chat is an authenticated SSE endpoint. It accepts a message, user identity, optional thread, caller metadata, role, and page-derived face. It emits:
textevents during generation;- a structured
sourcesevent; - an
actionsevent for follow-ups, deep dives, and report generation; - a
metaevent carrying the thread title; doneorerror.
The portal maps email identities to Slack user IDs where possible so both surfaces can share the same user-scoped working memory. If no mapping exists, the verified portal identity becomes the memory resource key.
3.4 Response delivery#
Slack's stream API rejects oversized individual messages. Mímir now rotates a generated answer into additional messages in the same thread before 3,500 characters. The exact generated text is preserved across those messages.
Non-streamed environments fall back to agent.generate() and a normal Slack reply. Empty generated streams fall through to an explicit no-answer message rather than leaving the user with silence.
4. Agent construction and instructions#
4.1 Agent singleton#
src/lib/mastra-agent.js creates one Agent named Mimir with:
- instructions loaded from
src/config/system-prompt.mdat process start; - a model selected by
MIMIR_MODEL, defaulting toopenai/gpt-5.2; - the registered tool map;
- Mastra memory when Postgres persistence is available.
Changing MIMIR_MODEL and restarting changes the answer model without a code deployment. OpenAI-only secondary tools continue to use an OpenAI model even when the main agent is routed through another provider.
4.2 Main instruction hierarchy#
The system prompt gives the agent the following priorities:
- Be Viska's analyst, not a search-results relay. Take a position and explain what Viska should do.
- Lead with the call. Open with one bold action-oriented conclusion.
- Use evidence compactly. Hard figures, ratings, prices, and source claims must come from retrieved evidence.
- Use at most one short verbatim quote. The rest is synthesis.
- Check current positioning and strategy before asset advice.
- Never present a research price as current. Run it through
price_context. - Search the internal corpus first. Web search is a last resort.
- Choose structured retrieval when the query maps to known fields. Use vectors for conceptual or paraphrased questions.
- Read every ordered body part for long articles. Do not analyze only the first excerpt.
- Never reproduce a private newsletter body in full. Synthesize it and quote only briefly.
- Use reader language. Internal terms such as “OKF,” parity, watermark, and indexed-through do not appear unless operational details are requested.
- LIST before DIVE. Index listings are metadata-only; full analysis and validated links follow after the user selects up to three titles.
- Be honest about empty results and coverage. A clean empty query does not prove an empty corpus if the read surface may be unavailable.
- Keep Slack concise. No markdown headers, no default bullet dump, and no AI-fingerprint filler language.
4.3 Request-local context#
The application augments the static instructions with request facts:
- current date;
- language instruction;
- Slack or portal user identity;
- thread ID and surface;
- optional portal face/page context;
- progress callback;
- source metadata accumulated by tools;
- last-used tool telemetry.
The model cannot choose or rewrite these request facts.
5. Memory architecture#
“Memory” in this application refers to several different stores. Only some are read back into the model. Keeping these categories separate is essential.
5.1 Active Mastra conversation memory — live#
The active runtime memory is @mastra/memory backed by one shared @mastra/pg PostgresStore.
| Memory | Key | Scope | What the model receives |
|---|---|---|---|
| Thread history | Slack thread timestamp or portal thread ID | One conversation | Up to the last 20 messages. |
| Working memory | Slack user ID, or mapped portal identity | One user across that user's threads | A compact profile containing research focus, preferred organisations, sectors, recurring topics, query patterns, date habits, language, and context notes. |
The agent automatically reads and updates working memory through Mastra's updateWorkingMemory mechanism. Thread history enables follow-up questions without requiring the user to restate the document or topic.
Slack and portal can share working memory when the portal identity bridge resolves to the same Slack user. Thread history remains thread-specific.
Storage behavior#
VISKA_DB_URLis preferred;DATABASE_URLis the fallback.- The shared connection pool defaults to six connections.
- Initialization retries four times with exponential backoff.
- On total failure, the service remains online but emits a loud
MEMORY-DISABLEDalert and answers statelessly. - The same store persists Mastra observability spans.
5.2 Query logs — active, not model memory#
mimir_queries stores the user query, last-used tool, answer length, and up to 4,000 characters of response text. App Home uses this for recent-query features.
This table is operational history. It is not automatically recalled into an answer.
5.3 Semantic conversation copies — write-only in the current primary path#
After a Slack response, mention-mastra.js writes user and assistant messages with embeddings to mimir_conversation_memory.
The current primary handler has no corresponding read path. Therefore:
- the data exists for future semantic recall or analysis;
- the live agent does not currently search it as memory;
- it must not be described as active long-term recall.
5.4 Legacy file-based adaptive memory — implemented, not primary#
The repo contains a second memory subsystem under:
agentMemory.js;memoryContext.js;userSession.js;goalRegistry.js;themeTracker.js;memoryCommit.js.
It can maintain per-user JSON profiles, session summaries, team goals, and theme frequencies in a git-tracked memory/ directory. The legacy handlers/mention.js reads and writes it.
The live src/index.js routes Slack to mention-mastra.js, not mention.js. No memory/ files are currently tracked. This subsystem is therefore code present in the repository, not active memory for the deployed agent.
5.5 Preferences and identity#
mimir_user_prefs and viska_chat_users provide user configuration and access control. Preferences can include watchlists, preferred organisations, language, and digest choices. They are explicit user settings, not free-form agent memory.
5.6 What memory does not contain#
The runtime does not remember:
- the Viska Wiki;
- GitHub work state;
- deployment history unless it appears in the current answer context;
- arbitrary previous Slack channels;
- another user's conversations;
- the full research corpus as prompt memory;
- secret values.
Research documents are retrieved on demand through tools. They are knowledge sources, not conversation memory.
5.7 Current memory limitations#
- Memory is physically shared in the default Mastra schema rather than isolated by face.
- Working memory is keyed by user, not user-plus-face.
- The portal v1 face changes framing but does not partition memory or tools.
- GDPR deletion and retention for the active Mastra tables remain tracked work (#23 and #31).
- The documented 90-day policy applies to the legacy
viska_chat_memorysurface; it is not, by itself, proof that every activemastra_*table is pruned on that schedule.
5.8 Planned memory model#
The ratified sharding spec proposes:
- one memory schema per shard, such as
mem_slack; - resource IDs shaped as
user::shard; - thread IDs shaped as
shard::session; - physical separation of semantic vector recall;
- an explicit
agent_identitytable for facts intentionally shared across faces; - a controlled migration of existing threads and working profiles.
That architecture is planned under epic #26 and phase #31. It is not the current runtime.
6. Research and data architecture#
6.1 Two complementary retrieval systems#
Mímir uses two research paths:
Structured research#
Structured retrieval reads stable research parcels and private article bodies through scoped PostgREST calls.
Use it when the request contains a field the database understands:
- ticker;
- controlled narrative;
- publisher/source;
- source kind such as Substack;
- publication date;
- named article title;
- explicit body-search terms.
Advantages:
- deterministic filters;
- current stable revision only;
- exact source and publication metadata;
- complete article bodies where available;
- honest totals and pagination;
- normalized stored links;
- no need to pretend a vector similarity score is a database fact.
Vector/hybrid retrieval#
Conceptual questions use embeddings, keyword search, and optional reranking over research chunks. Structured searches also fall back to vectors when the concept does not map cleanly to controlled fields or when the structured surface cannot answer.
Advantages:
- handles paraphrases and themes;
- searches broker research chunks effectively;
- finds semantically related evidence when metadata does not carry the idea.
The two paths are complementary. Structured parcels supersede vectors for exact contracts; vectors remain the conceptual fallback.
6.2 LIST and DIVE#
LIST#
okf_corpus_page and okf_corpus_totals operate on the research index. They provide:
- complete measured totals;
- publisher and source-kind breakdowns;
- cursor pagination;
- current index rows;
- a source-coverage status.
A page is not mistaken for the total. Requests up to 50 rows should use one page call. The client sees a concise coverage note rather than internal parity language.
The index is metadata-only. It does not imply that article bodies or links were read.
DIVE#
okf_corpus_search retrieves a named private body or searches body text. Long bodies are split into ordered parts of at most 12,000 characters, at paragraph boundaries where possible, with no discarded text.
Body results include normalized links stored with the stable parcel. Mímir may surface validated links inline beside the claim they support. Missing or rejected links are not reconstructed.
6.3 Stable revisions#
Structured body, parcel, and visibility-probe reads filter to status=stable. Deprecated revisions remain historical database records but cannot contaminate normal answers.
6.4 Source coverage language#
The research index is a live view of current database rows. A newer visible article proves that article is present; it does not prove that every source item has been reconciled.
The reader-facing default is:
Current articles are visible, but complete source coverage has not yet been independently verified.
Operational parity, watermark, and indexed-through values remain available for engineering diagnosis but are not normal client vocabulary.
6.5 Access pattern#
Structured research and market/fund reads use the pgGet()/pgRpc() helper:
apikeycarries the Supabase project gate key;Authorization: Bearercarries the scoped Postgres-role JWT;- PostgREST applies the role's grants and RLS;
- application code receives rows, not arbitrary SQL capability.
The broad SUPABASE_SERVICE_KEY still exists in other runtime call sites. Replacing those reads and writes with explicit RO/RW identities is open issue #126.
7. Tool inventory#
The current agent map contains 32 registered tool IDs:
- 20 core tools from
mastra-tools.js; - 9 fund, market, and structured-corpus tools from
market-data-tools.js; - 2 active module tools;
- 1 registry-owned explanation tool.
The example_skill module is a live reference/template, not a meaningful client capability. Client value comes from the remaining 31 surfaces.
7.1 Research corpus and document tools#
| Tool | Capability | Best use | Important limit |
|---|---|---|---|
okf_corpus_search | Structured parcel search, named private-body retrieval, and body full-text search; vectors on conceptual fallback. | Fresh research by source/ticker/date; full article analysis. | Metadata max 20; body results max 3; explicit body misses do not vector-fallback. |
okf_corpus_page | Complete cursor-paginated research index. | Recent documents, publisher/source lists, exhaustive metadata navigation. | 50 rows per page; no bodies or validated links. |
okf_corpus_totals | Complete totals by kind and publisher, publisher collisions, and coverage state. | “How many reports?” and coverage inventory. | Counts database rows; coverage status may remain unverified. |
corpus_search | General hybrid/vector search across all organisations. | Broad conceptual or paraphrased research questions. | Quality depends on chunking, embeddings, and optional reranking. |
search_by_org | Hybrid search constrained to one named organisation. | “What does Goldman say about oil?” | Requires one explicit organisation. |
scan_doc_matches | Finds reports whose titles or narrative mention a company, ticker, or term. | Coverage and mention discovery. | Entity ambiguity can trigger weak or repeated searches. |
system_two_briefing | Detailed briefing on a specific document. | Broker-note deep dives. | Secondary synthesis is OpenAI-only. |
librarian_scan | Measures how often a term appears across document days. | Recurrence and breadth checks. | Lexical occurrence is not conviction or topicality. |
doc_manifest_list | Lists documents ingested for a specified date. | “What came in today?” | Uses ingestion date conventions; not a content search. |
discovery | Lists publishing organisations or publication frequency. | Corpus activity and source coverage. | Organisation activity is not article analysis. |
doc_link_lookup | Resolves a specific document's Dropbox link. | Direct access to a known broker document. | Requires a known document identity and link workflow. |
7.2 Portfolio, strategy, and market tools#
| Tool | Capability | Best use | Important limit |
|---|---|---|---|
get_positions | Reads the current Interactive Brokers book, value, weight, cost basis, and unrealized P&L. | Ground any recommendation in current exposure. | Accuracy follows the latest available position snapshot. |
get_position_history | Reads one ticker's EOD position trajectory. | Explain adds, trims, and exposure changes. | One ticker; limited retained history. |
get_theme_transitions | Reads entered, exited, scaled-up, and scaled-down themes. | Explain portfolio rotation by theme. | Depends on upstream transition production. |
get_strategy | Reads strategy reports and conviction verdicts. | Test whether an idea fits Viska's intended themes. | Scope and freshness follow the strategy surface. |
get_market_data | Reads cached candles and invokes the authenticated fetch path on a miss. | Mark research against current trading data. | Vendor coverage and daily budget may return typed gaps. |
price_context | Compares a research price with latest close and calculates delta/staleness. | Mandatory before advice based on a report price. | Requires a valid referenced price and available market data. |
sector_sentiment | Reads computed sentiment, direction, deltas, rolling averages, and drivers for tracked sectors. | Sector risk and trend questions. | Limited to the configured sector set and scoring pipeline. |
7.3 Live, web, and digital-asset tools#
| Tool | Capability | Best use | Important limit |
|---|---|---|---|
crypto_price | Live crypto pricing through exchange data. | A current crypto quote or 24-hour context. | Public exchange availability. |
market_sentiment | Fear/greed and top funding-rate context. | Crypto market mood. | Indicator, not research evidence. |
derivatives_data | Funding rates, open interest, and perpetual-futures positioning. | Derivatives positioning by symbol. | External API coverage and bindings. |
defi_stats | Protocol TVL, chain rankings, stablecoins, and DeFi overview. | DeFi market structure. | External API semantics and freshness. |
web_search | Explicit web/internet search fallback. | Off-corpus or explicitly requested current information. | Deliberately not the default for financial research. |
last30days | Multi-platform recent discussion across Reddit, Hacker News, GitHub, web, SEC/news/prediction markets for finance topics. | “What are people saying lately?” | Optional sources depend on API keys; engagement is not truth. |
7.4 Workflow and personalization tools#
| Tool | Capability | Best use | Important limit |
|---|---|---|---|
trigger_report | Starts the daily-report workflow. | Explicit report-generation requests. | Depends on authenticated n8n workflow availability. |
custom_report | Builds a report from the current Slack thread. | Turn a completed research discussion into a durable deliverable. | Requires enough thread content and report pipeline health. |
user_prefs | Reads and updates watchlist, preferred organisations, language, and digest choices. | Personalize App Home and agent behavior. | Explicit preferences only; not free-form memory. |
7.5 Language and presentation tools#
| Tool | Capability | Best use | Important limit |
|---|---|---|---|
bin_lookup | Icelandic morphology lookup through BÍN. | Correct inflection and word forms. | Morphology, not broad translation. |
icelandic_translate | English-to-Icelandic translation. | Explicit translation requests. | Depends on Miðeind binding. |
humanize_text | Removes formulaic AI phrasing and makes supplied text more natural. | Rewrite an existing passage. | It does not fact-check the passage. |
7.6 Module-system tools#
| Tool | Capability | Status |
|---|---|---|
last30days | Client-facing recent multi-platform research module. | Active. |
explain_skill | Explains registered module capabilities from their manifests. | Active registry tool. |
example_skill | Echo/reference module proving tool, slash-command, and App Home wiring. | Active template; not a client research feature. |
8. Proven production capabilities#
The following claims have live or process receipts. They are narrower than “the code exists.”
| Capability | Proof status | Receipt |
|---|---|---|
| Production service boots, connects Slack Socket Mode, and serves health checks. | Production-proven | Railway deployment 4824852e succeeded on 2026-08-13. |
| Complete corpus totals and cursor pagination reach the registered Mímir tools. | Production-proven | Issue #124 / PR #129: 1,969 total at the time of control; 89-row Substack walk, two pages, zero duplicates. These are historical control figures, not current totals. |
| Structured source, date, ticker, and narrative filtering reaches live parcel rows. | Production-proven | Issue #117 / PR #118 and Railway controls. |
| Named private Substack bodies are retrievable through the Mímir application path. | Production-proven | Issue #122 / PR #123. |
| Long articles are delivered without application truncation. | Production-proven at tool boundary | PR #140: 48,920-character control returned in five ordered parts; focused retrieval suite 53/53. |
| Stable revisions exclude deprecated parcel versions. | Production-proven | PR #145 and stable-v4 audit. |
| Stored normalized body links reach the application result. | Production-proven at tool boundary | PR #142. |
| Stable Substack link surface contains no unsafe redirects or navigation links in the audited set. | Production-proven for the audited snapshot | Final stable-v4 audit: nine retained related links, zero unsafe/chrome/unclassified remainder. |
| Real Mastra tool calls are observable by tool name. | Production-proven | PR #134 fixed nested ToolCallChunk.payload handling. |
| Slack can deliver long answers over multiple thread messages. | Production-proven | 4,087 characters delivered across two messages after PR #148, with no msg_too_long. |
| Empty-stream and progress-flood failure classes do not leave users with silent or erratic replies. | Regression-proven; prior production incidents reproduced | Response-delivery and think-post regression suites. |
| Mastra thread and working memory initialize in production. | Production-observed | Current startup logs show PostgresStore connected and memory enabled. |
8.1 Acceptance still open#
The tool boundary has proven complete five-part article delivery. The final client-facing acceptance for issue #122 remains a fresh, fully delivered model synthesis that demonstrably uses evidence across all parts after the Slack split fix. A partial answer before the fix proved synthesis was occurring, but the delivery ceiling prevented a clean end-to-end acceptance receipt.
Validated links are proven in body results. Broad inline-link presentation across multiple selected articles remains a reader-level behavior to continue testing; metadata LISTs intentionally carry no links.
9. Where Mímir excels#
9.1 Named-document analysis#
A named title maps directly to a private body. This avoids broad retrieval noise, supports complete ordered long-body analysis, and can carry validated stored links.
9.2 Structured research discovery#
Questions that specify publisher, source type, ticker, narrative, or date can use indexed filters. This is faster, more complete, and more interpretable than pretending every question is vector similarity.
9.3 Cross-source synthesis with bounded scope#
Mímir is effective when a question names a theme, time window, and manageable source set. It can compare agreements, disagreements, catalysts, and risks across broker, internal, and Substack research.
9.4 Research-to-portfolio reasoning#
The combination of positions, history, strategy, market data, and price context lets the agent answer “what should Viska do?” rather than “what did the report say?”
9.5 Provenance and refusal#
Mímir is designed to withhold unsupported figures, false totals, invented links, and unavailable citations. Stable-only retrieval and explicit coverage notes make uncertainty visible.
9.6 Slack-native interaction#
Thread memory, progressive streaming, long-message rotation, buttons, report generation, and App Home make the agent useful without requiring analysts to leave Slack.
10. Current limitations and failure boundaries#
| Limitation | User effect | Current handling / owner |
|---|---|---|
| Index LISTs contain metadata, not bodies or links. | A broad list cannot display every article's validated links. | User selects up to three titles for DIVE. |
| Source parity is not independently verified. | Mímir cannot promise every possible upstream article is present. | Plain coverage note; source reconciliation is upstream. |
| Five tool steps per request. | An exhaustive query combining discovery, many bodies, positions, prices, and synthesis may run out of steps. | Split into a thread: LIST, DIVE, portfolio check, report. |
| Body search returns at most three rows. | Large cross-article body sweeps need multiple turns or a purpose-built batch tool. | Use index selection and bounded DIVE. |
| Structured metadata search returns at most 20 rows. | Use the index tool for larger lists. | okf_corpus_page supports 50 rows and cursor continuation. |
| Entity ambiguity. | Short names such as “MYR” can route to the wrong company or empty searches. | Entity-resolution protocol is open issue #61. |
| External API and webhook dependencies. | Optional market, web, rerank, language, or report features can degrade. | Typed errors and graceful fallback; bindings monitored separately. |
| Broad service credential remains in some call sites. | The application has more database authority than the final design permits. | Scoped RO/RW cutover is issue #126. |
| Public HTTP hardening remains open. | Some current gates and health semantics are weaker than the intended final posture. | Security issue #113; must ship red auth cases. |
| Slack authorization currently fails open on DB errors. | A database error can bypass the allowlist check in current code. | Explicitly in #113 scope; not a settled security posture. |
| Missing channel configuration currently warns rather than fully refusing all paths. | Misconfiguration can weaken channel isolation. | #113 changes this to fail closed. |
/health proves process liveness only. | A green health response does not prove Slack, corpus, or memory. | Startup logs carry advisory checks; #113 proposes degraded health state. |
| Active memory is not face-sharded. | Future page-specific agents would share user working memory. | Epic #26 / phase #31. |
| Semantic conversation copies are not read. | No long-term semantic recall despite embeddings being written. | Either wire scoped recall or remove the misleading write path in dedicated work. |
| Legacy file memory remains in the repo. | Documentation can mistake inactive code for current behavior. | Treat mention-mastra.js as primary; retire or isolate legacy path later. |
| Documentation has drifted historically. | Counts, models, handlers, and pipeline descriptions can become stale. | This guide cites code and labels roadmap separately; update it with architecture-changing PRs. |
11. High-value client queries#
The following query shapes make the best use of current capabilities.
11.1 Discover, then select#
What are the 15 newest Substack articles? Group them by publisher and give each a one-sentence thesis.
Follow with:
Read these three in full: “[title],” “[title],” and “[title].” Compare their conclusions and include validated links inline.
11.2 Cross-source investment thesis#
Across recent broker, Substack, and internal research, what is the strongest investable view on European defense-production capacity? Show agreement, disagreement, catalysts, named securities, and risks.
11.3 Research against the live book#
What does recent research imply for our current defense exposure? Check our positions and strategy first, then identify where we are overexposed, underexposed, or missing the theme.
11.4 Reprice an old call#
Find the latest research on Rheinmetall, compare every cited research price with the latest available market price, and tell us whether the original upside still exists.
11.5 Explain portfolio changes#
How has our position in [ticker] changed over the last month, and does the latest research support the adds or trims?
11.6 Find consensus and disagreement#
Compare the latest views from Goldman, Pareto, Citrini, and Doomberg on AI capital expenditure and credit creation. Where do they disagree, and which evidence should Viska trust most?
11.7 Locate evidence#
Which documents mention missile shortages, interceptor depletion, or defense-production bottlenecks? Group results by publisher and date.
11.8 Full long-article analysis#
Read “[article title]” in full. Reconstruct its argument in order, identify every investment category and named company, separate evidence from inference, and display validated links inline.
11.9 Monitor change#
What changed in the research corpus over the last seven days regarding European energy security? Focus only on genuinely new evidence or changed conclusions.
11.10 Produce a deliverable#
Turn this thread into a client-ready research report with the investment call, evidence, risks, catalysts, and sources.
12. Extension architecture#
12.1 Adding a core tool#
Core tools use Mastra createTool() with a Zod input schema. The preferred pattern is:
- define a narrow capability and trust boundary;
- reuse an existing data helper before adding a client layer;
- expose only structured arguments the model is allowed to choose;
- return both machine-readable fields and concise
slackText; - record source metadata when post-answer link enrichment applies;
- add one focused red/green test;
- register the tool in
mastra-agent.js; - update the system-prompt routing rule;
- prove the arriving production identity, not only a direct helper call.
A tool should not expose arbitrary SQL, arbitrary HTTP, or credential material.
12.2 Adding a module#
The module system is the preferred front door for a self-contained capability with multiple surfaces. A module can provide:
- a natural-language Mastra tool;
- a
/mimir <command>handler; - an App Home card and guide;
- help text and examples;
- required and optional environment bindings.
A manifest is validated on every registry read. Invalid or inactive modules are skipped rather than breaking agent startup. New modules begin through /spec-module, then add one manifest to the explicit registry.
The last30days module demonstrates the pattern. The example module is the reference template.
12.3 Adding a face#
Current portal faces are instruction addenda only. A new v1 face requires:
- ViskaFront route-to-face mapping at the trusted edge;
- a face entry in
src/config/faces.json; - a host instruction posture;
- tests for known and unknown face behavior.
A face currently does not change tools, data grants, or memory. Those hard boundaries belong to the future sharded architecture.
12.4 Adding a data surface#
A data surface needs three owners:
- producer owner for how data is created and refreshed;
- ViskaDB for schema, grants, RLS, and RPC contracts;
- ViskaMimir for the minimum application read/write call and user behavior.
Before wiring:
- inspect live schema;
- define exact columns and operations;
- assign RO versus scoped RW identity;
- add positive and denial controls;
- prove the request through Mímir's actual transport;
- disclose freshness and completeness semantics.
12.5 Adding a report or visual artifact#
Research conversation reports use the existing custom-report flow. New application-maintenance documents use OKF frontmatter in repo docs and render through /mockup for review. Client /skjol reports follow the separate report-bucket and projection contract.
13. Maintenance and operations#
13.1 Source-of-truth order#
For application maintenance:
- GitHub issue/project — work state and acceptance;
- Viska Engineering Wiki — decisions, directives, and cross-seat context;
- current repo code — running contracts;
- this guide and task-specific docs — orientation.
When prose and code disagree about runtime behavior, code wins and the prose is updated.
13.2 Development lifecycle#
- Open or use a board-backed issue.
- Read the live PRD and relevant vault decisions.
- Work on a Mímir-owned branch.
- Reuse existing helpers and patterns.
- Add the smallest test that fails on the reproduced defect.
- Run focused tests, then broader tests where appropriate.
- Commit as
Mímir Agent <mimir@viskasjodir.is>. - Push with the declared credential helper and verify the remote ref landed.
- Open a linked PR.
- Merge with rebase when ready.
- Production deploys from
main; no local-tree production deploy. - Verify Railway deployment state and the live behavior relevant to the issue.
- Persist the receipt on the issue and update architecture docs when contracts changed.
13.3 Testing#
The repository uses Node's built-in node:test runner. Tests cover:
- tool argument and formatter contracts;
- retrieval filters, long bodies, negative controls, and fallbacks;
- streaming and multi-message delivery;
- prompt requirements;
- memory and storage resilience;
- handlers and routing;
- module manifests and registry behavior;
- market data, scoring, reports, auth, and error surfaces.
A green helper test is not enough for a deployed capability. Production acceptance should exercise:
- the registered tool;
- the arriving role/JWT;
- the actual Slack or portal transport;
- the failure twin where access or data is absent;
- the final reader-visible answer.
13.4 Deployment#
Railway tracks:
| Service | Branch | Policy |
|---|---|---|
mimir | main | Production; branch-tracked only. |
mimir-staging | develop | Staging; local-tree deploys allowed when needed. |
railway.toml starts node src/index.js, checks /health, and restarts on process failure up to three times.
Rollback is a previous successful deployment or a reverted commit on main. Production should never be repaired by pushing an unreviewable local tree directly to the service.
13.5 Observability#
Current observability includes:
- Mastra spans persisted to
mastra_ai_spanswith sensitive-data filtering; - per-step tool-call names and argument-key shapes;
- startup checks and memory-disabled alerts;
- structured request completion logs;
- query logs and feedback;
- error lifecycle and verification actions;
- Railway deployment and process logs.
Known limitation: tool telemetry records argument names, not argument values. This protects sensitive inputs but means logs can prove that sourceKind was available, not that it equaled substack on a specific call unless a separate receipt captures the result.
13.6 Credentials and bindings#
Documentation records environment variable names only. Credential values are injected through the approved environment and never enter repository docs or agent context.
Critical binding families include:
- Slack bot, signing, app, and channel identifiers;
- scoped Supabase gate/read/write identities;
- direct Postgres memory connection;
- model and embedding provider keys;
- optional reranking, language, web, market, and report integrations;
MIMIR_API_SECRETfor portal/HTTP integration.
Changes to credential bindings are owned by ViskaOps; application call sites and probes are owned here; roles and grants are owned by ViskaDB.
13.7 Documentation maintenance rule#
Update this guide when a PR changes any of:
- primary entry points;
- active memory read/write behavior;
- registered tool inventory;
- system-prompt contract;
- authentication or database identity;
- deployment topology;
- production-proven capability state;
- face/shard architecture.
Do not append session narratives. Replace the affected primitive and cite the issue or specification that changed it.
14. Roadmap#
Roadmap items are grouped by value and risk, not presented as current behavior.
14.1 Near-term hardening#
Scoped database identities — #126#
Replace remaining broad service-key call sites with explicit RO and scoped RW paths. Preserve overlap until positive and denial controls pass. This is the most important security/operability improvement because it reduces what a prompt-injected or defective path can reach.
Public HTTP hardening — #113#
Add one timing-safe pre-handler, fail closed on missing auth/channel configuration, restrict the page proxy, expose memory degradation in health, remove obsolete routes, and check in unauthenticated red cases.
Entity resolution — #61#
Rank candidate entities, ask for confirmation only when confidence is low, and connect the resolved identity to canonical tickers. This improves recall and prevents repeated empty tool loops.
Complete issue #122 reader acceptance#
Run a fresh long-article prompt after explicit token-spend consent and verify evidence from all ordered parts arrives completely after the Slack split fix.
14.2 Memory and face architecture — #26#
Planned phases:
- face/shard registry and SQL helpers;
- request-scoped face client replacing broad credentials;
- dynamic agent instructions, tools, model, and memory by face;
- schema-per-shard memory and migration;
- identity facts promoted to a governed shared table;
- webhook auth audit and soak verification.
This changes faces from presentation context into real capability, data, and memory boundaries.
14.3 Portal expansion#
The portal v1 contract already supports page-aware framing and shared identity mapping. Future value includes:
- research-page LIST and DIVE controls;
- portfolio-aware prompts seeded by page context;
- structured source cards and inline links;
- persistent report generation;
- user-visible memory controls;
- authenticated per-user artifacts;
- clear operator versus client roles enforced at the edge.
14.4 Retrieval improvements#
Potential extensions that fit the current architecture:
- batch DIVE for a bounded selected set larger than three, with explicit cost controls;
- article-level related-document graph using structured provenance;
- source reconciliation receipts to promote coverage from “unverified” to “verified”;
- deterministic link cards for selected articles;
- retrieval plans that reserve tool steps before multi-stage analysis;
- canonical entity IDs across broker, Substack, positions, and market data;
- safe semantic recall over selected research notes without collapsing provenance.
14.5 Memory improvements#
Before adding more memory, decide what class each fact belongs to:
- thread memory: temporary conversational continuity;
- working memory: user preferences and research habits;
- identity: explicit cross-face durable facts;
- research corpus: sourced documents retrieved by tools;
- telemetry: logs and analytics, never silently recalled.
Future memory work should add:
- user-visible “what Mímir remembers” inspection;
- correction and deletion controls;
- retention enforcement on active Mastra tables;
- scoped semantic recall only after shard isolation;
- explicit promotion from conversation to identity rather than ambient extraction.
14.6 New modules#
High-value modules that fit the registry contract include:
- earnings-event preparation;
- watchlist change monitor;
- thesis tracker with evidence deltas;
- source-consensus matrix;
- portfolio exposure stress brief;
- morning research inbox triage;
- “what changed since my last read?” document comparison.
Each should be built only when there is a board-backed client use case, a data contract, and one clear acceptance query.
15. Recommended product sequence#
The shortest path to a stronger client product is:
- finish scoped RO/RW identities;
- close HTTP/channel fail-closed hardening;
- complete long-body reader acceptance;
- add entity resolution;
- expose LIST → selected DIVE → portfolio-context → report as a first-class portal and Slack flow;
- add memory inspection/deletion before increasing recall;
- implement true face capability/data/memory sharding only when additional faces are ready to ship.
This sequence improves trust and client utility without creating a second agent, a second corpus, or a second memory system.
16. Quick reference#
Best current use#
- recent document discovery by publisher/source/date;
- complete named-article analysis;
- bounded cross-source synthesis;
- research checked against the live fund book and current prices;
- document links and validated media links after DIVE;
- thread-to-report conversion.
Avoid as one giant prompt#
- exhaustive entire-corpus body analysis;
- analysis of more than three private articles plus positions and live prices in one turn;
- claims that every upstream source item has arrived;
- requests for unavailable original citation links;
- ambiguous short entity names without context;
- general web browsing disguised as internal research.
Canonical implementation files#
| Concern | File |
|---|---|
| Entry point and routes | src/index.js |
| Slack request lifecycle | src/handlers/mention-mastra.js |
| Portal SSE lifecycle | src/handlers/chat-http.js |
| Agent registration | src/lib/mastra-agent.js |
| Main instructions | src/config/system-prompt.md |
| Core tools | src/lib/mastra-tools.js |
| Structured/fund/market tools | src/lib/market-data-tools.js |
| Conversation memory | src/lib/mastra-memory.js |
| Shared storage | src/lib/mastra-storage.js |
| Streaming | src/lib/mastra-stream.js, src/lib/slack-stream.js |
| Modules | src/modules/registry.js |
| Deployment | railway.toml, docs/DEPLOYMENT.md |
| Environment names | docs/ENV-REFERENCE.md |
| Future sharding | docs/specs/SPEC-runtime-memory-sharding.md |
| Portal face contract | docs/specs/SPEC-viska-gg-face-chat-v1-contract.md |
17. Final assessment#
Mímir is already a capable internal research application, not merely a chat wrapper. Its strongest differentiated asset is the combination of structured and semantic research retrieval, complete private article access, fund positioning, strategy, market context, and Slack-native delivery.
Its next stage should not be “more tools” by default. The highest-return work is to tighten identity boundaries, complete reader-level acceptance, resolve entities cleanly, make memory inspectable, and turn the existing LIST → DIVE → portfolio → report sequence into an obvious product workflow.
That preserves the current strengths while removing the places where a capable system can still appear uncertain, over-broad, or operationally opaque.