wyrm-mcp 8.3.0 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import{homedir as l}from"os";import{join as a,dirname as c}from"path";import{readFileSync as m,existsSync as y}from"fs";import{resolveEmbeddingState as h}from"./providers/embedding-provider.js";import{fileURLToPath as d}from"url";const w=d(import.meta.url),n=c(w),u=`Wyrm is not "AI memory" in the file-write sense. It's a layered intelligence
1
+ import{homedir as l}from"os";import{join as a,dirname as c}from"path";import{readFileSync as m,existsSync as y}from"fs";import{resolveEmbeddingState as h}from"./providers/embedding-provider.js";import{fileURLToPath as d}from"url";const u=d(import.meta.url),n=c(u),w=`Wyrm is not "AI memory" in the file-write sense. It's a layered intelligence
2
2
  substrate that solves the four specific problems AI assistants share:
3
3
 
4
4
  1. Amnesia. Sessions inherit prior state via lossless rehydration.
@@ -11,5 +11,5 @@ substrate that solves the four specific problems AI assistants share:
11
11
  Every Wyrm session is local-first SQLite with FTS5 + hybrid semantic search.
12
12
  Encryption is operator-controlled. Federation is opt-in per row. The audit
13
13
  log is hash-chained and Ed25519-signable for SOC2/HIPAA scenarios. None of
14
- this requires you to leave your laptop.`,p=[{name:"Counter-pattern memory",what:"Records failed approaches with (scope, target, description) and blocks the same suggestion next time.",why:"Other memory tools learn only from success. Wyrm refuses to repeat mistakes \u2014 saves the most expensive class of tokens (the regenerated wrong answer).",tools:["wyrm_failure_record","wyrm_failure_check","wyrm_failure_list","wyrm_failure_resolve"]},{name:"Ground truths (versioned, cascade-aware)",what:"Validated project facts (architecture, constraints, decisions) \u2014 each truth carries a version, last-verified timestamp, and outbound decision links.",why:"Truths are injected first into every wyrm_context_build call so the AI never works from contradicted assumptions. When a truth changes, dependent decisions auto-invalidate.",tools:["wyrm_truth_set","wyrm_truth_get","wyrm_decided_because","wyrm_decision_upstream","wyrm_decision_downstream","wyrm_decision_invalidate"]},{name:"Reasoning scaffolds",what:'Saved step-by-step checklists for problem types ("debug a slow query", "ship a migration") that surface automatically when a task description matches.',why:"Replaces ad-hoc rediscovery of process knowledge with structured prompts. Cheaper than re-explaining the playbook every session.",tools:["wyrm_scaffold_save","wyrm_scaffold_get"]},{name:"Lossless session rehydration",what:"wyrm_session_rehydrate produces a complete briefing markdown a fresh AI ingests to inherit prior state \u2014 decisions, open quests, failures, recent commits, files touched.",why:"Cross-session continuity without re-exploration. The single biggest token-saver in the toolkit.",tools:["wyrm_session_rehydrate","wyrm_session_prime","wyrm_session_start","wyrm_session_update"]},{name:"Distillation queue",what:"Candidate memory artifacts extracted from sessions land in a review queue; the operator approves or rejects each.",why:"Bayesian confidence updates after each review \u2014 Wyrm gets sharper with use without auto-trusting noise.",tools:["wyrm_distill","wyrm_review","wyrm_remember","wyrm_recall","wyrm_feedback"]},{name:"Knowledge graph",what:"Named entities + typed directional relationships, neighborhood traversal, shortest-path queries.",why:'When the AI asks "how does X relate to Y?", Wyrm answers in O(log n) instead of grepping the corpus.',tools:["wyrm_entity_add","wyrm_entity_link","wyrm_entity_search","wyrm_entity_graph","wyrm_entity_path","wyrm_entity_merge"]},{name:"Cross-repo symbol index",what:"Workspace-wide function/class/type index across TS/JS/Python/Rust/Go/PHP/Ruby with caller resolution.",why:`Symbol lookups don't require re-grepping every project. "Who calls handleAuth?" answers immediately.`,tools:["wyrm_symbol_index","wyrm_symbol_search","wyrm_symbol_callers","wyrm_symbol_stats"]},{name:"OODA agent loop (autonomous goals)",what:"wyrm_goal_set persists an objective with success criteria. The wyrm-loop daemon runs Observe\u2192Orient\u2192Decide\u2192Act every interval, dispatching whitelisted tools. Replayable, capped, auditable.",why:"Lets you hand the AI a multi-step objective and walk away. No data-exfil tools accessible from inside the loop.",tools:["wyrm_goal_set","wyrm_goal_list","wyrm_goal_pause","wyrm_goal_resume","wyrm_goal_complete","wyrm_goal_abandon","wyrm_goal_iterations","wyrm_act","wyrm_agent_init","wyrm_agent_status","wyrm_agent_stop","wyrm_agent_restart"]},{name:"Outbound MCP client",what:"Wyrm calls other MCP servers (GitHub, Slack, Linear, \u2026) over stdio. The agent loop dispatches them like internal tools.",why:"Composability: Wyrm becomes the orchestration layer over your entire MCP stack instead of a sibling.",tools:["wyrm_mcp_register","wyrm_mcp_list","wyrm_mcp_disable","wyrm_mcp_tools","wyrm_call_external"]},{name:"Hybrid semantic search",what:"wyrm_search fuses FTS5 keyword candidates with vector candidates (Ollama nomic-embed-text by default; OpenAI optional) via Reciprocal Rank Fusion.",why:`Catches both "exact term" and "semantically similar" hits in one call. Silently falls back to FTS5-only if Ollama isn't running.`,tools:["wyrm_search","wyrm_vector_setup"]},{name:"Multi-agent presence + work-stealing",what:"Claude, Copilot, Cursor, and Codex announce themselves via TTL heartbeats. Quests can be claimed exclusively.",why:"No two agents step on the same quest. The work-stealing semantics scale to teams.",tools:["wyrm_presence_announce","wyrm_presence_list","wyrm_presence_release","wyrm_quest_claim","wyrm_quest_release"]},{name:"Federated team sync",what:"Per-row is_shared flag, /sync/push endpoint, conflict-as-quest semantics. PII never leaves the box unless explicitly shared.",why:"Team memory without a SaaS. No silent overwrites \u2014 every conflict becomes a quest the operator resolves.",tools:["wyrm_share","wyrm_unshare","wyrm_sync","wyrm_sync_export","wyrm_sync_import","wyrm_sync_conflicts","wyrm_sync_resolve"]},{name:"Compliance audit trail",what:"Hash-chained audit log with optional Ed25519 signatures. wyrm_audit_export produces tamper-evident bundles.",why:"SOC2/HIPAA reviewers get a real artifact. The hash chain detects any tampering after the fact.",tools:["wyrm_audit_log","wyrm_audit_export","wyrm_audit_verify"]},{name:"Hour ledger + invoice generation",what:"Derives billable hours from session content and emits markdown invoices.",why:"For solo operators and small teams: Wyrm is also the time tracker. No second tool to keep in sync.",tools:["wyrm_hours_report","wyrm_invoice_generate","wyrm_cost_report"]},{name:"Self-improvement analytics",what:"Per-tool error rate, p50/p95 latency, cost-per-tool. Wyrm watches Wyrm.",why:"Lets you spot which tools are hot, which are slow, which fail \u2014 input for both ops tuning and product decisions.",tools:["wyrm_analytics_dashboard","wyrm_tool_analytics","wyrm_usage","wyrm_stats"]}];function _(){try{const t=[a(n,"..","package.json"),a(n,"..","..","package.json")];for(const e of t)if(y(e))return JSON.parse(m(e,"utf-8")).version??"unknown"}catch{}return"unknown"}function x(t,e,s){const o=process.env.WYRM_DB_PATH??a(l(),".wyrm","wyrm.db"),i=s.isAgentRunning(),r=h();return{product:"Wyrm",version:_(),pitch:u,install:{dbPath:o,nodeVersion:process.version,platform:`${process.platform}-${process.arch}`},runtime:{vectorProvider:s.detectVectorProvider?s.detectVectorProvider():r.resolved,vectorReason:r.reason,vectorFix:r.fix,encryption:s.hasEncryption(),federation:s.isFederationEnabled(),agentLoop:i===null?"unknown":i?"running":"stopped"},features:p,toolCount:e}}function S(t){const e=[];e.push(`# \u{F115D} ${t.product} ${t.version} \u2014 Capability Briefing`),e.push(""),e.push(t.pitch),e.push("");const s=t.runtime.vectorProvider==="none"?"vectors OFF (FTS-only recall)":`${t.runtime.vectorProvider} embeddings`;e.push(`**Runtime:** ${t.toolCount} MCP tools \xB7 ${s} \xB7 ${t.runtime.encryption?"encryption ON":"encryption off"} \xB7 agent ${t.runtime.agentLoop}`),t.runtime.vectorReason&&(e.push(""),e.push(`> WARNING: semantic recall degraded (${t.runtime.vectorReason}) - recall is FTS5 keyword-only. Fix: ${t.runtime.vectorFix}`)),e.push(`**Install:** ${t.install.platform} \xB7 Node ${t.install.nodeVersion} \xB7 DB ${t.install.dbPath}`),e.push(""),e.push('## What Wyrm gives you beyond "AI memory"'),e.push("");for(const o of t.features)e.push(`### ${o.name}`),e.push(`- **What:** ${o.what}`),e.push(`- **Why:** ${o.why}`),e.push(`- **Tools:** \`${o.tools.join("`, `")}\``),e.push("");return e.join(`
14
+ this requires you to leave your laptop.`,p=[{name:"Counter-pattern memory",what:"Records failed approaches with (scope, target, description) and blocks the same suggestion next time.",why:"Other memory tools learn only from success. Wyrm refuses to repeat mistakes \u2014 saves the most expensive class of tokens (the regenerated wrong answer).",tools:["wyrm_failure_record","wyrm_failure_check","wyrm_failure_list","wyrm_failure_resolve"]},{name:"Ground truths (versioned, cascade-aware)",what:"Validated project facts (architecture, constraints, decisions) \u2014 each truth carries a version, last-verified timestamp, and outbound decision links.",why:"Truths are injected first into every wyrm_context_build call so the AI never works from contradicted assumptions. When a truth changes, dependent decisions auto-invalidate.",tools:["wyrm_truth_set","wyrm_truth_get","wyrm_decided_because","wyrm_decision_upstream","wyrm_decision_downstream","wyrm_decision_invalidate"]},{name:"Reasoning scaffolds",what:'Saved step-by-step checklists for problem types ("debug a slow query", "ship a migration") that surface automatically when a task description matches.',why:"Replaces ad-hoc rediscovery of process knowledge with structured prompts. Cheaper than re-explaining the playbook every session.",tools:["wyrm_scaffold_save","wyrm_scaffold_get"]},{name:"Lossless session rehydration",what:"wyrm_session_rehydrate produces a complete briefing markdown a fresh AI ingests to inherit prior state \u2014 decisions, open quests, failures, recent commits, files touched.",why:"Cross-session continuity without re-exploration. The single biggest token-saver in the toolkit.",tools:["wyrm_session_rehydrate","wyrm_session_prime","wyrm_session_start","wyrm_session_update"]},{name:"Distillation queue",what:"Candidate memory artifacts extracted from sessions land in a review queue; the operator approves or rejects each.",why:"Bayesian confidence updates after each review \u2014 Wyrm gets sharper with use without auto-trusting noise.",tools:["wyrm_distill","wyrm_review","wyrm_remember","wyrm_recall","wyrm_feedback"]},{name:"Knowledge graph",what:"Named entities + typed directional relationships, neighborhood traversal, shortest-path queries.",why:'When the AI asks "how does X relate to Y?", Wyrm answers in O(log n) instead of grepping the corpus.',tools:["wyrm_entity_add","wyrm_entity_link","wyrm_entity_search","wyrm_entity_graph","wyrm_entity_path","wyrm_entity_merge"]},{name:"Cross-repo symbol index",what:"Workspace-wide function/class/type index across TS/JS/Python/Rust/Go/PHP/Ruby with caller resolution.",why:`Symbol lookups don't require re-grepping every project. "Who calls handleAuth?" answers immediately.`,tools:["wyrm_symbol_index","wyrm_symbol_search","wyrm_symbol_callers","wyrm_symbol_stats"]},{name:"OODA agent loop (autonomous goals)",what:"wyrm_goal_set persists an objective with success criteria. The wyrm-loop daemon runs Observe\u2192Orient\u2192Decide\u2192Act every interval, dispatching whitelisted tools. Replayable, capped, auditable.",why:"Lets you hand the AI a multi-step objective and walk away. No data-exfil tools accessible from inside the loop.",tools:["wyrm_goal_set","wyrm_goal_list","wyrm_goal_pause","wyrm_goal_resume","wyrm_goal_complete","wyrm_goal_abandon","wyrm_goal_iterations","wyrm_act","wyrm_agent_init","wyrm_agent_status","wyrm_agent_stop","wyrm_agent_restart"]},{name:"Outbound MCP client",what:"Wyrm calls other MCP servers (GitHub, Slack, Linear, \u2026) over stdio. The agent loop dispatches them like internal tools.",why:"Composability: Wyrm becomes the orchestration layer over your entire MCP stack instead of a sibling.",tools:["wyrm_mcp_register","wyrm_mcp_list","wyrm_mcp_disable","wyrm_mcp_tools","wyrm_call_external"]},{name:"Hybrid semantic search",what:"wyrm_search fuses FTS5 keyword candidates with vector candidates (Ollama nomic-embed-text by default; OpenAI optional) via Reciprocal Rank Fusion.",why:`Catches both "exact term" and "semantically similar" hits in one call. Silently falls back to FTS5-only if Ollama isn't running.`,tools:["wyrm_search","wyrm_vector_setup"]},{name:"Multi-agent presence + work-stealing",what:"Claude, Copilot, Cursor, and Codex announce themselves via TTL heartbeats. Quests can be claimed exclusively.",why:"No two agents step on the same quest. The work-stealing semantics scale to teams.",tools:["wyrm_presence_announce","wyrm_presence_list","wyrm_presence_release","wyrm_quest_claim","wyrm_quest_release"]},{name:"Federated team sync",what:"Per-row is_shared flag, /sync/push endpoint, conflict-as-quest semantics. PII never leaves the box unless explicitly shared.",why:"Team memory without a SaaS. No silent overwrites \u2014 every conflict becomes a quest the operator resolves.",tools:["wyrm_share","wyrm_unshare","wyrm_sync","wyrm_sync_export","wyrm_sync_import","wyrm_sync_conflicts","wyrm_sync_resolve"]},{name:"Compliance audit trail",what:"Hash-chained audit log with optional Ed25519 signatures. wyrm_audit_export produces tamper-evident bundles.",why:"SOC2/HIPAA reviewers get a real artifact. The hash chain detects any tampering after the fact.",tools:["wyrm_audit_log","wyrm_audit_export","wyrm_audit_verify"]},{name:"Hour ledger + invoice generation",what:"Derives billable hours from session content and emits markdown invoices.",why:"For solo operators and small teams: Wyrm is also the time tracker. No second tool to keep in sync.",tools:["wyrm_hours_report","wyrm_invoice_generate","wyrm_cost_report"]},{name:"Self-improvement analytics",what:"Per-tool error rate, p50/p95 latency, cost-per-tool. Wyrm watches Wyrm.",why:"Lets you spot which tools are hot, which are slow, which fail \u2014 input for both ops tuning and product decisions.",tools:["wyrm_analytics_dashboard","wyrm_tool_analytics","wyrm_usage","wyrm_stats"]}];function _(){try{const t=[a(n,"..","package.json"),a(n,"..","..","package.json")];for(const e of t)if(y(e))return JSON.parse(m(e,"utf-8")).version??"unknown"}catch{}return"unknown"}function x(t,e,s){const o=process.env.WYRM_DB_PATH??a(l(),".wyrm","wyrm.db"),i=s.isAgentRunning(),r=h();return{product:"Wyrm",version:_(),pitch:w,install:{dbPath:o,nodeVersion:process.version,platform:`${process.platform}-${process.arch}`},runtime:{vectorProvider:s.detectVectorProvider?s.detectVectorProvider():r.resolved,vectorReason:r.reason,vectorFix:r.fix,encryption:s.hasEncryption(),federation:s.isFederationEnabled(),agentLoop:i===null?"unknown":i?"running":"stopped"},features:p,toolCount:e}}function S(t){const e=[];e.push(`# ${t.product} ${t.version} \u2014 Capability Briefing`),e.push(""),e.push(t.pitch),e.push("");const s=t.runtime.vectorProvider==="none"?"vectors OFF (FTS-only recall)":`${t.runtime.vectorProvider} embeddings`;e.push(`**Runtime:** ${t.toolCount} MCP tools \xB7 ${s} \xB7 ${t.runtime.encryption?"encryption ON":"encryption off"} \xB7 agent ${t.runtime.agentLoop}`),t.runtime.vectorReason&&(e.push(""),t.runtime.vectorProvider==="none"?e.push(`> WARNING: semantic recall degraded (${t.runtime.vectorReason}) - recall is FTS5 keyword-only. Fix: ${t.runtime.vectorFix}`):e.push(`> NOTE: embedding provider is advisory (${t.runtime.vectorReason}) - semantic recall is running. Fix: ${t.runtime.vectorFix}`)),e.push(`**Install:** ${t.install.platform} \xB7 Node ${t.install.nodeVersion} \xB7 DB ${t.install.dbPath}`),e.push(""),e.push('## What Wyrm gives you beyond "AI memory"'),e.push("");for(const o of t.features)e.push(`### ${o.name}`),e.push(`- **What:** ${o.what}`),e.push(`- **Why:** ${o.why}`),e.push(`- **Tools:** \`${o.tools.join("`, `")}\``),e.push("");return e.join(`
15
15
  `)}export{x as getCapabilities,S as renderCapabilityBriefing};
@@ -1,13 +1,13 @@
1
- import{TOOL_ANNOTATIONS as L}from"../tool-annotations.js";import{renderResult as T,withGlyph as k}from"../render.js";import{memoryUri as A,resourceLink as H}from"./resources.js";import{buildDeterminismReceipt as N,attestation as U}from"../receipt.js";import{rerankConfigured as W,rerankEgressHost as F}from"../rerank.js";const V=[{name:"wyrm_recall",description:"Use before starting any non-trivial task - what do we know about this? Pull up relevant lessons learned, proven patterns, anti-patterns, and reasoning traces from past work. Hybrid lexical+semantic retrieval, fully local, no LLM. Mark what you recalled as useful (or not) with wyrm_feedback.",inputSchema:{type:"object",properties:{projectPath:{type:"string"},query:{type:"string",description:"The task or problem"},kind:{type:"string",enum:["reasoning_trace","lesson","pattern","anti_pattern","heuristic"]},limit:{type:"number",description:"Default 10"},minConfidence:{type:"number",description:"0-1 (default 0)"},detail:{type:"string",enum:["full","link"],description:"Default full"}},required:["projectPath","query"]},outputSchema:{type:"object",properties:{query:{type:"string"},count:{type:"integer"},results:{type:"array",items:{type:"object",properties:{id:{type:"integer"},kind:{type:"string"},relevance:{type:"number"},problem:{type:"string"},validated_fix:{type:["string","null"]},confidence:{type:"number"},reuse_count:{type:"integer"}},required:["id","kind","relevance","problem","confidence","reuse_count"]}}},required:["query","count","results"]},annotations:L.wyrm_recall,aliases:[],handler:async($,{store:E,raw:S,memory:v,vectors:h,clientSupportsResources:P})=>{const{projectPath:w,query:d,kind:R,limit:j,minConfidence:q,detail:g}=$,u=E.getProject(w);if(!u)return{content:[{type:"text",text:`Project not found: ${w}`}],isError:!0};const p=!!h(),O=p&&W()&&/^(1|true|yes|on)$/i.test(process.env.WYRM_RERANK??"");let r;const b={kind:R,limit:j??10,minConfidence:q,rerank:O,onStats:t=>{r=t}},a=p?await v.recallHybrid(u.id,d,b):v.recall(u.id,d,b),c=r!==void 0,f=r?.rerankMs!=null,s=h(),M=typeof s?.getProvider=="function"?s.getProvider():void 0;let l;if(c&&typeof s?.indexCoverage=="function")try{l=s.indexCoverage()}catch{}let m=null;if(!c)if(p){let t=-1;if(typeof s?.getStats=="function")try{t=s.getStats().total}catch{}m=t===0?"empty_index":"embed_error"}else{const{resolveEmbeddingState:t}=await import("../providers/embedding-provider.js");m=t().reason||"vector_store_unavailable"}const C=N({vectorsAvailable:c,rerankActive:f,embedModel:c?M?.model??(process.env.WYRM_EMBED_MODEL||"nomic-embed-text"):null,rerankModel:f&&process.env.WYRM_RERANK_MODEL||null,embedEgressHost:c?M?.remoteHost??null:null,rerankEgressHost:f?F():null,stageMs:r?{fts5:r.ftsMs,vector:r.vectorMs,fusion:r.fusionMs,...r.rerankMs!=null?{rerank:r.rerankMs}:{}}:void 0,indexStale:l?.stale?{activeModel:l.activeModel,indexedUnder:Object.keys(l.otherModels)}:void 0,ftsOnlyReason:m});if(a.length>0){const t=a.map(o=>o.artifact.id),e=t.map(()=>"?").join(",");S().prepare(`UPDATE memory_artifacts SET last_accessed_at = datetime('now'), access_count = access_count + 1 WHERE id IN (${e})`).run(...t)}const x=t=>Math.round(t*1e4)/1e4,y={query:d,count:a.length,results:a.map(t=>{const e=t.artifact,o={id:e.id,kind:e.kind,relevance:x(t.relevance_score),problem:e.problem,outcome:e.outcome,confidence:e.confidence==null?e.confidence:x(e.confidence),reuse_count:e.reuse_count,reuse_success_count:e.reuse_success_count};return e.constraints!=null&&(o.constraints=e.constraints),e.validated_fix!=null&&(o.validated_fix=e.validated_fix),e.why_it_worked!=null&&(o.why_it_worked=e.why_it_worked),o}),receipt:C},_=T(y,(t,e)=>{const o=`
1
+ import{TOOL_ANNOTATIONS as L}from"../tool-annotations.js";import{renderResult as T,withGlyph as v}from"../render.js";import{memoryUri as A,resourceLink as N}from"./resources.js";import{buildDeterminismReceipt as H,attestation as U}from"../receipt.js";import{rerankConfigured as W,rerankEgressHost as F}from"../rerank.js";const V=[{name:"wyrm_recall",description:"Use before starting any non-trivial task - what do we know about this? Pull up relevant lessons learned, proven patterns, anti-patterns, and reasoning traces from past work. Hybrid lexical+semantic retrieval, fully local, no LLM. Mark what you recalled as useful (or not) with wyrm_feedback.",inputSchema:{type:"object",properties:{projectPath:{type:"string"},query:{type:"string",description:"The task or problem"},kind:{type:"string",enum:["reasoning_trace","lesson","pattern","anti_pattern","heuristic"]},limit:{type:"number",description:"Default 10"},minConfidence:{type:"number",description:"0-1 (default 0)"},detail:{type:"string",enum:["full","link"],description:"Default full"}},required:["projectPath","query"]},outputSchema:{type:"object",properties:{query:{type:"string"},count:{type:"integer"},results:{type:"array",items:{type:"object",properties:{id:{type:"integer"},kind:{type:"string"},relevance:{type:"number"},problem:{type:"string"},validated_fix:{type:["string","null"]},confidence:{type:"number"},reuse_count:{type:"integer"}},required:["id","kind","relevance","problem","confidence","reuse_count"]}}},required:["query","count","results"]},annotations:L.wyrm_recall,aliases:[],handler:async($,{store:E,raw:S,memory:h,vectors:w,clientSupportsResources:P})=>{const{projectPath:g,query:u,kind:R,limit:j,minConfidence:q,detail:b}=$,p=E.getProject(g);if(!p)return{content:[{type:"text",text:`Project not found: ${g}`}],isError:!0};const f=!!w(),O=f&&W()&&/^(1|true|yes|on)$/i.test(process.env.WYRM_RERANK??"");let r;const x={kind:R,limit:j??10,minConfidence:q,rerank:O,onStats:t=>{r=t}},a=f?await h.recallHybrid(p.id,u,x):h.recall(p.id,u,x),l=r!==void 0,m=r?.rerankMs!=null,s=w(),y=typeof s?.getProvider=="function"?s.getProvider():void 0;let c;if(typeof s?.indexCoverage=="function")try{c=s.indexCoverage()}catch{}let d=null;if(!l){const t=y?.name;if(!f||t==="none"){const{resolveEmbeddingState:e}=await import("../providers/embedding-provider.js");d=e().reason||"vector_store_unavailable"}else if(c?.stale)d="stale_index";else{let e=-1;if(typeof s?.getStats=="function")try{e=s.getStats().total}catch{}d=e===0?"empty_index":"embed_error"}}const C=H({vectorsAvailable:l,rerankActive:m,embedModel:l?y?.model??(process.env.WYRM_EMBED_MODEL||"nomic-embed-text"):null,rerankModel:m&&process.env.WYRM_RERANK_MODEL||null,embedEgressHost:l?y?.remoteHost??null:null,rerankEgressHost:m?F():null,stageMs:r?{fts5:r.ftsMs,vector:r.vectorMs,fusion:r.fusionMs,...r.rerankMs!=null?{rerank:r.rerankMs}:{}}:void 0,indexStale:c?.stale?{activeModel:c.activeModel,indexedUnder:Object.keys(c.otherModels)}:void 0,ftsOnlyReason:d});if(a.length>0){const t=a.map(o=>o.artifact.id),e=t.map(()=>"?").join(",");S().prepare(`UPDATE memory_artifacts SET last_accessed_at = datetime('now'), access_count = access_count + 1 WHERE id IN (${e})`).run(...t)}const M=t=>Math.round(t*1e4)/1e4,_={query:u,count:a.length,results:a.map(t=>{const e=t.artifact,o={id:e.id,kind:e.kind,relevance:M(t.relevance_score),problem:e.problem,outcome:e.outcome,confidence:e.confidence==null?e.confidence:M(e.confidence),reuse_count:e.reuse_count,reuse_success_count:e.reuse_success_count};return e.constraints!=null&&(o.constraints=e.constraints),e.validated_fix!=null&&(o.validated_fix=e.validated_fix),e.why_it_worked!=null&&(o.why_it_worked=e.why_it_worked),o}),receipt:C},k=T(_,(t,e)=>{const o=`
2
2
 
3
- _${U(t.receipt)}_`;if(t.count===0)return k(e.brand,`No relevant memory found for: "${t.query}"`)+"\n\nUse `wyrm_remember` to store knowledge as you work."+o;let i=k(e.brand,`**Memory Recall** -- "${t.query}" (${t.count} found)`)+`
3
+ _${U(t.receipt)}_`;if(t.count===0)return v(e.brand,`No relevant memory found for: "${t.query}"`)+"\n\nUse `wyrm_remember` to store knowledge as you work."+o;let i=v(e.brand,`**Memory Recall** -- "${t.query}" (${t.count} found)`)+`
4
4
 
5
5
  `;for(const n of t.results){i+=`### ${n.kind} (ID: ${n.id}) -- relevance ${(n.relevance*100).toFixed(0)}%
6
6
  `,i+=`**Problem:** ${n.problem}
7
7
  `,n.constraints&&(i+=`**Constraints:** ${n.constraints}
8
8
  `),n.validated_fix&&(i+=`**Solution:** ${n.validated_fix}
9
9
  `),n.why_it_worked&&(i+=`**Why:** ${n.why_it_worked}
10
- `),n.outcome==="negative"&&(i+=k(e.warn,"_This approach failed -- see solution for what to do instead_")+`
10
+ `),n.outcome==="negative"&&(i+=v(e.warn,"_This approach failed -- see solution for what to do instead_")+`
11
11
  `);const D=n.confidence==null?"unknown":`${(n.confidence*100).toFixed(0)}%`;i+=`_Confidence: ${D} | Used ${n.reuse_count}x | Success rate: ${n.reuse_count>0?(n.reuse_success_count/n.reuse_count*100).toFixed(0)+"%":"not yet used"}_
12
12
 
13
- `}return i+="_Use `wyrm_feedback` to record whether these were helpful._",i+o},{exemptStructured:!0});if((g==="link"||g!=="full"&&!!P?.())&&y.count>0){const t=y.results.map(e=>H(A(e.id),`memory #${e.id} (${e.kind})`,{description:e.problem.slice(0,120),mimeType:"application/json"}));return{..._,content:[..._.content,...t]}}return _}}];export{V as recallToolSpecs};
13
+ `}return i+="_Use `wyrm_feedback` to record whether these were helpful._",i+o},{exemptStructured:!0});if((b==="link"||b!=="full"&&!!P?.())&&_.count>0){const t=_.results.map(e=>N(A(e.id),`memory #${e.id} (${e.kind})`,{description:e.problem.slice(0,120),mimeType:"application/json"}));return{...k,content:[...k.content,...t]}}return k}}];export{V as recallToolSpecs};
@@ -1,4 +1,4 @@
1
- import{basename as X}from"path";import{createHash as Z}from"crypto";import{blockText as Q}from"./types.js";import{TOOL_ANNOTATIONS as T}from"../tool-annotations.js";import{renderResult as x,withGlyph as O,cacheKeyFor as N}from"../render.js";import{computeStaleness as ee}from"../intelligence.js";import{ValidationError as te,asInt as se,asString as ne}from"../validate.js";import{sanitizeActorId as ie}from"./boundary.js";import{makeEstimator as oe}from"../token-budget.js";function G(e){return!e||!e.thread.trim()?null:`### \u{1F9F5} Where We Left Off _(the living thread \u2014 what we were working on and thinking about)_
1
+ import{basename as X}from"path";import{createHash as Z}from"crypto";import{blockText as Q}from"./types.js";import{TOOL_ANNOTATIONS as T}from"../tool-annotations.js";import{renderResult as v,withGlyph as N,cacheKeyFor as O}from"../render.js";import{computeStaleness as ee}from"../intelligence.js";import{ValidationError as te,asInt as se,asString as ne}from"../validate.js";import{sanitizeActorId as ie}from"./boundary.js";import{makeEstimator as oe}from"../token-budget.js";function G(e){return!e||!e.thread.trim()?null:`### \u{1F9F5} Where We Left Off _(the living thread \u2014 what we were working on and thinking about)_
2
2
  `+`_updated ${e.updated_at}${e.updated_by?" \xB7 "+e.updated_by:""} \xB7 ${e.turn_count} turn(s)_`+`
3
3
 
4
4
  `+e.thread.slice(-1800)}const re={tech_stack:"\u{1F6E0}\uFE0F Tech Stack",architecture:"\u{1F3D7}\uFE0F Architecture",constraint:"\u{1F6A7} Constraints",decision:"\u{1F4CB} Decisions",contact:"\u{1F464} Contacts",other:"\u{1F4CC} Other Facts"};function J(e,t){let s=`### \u{1F4DA} Project Ground Truths _(treat as authoritative \u2014 do not infer or contradict)_
@@ -20,7 +20,7 @@ import{basename as X}from"path";import{createHash as Z}from"crypto";import{block
20
20
  `).get()}function P(e){const t=e.getAllProjects(20);return t.length===0?{content:[{type:"text",text:"No projects found. Run `wyrm_scan_projects` first."}],isError:!0}:{content:[{type:"text",text:`Project not found. Available projects:
21
21
  ${t.map(n=>`- ${n.name} (ID: ${n.id})`).join(`
22
22
  `)}`}],isError:!0}}const K=(e,t)=>{const s=e.session_id!==null?`
23
- Session logged: #${e.session_id}`:"";return O(t.brand,`Wyrm primed for **${e.project_name}** | ${e.truth_count} truth${e.truth_count!==1?"s":""} - ${e.quest_count} quest${e.quest_count!==1?"s":""} - ${e.memory_count} memor${e.memory_count!==1?"ies":"y"}${s}`)},L=(e,t)=>{const s=K(e,t);return e.sections.length>0?e.sections.map(n=>n.text).join(`
23
+ Session logged: #${e.session_id}`:"";return N(t.brand,`Wyrm primed for **${e.project_name}** | ${e.truth_count} truth${e.truth_count!==1?"s":""} - ${e.quest_count} quest${e.quest_count!==1?"s":""} - ${e.memory_count} memor${e.memory_count!==1?"ies":"y"}${s}`)},L=(e,t)=>{const s=K(e,t);return e.sections.length>0?e.sections.map(n=>n.text).join(`
24
24
 
25
25
  ---
26
26
 
@@ -28,16 +28,17 @@ Session logged: #${e.session_id}`:"";return O(t.brand,`Wyrm primed for **${e.pro
28
28
 
29
29
  `+s:s+`
30
30
 
31
- _No memory stored yet -- start working and Wyrm will learn._`},A={summary:K};async function F(e,t,s){try{const{logSavings:n}=await import("../statusline.js"),i=Q(s.content[0]).length,r=Math.round(i/4),a=e.prepare("SELECT id FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(t);n(e,"wyrm_session_prime","recovered_context",r,a?.id)}catch{}}const ce={continuity:{path:"continuity",tool:"wyrm_session_prime"},truths:{path:"truths",tool:"wyrm_truth_get"},scaffold:{path:"scaffolds",tool:"wyrm_context_build"},memory:{path:"memory",tool:"wyrm_recall"},quests:{path:"quests",tool:"wyrm_all_quests"}},ae=48,de=1200;async function pe(e,{store:t,raw:s,memory:n,truths:i,scaffolds:r,cache:a}){const{project_id:p,project_name:l,task:d,log_session:y}=e,c=ie(e.run_id);if(c===null)throw new te("run_id",'must be <=64 printable-ASCII chars (no ";")');const u=ne("role",e.role,{maxLen:200})??"",j=se("token_budget",e.token_budget,{min:1})??de,_=s(),v=()=>_.prepare("SELECT body_json FROM run_briefs WHERE run_id = ? AND role = ?").get(c,u),h=p||l?q(t,_,p,l):void 0;if((p||l)&&!h)return P(t);let S=v();if(S&&h){const o=JSON.parse(S.body_json);if(o.project_id!==h.id)return{content:[{type:"text",text:`Fleet brief for (run ${c}, role '${u}') is pinned to project '${o.project_name}' (ID: ${o.project_id}) \u2014 refusing to serve it for requested project '${h.name}' (ID: ${h.id}). Use a distinct role for a second project, or prime without run_id.`}],isError:!0}}if(!S){const o=h??q(t,_,p,l);if(!o)return P(t);const m=[],$=G(t.getContinuity(o.id));$&&m.push({kind:"continuity",text:$});const k=i.getCurrent(o.id);k.length>0&&m.push({kind:"truths",text:J(k,!1).slice(0,1200)});const w=d??(u||void 0);if(w){const b=r.findBest(w,o.id,.3);b&&m.push({kind:"scaffold",text:r.formatForContext(b,1500)})}const M=n.buildContextBrief(o.id,w??"general",{maxItems:10,minConfidence:.3});M.text&&m.push({kind:"memory",text:M.text.slice(0,2e3)});const R=_.prepare(V).all(o.id);R.length>0&&m.push({kind:"quests",text:Y(R).slice(0,800)});const D=oe(),I=[];let C=ae;for(const b of m){const W=D.count(b.text)+2;if(C+W<=j)I.push(b),C+=W;else{const H=ce[b.kind]??{path:b.kind,tool:"wyrm_search"},U=`[beyond token_budget] wyrm://project/${o.id}/${H.path} -- fetch via ${H.tool}`;I.push({kind:b.kind,text:U}),C+=D.count(U)+2}}let B=null;y&&(B=t.createSession(o.id,{objectives:d??"Session started via wyrm_session_prime",summary:""}).id);const z={project_id:o.id,project_name:o.name,truth_count:i.getStats(o.id).current,quest_count:R.length,memory_count:n.getStats(o.id).total,session_id:B,sections:I,run_id:c,role:u,token_budget:j};_.prepare("INSERT OR IGNORE INTO run_briefs (run_id, role, body_json, token_budget) VALUES (?, ?, ?, ?)").run(c,u,JSON.stringify(z),j),S=v()}const g=JSON.parse(S.body_json),E=x(g,L,A);if(e.for_spawn===!0){const o=x(g,L,{...A,exemptStructured:!0}),m=Q(o.content[0]),$=Z("sha256").update(m,"utf8").digest("hex"),k={project_id:g.project_id,project_name:g.project_name,session_id:g.session_id??null,sections:g.sections,for_spawn:!0,run_id:c,role:u,brief:m,brief_hash:$,brief_chars:m.length,cache_key:{table:"run_briefs",run_id:c,role:u}};await F(_,g.project_id,o);const w={content:[{type:"text",text:m}],structuredContent:k};return a.set(N("wyrm_session_prime",JSON.stringify(e)),w,3e4),w}return await F(_,g.project_id,E),a.set(N("wyrm_session_prime",JSON.stringify(e)),E,3e4),E}const Se=[{name:"wyrm_session_start",description:"Start or continue a session for a project",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project path"},objectives:{type:"string",description:"Session objectives"}},required:["projectPath"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project:{type:"string"},date:{type:"string"},objectives:{type:["string","null"]}},required:["session_id","project","date","objectives"]},annotations:T.wyrm_session_start,aliases:[],handler:(e,{store:t})=>{const{projectPath:s,objectives:n}=e;let i=t.getProject(s);i||(i=t.registerProject(X(s),s));let r=t.getTodaySession(i.id);r?n&&(r=t.updateSession(r.id,{objectives:r.objectives?`${r.objectives}
32
- ${n}`:n})):r=t.createSession(i.id,{objectives:n||""}),t.archiveOldSessions(i.id,10);const a={session_id:r.id,project:i.name,date:r.date,objectives:r.objectives||null};return x(a,(p,l)=>O(l.brand,`Session ${p.session_id} for ${p.project}`)+`
31
+ _No memory stored yet -- start working and Wyrm will learn._`},A={summary:K};async function F(e,t,s){try{const{logSavings:n}=await import("../statusline.js"),i=Q(s.content[0]).length,r=Math.round(i/4),a=e.prepare("SELECT id FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(t);n(e,"wyrm_session_prime","recovered_context",r,a?.id)}catch{}}const ce={continuity:{path:"continuity",tool:"wyrm_session_prime"},truths:{path:"truths",tool:"wyrm_truth_get"},scaffold:{path:"scaffolds",tool:"wyrm_context_build"},memory:{path:"memory",tool:"wyrm_recall"},quests:{path:"quests",tool:"wyrm_all_quests"}},ae=48,de=1200;async function pe(e,{store:t,raw:s,memory:n,truths:i,scaffolds:r,cache:a}){const{project_id:p,project_name:l,task:d,log_session:y}=e,c=ie(e.run_id);if(c===null)throw new te("run_id",'must be <=64 printable-ASCII chars (no ";")');const u=ne("role",e.role,{maxLen:200})??"",j=se("token_budget",e.token_budget,{min:1})??de,_=s(),w=()=>_.prepare("SELECT body_json FROM run_briefs WHERE run_id = ? AND role = ?").get(c,u),h=p||l?q(t,_,p,l):void 0;if((p||l)&&!h)return P(t);let S=w();if(S&&h){const o=JSON.parse(S.body_json);if(o.project_id!==h.id)return{content:[{type:"text",text:`Fleet brief for (run ${c}, role '${u}') is pinned to project '${o.project_name}' (ID: ${o.project_id}) \u2014 refusing to serve it for requested project '${h.name}' (ID: ${h.id}). Use a distinct role for a second project, or prime without run_id.`}],isError:!0}}if(!S){const o=h??q(t,_,p,l);if(!o)return P(t);const m=[],$=G(t.getContinuity(o.id));$&&m.push({kind:"continuity",text:$});const k=i.getCurrent(o.id);k.length>0&&m.push({kind:"truths",text:J(k,!1).slice(0,1200)});const x=d??(u||void 0);if(x){const b=r.findBest(x,o.id,.3);b&&m.push({kind:"scaffold",text:r.formatForContext(b,1500)})}const M=n.buildContextBrief(o.id,x??"general",{maxItems:10,minConfidence:.3});M.text&&m.push({kind:"memory",text:M.text.slice(0,2e3)});const R=_.prepare(V).all(o.id);R.length>0&&m.push({kind:"quests",text:Y(R).slice(0,800)});const D=oe(),I=[];let C=ae;for(const b of m){const W=D.count(b.text)+2;if(C+W<=j)I.push(b),C+=W;else{const H=ce[b.kind]??{path:b.kind,tool:"wyrm_search"},U=`[beyond token_budget] wyrm://project/${o.id}/${H.path} -- fetch via ${H.tool}`;I.push({kind:b.kind,text:U}),C+=D.count(U)+2}}let B=null;y&&(B=t.createSession(o.id,{objectives:d??"Session started via wyrm_session_prime",summary:""}).id);const z={project_id:o.id,project_name:o.name,truth_count:i.getStats(o.id).current,quest_count:R.length,memory_count:n.getStats(o.id).total,session_id:B,sections:I,run_id:c,role:u,token_budget:j};_.prepare("INSERT OR IGNORE INTO run_briefs (run_id, role, body_json, token_budget) VALUES (?, ?, ?, ?)").run(c,u,JSON.stringify(z),j),S=w()}const g=JSON.parse(S.body_json),E=v(g,L,A);if(e.for_spawn===!0){const o=v(g,L,{...A,exemptStructured:!0}),m=Q(o.content[0]),$=Z("sha256").update(m,"utf8").digest("hex"),k={project_id:g.project_id,project_name:g.project_name,session_id:g.session_id??null,sections:g.sections,for_spawn:!0,run_id:c,role:u,brief:m,brief_hash:$,brief_chars:m.length,cache_key:{table:"run_briefs",run_id:c,role:u}};await F(_,g.project_id,o);const x={content:[{type:"text",text:m}],structuredContent:k};return a.set(O("wyrm_session_prime",JSON.stringify(e)),x,3e4),x}return await F(_,g.project_id,E),a.set(O("wyrm_session_prime",JSON.stringify(e)),E,3e4),E}const Se=[{name:"wyrm_session_start",description:"Start or continue a session for a project",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project path"},objectives:{type:"string",description:"Session objectives"}},required:["projectPath"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project:{type:"string"},date:{type:"string"},objectives:{type:["string","null"]}},required:["session_id","project","date","objectives"]},annotations:T.wyrm_session_start,aliases:[],handler:(e,{store:t})=>{const{projectPath:s,objectives:n}=e;let i=t.getProject(s);i||(i=t.registerProject(X(s),s));let r=t.getTodaySession(i.id);r?n&&(r=t.updateSession(r.id,{objectives:r.objectives?`${r.objectives}
32
+ ${n}`:n})):r=t.createSession(i.id,{objectives:n||""}),t.archiveOldSessions(i.id,10);const a={session_id:r.id,project:i.name,date:r.date,objectives:r.objectives||null};return v(a,(p,l)=>N(l.brand,`Session ${p.session_id} for ${p.project}`)+`
33
33
  **Date:** ${p.date}
34
34
  **Objectives:** ${p.objectives??"None set"}`)}},{name:"wyrm_session_update",description:"Update the current session with completed work, issues, or notes",inputSchema:{type:"object",properties:{projectPath:{type:"string"},completed:{type:"string",description:"What was completed"},issues:{type:"string"},commits:{type:"string",description:"Git commits made"},notes:{type:"string"}},required:["projectPath"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project:{type:"string"},updated:{type:"array",items:{type:"string",enum:["completed","issues","commits","notes"]}}},required:["session_id","project","updated"]},annotations:T.wyrm_session_update,aliases:[],handler:(e,{store:t,indexing:s})=>{const{projectPath:n,completed:i,issues:r,commits:a,notes:p}=e,l=t.getProject(n);if(!l)return{content:[{type:"text",text:"Project not found"}],isError:!0};let d=t.getTodaySession(l.id);d||(d=t.createSession(l.id,{}));const y={};i&&(y.completed=d.completed?`${d.completed}
35
35
  ${i}`:i),r&&(y.issues=d.issues?`${d.issues}
36
36
  ${r}`:r),a&&(y.commits=d.commits?`${d.commits}
37
37
  ${a}`:a),p&&(y.notes=d.notes?`${d.notes}
38
- ${p}`:p),d=t.updateSession(d.id,y);const c=[i,r,p].filter(Boolean).join(" \xB7 ");c&&t.updateContinuity(l.id,c,"session_update"),s()?.enqueue("session",d.id,l.id);const u={session_id:d.id,project:l.name,updated:Object.keys(y)};return x(u,(j,_)=>O(_.brand,`Session updated for ${j.project}`))}},{name:"wyrm_session_rehydrate",description:"Lossless session rehydration. Given a past session ID, returns a complete briefing markdown a fresh AI agent can ingest to inherit prior state: objectives, completed work, notes, summary, current ground truths, open quests, validated patterns (artifacts), unresolved failure patterns. Cross-session continuity no other AI memory tool offers.",inputSchema:{type:"object",properties:{session_id:{type:"number",description:"ID of the session to rehydrate"},include_artifacts:{type:"boolean",description:"Include memory artifacts (default true)"},include_failures:{type:"boolean",description:"Include unresolved failure patterns (default true)"},max_truth_chars:{type:"number",description:"Cap on chars used for ground truths (default 2000)"}},required:["session_id"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project_name:{type:"string"},project_path:{type:"string"},briefing_markdown:{type:"string"},context_chars:{type:"integer"},attached:{type:"object",properties:{quests:{type:"integer"},truths:{type:"integer"},artifacts:{type:"integer"},failures:{type:"integer"}},required:["quests","truths","artifacts","failures"]}},required:["session_id","project_name","project_path","briefing_markdown","context_chars","attached"]},annotations:T.wyrm_session_rehydrate,aliases:[],handler:(e,{rehydration:t})=>{const{session_id:s,include_artifacts:n,include_failures:i,max_truth_chars:r}=e,a=t.rehydrate(s,{include_artifacts:n,include_failures:i,max_truth_chars:r});return a?x({...a},p=>p.briefing_markdown,{exemptStructured:!0}):{content:[{type:"text",text:`Session #${s} not found.`}],isError:!0}}},{name:"wyrm_session_prime",description:"Use first at session start - load everything you need to know before starting work, and catch me up on where we left off last time: ground truths, memory brief, reasoning scaffold, and active quests in one call. First call every agent makes in a run.",inputSchema:{type:"object",properties:{project_id:{type:"number"},project_name:{type:"string"},task:{type:"string",description:"Drives scaffold + memory selection"},log_session:{type:"boolean",description:"Also log a session row"},run_id:{type:"string",description:"Fleet: brief cached per (run_id, role)"},role:{type:"string"},token_budget:{type:"number"},for_spawn:{type:"boolean"}}},outputSchema:{type:"object",properties:{project_id:{type:"integer"},project_name:{type:"string"},session_id:{type:["integer","null"]},sections:{type:"array",items:{type:"object",properties:{kind:{type:"string"},text:{type:"string"}},required:["kind","text"]}}},required:["project_id","project_name","session_id","sections"]},annotations:T.wyrm_session_prime,aliases:[],handler:async(e,{store:t,raw:s,memory:n,truths:i,scaffolds:r,cache:a})=>{const{project_id:p,project_name:l,task:d,log_session:y}=e;if(e.run_id!==void 0&&e.run_id!==null)return pe(e,{store:t,raw:s,memory:n,truths:i,scaffolds:r,cache:a});const c=q(t,s(),p,l);if(!c)return P(t);const u=[],j=G(t.getContinuity(c.id));j&&u.push({kind:"continuity",text:j});const _=i.getCurrent(c.id);if(_.length>0&&u.push({kind:"truths",text:J(_,!0).slice(0,1200)}),d){const f=r.findBest(d,c.id,.3);f&&u.push({kind:"scaffold",text:r.formatForContext(f,1500)})}const v=n.buildContextBrief(c.id,d??"general",{maxItems:10,minConfidence:.3});v.text&&u.push({kind:"memory",text:v.text.slice(0,2e3)});const h=s().prepare(V).all(c.id);h.length>0&&u.push({kind:"quests",text:Y(h).slice(0,800)});try{const f=s().prepare("SELECT COUNT(*) AS n FROM memory_artifacts WHERE project_id = ? AND needs_review = 1").get(c.id).n;f>0&&u.push({kind:"review_queue",text:`### \u{1F4E5} Review Queue
39
- ${f} captured memor${f===1?"y awaits":"ies await"} review (wyrm_review) \u2014 queued memory is invisible to recall until approved.`})}catch{}try{const{resolveEmbeddingState:f}=await import("../providers/embedding-provider.js"),o=f();let m=-1;try{m=s().prepare("SELECT COUNT(*) AS n FROM vectors").get().n}catch{m=0}o.resolved==="none"||o.resolved==="local"?u.push({kind:"vector_health",text:`### [!] Semantic Recall is OFF
40
- No embedding provider is active (${o.reason||"no provider"}) - recall is FTS5 keyword-only (~59.9% vs 72.2% hybrid recall@10). Fix: ${o.fix}`}):m===0&&u.push({kind:"vector_health",text:`### [!] Vector Index is EMPTY
38
+ ${p}`:p),d=t.updateSession(d.id,y);const c=[i,r,p].filter(Boolean).join(" \xB7 ");c&&t.updateContinuity(l.id,c,"session_update"),s()?.enqueue("session",d.id,l.id);const u={session_id:d.id,project:l.name,updated:Object.keys(y)};return v(u,(j,_)=>N(_.brand,`Session updated for ${j.project}`))}},{name:"wyrm_session_rehydrate",description:"Lossless session rehydration. Given a past session ID, returns a complete briefing markdown a fresh AI agent can ingest to inherit prior state: objectives, completed work, notes, summary, current ground truths, open quests, validated patterns (artifacts), unresolved failure patterns. Cross-session continuity no other AI memory tool offers.",inputSchema:{type:"object",properties:{session_id:{type:"number",description:"ID of the session to rehydrate"},include_artifacts:{type:"boolean",description:"Include memory artifacts (default true)"},include_failures:{type:"boolean",description:"Include unresolved failure patterns (default true)"},max_truth_chars:{type:"number",description:"Cap on chars used for ground truths (default 2000)"}},required:["session_id"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project_name:{type:"string"},project_path:{type:"string"},briefing_markdown:{type:"string"},context_chars:{type:"integer"},attached:{type:"object",properties:{quests:{type:"integer"},truths:{type:"integer"},artifacts:{type:"integer"},failures:{type:"integer"}},required:["quests","truths","artifacts","failures"]}},required:["session_id","project_name","project_path","briefing_markdown","context_chars","attached"]},annotations:T.wyrm_session_rehydrate,aliases:[],handler:(e,{rehydration:t})=>{const{session_id:s,include_artifacts:n,include_failures:i,max_truth_chars:r}=e,a=t.rehydrate(s,{include_artifacts:n,include_failures:i,max_truth_chars:r});return a?v({...a},p=>p.briefing_markdown,{exemptStructured:!0}):{content:[{type:"text",text:`Session #${s} not found.`}],isError:!0}}},{name:"wyrm_session_prime",description:"Use first at session start - load everything you need to know before starting work, and catch me up on where we left off last time: ground truths, memory brief, reasoning scaffold, and active quests in one call. First call every agent makes in a run.",inputSchema:{type:"object",properties:{project_id:{type:"number"},project_name:{type:"string"},task:{type:"string",description:"Drives scaffold + memory selection"},log_session:{type:"boolean",description:"Also log a session row"},run_id:{type:"string",description:"Fleet: brief cached per (run_id, role)"},role:{type:"string"},token_budget:{type:"number"},for_spawn:{type:"boolean"}}},outputSchema:{type:"object",properties:{project_id:{type:"integer"},project_name:{type:"string"},session_id:{type:["integer","null"]},sections:{type:"array",items:{type:"object",properties:{kind:{type:"string"},text:{type:"string"}},required:["kind","text"]}}},required:["project_id","project_name","session_id","sections"]},annotations:T.wyrm_session_prime,aliases:[],handler:async(e,{store:t,raw:s,memory:n,truths:i,scaffolds:r,cache:a})=>{const{project_id:p,project_name:l,task:d,log_session:y}=e;if(e.run_id!==void 0&&e.run_id!==null)return pe(e,{store:t,raw:s,memory:n,truths:i,scaffolds:r,cache:a});const c=q(t,s(),p,l);if(!c)return P(t);const u=[],j=G(t.getContinuity(c.id));j&&u.push({kind:"continuity",text:j});const _=i.getCurrent(c.id);if(_.length>0&&u.push({kind:"truths",text:J(_,!0).slice(0,1200)}),d){const f=r.findBest(d,c.id,.3);f&&u.push({kind:"scaffold",text:r.formatForContext(f,1500)})}const w=n.buildContextBrief(c.id,d??"general",{maxItems:10,minConfidence:.3});w.text&&u.push({kind:"memory",text:w.text.slice(0,2e3)});const h=s().prepare(V).all(c.id);h.length>0&&u.push({kind:"quests",text:Y(h).slice(0,800)});try{const f=s().prepare("SELECT COUNT(*) AS n FROM memory_artifacts WHERE project_id = ? AND needs_review = 1").get(c.id).n;f>0&&u.push({kind:"review_queue",text:`### \u{1F4E5} Review Queue
39
+ ${f} captured memor${f===1?"y awaits":"ies await"} review (wyrm_review) \u2014 queued memory is invisible to recall until approved.`})}catch{}try{const{resolveEmbeddingState:f}=await import("../providers/embedding-provider.js"),o=f();let m=-1;try{m=s().prepare("SELECT COUNT(*) AS n FROM vectors").get().n}catch{m=0}o.resolved==="none"?u.push({kind:"vector_health",text:`### [!] Semantic Recall is OFF
40
+ No embedding provider is active (${o.reason||"no provider"}) - recall is FTS5 keyword-only (~59.9% vs 72.2% hybrid recall@10). Fix: ${o.fix}`}):o.resolved==="local"?u.push({kind:"vector_health",text:`### [!] Semantic Recall is NON-SEMANTIC
41
+ Provider 'local' (hash-384) is a test-grade fallback, not a real embedder - recall quality is at the FTS floor. Fix: ${o.fix}`}):m===0&&u.push({kind:"vector_health",text:`### [!] Vector Index is EMPTY
41
42
  Provider '${o.resolved}' (${o.model}) is configured but nothing is indexed - recall is running FTS5-only until the index is built. Fix: wyrm index rebuild`})}catch{}try{const{vaultAdvisory:f}=await import("../vault.js"),o=f();o.length>0&&u.push({kind:"vault",text:`### \u{1F510} Credential Vault
42
43
  `+o.map(m=>`- ${m}`).join(`
43
- `)})}catch{}let S=null;y&&(S=t.createSession(c.id,{objectives:d??"Session started via wyrm_session_prime",summary:""}).id);const g={project_id:c.id,project_name:c.name,truth_count:i.getStats(c.id).current,quest_count:h.length,memory_count:n.getStats(c.id).total,session_id:S,sections:u},E=x(g,L,A);return await F(s(),c.id,E),a.set(N("wyrm_session_prime",JSON.stringify(e)),E,3e4),E}}];export{Se as sessionToolSpecs};
44
+ `)})}catch{}let S=null;y&&(S=t.createSession(c.id,{objectives:d??"Session started via wyrm_session_prime",summary:""}).id);const g={project_id:c.id,project_name:c.name,truth_count:i.getStats(c.id).current,quest_count:h.length,memory_count:n.getStats(c.id).total,session_id:S,sections:u},E=v(g,L,A);return await F(s(),c.id,E),a.set(O("wyrm_session_prime",JSON.stringify(e)),E,3e4),E}}];export{Se as sessionToolSpecs};
@@ -0,0 +1 @@
1
+ import{resolveEmbeddingState as S}from"./providers/embedding-provider.js";function u(t,s){try{const e=t.prepare(s).get();if(!e)return null;const o=Object.values(e)[0];return typeof o=="number"?o:null}catch{return null}}function w(t){const s=u(t,"SELECT COUNT(*) AS n FROM memory_artifacts"),e=u(t,"SELECT COUNT(*) AS n FROM vectors")??0,o=u(t,"SELECT COUNT(*) AS n FROM memory_artifacts_fts"),n=u(t,"SELECT COUNT(*) AS n FROM memory_artifacts WHERE needs_review = 1"),c=u(t,"SELECT COUNT(*) AS n FROM failure_patterns WHERE resolved = 0"),l=S(),i=u(t,"SELECT MAX(version) AS v FROM schema_versions"),r=s&&s>0&&e!=null?Math.min(100,Math.round(e/s*100)):e===0?0:null,a=s!=null&&o!=null?s-o:null;return{memories:s,truths:u(t,"SELECT COUNT(*) AS n FROM ground_truths"),open_quests:u(t,"SELECT COUNT(*) AS n FROM quests WHERE status = 'pending'"),vectors:e,vector_coverage_pct:r,fts_drift:a,review_queue:n,unresolved_failures:c,provider:l.resolved,provider_degraded:l.resolved==="none"?1:0,schema_version:i,ok_vectors:l.resolved!=="none"&&(e??0)>0?1:0,ok_fts:a===0?1:0,ok_review:(n??0)===0?1:0}}function k(t){const s=r=>u(t,`SELECT COALESCE(SUM(estimated_tokens), 0) AS n FROM token_savings_log WHERE category = '${r}'`),e=r=>u(t,`SELECT COUNT(*) AS n FROM usage_events WHERE tool_name = '${r}'`),o=s("recovered_context"),n=s("blocked_retry"),c=s("cached_preamble"),l=s("truth_citation"),i=[o,n,c,l].every(r=>r!=null)?(o??0)+(n??0)+(c??0)+(l??0):null;return{failure_blocks:u(t,"SELECT COUNT(*) AS n FROM failure_blocks"),failure_check_calls:e("wyrm_failure_check"),rehydrate_calls:(e("wyrm_session_rehydrate")??0)+(e("wyrm_session_prime")??0),recall_calls:e("wyrm_recall"),capture_calls:e("wyrm_capture"),tokens_recovered:o,tokens_blocked_retry:n,tokens_saved_total:i}}function O(t,s,e,o){try{const n=t.prepare("INSERT INTO metrics_snapshots (wyrm_version, kind, metrics_json) VALUES (?, ?, ?)").run(o,s,JSON.stringify(e));return Number(n.lastInsertRowid)}catch{return null}}function T(t,s,e=30){try{return t.prepare("SELECT id, captured_at, wyrm_version, kind, metrics_json FROM metrics_snapshots WHERE kind = ? ORDER BY captured_at DESC, id DESC LIMIT ?").all(s,e).map(n=>({id:n.id,captured_at:n.captured_at,wyrm_version:n.wyrm_version,kind:n.kind,metrics:g(n.metrics_json)}))}catch{return[]}}function g(t){try{return JSON.parse(t)}catch{return{}}}function R(t,s){const e=[];if(t.length===0)return e;const o=t[0].metrics,n=t[1]?.metrics,c=(l,i)=>{const r=l?.[i];return typeof r=="number"?r:null};if(s==="health"){const l=t.findIndex(E=>c(E.metrics,"provider_degraded")!==1),i=l===-1?t.length:l;i>=2&&e.push(`vectors OFF for ${i} consecutive snapshots \u2014 recall has been FTS-only that long (wyrm doctor for the fix)`);const r=c(o,"review_queue"),a=c(n,"review_queue");r!=null&&r>0&&a!=null&&r>a&&e.push(`review queue GROWING: ${a} -> ${r} (queued memory is invisible to recall until \`wyrm review\`)`);const _=c(o,"unresolved_failures"),v=c(n,"unresolved_failures");_!=null&&v!=null&&_-v>=5&&e.push(`unresolved failures +${_-v} (${v} -> ${_}) \u2014 the firewall is accumulating debt (\`wyrm failure list\`)`);const f=c(o,"fts_drift");f!=null&&f!==0&&e.push(`FTS index drift: ${f} memories not mirrored in FTS \u2014 keyword recall is missing rows`);const d=c(o,"memories"),m=c(n,"memories"),h=c(o,"vector_coverage_pct"),p=c(n,"vector_coverage_pct");d!=null&&m!=null&&d>m&&h!=null&&p!=null&&h<p-5&&e.push(`corpus outgrowing the index: memories ${m} -> ${d} but coverage ${p}% -> ${h}% (\`wyrm index rebuild\`)`)}if(s==="effectiveness"){const l=c(o,"failure_blocks"),i=c(n,"failure_blocks");l!=null&&i!=null&&l>i&&e.push(`repeats prevented: +${l-i} failure blocks since last snapshot (negative learning paying off)`);const r=c(o,"tokens_saved_total"),a=c(n,"tokens_saved_total");r!=null&&a!=null&&r>a&&e.push(`+${(r-a).toLocaleString()} tokens saved since last snapshot (recovered context + blocked retries)`),r!=null&&a!=null&&r===a&&t.length>=3&&e.push("token savings FLAT across the last snapshots \u2014 memory is not being credited with any help (are the hooks + failure_check firing?)")}if(s==="retrieval"){const l=c(o,"recall_at_10"),i=c(n,"recall_at_10");if(l!=null&&i!=null){const r=Math.round((l-i)*1e3)/10;r<=-2?e.push(`recall@10 REGRESSED ${r}pts (${(i*100).toFixed(1)}% -> ${(l*100).toFixed(1)}%) between ${t[1].wyrm_version??"?"} and ${t[0].wyrm_version??"?"} \u2014 investigate the ranking change`):r>=2&&e.push(`recall@10 improved +${r}pts (${(i*100).toFixed(1)}% -> ${(l*100).toFixed(1)}%)`)}}return e}export{k as captureEffectiveness,w as captureHealth,T as getSnapshots,O as recordSnapshot,R as trendFlags};
@@ -210,7 +210,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
210
210
  timestamp TEXT DEFAULT (datetime('now'))
211
211
  );
212
212
  CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage_events(timestamp);
213
- `)}},{version:3,description:"Add visibility and created_by columns for future multi-user support",up:e=>{const E=["sessions","quests","context","data_lake"];for(const t of E){const i=e.prepare(`PRAGMA table_info(${t})`).all().map(T=>T.name);i.includes("visibility")||e.exec(`ALTER TABLE ${t} ADD COLUMN visibility TEXT DEFAULT 'private'`),i.includes("created_by")||e.exec(`ALTER TABLE ${t} ADD COLUMN created_by TEXT DEFAULT 'local'`)}}},{version:4,description:"Knowledge graph \u2014 entities, aliases, relationships with FTS5",up:e=>{e.exec(`
213
+ `)}},{version:3,description:"Add visibility and created_by columns for future multi-user support",up:e=>{const s=["sessions","quests","context","data_lake"];for(const t of s){const i=e.prepare(`PRAGMA table_info(${t})`).all().map(T=>T.name);i.includes("visibility")||e.exec(`ALTER TABLE ${t} ADD COLUMN visibility TEXT DEFAULT 'private'`),i.includes("created_by")||e.exec(`ALTER TABLE ${t} ADD COLUMN created_by TEXT DEFAULT 'local'`)}}},{version:4,description:"Knowledge graph \u2014 entities, aliases, relationships with FTS5",up:e=>{e.exec(`
214
214
  -- Entities: named things that can be linked together
215
215
  CREATE TABLE IF NOT EXISTS entities (
216
216
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -844,7 +844,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
844
844
  ON token_savings_log(session_id, timestamp DESC);
845
845
  CREATE INDEX IF NOT EXISTS idx_savings_category
846
846
  ON token_savings_log(category, timestamp DESC);
847
- `);const E=["ground_truths","memory_artifacts","quests","decision_edges","design_references","design_tokens"];for(const t of E)try{if(e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name==="cross_project_visibility"))continue;e.exec(`
847
+ `);const s=["ground_truths","memory_artifacts","quests","decision_edges","design_references","design_tokens"];for(const t of s)try{if(e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name==="cross_project_visibility"))continue;e.exec(`
848
848
  ALTER TABLE ${t} ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within'
849
849
  CHECK (cross_project_visibility IN ('within', 'org', 'public'));
850
850
  `)}catch{}}},{version:15,description:"Live Memory v6.4 \u2014 append-only events log + wyrm_meta (device identity)",up:e=>{e.exec(`
@@ -908,7 +908,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
908
908
  `)}},{version:17,description:'Backfill cross_project_visibility on decision_edges (migration 14 named a non-existent "decisions" table)',up:e=>{try{e.prepare("PRAGMA table_info(decision_edges)").all().some(t=>t.name==="cross_project_visibility")||e.exec(`
909
909
  ALTER TABLE decision_edges ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within'
910
910
  CHECK (cross_project_visibility IN ('within', 'org', 'public'));
911
- `)}catch{}}},{version:18,description:"GOD-SKILL SPEC v2 \u2014 skill tier/governs/composes + spec-kit registry (specs table)",up:e=>{const E=e.prepare("PRAGMA table_info(skills)").all(),t=s=>E.some(i=>i.name===s);t("tier")||e.exec(`
911
+ `)}catch{}}},{version:18,description:"GOD-SKILL SPEC v2 \u2014 skill tier/governs/composes + spec-kit registry (specs table)",up:e=>{const s=e.prepare("PRAGMA table_info(skills)").all(),t=E=>s.some(i=>i.name===E);t("tier")||e.exec(`
912
912
  ALTER TABLE skills ADD COLUMN tier TEXT NOT NULL DEFAULT 'atomic'
913
913
  CHECK (tier IN ('atomic', 'mega', 'god'));
914
914
  `),t("governs")||e.exec("ALTER TABLE skills ADD COLUMN governs TEXT NOT NULL DEFAULT '[]';"),t("composes")||e.exec("ALTER TABLE skills ADD COLUMN composes TEXT NOT NULL DEFAULT '[]';"),e.exec("CREATE INDEX IF NOT EXISTS idx_skills_tier ON skills(tier);"),e.exec(`
@@ -929,7 +929,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
929
929
  `)}},{version:19,description:"Grove sync policy: projects.sync_policy gate (private by default) for cloud + federation isolation",up:e=>{e.prepare("PRAGMA table_info(projects)").all().some(o=>o.name==="sync_policy")||e.exec(`
930
930
  ALTER TABLE projects ADD COLUMN sync_policy TEXT NOT NULL DEFAULT 'private'
931
931
  CHECK (sync_policy IN ('private', 'cloud', 'team'));
932
- `);const t=(o,a)=>{try{return e.prepare(`PRAGMA table_info(${o})`).all().some(r=>r.name===a)}catch{return!1}},s=o=>{try{return e.prepare(o).all().map(a=>a.pid)}catch{return[]}},i=new Set;for(const o of["ground_truths","memory_artifacts","quests","design_tokens","design_references"])if(!(!t(o,"cross_project_visibility")||!t(o,"project_id")))for(const a of s(`SELECT DISTINCT project_id AS pid FROM ${o}
932
+ `);const t=(o,a)=>{try{return e.prepare(`PRAGMA table_info(${o})`).all().some(r=>r.name===a)}catch{return!1}},E=o=>{try{return e.prepare(o).all().map(a=>a.pid)}catch{return[]}},i=new Set;for(const o of["ground_truths","memory_artifacts","quests","design_tokens","design_references"])if(!(!t(o,"cross_project_visibility")||!t(o,"project_id")))for(const a of E(`SELECT DISTINCT project_id AS pid FROM ${o}
933
933
  WHERE cross_project_visibility IN ('org', 'public') AND project_id IS NOT NULL`))i.add(a);const T=e.prepare("UPDATE projects SET sync_policy = 'cloud' WHERE id = ? AND sync_policy = 'private'");for(const o of i)T.run(o)}},{version:20,description:"v7 F2 (T008) \u2014 run-native provenance: runs + run_agents tables, agent_id/run_id attribution on 9 tables, failure_patterns.quarantine_scope",up:e=>{e.exec(`
934
934
  CREATE TABLE IF NOT EXISTS runs (
935
935
  run_id TEXT PRIMARY KEY,
@@ -955,11 +955,11 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
955
955
  FOREIGN KEY (run_id) REFERENCES runs(run_id) ON DELETE CASCADE
956
956
  );
957
957
  CREATE INDEX IF NOT EXISTS idx_run_agents_agent ON run_agents(agent_id);
958
- `);const E=(s,i)=>{try{return e.prepare(`PRAGMA table_info(${s})`).all().some(T=>T.name===i)}catch{return!1}},t=["memory_artifacts","failure_patterns","decision_edges","ground_truths","quests","sessions","quest_claims","events","audit_log"];for(const s of t)E(s,"agent_id")||e.exec(`ALTER TABLE ${s} ADD COLUMN agent_id TEXT;`),E(s,"run_id")||e.exec(`ALTER TABLE ${s} ADD COLUMN run_id TEXT;`);e.exec(`
958
+ `);const s=(E,i)=>{try{return e.prepare(`PRAGMA table_info(${E})`).all().some(T=>T.name===i)}catch{return!1}},t=["memory_artifacts","failure_patterns","decision_edges","ground_truths","quests","sessions","quest_claims","events","audit_log"];for(const E of t)s(E,"agent_id")||e.exec(`ALTER TABLE ${E} ADD COLUMN agent_id TEXT;`),s(E,"run_id")||e.exec(`ALTER TABLE ${E} ADD COLUMN run_id TEXT;`);e.exec(`
959
959
  CREATE INDEX IF NOT EXISTS idx_failure_run ON failure_patterns(run_id) WHERE run_id IS NOT NULL;
960
960
  CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id) WHERE run_id IS NOT NULL;
961
961
  CREATE INDEX IF NOT EXISTS idx_artifacts_run ON memory_artifacts(run_id) WHERE run_id IS NOT NULL;
962
- `),E("failure_patterns","quarantine_scope")||(e.exec(`
962
+ `),s("failure_patterns","quarantine_scope")||(e.exec(`
963
963
  ALTER TABLE failure_patterns ADD COLUMN quarantine_scope TEXT NOT NULL DEFAULT 'project'
964
964
  CHECK (quarantine_scope IN ('run', 'project', 'global'));
965
965
  `),e.exec("UPDATE failure_patterns SET quarantine_scope = 'global' WHERE project_id IS NULL;")),e.exec("CREATE INDEX IF NOT EXISTS idx_failure_quarantine ON failure_patterns(quarantine_scope, resolved);")}},{version:21,description:"v7 F2 (T015) \u2014 failure_confirmations: distinct-agent confirmation ledger behind run-quarantine auto-promotion",up:e=>{e.exec(`
@@ -990,10 +990,10 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
990
990
  created_at TEXT DEFAULT (datetime('now')),
991
991
  PRIMARY KEY (run_id, role)
992
992
  );
993
- `)}},{version:24,description:"v7 F3 (T029) \u2014 claims/presence hardening: role on quest_claims, run_id/role on agent_presence, run-scoped partial indexes",up:e=>{const E=(t,s)=>{try{return e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name===s)}catch{return!1}};E("quest_claims","role")||e.exec("ALTER TABLE quest_claims ADD COLUMN role TEXT;"),E("agent_presence","run_id")||e.exec("ALTER TABLE agent_presence ADD COLUMN run_id TEXT;"),E("agent_presence","role")||e.exec("ALTER TABLE agent_presence ADD COLUMN role TEXT;"),e.exec(`
993
+ `)}},{version:24,description:"v7 F3 (T029) \u2014 claims/presence hardening: role on quest_claims, run_id/role on agent_presence, run-scoped partial indexes",up:e=>{const s=(t,E)=>{try{return e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name===E)}catch{return!1}};s("quest_claims","role")||e.exec("ALTER TABLE quest_claims ADD COLUMN role TEXT;"),s("agent_presence","run_id")||e.exec("ALTER TABLE agent_presence ADD COLUMN run_id TEXT;"),s("agent_presence","role")||e.exec("ALTER TABLE agent_presence ADD COLUMN role TEXT;"),e.exec(`
994
994
  CREATE INDEX IF NOT EXISTS idx_quest_claims_run ON quest_claims(run_id) WHERE run_id IS NOT NULL;
995
995
  CREATE INDEX IF NOT EXISTS idx_presence_run ON agent_presence(run_id) WHERE run_id IS NOT NULL;
996
- `)}},{version:25,description:"Skills portability \u2014 store SKILL.md content (+ sha/ts) in the registry and make skills cloud-sync-eligible behind the per-row visibility gate",up:e=>{const E=(t,s)=>{try{return e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name===s)}catch{return!1}};E("skills","content")||e.exec("ALTER TABLE skills ADD COLUMN content TEXT;"),E("skills","content_sha256")||e.exec("ALTER TABLE skills ADD COLUMN content_sha256 TEXT;"),E("skills","content_updated_at")||e.exec("ALTER TABLE skills ADD COLUMN content_updated_at TEXT;"),E("skills","cross_project_visibility")||e.exec("ALTER TABLE skills ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within';"),E("skills","is_shared")||e.exec("ALTER TABLE skills ADD COLUMN is_shared INTEGER NOT NULL DEFAULT 0;"),e.exec("CREATE INDEX IF NOT EXISTS idx_skills_visibility ON skills(cross_project_visibility) WHERE cross_project_visibility != 'within';")}},{version:26,description:"Reverse-bridge rejection tombstones \u2014 remember a once-rejected outside-prose sig so the next sweep never re-queues content the operator already deleted",up:e=>{e.exec(`
996
+ `)}},{version:25,description:"Skills portability \u2014 store SKILL.md content (+ sha/ts) in the registry and make skills cloud-sync-eligible behind the per-row visibility gate",up:e=>{const s=(t,E)=>{try{return e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name===E)}catch{return!1}};s("skills","content")||e.exec("ALTER TABLE skills ADD COLUMN content TEXT;"),s("skills","content_sha256")||e.exec("ALTER TABLE skills ADD COLUMN content_sha256 TEXT;"),s("skills","content_updated_at")||e.exec("ALTER TABLE skills ADD COLUMN content_updated_at TEXT;"),s("skills","cross_project_visibility")||e.exec("ALTER TABLE skills ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within';"),s("skills","is_shared")||e.exec("ALTER TABLE skills ADD COLUMN is_shared INTEGER NOT NULL DEFAULT 0;"),e.exec("CREATE INDEX IF NOT EXISTS idx_skills_visibility ON skills(cross_project_visibility) WHERE cross_project_visibility != 'within';")}},{version:26,description:"Reverse-bridge rejection tombstones \u2014 remember a once-rejected outside-prose sig so the next sweep never re-queues content the operator already deleted",up:e=>{e.exec(`
997
997
  CREATE TABLE IF NOT EXISTS reverse_bridge_tombstones (
998
998
  project_id INTEGER NOT NULL,
999
999
  sig TEXT NOT NULL,
@@ -1036,7 +1036,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
1036
1036
  );
1037
1037
  CREATE UNIQUE INDEX IF NOT EXISTS ux_failure_anchors_failure_rel
1038
1038
  ON failure_anchors(failure_id, rel_path);
1039
- `)}},{version:31,description:"FABLE skill tier \u2014 widen skills.tier to (atomic, mega, god, fable): model-vintage master-craft skills authored by Fable-class models during real builds, every law validated by shipping the artifact in the authoring session (fable-skill-spec)",up:e=>{const E=e.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'skills'").get();!E?.sql||E.sql.includes("'fable'")||e.exec(`
1039
+ `)}},{version:31,description:"FABLE skill tier \u2014 widen skills.tier to (atomic, mega, god, fable): model-vintage master-craft skills authored by Fable-class models during real builds, every law validated by shipping the artifact in the authoring session (fable-skill-spec)",up:e=>{const s=e.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'skills'").get();!s?.sql||s.sql.includes("'fable'")||e.exec(`
1040
1040
  CREATE TABLE skills_v31 (
1041
1041
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1042
1042
  name TEXT UNIQUE NOT NULL,
@@ -1130,23 +1130,31 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
1130
1130
  UNIQUE(kind, row_id)
1131
1131
  );
1132
1132
  CREATE INDEX IF NOT EXISTS idx_sync_tombstones_pending ON sync_tombstones(pushed_at_ms);
1133
- `);const E=[["ground_truths","truth"],["memory_artifacts","memory_artifact"],["quests","quest"],["design_tokens","design_token"],["design_references","design_reference"],["skills","skill"]];for(const[t,s]of E)e.exec(`
1133
+ `);const s=[["ground_truths","truth"],["memory_artifacts","memory_artifact"],["quests","quest"],["design_tokens","design_token"],["design_references","design_reference"],["skills","skill"]];for(const[t,E]of s)e.exec(`
1134
1134
  CREATE TRIGGER IF NOT EXISTS trg_tombstone_${t} AFTER DELETE ON ${t}
1135
1135
  BEGIN
1136
1136
  INSERT OR IGNORE INTO sync_tombstones (kind, row_id, deleted_at_ms)
1137
- VALUES ('${s}', CAST(OLD.id AS TEXT), CAST(strftime('%s','now') AS INTEGER) * 1000);
1137
+ VALUES ('${E}', CAST(OLD.id AS TEXT), CAST(strftime('%s','now') AS INTEGER) * 1000);
1138
1138
  END;
1139
- `)}},{version:35,description:"Grove-gate the tombstone triggers (quest #113): a tombstone must record ONLY when the deleted row would have SYNCED, matching the upsert gate exactly (cross_project_visibility IN org/public AND grove sync_policy IN cloud/team). Migration 34 recorded a tombstone for EVERY delete, so a PRIVATE grove's (or unshared skill's) deletion leaked content-free deletion metadata to the cloud as noise. Recreates all 6 triggers gated.",up:e=>{const E=[["ground_truths","truth"],["memory_artifacts","memory_artifact"],["quests","quest"],["design_tokens","design_token"],["design_references","design_reference"],["skills","skill"]];for(const[t,s]of E){e.exec(`DROP TRIGGER IF EXISTS trg_tombstone_${t};`);const i=t==="skills"?"OLD.cross_project_visibility IN ('org','public')":"OLD.cross_project_visibility IN ('org','public') AND (OLD.project_id IS NULL OR (SELECT sync_policy FROM projects WHERE id = OLD.project_id) IN ('cloud','team'))";e.exec(`
1139
+ `)}},{version:35,description:"Grove-gate the tombstone triggers (quest #113): a tombstone must record ONLY when the deleted row would have SYNCED, matching the upsert gate exactly (cross_project_visibility IN org/public AND grove sync_policy IN cloud/team). Migration 34 recorded a tombstone for EVERY delete, so a PRIVATE grove's (or unshared skill's) deletion leaked content-free deletion metadata to the cloud as noise. Recreates all 6 triggers gated.",up:e=>{const s=[["ground_truths","truth"],["memory_artifacts","memory_artifact"],["quests","quest"],["design_tokens","design_token"],["design_references","design_reference"],["skills","skill"]];for(const[t,E]of s){e.exec(`DROP TRIGGER IF EXISTS trg_tombstone_${t};`);const i=t==="skills"?"OLD.cross_project_visibility IN ('org','public')":"OLD.cross_project_visibility IN ('org','public') AND (OLD.project_id IS NULL OR (SELECT sync_policy FROM projects WHERE id = OLD.project_id) IN ('cloud','team'))";e.exec(`
1140
1140
  CREATE TRIGGER trg_tombstone_${t} AFTER DELETE ON ${t}
1141
1141
  WHEN ${i}
1142
1142
  BEGIN
1143
1143
  INSERT OR IGNORE INTO sync_tombstones (kind, row_id, deleted_at_ms)
1144
- VALUES ('${s}', CAST(OLD.id AS TEXT), CAST(strftime('%s','now') AS INTEGER) * 1000);
1144
+ VALUES ('${E}', CAST(OLD.id AS TEXT), CAST(strftime('%s','now') AS INTEGER) * 1000);
1145
1145
  END;
1146
- `)}}},{version:36,description:`Add updated_at to quests + design_references so cloud sync can watermark-filter them. Root cause of "sync never settles" (2026-07-17): collectChangedRows skips its incremental time filter for any synced table WITHOUT an updated_at column, so it re-collected ALL org/public quests (101) + design_references every sync, stamping each with Date.now() and re-pushing forever (~1s/row server-side). The 4 other synced tables already have updated_at. This migration adds the column + backfills from the row's real last-change time (normalized via datetime() to the clean "YYYY-MM-DD HH:MM:SS" format strftime expects). No triggers: both tables are external-content FTS5, where a self-UPDATE inside a trigger corrupts the FTS index when an indexed column changes ("database disk image is malformed"). updated_at is stamped in app code on every insert/update instead (database.ts addQuest/updateQuest/dynamic-update, design.ts) \u2014 the same convention the other synced tables use.`,up:e=>{const E=(t,s)=>e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name===s);E("quests","updated_at")||(e.exec("ALTER TABLE quests ADD COLUMN updated_at TEXT"),e.exec("UPDATE quests SET updated_at = datetime(COALESCE(completed_at, created_at, 'now'))")),E("design_references","updated_at")||(e.exec("ALTER TABLE design_references ADD COLUMN updated_at TEXT"),e.exec("UPDATE design_references SET updated_at = datetime('now')"))}}];function d(e){e.exec(`
1146
+ `)}}},{version:36,description:`Add updated_at to quests + design_references so cloud sync can watermark-filter them. Root cause of "sync never settles" (2026-07-17): collectChangedRows skips its incremental time filter for any synced table WITHOUT an updated_at column, so it re-collected ALL org/public quests (101) + design_references every sync, stamping each with Date.now() and re-pushing forever (~1s/row server-side). The 4 other synced tables already have updated_at. This migration adds the column + backfills from the row's real last-change time (normalized via datetime() to the clean "YYYY-MM-DD HH:MM:SS" format strftime expects). No triggers: both tables are external-content FTS5, where a self-UPDATE inside a trigger corrupts the FTS index when an indexed column changes ("database disk image is malformed"). updated_at is stamped in app code on every insert/update instead (database.ts addQuest/updateQuest/dynamic-update, design.ts) \u2014 the same convention the other synced tables use.`,up:e=>{const s=(t,E)=>e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name===E);s("quests","updated_at")||(e.exec("ALTER TABLE quests ADD COLUMN updated_at TEXT"),e.exec("UPDATE quests SET updated_at = datetime(COALESCE(completed_at, created_at, 'now'))")),s("design_references","updated_at")||(e.exec("ALTER TABLE design_references ADD COLUMN updated_at TEXT"),e.exec("UPDATE design_references SET updated_at = datetime('now')"))}},{version:37,description:"Add metrics_snapshots \u2014 the longitudinal flight recorder for Wyrm effectiveness + quality over time. wyrm doctor / stats / benches all measure a MOMENT; nothing timestamped a trend, so a slow degradation (recall drifting as the corpus outgrows the index, vectors quietly off for days, a crucible-caught defect class recurring) was invisible until someone re-ran a tool by hand. One row per (kind, capture): kind=health (corpus/coverage/queue/failures/doctor checks), kind=effectiveness (recall hit-rate, failure-block rate, rehydrate non-empty rate, token savings), kind=retrieval (recall@k from the eval bench). metrics_json keeps the schema flexible so new metrics need no migration. Written nightly (health+effectiveness) + on-demand/per-release (retrieval); read by `wyrm metrics trend`.",up:e=>{e.exec(`
1147
+ CREATE TABLE IF NOT EXISTS metrics_snapshots (
1148
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1149
+ captured_at TEXT NOT NULL DEFAULT (datetime('now')),
1150
+ wyrm_version TEXT,
1151
+ kind TEXT NOT NULL CHECK (kind IN ('health', 'effectiveness', 'retrieval')),
1152
+ metrics_json TEXT NOT NULL
1153
+ );
1154
+ `),e.exec("CREATE INDEX IF NOT EXISTS idx_metrics_kind_time ON metrics_snapshots(kind, captured_at DESC);")}}];function d(e){e.exec(`
1147
1155
  CREATE TABLE IF NOT EXISTS schema_versions (
1148
1156
  version INTEGER PRIMARY KEY,
1149
1157
  description TEXT NOT NULL,
1150
1158
  applied_at TEXT DEFAULT (datetime('now'))
1151
1159
  );
1152
- `);const E=e.prepare("SELECT COALESCE(MAX(version), 0) as v FROM schema_versions").get().v,t=n.filter(T=>T.version>E).slice().sort((T,o)=>T.version-o.version);if(t.length===0)return[];const s=[],i=e.prepare("INSERT INTO schema_versions (version, description) VALUES (?, ?)");for(const T of t)e.transaction(()=>{T.up(e),i.run(T.version,T.description)})(),s.push(T);return s}function N(e){try{return e.prepare("SELECT COALESCE(MAX(version), 0) as v FROM schema_versions").get().v}catch{return 0}}export{N as getSchemaVersion,n as migrations,d as runMigrations};
1160
+ `);const s=e.prepare("SELECT COALESCE(MAX(version), 0) as v FROM schema_versions").get().v,t=n.filter(T=>T.version>s).slice().sort((T,o)=>T.version-o.version);if(t.length===0)return[];const E=[],i=e.prepare("INSERT INTO schema_versions (version, description) VALUES (?, ?)");for(const T of t)e.transaction(()=>{T.up(e),i.run(T.version,T.description)})(),E.push(T);return E}function N(e){try{return e.prepare("SELECT COALESCE(MAX(version), 0) as v FROM schema_versions").get().v}catch{return 0}}export{N as getSchemaVersion,n as migrations,d as runMigrations};
@@ -1 +1 @@
1
- import{isLoopbackHost as _}from"../receipt.js";function m(o){try{const t=new URL(o).hostname.toLowerCase();return _(t)?null:t}catch{return null}}class b{name="local-hash";model="hash-384";dimensions=384;async embed(t){const e=new Float32Array(this.dimensions),s=t.toLowerCase().split(/\s+/);for(let r=0;r<s.length;r++){const i=s[r];for(let a=0;a<i.length;a++){const d=i.charCodeAt(a)*(r+1)*(a+1)%this.dimensions;e[d]+=1/s.length}}const n=Math.sqrt(e.reduce((r,i)=>r+i*i,0));if(n>0)for(let r=0;r<e.length;r++)e[r]/=n;return e}async isReady(){return!0}}class v{name="openai";model;dimensions;remoteHost="api.openai.com";apiKey;timeoutMs;constructor(t,e){this.apiKey=t,this.model=e||"text-embedding-3-small",this.dimensions=this.model.includes("large")?3072:1536;const s=Number(process.env.WYRM_OPENAI_TIMEOUT);this.timeoutMs=Number.isFinite(s)&&s>0?s:8e3}async embed(t){const e=await fetch("https://api.openai.com/v1/embeddings",{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify({input:t,model:this.model}),signal:AbortSignal.timeout(this.timeoutMs)});if(!e.ok){const n=await e.text();throw new Error(`OpenAI embedding API error ${e.status}: ${n}`)}const s=await e.json();return new Float32Array(s.data[0].embedding)}async isReady(){return!!this.apiKey}}class I{name="ollama";model;dimensions;remoteHost;baseUrl;constructor(t,e){this.baseUrl=t||"http://localhost:11434",this.model=e||"nomic-embed-text",this.dimensions=this.model.includes("mxbai")?1024:768,this.remoteHost=m(this.baseUrl)}async embed(t){const e=await fetch(`${this.baseUrl}/api/embeddings`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:this.model,prompt:t})});if(!e.ok){const n=await e.text();throw new Error(`Ollama embedding API error ${e.status}: ${n}`)}const s=await e.json();return new Float32Array(s.embedding)}async isReady(){try{return(await fetch(`${this.baseUrl}/api/tags`,{signal:AbortSignal.timeout(2e3)})).ok}catch{return!1}}}class p{name="nim";model;remoteHost;dims;baseUrl;apiKey;timeoutMs;static KNOWN_DIMS={"nvidia/llama-nemotron-embed-1b-v2":2048,"nvidia/llama-3.2-nv-embedqa-1b-v2":2048,"nvidia/nv-embedqa-e5-v5":1024,"nvidia/nv-embedqa-mistral-7b-v2":4096,"nvidia/nv-embed-v1":4096};get dimensions(){return this.dims}constructor(t,e,s){this.apiKey=t,this.model=e||process.env.WYRM_NIM_EMBED_MODEL||"nvidia/llama-nemotron-embed-1b-v2",this.baseUrl=(s||process.env.WYRM_NIM_BASE_URL||"https://integrate.api.nvidia.com").replace(/\/+$/,"");const n=Number(process.env.WYRM_NIM_EMBED_DIM);this.dims=Number.isFinite(n)&&n>0?n:p.KNOWN_DIMS[this.model]??2048;const r=Number(process.env.WYRM_NIM_TIMEOUT);this.timeoutMs=Number.isFinite(r)&&r>0?r:8e3,this.remoteHost=m(this.baseUrl)}async embed(t,e="passage"){let s;for(let i=0;;i++){let a=null;try{const d={"Content-Type":"application/json"};if(this.apiKey&&(d.Authorization=`Bearer ${this.apiKey}`),s=await fetch(`${this.baseUrl}/v1/embeddings`,{method:"POST",headers:d,body:JSON.stringify({input:[t],model:this.model,input_type:e,encoding_format:"float",truncate:"END"}),signal:AbortSignal.timeout(this.timeoutMs)}),s.ok||s.status!==429&&s.status<500)break;const c=Number(s.headers?.get?.("retry-after"));a=Number.isFinite(c)&&c>0?Math.min(c*1e3,15e3):500*2**i}catch(d){if(i>=2)throw d;a=500*2**i}if(i>=2)break;await new Promise(d=>setTimeout(d,a??500))}if(!s.ok){const i=await s.text();throw new Error(`NIM embedding API error ${s.status}: ${i.slice(0,300)}`)}const r=(await s.json()).data?.[0]?.embedding;if(!Array.isArray(r)||r.length===0)throw new Error("NIM embedding API returned no embedding");return r.length!==this.dims&&(this.dims=r.length),new Float32Array(r)}async isReady(){return this.remoteHost?!!this.apiKey:!0}}class u{name="none";model="none";dimensions=0;async embed(t){return new Float32Array(0)}async isReady(){return!0}}const l="set WYRM_VECTOR_PROVIDER=nim and NVIDIA_API_KEY (or WYRM_NIM_API_KEY) in the server env, then: wyrm index rebuild";function M(o){const t=o||process.env.WYRM_VECTOR_PROVIDER||"auto";switch(t){case"nim":{const e=process.env.NVIDIA_API_KEY||process.env.WYRM_NIM_API_KEY||"",s=process.env.WYRM_NIM_BASE_URL||"https://integrate.api.nvidia.com",n=m(s);if(!e&&n)return{requested:t,resolved:"none",model:null,reason:"nim_missing_key",fix:l,egressHost:null};const r=process.env.WYRM_NIM_EMBED_MODEL||"nvidia/llama-nemotron-embed-1b-v2";return{requested:t,resolved:"nim",model:r,reason:"",fix:"",egressHost:n}}case"openai":return process.env.OPENAI_API_KEY?{requested:t,resolved:"openai",model:"text-embedding-3-small",reason:"",fix:"",egressHost:"api.openai.com"}:{requested:t,resolved:"none",model:null,reason:"openai_missing_key",fix:"set OPENAI_API_KEY, or use the supported provider: "+l,egressHost:null};case"ollama":{const e=process.env.OLLAMA_URL||"http://localhost:11434";return{requested:t,resolved:"ollama",model:process.env.WYRM_EMBED_MODEL||"nomic-embed-text",reason:"ollama_deprecated",fix:"Ollama support is deprecated and leaves in the next major \u2014 "+l,egressHost:m(e)}}case"none":return{requested:t,resolved:"none",model:null,reason:"disabled",fix:"vectors were explicitly disabled; to enable: "+l,egressHost:null};case"local":return{requested:t,resolved:"local",model:"hash-384",reason:"",fix:"",egressHost:null};default:return{requested:t,resolved:"none",model:null,reason:"no_provider_configured",fix:l,egressHost:null}}}let h=!1;function A(o){switch(o?.provider||process.env.WYRM_VECTOR_PROVIDER||"auto"){case"openai":{const e=o?.apiKey||process.env.OPENAI_API_KEY;if(!e)throw new Error("OpenAI provider requires OPENAI_API_KEY");return new v(e,o?.model)}case"ollama":return h||(h=!0,console.error("Wyrm: WYRM_VECTOR_PROVIDER=ollama is deprecated \u2014 the supported embedding provider is NVIDIA NIM ("+l+")")),new I(o?.ollamaUrl||process.env.OLLAMA_URL,o?.model);case"nim":{const e=process.env.NVIDIA_API_KEY||process.env.WYRM_NIM_API_KEY||o?.apiKey||"",s=o?.nimBaseUrl||process.env.WYRM_NIM_BASE_URL||"https://integrate.api.nvidia.com";if(!e&&m(s))throw new Error("NIM provider requires NVIDIA_API_KEY (or WYRM_NIM_API_KEY) for a remote endpoint");return new p(e,o?.model,o?.nimBaseUrl)}case"none":return new u;case"auto":return new u;default:return new b}}export{b as LocalHashProvider,l as NIM_FIX_LINE,p as NimProvider,u as NoneProvider,I as OllamaProvider,v as OpenAIProvider,A as createProvider,m as hostIfRemote,M as resolveEmbeddingState};
1
+ import{isLoopbackHost as b}from"../receipt.js";function m(o){try{const e=new URL(o).hostname.toLowerCase();return b(e)?null:e}catch{return null}}class v{name="local-hash";model="hash-384";dimensions=384;async embed(e){const t=new Float32Array(this.dimensions),s=e.toLowerCase().split(/\s+/);for(let r=0;r<s.length;r++){const i=s[r];for(let a=0;a<i.length;a++){const l=i.charCodeAt(a)*(r+1)*(a+1)%this.dimensions;t[l]+=1/s.length}}const n=Math.sqrt(t.reduce((r,i)=>r+i*i,0));if(n>0)for(let r=0;r<t.length;r++)t[r]/=n;return t}async isReady(){return!0}}class y{name="openai";model;dimensions;remoteHost="api.openai.com";apiKey;timeoutMs;constructor(e,t){this.apiKey=e,this.model=t||"text-embedding-3-small",this.dimensions=this.model.includes("large")?3072:1536;const s=Number(process.env.WYRM_OPENAI_TIMEOUT);this.timeoutMs=Number.isFinite(s)&&s>0?s:8e3}async embed(e){const t=await fetch("https://api.openai.com/v1/embeddings",{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify({input:e,model:this.model}),signal:AbortSignal.timeout(this.timeoutMs)});if(!t.ok){const n=await t.text();throw new Error(`OpenAI embedding API error ${t.status}: ${n}`)}const s=await t.json();return new Float32Array(s.data[0].embedding)}async isReady(){return!!this.apiKey}}class I{name="ollama";model;dimensions;remoteHost;baseUrl;timeoutMs;constructor(e,t){this.baseUrl=e||"http://localhost:11434",this.model=t||"nomic-embed-text",this.dimensions=this.model.includes("mxbai")?1024:768,this.remoteHost=m(this.baseUrl);const s=Number(process.env.WYRM_OLLAMA_TIMEOUT);this.timeoutMs=Number.isFinite(s)&&s>0?s:8e3}async embed(e){const t=await fetch(`${this.baseUrl}/api/embeddings`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:this.model,prompt:e}),signal:AbortSignal.timeout(this.timeoutMs)});if(!t.ok){const n=await t.text();throw new Error(`Ollama embedding API error ${t.status}: ${n}`)}const s=await t.json();return new Float32Array(s.embedding)}async isReady(){try{return(await fetch(`${this.baseUrl}/api/tags`,{signal:AbortSignal.timeout(2e3)})).ok}catch{return!1}}}class p{name="nim";model;remoteHost;dims;baseUrl;apiKey;timeoutMs;static KNOWN_DIMS={"nvidia/llama-nemotron-embed-1b-v2":2048,"nvidia/llama-3.2-nv-embedqa-1b-v2":2048,"nvidia/nv-embedqa-e5-v5":1024,"nvidia/nv-embedqa-mistral-7b-v2":4096,"nvidia/nv-embed-v1":4096};get dimensions(){return this.dims}constructor(e,t,s){this.apiKey=e,this.model=t||process.env.WYRM_NIM_EMBED_MODEL||"nvidia/llama-nemotron-embed-1b-v2",this.baseUrl=(s||process.env.WYRM_NIM_BASE_URL||"https://integrate.api.nvidia.com").replace(/\/+$/,"");const n=Number(process.env.WYRM_NIM_EMBED_DIM);this.dims=Number.isFinite(n)&&n>0?n:p.KNOWN_DIMS[this.model]??2048;const r=Number(process.env.WYRM_NIM_TIMEOUT);this.timeoutMs=Number.isFinite(r)&&r>0?r:8e3,this.remoteHost=m(this.baseUrl)}async embed(e,t="passage"){let s;for(let i=0;;i++){let a=null;try{const l={"Content-Type":"application/json"};if(this.apiKey&&(l.Authorization=`Bearer ${this.apiKey}`),s=await fetch(`${this.baseUrl}/v1/embeddings`,{method:"POST",headers:l,body:JSON.stringify({input:[e],model:this.model,input_type:t,encoding_format:"float",truncate:"END"}),signal:AbortSignal.timeout(this.timeoutMs)}),s.ok||s.status!==429&&s.status<500)break;const c=Number(s.headers?.get?.("retry-after"));a=Number.isFinite(c)&&c>0?Math.min(c*1e3,15e3):500*2**i}catch(l){if(i>=2)throw l;a=500*2**i}if(i>=2)break;await new Promise(l=>setTimeout(l,a??500))}if(!s.ok){const i=await s.text();throw new Error(`NIM embedding API error ${s.status}: ${i.slice(0,300)}`)}const r=(await s.json()).data?.[0]?.embedding;if(!Array.isArray(r)||r.length===0)throw new Error("NIM embedding API returned no embedding");return r.length!==this.dims&&(this.dims=r.length),new Float32Array(r)}async isReady(){return this.remoteHost?!!this.apiKey:!0}}class u{name="none";model="none";dimensions=0;async embed(e){return new Float32Array(0)}async isReady(){return!0}}const d="set WYRM_VECTOR_PROVIDER=nim and NVIDIA_API_KEY (or WYRM_NIM_API_KEY) in the server env, then: wyrm index rebuild";function h(o){const e=String(o??"").trim().toLowerCase();return e===""?"auto":e}function M(o){try{return new URL(o),!1}catch{return!0}}function E(o){const e=h(o??process.env.WYRM_VECTOR_PROVIDER);switch(e){case"nim":{const t=process.env.NVIDIA_API_KEY||process.env.WYRM_NIM_API_KEY||"",s=process.env.WYRM_NIM_BASE_URL||"https://integrate.api.nvidia.com";if(M(s))return{requested:e,resolved:"none",model:null,reason:"nim_bad_base_url",fix:"WYRM_NIM_BASE_URL is not a valid URL (needs https://\u2026): "+s,egressHost:null};const n=m(s);if(!t&&n)return{requested:e,resolved:"none",model:null,reason:"nim_missing_key",fix:d,egressHost:null};const r=process.env.WYRM_NIM_EMBED_MODEL||"nvidia/llama-nemotron-embed-1b-v2";return{requested:e,resolved:"nim",model:r,reason:"",fix:"",egressHost:n}}case"openai":return process.env.OPENAI_API_KEY?{requested:e,resolved:"openai",model:"text-embedding-3-small",reason:"",fix:"",egressHost:"api.openai.com"}:{requested:e,resolved:"none",model:null,reason:"openai_missing_key",fix:"set OPENAI_API_KEY, or use the supported provider: "+d,egressHost:null};case"ollama":{const t=process.env.OLLAMA_URL||"http://localhost:11434";return{requested:e,resolved:"ollama",model:process.env.WYRM_EMBED_MODEL||"nomic-embed-text",reason:"ollama_deprecated",fix:"Ollama support is deprecated and leaves in the next major \u2014 "+d,egressHost:m(t),advisory:!0}}case"none":return{requested:e,resolved:"none",model:null,reason:"disabled",fix:"vectors were explicitly disabled; to enable: "+d,egressHost:null};case"local":return{requested:e,resolved:"local",model:"hash-384",reason:"local_hash_fallback",fix:"local-hash is non-semantic (test only) \u2014 "+d,egressHost:null,advisory:!0};default:return{requested:e,resolved:"none",model:null,reason:"no_provider_configured",fix:d,egressHost:null}}}let _=!1;function g(o){switch(h(o?.provider??process.env.WYRM_VECTOR_PROVIDER)){case"openai":{const t=o?.apiKey||process.env.OPENAI_API_KEY;if(!t)throw new Error("OpenAI provider requires OPENAI_API_KEY");return new y(t,o?.model)}case"ollama":return _||(_=!0,console.error("Wyrm: WYRM_VECTOR_PROVIDER=ollama is deprecated \u2014 the supported embedding provider is NVIDIA NIM ("+d+")")),new I(o?.ollamaUrl||process.env.OLLAMA_URL,o?.model);case"nim":{const t=process.env.NVIDIA_API_KEY||process.env.WYRM_NIM_API_KEY||o?.apiKey||"",s=o?.nimBaseUrl||process.env.WYRM_NIM_BASE_URL||"https://integrate.api.nvidia.com";if(!t&&m(s))throw new Error("NIM provider requires NVIDIA_API_KEY (or WYRM_NIM_API_KEY) for a remote endpoint");return new p(t,o?.model,o?.nimBaseUrl)}case"none":return new u;case"auto":return new u;case"local":return new v;default:return new u}}export{v as LocalHashProvider,d as NIM_FIX_LINE,p as NimProvider,u as NoneProvider,I as OllamaProvider,y as OpenAIProvider,g as createProvider,m as hostIfRemote,h as normalizeProviderKey,E as resolveEmbeddingState};
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v2",
3
- "version": "8.3.0",
3
+ "version": "8.4.0",
4
4
  "derivedFrom": "standard tier MINUS device-local/egress/subprocess (src/cloud-profile.ts)",
5
5
  "count": 19,
6
6
  "tools": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v1",
3
- "version": "8.3.0",
3
+ "version": "8.4.0",
4
4
  "tools": [
5
5
  {
6
6
  "name": "wyrm_recall",
@@ -1 +1 @@
1
- import{createVectorStore as p}from"./vectors.js";import{IndexingPipeline as E}from"./indexer.js";function v(t,s,i){let r=null,o=null;try{const n=process.env.WYRM_VECTOR_PROVIDER||"auto";if(n!=="none"){r=p({provider:n},t.getDatabase()),s.setVectorStore(r);const c=(u,a)=>{try{switch(u){case"session":{const e=t.getSession(a);return e&&[e.objectives,e.completed,e.notes,e.summary].filter(Boolean).join(" ")||null}case"quest":{const e=t.getDatabase().prepare("SELECT * FROM quests WHERE id = ?").get(a);return e&&[e.title,e.description].filter(Boolean).join(" ")||null}case"note":case"context":{const e=t.getDatabase().prepare("SELECT value FROM data_lake WHERE id = ?").all(a);if(!e.length)return null;const l=i.maybeDecrypt(e[0].value);return l.startsWith("ENCRYPTED:")?null:l}default:return null}}catch{return null}};o=new E(t.getDatabase(),r,c,{batchSize:20,maxRetries:3,intervalMs:1e4}),o.start()}}catch(n){console.error("Wyrm: Failed to initialize vector store:",n)}return{vectorStore:r,indexingPipeline:o}}export{v as initVectorSearch};
1
+ import{createVectorStore as p}from"./vectors.js";import{resolveEmbeddingState as m}from"./providers/embedding-provider.js";import{IndexingPipeline as v}from"./indexer.js";function R(t,s,i){let r=null,o=null;try{const n=process.env.WYRM_VECTOR_PROVIDER||"auto";if(m(n).resolved!=="none"){r=p({provider:n},t.getDatabase()),s.setVectorStore(r);const c=(u,a)=>{try{switch(u){case"session":{const e=t.getSession(a);return e&&[e.objectives,e.completed,e.notes,e.summary].filter(Boolean).join(" ")||null}case"quest":{const e=t.getDatabase().prepare("SELECT * FROM quests WHERE id = ?").get(a);return e&&[e.title,e.description].filter(Boolean).join(" ")||null}case"note":case"context":{const e=t.getDatabase().prepare("SELECT value FROM data_lake WHERE id = ?").all(a);if(!e.length)return null;const l=i.maybeDecrypt(e[0].value);return l.startsWith("ENCRYPTED:")?null:l}default:return null}}catch{return null}};o=new v(t.getDatabase(),r,c,{batchSize:20,maxRetries:3,intervalMs:1e4}),o.start()}}catch(n){console.error("Wyrm: Failed to initialize vector store:",n)}return{vectorStore:r,indexingPipeline:o}}export{R as initVectorSearch};