wyrm-mcp 8.3.1 → 8.5.1
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.
- package/dist/branding.js +1 -0
- package/dist/http-auth.js +1 -1
- package/dist/http-fast.js +19 -19
- package/dist/metrics.js +1 -0
- package/dist/migrations.js +23 -15
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/ui-dashboard.js +167 -5
- package/dist/wyrm-cli.js +114 -113
- package/dist/wyrm-manifest.json +1 -1
- package/package.json +1 -2
- package/ui/dragon-mark.svg +0 -931
package/dist/branding.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync as d}from"node:fs";import{homedir as p}from"node:os";import{join as R}from"node:path";const n={product:"Wyrm",fleetLabel:"Fleet",fleetTitle:"Fleet Channel",fleetGrove:"fleet-warroom",fleetBlurb:"The multi-agent coordination channel. Findings, claims, fix-receipts and verdicts flow as structured work between agents."};function L(){try{const t=process.env.WYRM_BRAND_FILE||R(p(),".wyrm","brand.json");return JSON.parse(d(t,"utf8"))}catch{return{}}}function F(t=!1){const e=L(),l=i=>{const r=process.env[i];return r&&r.trim()?r.trim():void 0},o=(i,r,_)=>l(i)??(r&&r.trim()?r.trim():void 0)??_,a=!!(e.fleetLabel||e.fleetTitle||e.fleet==="on"||e.profile),c=(l("WYRM_UI_FLEET")||e.fleet||(a?"on":"auto")).toLowerCase(),u=c==="on"?!0:c==="off"?!1:t,f=l("WYRM_UI_BRAND_MARK_URL")??e.markUrl,s=l("WYRM_UI_BRAND_MARK")??e.mark,m=f?`<img src="${E(f)}" alt="" class="logo-mark" />`:s?B(s):"";return{product:o("WYRM_UI_BRAND_NAME",e.product,n.product),markHtml:m,showFleet:u,fleetLabel:o("WYRM_UI_FLEET_LABEL",e.fleetLabel,n.fleetLabel),fleetTitle:o("WYRM_UI_FLEET_TITLE",e.fleetTitle,n.fleetTitle),fleetGrove:o("WYRM_UI_FLEET_GROVE",e.fleetGrove,n.fleetGrove),fleetBlurb:l("WYRM_UI_FLEET_BLURB")??(e.fleetBlurb&&e.fleetBlurb.trim()?e.fleetBlurb.trim():void 0)??n.fleetBlurb}}function E(t){return String(t).replace(/["'<>]/g,"").slice(0,300)}function B(t){return String(t).replace(/[<>&"']/g,"").slice(0,24)}export{F as resolveBranding};
|
package/dist/http-auth.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import{existsSync as w,readFileSync as I,writeFileSync as u,mkdirSync as R}from"fs";import{join as y}from"path";import{homedir as x}from"os";import{randomBytes as O,createHash as T}from"crypto";import{WyrmLogger as k}from"./logger.js";const g=y(x(),".wyrm"),a=y(g,"http-config.json"),L={enabled:!0,requests:100,windowMs:6e4},S=["http://localhost:3333","http://127.0.0.1:3333"],d=new Map;let c=null;const i=new k;function m(e){return T("sha256").update(e).digest("hex")}function P(e,t){if(e.length!==t.length)return!1;let r=0;for(let n=0;n<e.length;n++)r|=e.charCodeAt(n)^t.charCodeAt(n);return r===0}function M(){if(w(g)||R(g,{recursive:!0,mode:448}),w(a))try{const r=JSON.parse(I(a,"utf-8"));return{apiKeyHash:r.apiKeyHash,allowedOrigins:r.allowedOrigins||S,rateLimit:{...L,...r.rateLimit},requireAuth:r.requireAuth??!0,devMode:process.env.WYRM_DEV==="true"}}catch(r){i.error("Failed to load HTTP config, regenerating",{error:r.message})}const e=O(32).toString("hex");i.info("Generated new API key for HTTP server"),console.log(`
|
|
2
2
|
`+"\u2550".repeat(60)),console.log("\u{1F510} WYRM API KEY (save this securely, shown once):"),console.log(""),console.log(` ${e}`),console.log(""),console.log(" Use with: Authorization: Bearer <key>"),console.log("\u2550".repeat(60)+`
|
|
3
|
-
`);const t={apiKeyHash:m(e),allowedOrigins:S,rateLimit:L,requireAuth:!0,devMode:process.env.WYRM_DEV==="true"};return u(a,JSON.stringify(t,null,2),{mode:384}),t}function s(){return c||(c=M()),c}function N(e){const t=s();if(t.devMode){const h=e.socket.remoteAddress;if(h==="127.0.0.1"||h==="::1"||h==="::ffff:127.0.0.1")return!0}const r=e.socket.remoteAddress??"",n=r==="127.0.0.1"||r==="::1"||r==="::ffff:127.0.0.1",o=(e.headers.host??"").split(":")[0].toLowerCase();if(n&&(o==="localhost"||o==="127.0.0.1"||o==="::1"||o==="[::1]")&&e.headers["x-wyrm-origin"]==="ui")return!0;if(!t.requireAuth)return i.warn("Authentication disabled - API is open"),!0;const l=e.headers.authorization;if(!l||!l.startsWith("Bearer "))return!1;const f=l.slice(7);if(!f||f.length<32)return!1;const C=m(f);return P(C,t.apiKeyHash)}function H(e){const t=s();if(!t.rateLimit.enabled)return{allowed:!0,remaining:1/0,resetAt:0};const r=e.socket.remoteAddress||"unknown",n=Date.now();if(d.size>1e4)for(const[A,l]of d)l.resetAt<n&&d.delete(A);let o=d.get(r);return(!o||o.resetAt<n)&&(o={count:0,resetAt:n+t.rateLimit.windowMs},d.set(r,o)),o.count++,{allowed:o.count<=t.rateLimit.requests,remaining:Math.max(0,t.rateLimit.requests-o.count),resetAt:o.resetAt}}function v(e){const t=s(),r=e.headers.origin;return r&&t.allowedOrigins.includes(r)?r:t.allowedOrigins[0]}function p(e){return{"Access-Control-Allow-Origin":v(e),"Access-Control-Allow-Methods":"GET, POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, Authorization","X-Content-Type-Options":"nosniff","X-Frame-Options":"DENY","Cache-Control":"no-store","Content-Security-Policy":"default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'","Referrer-Policy":"no-referrer","Permissions-Policy":"geolocation=(), microphone=(), camera=()","X-Robots-Tag":"noindex, nofollow"}}const D=["/health","/auth/status","/ui"];function J(e,t){if(e.method==="OPTIONS"){const o=p(e);return t.writeHead(204,o),t.end(),{error:!0}}const r=H(e);if(!r.allowed){const o=p(e);return t.writeHead(429,{...o,"X-RateLimit-Remaining":"0","X-RateLimit-Reset":String(r.resetAt),"Retry-After":String(Math.ceil((r.resetAt-Date.now())/1e3))}),t.end(JSON.stringify({error:"Rate limit exceeded"})),i.warn("Rate limit exceeded",{ip:e.socket.remoteAddress,path:e.url}),{error:!0}}const n=new URL(e.url||"/","http://localhost").pathname;if(D.includes(n))return{error:!1};if(!N(e)){const o=p(e);return t.writeHead(401,{...o,"WWW-Authenticate":'Bearer realm="Wyrm API"'}),t.end(JSON.stringify({error:"Unauthorized"})),i.warn("Authentication failed",{ip:e.socket.remoteAddress,path:e.url}),{error:!0}}return{error:!1}}function _(){const e=O(32).toString("hex"),t=s();return t.apiKeyHash=m(e),u(a,JSON.stringify(t,null,2),{mode:384}),c=t,i.info("API key regenerated"),e}function U(e){const t=s();t.allowedOrigins=e,u(a,JSON.stringify(t,null,2),{mode:384}),c=t,i.info("CORS origins updated",{origins:e})}function z(e,t){const r=s();r.rateLimit={enabled:!0,requests:e,windowMs:t},u(a,JSON.stringify(r,null,2),{mode:384}),c=r,i.info("Rate limit updated",{requests:e,windowMs:t})}function B(e){const t=s();t.requireAuth=e,u(a,JSON.stringify(t,null,2),{mode:384}),c=t,e||i.warn("Authentication disabled - API is now open")}function G(){const e=s();e.devMode=!0}function X(){const e=s();return{requireAuth:e.requireAuth,devMode:e.devMode,rateLimit:e.rateLimit,allowedOrigins:e.allowedOrigins}}export{J as authMiddleware,N as authenticate,H as checkRateLimit,G as enableDevMode,X as getAuthStatus,v as getCorsOrigin,p as getSecurityHeaders,_ as regenerateApiKey,U as setAllowedOrigins,z as setRateLimit,B as setRequireAuth};
|
|
3
|
+
`);const t={apiKeyHash:m(e),allowedOrigins:S,rateLimit:L,requireAuth:!0,devMode:process.env.WYRM_DEV==="true"};return u(a,JSON.stringify(t,null,2),{mode:384}),t}function s(){return c||(c=M()),c}function N(e){const t=s();if(t.devMode){const h=e.socket.remoteAddress;if(h==="127.0.0.1"||h==="::1"||h==="::ffff:127.0.0.1")return!0}const r=e.socket.remoteAddress??"",n=r==="127.0.0.1"||r==="::1"||r==="::ffff:127.0.0.1",o=(e.headers.host??"").split(":")[0].toLowerCase();if(n&&(o==="localhost"||o==="127.0.0.1"||o==="::1"||o==="[::1]")&&e.headers["x-wyrm-origin"]==="ui")return!0;if(!t.requireAuth)return i.warn("Authentication disabled - API is open"),!0;const l=e.headers.authorization;if(!l||!l.startsWith("Bearer "))return!1;const f=l.slice(7);if(!f||f.length<32)return!1;const C=m(f);return P(C,t.apiKeyHash)}function H(e){const t=s();if(!t.rateLimit.enabled)return{allowed:!0,remaining:1/0,resetAt:0};const r=e.socket.remoteAddress||"unknown",n=Date.now();if(d.size>1e4)for(const[A,l]of d)l.resetAt<n&&d.delete(A);let o=d.get(r);return(!o||o.resetAt<n)&&(o={count:0,resetAt:n+t.rateLimit.windowMs},d.set(r,o)),o.count++,{allowed:o.count<=t.rateLimit.requests,remaining:Math.max(0,t.rateLimit.requests-o.count),resetAt:o.resetAt}}function v(e){const t=s(),r=e.headers.origin;return r&&t.allowedOrigins.includes(r)?r:t.allowedOrigins[0]}function p(e){return{"Access-Control-Allow-Origin":v(e),"Access-Control-Allow-Methods":"GET, POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, Authorization","X-Content-Type-Options":"nosniff","X-Frame-Options":"DENY","Cache-Control":"no-store","Content-Security-Policy":"default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'","Referrer-Policy":"no-referrer","Permissions-Policy":"geolocation=(), microphone=(), camera=()","X-Robots-Tag":"noindex, nofollow"}}const D=["/health","/auth/status","/ui"];function J(e,t){if(e.method==="OPTIONS"){const o=p(e);return t.writeHead(204,o),t.end(),{error:!0}}const r=H(e);if(!r.allowed){const o=p(e);return t.writeHead(429,{...o,"X-RateLimit-Remaining":"0","X-RateLimit-Reset":String(r.resetAt),"Retry-After":String(Math.ceil((r.resetAt-Date.now())/1e3))}),t.end(JSON.stringify({error:"Rate limit exceeded"})),i.warn("Rate limit exceeded",{ip:e.socket.remoteAddress,path:e.url}),{error:!0}}const n=new URL(e.url||"/","http://localhost").pathname;if(D.includes(n))return{error:!1};if(!N(e)){const o=p(e);return t.writeHead(401,{...o,"WWW-Authenticate":'Bearer realm="Wyrm API"'}),t.end(JSON.stringify({error:"Unauthorized"})),i.warn("Authentication failed",{ip:e.socket.remoteAddress,path:e.url}),{error:!0}}return{error:!1}}function _(){const e=O(32).toString("hex"),t=s();return t.apiKeyHash=m(e),u(a,JSON.stringify(t,null,2),{mode:384}),c=t,i.info("API key regenerated"),e}function U(e){const t=s();t.allowedOrigins=e,u(a,JSON.stringify(t,null,2),{mode:384}),c=t,i.info("CORS origins updated",{origins:e})}function z(e,t){const r=s();r.rateLimit={enabled:!0,requests:e,windowMs:t},u(a,JSON.stringify(r,null,2),{mode:384}),c=r,i.info("Rate limit updated",{requests:e,windowMs:t})}function B(e){const t=s();t.requireAuth=e,u(a,JSON.stringify(t,null,2),{mode:384}),c=t,e||i.warn("Authentication disabled - API is now open")}function G(){const e=s();e.devMode=!0}function X(){const e=s();return{requireAuth:e.requireAuth,devMode:e.devMode,rateLimit:e.rateLimit,allowedOrigins:e.allowedOrigins}}export{J as authMiddleware,N as authenticate,H as checkRateLimit,G as enableDevMode,X as getAuthStatus,v as getCorsOrigin,p as getSecurityHeaders,_ as regenerateApiKey,U as setAllowedOrigins,z as setRateLimit,B as setRequireAuth};
|
package/dist/http-fast.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import z from"http";import{WyrmDB as W}from"./database.js";import{cache as h,estimateTokens as Z,truncateToTokens as ee,timed as G}from"./performance.js";import{authMiddleware as te,getAuthStatus as k,getSecurityHeaders as b}from"./http-auth.js";import{WyrmLogger as re}from"./logger.js";import{sanitizeFtsQuery as U,validateProjectPath as ne}from"./security.js";import{readFileSync as R,existsSync as y,readdirSync as se,realpathSync as x}from"fs";import{homedir as oe}from"os";import{dirname as q,join as S}from"path";import{fileURLToPath as ae}from"url";const B=S(q(ae(import.meta.url)),"..","ui","dragon-mark.svg");let v=null;import{getUIDashboardHTML as ie}from"./ui-dashboard.js";import{resolveBranding as I}from"./branding.js";import{verifyLicense as $}from"./license.js";import{rerankConfigured as ce,rerankEgressHost as le}from"./rerank.js";import{MemoryArtifacts as de}from"./memory-artifacts.js";import{createVectorStore as ue}from"./vectors.js";import{GroundTruths as pe,computeStaleness as me}from"./intelligence.js";import{Causality as fe}from"./causality.js";import{wireTruthAutoCascade as Ee}from"./truth-cascade.js";import{FailurePatterns as he}from"./failure-patterns.js";import{handleDaemonWrite as _e}from"./daemon-write-endpoint.js";import{retryableWriteCause as Y,busyErrorBody as J}from"./sqlite-busy.js";import{sseFrame as Se,resolveStartCursor as K,SSE_KEEPALIVE as ye}from"./events-sse.js";import{readonlyBlocks as ge}from"./readonly-gate.js";import{resolveActorEnvelope as Te,runWithActor as Oe}from"./handlers/boundary.js";const j=parseInt(process.env.WYRM_PORT||"3333")||3333,g=process.env.WYRM_UI_READONLY==="1",Re=512*1024,M=500,ve=parseInt(process.env.WYRM_MAX_SSE_STREAMS||"64",10)||64,Ce=parseInt(process.env.WYRM_MAX_SSE_PER_IP||"8",10)||8;let w=0;const T=new Map;let O=null,C=null,L=null,N=null;function a(){return O||(O=new W(process.env.WYRM_DB_PATH)),O}function A(){if(!C){C=new de(a().getDatabase());try{const e=process.env.WYRM_VECTOR_PROVIDER||"auto";e!=="none"&&C.setVectorStore(ue({provider:e},a().getDatabase()))}catch{}}return C}function Le(){return L||(L=new pe(a().getDatabase()),Ee(L,new fe(a().getDatabase()))),L}function be(){return N||(N=new he(a().getDatabase())),N}function je(e){const t=O;O=e,C=null,L=null,N=null;try{t?.close()}catch{}}function Q(){const e=[],t=oe(),r=new Set;let n=[];try{n=se(t)}catch{return e}n.sort((s,o)=>o.localeCompare(s));for(const s of n){if(s!==".wyrm"&&!s.startsWith(".wyrm-"))continue;let o;try{o=x(S(t,s))}catch{continue}if(r.has(o))continue;const d=S(o,"wyrm.db");if(!y(d))continue;r.add(o);let i="Local",l="free";try{const c=S(o,"license.json");if(y(c)){const u=JSON.parse(R(c,"utf-8")),m=$(u);i=u.license&&u.license.issued_to||i,l=m.valid?m.tier:"free"}else{const u=S(o,"cloud.json");y(u)&&(i=JSON.parse(R(u,"utf-8")).email||i)}}catch{}const p=s.replace(/^\.wyrm-?/,"")||"default";e.push({name:p,dbPath:d,account:i,tier:l})}return e}const D=new re({level:"info"});function _(e,t,r){const n=Number.parseInt(String(e??""),10);return!Number.isFinite(n)||n<1?t:Math.min(n,r)}function P(e){const t=e==null?"":String(e);if(t)return a().getProject(t)||a().getProjectByName(t)}function E(e,t,r,n=200){const s=JSON.stringify(r),o=b(t);e.writeHead(n,{"Content-Type":"application/json","Content-Length":Buffer.byteLength(s),"X-Tokens":String(Z(r)),...o}),e.end(s)}function Me(e,t,r){const n=b(t);e.writeHead(200,{...n,"Content-Type":"text/html; charset=utf-8","Content-Length":Buffer.byteLength(r),"Cache-Control":"no-store"}),e.end(r)}function Ne(e){return new Promise((t,r)=>{if(e.method==="GET"){t({});return}let n="",s=0;e.on("data",o=>{if(s+=o.length,s>Re){e.destroy(),r(new Error("Request body too large"));return}n+=o}),e.on("end",()=>{try{t(n?JSON.parse(n):{})}catch{r(new Error("Invalid JSON"))}}),e.on("error",r)})}const X={"GET /c":e=>{const t=`ctx:${e.p||"global"}`,r=h.get(t);if(r)return r;const n=e.p;let s;if(n){const o=a().getProject(n);if(!o)return{e:"not found"};const d=a().getPendingQuests(o.id),i=a().getRecentSessions(o.id,3);s={n:o.name,s:o.stack,q:d.length,qt:d.slice(0,5).map(l=>l.title),r:i[0]?.summary||i[0]?.notes?.slice(0,200)||null}}else{const o=a().getAllProjects(20),d=a().getAllPendingQuests();s={p:o.map(i=>({n:i.name,s:i.stack,q:a().getPendingQuests(i.id).length})),tq:d.length}}return h.set(t,s),s},"GET /p":()=>{const e=h.get("projects");if(e)return e;const r=a().getAllProjects(50).map(n=>({i:n.id,n:n.name,p:n.path,s:n.stack}));return h.set("projects",r),r},"POST /scan":e=>{const t=e.d||e.directory||process.env.HOME+"/Git Projects",r=ne(t);return r?(h.invalidate(),{found:a().scanForProjects(r,!0).length}):{e:"path not allowed"}},"GET /q":e=>{const t=e.p;if(t){const r=a().getProject(t);return r?a().getPendingQuests(r.id).map(n=>({i:n.id,t:n.title,p:n.priority[0]})):{e:"not found"}}return a().getAllPendingQuests().slice(0,20).map(r=>({i:r.id,t:r.title,p:r.priority[0]}))},"POST /q":e=>{const t=e.p,r=a().getProject(t);return r?(h.invalidate("ctx"),{i:a().addQuest(r.id,e.t,e.d,e.pr||"medium").id}):{e:"not found"}},"POST /qc":e=>{const t=typeof e.i=="number"?e.i:parseInt(String(e.i),10);return!t||t<1?{e:"id required"}:(h.invalidate("ctx"),a().updateQuest(t,"completed"),{ok:1})},"POST /s":e=>{const t=e.p;let r=a().getProject(t);if(r||(a().scanForProjects(t,!1),r=a().getProject(t)),!r)return{e:"not found"};let n=a().getTodaySession(r.id);return n||(n=a().createSession(r.id,{objectives:e.o||"",notes:""})),h.invalidate("ctx"),{i:n.id,d:n.date}},"POST /su":e=>{const t=e.i;return h.invalidate("ctx"),a().updateSession(t,{notes:e.n,summary:e.s,completed:e.c}),{ok:1}},"POST /x":e=>{const t=e.p,r=a().getProject(t);return r?(a().setContext(r.id,e.k,e.v),h.invalidate("ctx"),{ok:1}):{e:"not found"}},"GET /x":e=>{const t=e.p,r=a().getProject(t);return r?a().getAllContext(r.id):{e:"not found"}},"POST /g":e=>{const t=e.k,r=e.v;return!t||typeof t!="string"?{e:"k (key) required"}:r==null?{e:"v (value) required"}:(a().setGlobalContext(t,String(r)),h.invalidate(),{ok:1})},"GET /g":()=>a().getAllGlobalContext(),"GET /s":e=>{const t=e.q;if(!t)return{e:"query required"};const r=U(t);return{q:a().searchQuests(r).slice(0,5).map(s=>({i:s.id,t:s.title})),s:a().searchSessions(r).slice(0,3).map(s=>({i:s.id,d:s.date})),p:a().searchProjects(r).slice(0,3).map(s=>({n:s.name,p:s.path}))}},"POST /d":e=>{const t=e.p,r=a().getProject(t);return r?{i:a().insertData(r.id,e.c,e.k,e.v,e.m).id}:{e:"not found"}},"GET /d":e=>{const t=e.p,r=a().getProject(t);if(!r)return{e:"not found"};const n=_(e.l,20,100);return a().queryData(r.id,e.c,n).map(s=>({k:s.key,v:ee(s.value,100)}))},"GET /d/search":e=>{const t=e.p,r=a().getProject(t);if(!r)return{e:"not found"};const n=e.q;if(!n)return{e:"q (query) required"};const s=e.c,o=_(e.l,10,100);let d=a().searchData(n,r.id);return s&&(d=d.filter(i=>i.category===s)),d.slice(0,o).map(i=>({k:i.key,v:i.value,c:i.category,m:i.metadata}))},"GET /d/agg":e=>{const t=e.p,r=a().getProject(t);if(!r)return{e:"not found"};const n=e.c;if(!n)return{e:"c (category) required"};const s=e.stack_key,o=e.scanner,d=_(e.l,1e3,1e4),i={};for(const l of a().queryData(r.id,n,d)){let p;try{p=JSON.parse(l.value)}catch{continue}if(s&&p.stack_key!==s||o&&p.scanner!==o)continue;const c=String(p.scanner??""),u=String(p.payload_hash??"");if(!c||!u)continue;const m=`${c}:${u}`,f=i[m]??{s:c,ph:u,sk:String(p.stack_key??""),h:0,t:0};f.t+=1,p.hit&&(f.h+=1),i[m]=f}return Object.values(i).map(l=>({...l,r:l.t>0?l.h/l.t:0})).sort((l,p)=>p.r-l.r||p.h-l.h)},"POST /batch":e=>{const t=e.ops;if(!t||!Array.isArray(t))return{e:"ops required"};if(t.length>M)return{e:`max ${M} ops per batch`};const r=performance.now();return{r:t.map(s=>{const o=`${s.m} ${s.r}`,d=X[o];if(!d)return{e:"unknown"};try{return{d:d(s.a||{})}}catch{return{e:"op failed"}}}),ms:Math.round(performance.now()-r)}},"POST /write":e=>_e({db:a(),artifacts:A(),truths:Le(),failures:be()},e),"GET /syms":e=>{const t=String(e.q??"").slice(0,200);if(!t)return[];const r=_(e.limit,20,200),n=a().getDatabase();try{return n.prepare(`
|
|
3
3
|
SELECT symbol, kind, language, file_path, line, signature, project_id
|
|
4
4
|
FROM symbol_index
|
|
5
5
|
WHERE symbol LIKE ?
|
|
6
6
|
ORDER BY project_id, file_path, line
|
|
7
7
|
LIMIT ?
|
|
8
|
-
`).all(`%${t}%`,r)}catch{return[]}},"GET /failures":e=>{const t=String(e.target??"").slice(0,200),r=
|
|
8
|
+
`).all(`%${t}%`,r)}catch{return[]}},"GET /failures":e=>{const t=String(e.target??"").slice(0,200),r=_(e.limit,10,100),n=a().getDatabase();try{return n.prepare(`
|
|
9
9
|
SELECT id, scope, target, description, why_failed, occurrences,
|
|
10
10
|
severity, last_seen
|
|
11
11
|
FROM failure_patterns
|
|
12
12
|
WHERE resolved = 0 AND (target = ? OR target LIKE ?)
|
|
13
13
|
ORDER BY occurrences DESC, last_seen DESC
|
|
14
14
|
LIMIT ?
|
|
15
|
-
`).all(t,`%${t}%`,r)}catch{return[]}},"GET /truths":e=>{const t=String(e.q??"").slice(0,200),r=
|
|
15
|
+
`).all(t,`%${t}%`,r)}catch{return[]}},"GET /truths":e=>{const t=String(e.q??"").slice(0,200),r=_(e.limit,5,50);if(!t)return[];const n=a().getDatabase();try{return n.prepare(`
|
|
16
16
|
SELECT category, key, value, rationale, confidence, ttl_days
|
|
17
17
|
FROM ground_truths
|
|
18
18
|
WHERE is_current = 1
|
|
19
19
|
AND (key LIKE ? OR value LIKE ? OR rationale LIKE ?)
|
|
20
20
|
ORDER BY confidence DESC
|
|
21
21
|
LIMIT ?
|
|
22
|
-
`).all(`%${t}%`,`%${t}%`,`%${t}%`,r)}catch{return[]}},"GET /routing":e=>{const t=String(e.task_type??e.t??"").slice(0,200).trim();if(!t)return{e:"task_type required"};const r=
|
|
23
|
-
WHERE is_shared = 1 ${
|
|
24
|
-
ORDER BY ${
|
|
25
|
-
LIMIT ?`).all(
|
|
22
|
+
`).all(`%${t}%`,`%${t}%`,`%${t}%`,r)}catch{return[]}},"GET /routing":e=>{const t=String(e.task_type??e.t??"").slice(0,200).trim();if(!t)return{e:"task_type required"};const r=_(e.limit,10,100);let n;const s=e.p;if(s){const i=a().getProject(s);if(!i)return{e:"not found"};n=i.id}const o=e.since_days!=null?Number(e.since_days):void 0;return a().recallRouting(t,{projectId:n,limit:r,sinceDays:Number.isFinite(o)?o:void 0}).map(i=>({m:i.model_id,tools:i.tool_names,runs:i.runs,ok:i.successes,fail:i.failures,rate:Math.round(i.success_rate*1e3)/1e3,ms:i.avg_latency_ms,last:i.last_ts}))},"POST /routing":e=>{const t=String(e.task_type??"").slice(0,200).trim(),r=String(e.model_id??"").slice(0,200).trim();if(!t)return{e:"task_type required"};if(!r)return{e:"model_id required"};if(e.success===void 0||e.success===null)return{e:"success required"};let n;const s=e.p;if(s){const l=a().getProject(s);l&&(n=l.id)}const o=Array.isArray(e.tool_names)?e.tool_names.map(l=>String(l)):e.tool_names!=null?String(e.tool_names):null,d=e.latency_ms!=null?Number(e.latency_ms):null,i=e.success===!0||e.success===1||e.success==="1"||e.success==="true";try{return{i:a().recordRouting({projectId:n,taskType:t,modelId:r,toolNames:o,success:i,latencyMs:Number.isFinite(d)?d:null})}}catch(l){return{e:l.message}}},"GET /audit":e=>{const t=_(e.limit,50,500),r=e.kind,n=a().getDatabase();try{return r?n.prepare("SELECT * FROM audit_log WHERE event_kind = ? ORDER BY id DESC LIMIT ?").all(r,t):n.prepare("SELECT * FROM audit_log ORDER BY id DESC LIMIT ?").all(t)}catch{return[]}},"GET /sync/push":e=>{const t=e.since,r=_(e.limit,500,5e3),n=a().getDatabase(),s=[],o=t??"1970-01-01",d=[["session","sessions","created_at"],["quest","quests","created_at"],["truth","ground_truths","updated_at"],["artifact","memory_artifacts","last_accessed_at"],["edge","decision_edges","created_at"]],i=(()=>{try{return n.prepare("PRAGMA table_info(projects)").all().some(p=>p.name==="sync_policy")?"AND (project_id IS NULL OR project_id IN (SELECT id FROM projects WHERE sync_policy = 'team'))":""}catch{return""}})();for(const[l,p,c]of d){if(s.length>=r)break;try{const u=n.prepare(`SELECT * FROM ${p}
|
|
23
|
+
WHERE is_shared = 1 ${i} AND ${c} > ?
|
|
24
|
+
ORDER BY ${c} ASC
|
|
25
|
+
LIMIT ?`).all(o,Math.max(1,r-s.length));for(const m of u)s.push({kind:l,id:m.id,project_id:m.project_id??null,updated_at:m[c]??null,payload:m})}catch{}}return{rows:s,count:s.length,cursor:new Date().toISOString()}},"GET /events":e=>{if(!a().liveMemoryEnabled())return{e:"live memory disabled",disabled:1};const t=P(e.project??e.p);if(!t)return{e:"not found"};const r=K(void 0,e.since),n=_(e.limit??e.l,100,1e3),o=e.shared==="1"||e.shared==="true"?a().eventsForPush(t.id,r,n):a().eventsSince(t.id,r,n),d=o.length?o[o.length-1].cursor:r;return{project:t.name,cursor:d,count:o.length,events:o}},"POST /events":e=>{if(!a().liveMemoryEnabled())return{e:"live memory disabled",disabled:1};const t=P(e.project??e.p);if(!t)return{e:"not found"};const r=Array.isArray(e.events)?e.events:e.event?[e.event]:[];if(r.length===0)return{e:"events[] required"};if(r.length>M)return{e:`max ${M} events per request`};let n=0,s=0,o=0;for(const d of r){const i=a().ingestRemoteEvent(t.id,d);i==="inserted"?n++:i==="echo"?o++:s++}return n>0&&h.invalidate("ctx"),{project:t.name,inserted:n,duplicate:s,echo:o}},"GET /stats":()=>{const{result:e,ms:t}=G(()=>a().getStats());return{...e,ms:t,cache:h.stats()}},"GET /health":async e=>{const t={ok:1,ts:Date.now()};if(!/^(1|true|yes)$/i.test(String(e.check_dependencies??"")))return t;const r={...t};try{const s=a().getDatabase().prepare("SELECT MAX(version) v FROM schema_versions").get().v;r.db={ok:1,migration:s}}catch{r.ok=0,r.db={ok:0}}try{const n=A().getVectorStore();if(n){const s=n.getProvider(),o=n.indexCoverage();r.vectors={provider:s.name,model:s.model,ready:await s.isReady(),indexed:o.activeCount,stale:o.stale?1:0,...o.stale?{indexed_under:Object.keys(o.otherModels),hint:"run wyrm_reindex"}:{},egress:s.remoteHost??"none"}}else r.vectors={provider:"none",ready:!1}}catch{r.vectors={ok:0}}return r.rerank={configured:ce()?1:0,provider:(process.env.WYRM_RERANK_PROVIDER||"llamacpp").trim()||"llamacpp",egress:le()??"none"},r},"GET /auth/status":()=>{const e=k();return{auth:e.requireAuth?1:0,origins:e.allowedOrigins.length}},"GET /ui/stats":()=>{const e=a().getDatabase(),t=e.prepare("SELECT COUNT(*) as c FROM projects").get().c,r=e.prepare("SELECT COUNT(*) as c FROM sessions").get().c,n=e.prepare("SELECT COUNT(*) as c FROM quests WHERE status IN ('pending','in_progress')").get().c,s=e.prepare("SELECT COUNT(*) as c FROM data_lake").get().c,o=e.prepare("SELECT COUNT(*) as c FROM memory_artifacts WHERE needs_review = 0").get().c,d=e.prepare("SELECT COUNT(*) as c FROM memory_artifacts WHERE needs_review = 1").get().c,i=e.prepare("SELECT COUNT(*) as c FROM ground_truths WHERE is_current = 1").get().c,l=e.prepare(`
|
|
26
26
|
SELECT date, summary, objectives FROM sessions
|
|
27
27
|
ORDER BY created_at DESC LIMIT 8
|
|
28
|
-
`).all();return{projects:t,sessions:r,active_quests:n,data_points:s,artifacts:
|
|
28
|
+
`).all();return{projects:t,sessions:r,active_quests:n,data_points:s,artifacts:o,review_queue:d,truths:i,recent_sessions:l}},"GET /ui/memories":e=>{const t=a().getDatabase(),r=Math.max(1,parseInt(String(e.page||"1"),10)),n=Math.min(100,Math.max(1,parseInt(String(e.limit||"20"),10))),s=e.kind?String(e.kind):"",o=e.search?String(e.search):"",d=(r-1)*n;let i,l,p,c;if(o){const f=U(o);s?(i=`
|
|
29
29
|
SELECT ma.*, p.name AS project_name
|
|
30
30
|
FROM memory_artifacts ma
|
|
31
31
|
JOIN memory_artifacts_fts fts ON fts.rowid = ma.id
|
|
@@ -36,7 +36,7 @@ import V from"http";import{WyrmDB as H}from"./database.js";import{cache as h,est
|
|
|
36
36
|
SELECT COUNT(*) as c FROM memory_artifacts ma
|
|
37
37
|
JOIN memory_artifacts_fts fts ON fts.rowid = ma.id
|
|
38
38
|
WHERE memory_artifacts_fts MATCH ? AND ma.kind = ?
|
|
39
|
-
AND ma.supersedes_id IS NULL`,
|
|
39
|
+
AND ma.supersedes_id IS NULL`,p=[f,s,n,d],c=[f,s]):(i=`
|
|
40
40
|
SELECT ma.*, p.name AS project_name
|
|
41
41
|
FROM memory_artifacts ma
|
|
42
42
|
JOIN memory_artifacts_fts fts ON fts.rowid = ma.id
|
|
@@ -45,37 +45,37 @@ import V from"http";import{WyrmDB as H}from"./database.js";import{cache as h,est
|
|
|
45
45
|
ORDER BY ma.confidence DESC LIMIT ? OFFSET ?`,l=`
|
|
46
46
|
SELECT COUNT(*) as c FROM memory_artifacts ma
|
|
47
47
|
JOIN memory_artifacts_fts fts ON fts.rowid = ma.id
|
|
48
|
-
WHERE memory_artifacts_fts MATCH ? AND ma.supersedes_id IS NULL`,
|
|
48
|
+
WHERE memory_artifacts_fts MATCH ? AND ma.supersedes_id IS NULL`,p=[f,n,d],c=[f])}else s?(i=`
|
|
49
49
|
SELECT ma.*, p.name AS project_name
|
|
50
50
|
FROM memory_artifacts ma
|
|
51
51
|
LEFT JOIN projects p ON ma.project_id = p.id
|
|
52
52
|
WHERE ma.kind = ? AND ma.supersedes_id IS NULL
|
|
53
|
-
ORDER BY ma.confidence DESC LIMIT ? OFFSET ?`,l="SELECT COUNT(*) as c FROM memory_artifacts WHERE kind = ? AND supersedes_id IS NULL",
|
|
53
|
+
ORDER BY ma.confidence DESC LIMIT ? OFFSET ?`,l="SELECT COUNT(*) as c FROM memory_artifacts WHERE kind = ? AND supersedes_id IS NULL",p=[s,n,d],c=[s]):(i=`
|
|
54
54
|
SELECT ma.*, p.name AS project_name
|
|
55
55
|
FROM memory_artifacts ma
|
|
56
56
|
LEFT JOIN projects p ON ma.project_id = p.id
|
|
57
57
|
WHERE ma.supersedes_id IS NULL
|
|
58
|
-
ORDER BY ma.confidence DESC LIMIT ? OFFSET ?`,l="SELECT COUNT(*) as c FROM memory_artifacts WHERE supersedes_id IS NULL",
|
|
58
|
+
ORDER BY ma.confidence DESC LIMIT ? OFFSET ?`,l="SELECT COUNT(*) as c FROM memory_artifacts WHERE supersedes_id IS NULL",p=[n,d],c=[]);const u=t.prepare(i).all(...p),m=t.prepare(l).get(...c).c;return{items:u,total:m,page:r,limit:n}},"GET /ui/quests":()=>{const t=a().getDatabase().prepare(`
|
|
59
59
|
SELECT q.id, q.title, q.status, q.priority, p.name AS project_name
|
|
60
60
|
FROM quests q
|
|
61
61
|
LEFT JOIN projects p ON q.project_id = p.id
|
|
62
62
|
ORDER BY q.created_at DESC
|
|
63
|
-
`).all(),r={pending:[],in_progress:[],completed:[],abandoned:[]};for(const n of t)r[n.status]&&r[n.status].push(n);return r},"GET /ui/
|
|
63
|
+
`).all(),r={pending:[],in_progress:[],completed:[],abandoned:[]};for(const n of t)r[n.status]&&r[n.status].push(n);return r},"GET /ui/warroom":()=>{const e=a().getDatabase(),t=(c,...u)=>{try{const m=e.prepare(c).get(...u);return m?Object.values(m)[0]??0:0}catch{return 0}},r=(c,...u)=>{try{return e.prepare(c).all(...u)}catch{return[]}},n=I(!1).fleetGrove,o=(()=>{try{return e.prepare("SELECT id FROM projects WHERE name = ? LIMIT 1").get(n)}catch{return}})()?.id??-1,d=r("SELECT agent_id, agent_kind, last_heartbeat, ttl_seconds, role FROM agent_presence ORDER BY last_heartbeat DESC LIMIT 24").map(c=>({agent:c.agent_id,kind:c.agent_kind,role:c.role,last:c.last_heartbeat,alive:(()=>{try{const u=(Date.now()-new Date(c.last_heartbeat.replace(" ","T")+"Z").getTime())/1e3;return u>=0&&u<(c.ttl_seconds||300)}catch{return!1}})()})),i={findings:t("SELECT COUNT(*) n FROM quests WHERE project_id = ? AND status = ?",o,"pending")+t("SELECT COUNT(*) n FROM failure_patterns WHERE project_id = ? AND resolved = 0",o),claimed:t("SELECT COUNT(*) n FROM quests WHERE project_id = ? AND agent_id IS NOT NULL AND status IN ('pending','in_progress')",o),fixed:t("SELECT COUNT(*) n FROM quests WHERE project_id = ? AND status = 'completed'",o)+t("SELECT COUNT(*) n FROM memory_artifacts WHERE project_id = ? AND needs_review = 0 AND tags LIKE '%receipt%'",o),verified:t("SELECT COUNT(*) n FROM memory_artifacts WHERE project_id = ? AND needs_review = 0 AND tags LIKE '%verdict%'",o)},l=[];for(const c of r("SELECT id, title, status, agent_id, COALESCE(updated_at, created_at) AS ts, COALESCE(description, '') AS description FROM quests WHERE project_id = ?",o))l.push({type:"request",id:c.id,title:c.title,status:c.status,agent:c.agent_id,ts:c.ts,detail:String(c.description).slice(0,240)});for(const c of r("SELECT id, COALESCE(problem,'') AS problem, COALESCE(validated_fix,'') AS validated_fix, COALESCE(tags,'') AS tags, created_at FROM memory_artifacts WHERE project_id = ? AND needs_review = 0 AND tags LIKE '%warroom%'",o)){const u=String(c.tags).includes("verdict")?"verdict":String(c.tags).includes("receipt")?"receipt":"note";l.push({type:u,id:c.id,title:String(c.problem).slice(0,120),status:"",agent:null,ts:c.created_at,detail:String(c.validated_fix).slice(0,240)})}for(const c of r("SELECT id, scope, target, COALESCE(description,'') AS description, last_seen, resolved FROM failure_patterns WHERE project_id = ?",o))l.push({type:"finding",id:c.id,title:`[${c.scope}] ${c.target}`,status:c.resolved?"resolved":"open",agent:null,ts:c.last_seen,detail:String(c.description).slice(0,240)});l.sort((c,u)=>String(u.ts).localeCompare(String(c.ts)));const p={failure_blocks:t("SELECT COUNT(*) n FROM failure_blocks"),tokens_saved:t("SELECT COALESCE(SUM(estimated_tokens),0) n FROM token_savings_log"),rows_shared:t("SELECT COUNT(*) n FROM memory_artifacts WHERE project_id = ? AND is_shared = 1",o)+t("SELECT COUNT(*) n FROM quests WHERE project_id = ? AND is_shared = 1",o)};return{grove:n,found:o!==-1,presence:d,loop:i,metrics:p,feed:l.slice(0,40)}},"GET /ui/truths":e=>{const t=a().getDatabase(),r=Math.max(1,parseInt(String(e.page||"1"),10)),n=Math.min(100,Math.max(1,parseInt(String(e.limit||"20"),10))),s=(r-1)*n,o=t.prepare(`
|
|
64
64
|
SELECT gt.*, p.name AS project_name
|
|
65
65
|
FROM ground_truths gt
|
|
66
66
|
LEFT JOIN projects p ON gt.project_id = p.id
|
|
67
67
|
WHERE gt.is_current = 1
|
|
68
68
|
ORDER BY gt.updated_at DESC LIMIT ? OFFSET ?
|
|
69
|
-
`).all(n,s),
|
|
69
|
+
`).all(n,s),d=t.prepare("SELECT COUNT(*) as c FROM ground_truths WHERE is_current = 1").get().c;return{items:o.map(l=>({...l,staleness:me(l)})),total:d,page:r,limit:n}},"GET /ui/review":()=>({items:a().getDatabase().prepare(`
|
|
70
70
|
SELECT ma.*, p.name AS project_name
|
|
71
71
|
FROM memory_artifacts ma
|
|
72
72
|
LEFT JOIN projects p ON ma.project_id = p.id
|
|
73
73
|
WHERE ma.needs_review = 1
|
|
74
74
|
ORDER BY ma.created_at DESC
|
|
75
|
-
`).all()}),"GET /ui/account":()=>{const e=
|
|
75
|
+
`).all()}),"GET /ui/account":()=>{const e=q(a().getDatabasePath());try{const t=S(e,"license.json");if(y(t)){const r=JSON.parse(R(t,"utf-8")),n=$(r);return{account:r.license&&r.license.issued_to||"unknown",tier:n.valid?n.tier:"free",valid:!!n.valid,expires:n.expiresAt||null,readonly:g}}}catch{}try{const t=S(e,"cloud.json");if(y(t))return{account:JSON.parse(R(t,"utf-8")).email||"Local",tier:"free",valid:!1,expires:null,readonly:g}}catch{}return{account:"Local",tier:"free",valid:!1,expires:null,readonly:g}},"GET /ui/impact":()=>{const e=a().getDatabase(),t=r=>{try{return e.prepare(r).get()?.n||0}catch{return 0}};return{failures_blocked:t("SELECT COALESCE(SUM(occurrences),0) AS n FROM failure_patterns"),tokens_saved:t("SELECT COALESCE(SUM(estimated_tokens),0) AS n FROM token_savings_log"),memories:t("SELECT COUNT(*) AS n FROM memory_artifacts WHERE supersedes_id IS NULL"),memories_week:t("SELECT COUNT(*) AS n FROM memory_artifacts WHERE created_at >= datetime('now','-7 days')"),truths:t("SELECT COUNT(*) AS n FROM ground_truths WHERE is_current = 1"),sessions:t("SELECT COUNT(*) AS n FROM sessions"),quests_completed:t("SELECT COUNT(*) AS n FROM quests WHERE status = 'completed'"),skills:t("SELECT COUNT(*) AS n FROM skills"),projects:t("SELECT COUNT(*) AS n FROM projects")}},"GET /ui/memory":e=>{const t=a().getDatabase(),r=parseInt(String(e&&e.id||"0"),10);if(!r)return{item:null};try{return{item:t.prepare(`SELECT ma.*, p.name AS project_name
|
|
76
76
|
FROM memory_artifacts ma LEFT JOIN projects p ON ma.project_id = p.id
|
|
77
|
-
WHERE ma.id = ?`).get(r)||null}}catch{return{item:null}}},"GET /ui/recall":async e=>{const t=e.search?String(e.search):e.q?String(e.q):"";if(!t)return{items:[],total:0};const r=
|
|
78
|
-
`),tokens:
|
|
79
|
-
FROM skills ORDER BY category COLLATE NOCASE, name COLLATE NOCASE`).all();return{items:t,total:t.length}}catch{return{items:[],total:0}}},"GET /ui/homes":()=>{const e=Q();let t=
|
|
77
|
+
WHERE ma.id = ?`).get(r)||null}}catch{return{item:null}}},"GET /ui/recall":async e=>{const t=e.search?String(e.search):e.q?String(e.q):"";if(!t)return{items:[],total:0};const r=_(e.limit,6,25),n=e.kind?String(e.kind):void 0,o=(await A().recallHybridGlobal(t,{limit:r,kind:n})).map(d=>({...d.artifact,relevance:Math.round(d.relevance_score*1e3)/1e3,match_type:d.match_type}));return{items:o,total:o.length}},"GET /ui/context":async e=>{const t=e.search?String(e.search):e.q?String(e.q):"";if(!t)return{context:"",items:[],total:0,tokens:0};const r=_(e.limit,10,25),n=_(e.budget,900,2400),s=await A().recallHybridGlobal(t,{limit:r}),o=[],d=[];let i=0;for(const l of s){const p=l.artifact,c=String(p.problem??p.title??p.summary??p.key??p.kind??"").trim(),u=String(p.validated_fix??p.content??p.body??p.value??p.problem??"").replace(/\s+/g," ").trim(),m=`- ${c}${u?": "+u.slice(0,320):""}`,f=Math.ceil(m.length/4);i+f<=n?(o.push(m),i+=f):o.push(`- ${c.slice(0,64)} \u2026 (recall for detail)`),d.push({...p,relevance:Math.round(l.relevance_score*1e3)/1e3,match_type:l.match_type})}return{context:o.join(`
|
|
78
|
+
`),tokens:i,items:d,total:d.length}},"GET /ui/skills":()=>{const e=a().getDatabase();try{const t=e.prepare(`SELECT name, description, category, tier, usage_count, is_active
|
|
79
|
+
FROM skills ORDER BY category COLLATE NOCASE, name COLLATE NOCASE`).all();return{items:t,total:t.length}}catch{return{items:[],total:0}}},"GET /ui/homes":()=>{const e=Q();let t=a().getDatabasePath();try{t=x(t)}catch{}return{homes:e.map(r=>({...r,active:r.dbPath===t}))}},"POST /ui/switch":e=>{const t=String(e&&e.path||""),r=Q().find(n=>n.dbPath===t);if(!r)return{ok:!1,error:"unknown home"};try{return je(new W(r.dbPath)),{ok:!0,account:r.account,tier:r.tier}}catch(n){return{ok:!1,error:n instanceof Error?n.message:String(n)}}}};function Ae(e,t,r){if(!a().liveMemoryEnabled()){E(t,e,{e:"live memory disabled",disabled:1},503);return}const n=P(r.project??r.p);if(!n){E(t,e,{e:"not found"},404);return}const s=e.socket.remoteAddress||"unknown";if(w>=ve){E(t,e,{e:"too many concurrent streams",retry_after:5},503);return}if((T.get(s)??0)>=Ce){E(t,e,{e:"too many concurrent streams from this client",retry_after:5},503);return}w++,T.set(s,(T.get(s)??0)+1);let o=K(e.headers["last-event-id"],r.since);const i=!(s==="127.0.0.1"||s==="::1"||s==="::ffff:127.0.0.1")||r.shared==="1"||r.shared==="true";t.writeHead(200,{...b(e),"Content-Type":"text/event-stream; charset=utf-8","Cache-Control":"no-cache, no-transform",Connection:"keep-alive","X-Accel-Buffering":"no"}),t.write(`retry: 3000
|
|
80
80
|
|
|
81
|
-
`);let l,
|
|
81
|
+
`);let l,p,c=!1;const u=()=>{if(c)return;c=!0,l&&clearInterval(l),p&&clearInterval(p),w--;const f=(T.get(s)??1)-1;f<=0?T.delete(s):T.set(s,f)},m=()=>{if(t.writableEnded){u();return}try{const f=i?a().eventsForPush(n.id,o,200):a().eventsSince(n.id,o,200);for(const H of f)o=H.cursor,t.write(Se(H))}catch{}};m(),l=setInterval(m,1e3),p=setInterval(()=>{try{t.write(ye)}catch{}},25e3),e.on("close",u),t.on("close",u),t.on("error",u)}const F=z.createServer(async(e,t)=>{if(!te(e,t).error)try{const n=new URL(e.url||"/",`http://localhost:${j}`);if(g&&ge(e.method||"GET",n.pathname)){E(t,e,{e:"read-only mode",readonly:1},403);return}let s;try{s=await Ne(e)}catch(u){if(u.message==="Invalid JSON"){E(t,e,{e:"Invalid JSON body"},400);return}throw u}if(n.searchParams.forEach((u,m)=>s[m]=u),e.method==="GET"&&n.pathname==="/dragon-mark.svg"){try{if(!v&&y(B)&&(v=R(B)),v){t.writeHead(200,{...b(e),"Content-Type":"image/svg+xml","Cache-Control":"public, max-age=86400","Content-Length":v.length}),t.end(v);return}}catch{}E(t,e,{e:"not found"},404);return}if(e.method==="GET"&&n.pathname==="/ui"){{const u=I(!1);let m=!1;try{m=!!a().getDatabase().prepare("SELECT 1 FROM projects WHERE name = ? LIMIT 1").get(u.fleetGrove)}catch{}Me(t,e,ie(I(m)))}return}if(e.method==="GET"&&n.pathname==="/events/stream"){Ae(e,t,s);return}const o=n.pathname.match(/^\/ui\/review\/(\d+)\/(approve|reject)$/);if(e.method==="POST"&&o){const u=parseInt(o[1],10),m=o[2],f=a().getDatabase();m==="approve"?f.prepare("UPDATE memory_artifacts SET needs_review = 0 WHERE id = ?").run(u):f.prepare("DELETE FROM memory_artifacts WHERE id = ?").run(u),E(t,e,{ok:!0});return}const d=`${e.method} ${n.pathname}`,i=X[d];if(!i){if(n.pathname==="/"){E(t,e,{wyrm:"3.0",auth:k().requireAuth?"required":"disabled",tip:"GET /c for quick context"});return}E(t,e,{e:"not found"},404);return}const l=Te({header:e.headers["wyrm-actor"]}),{result:p,ms:c}=G(()=>Oe(l,()=>i(s)));t.setHeader("X-Time-Ms",String(c)),p instanceof Promise?p.then(u=>E(t,e,u)).catch(u=>{const m=Y(u);if(m!==null){const f=J("http",m);t.setHeader("Retry-After",String(Math.ceil(f.error.retry_after_ms/1e3))),E(t,e,f,503);return}D.error("Fast API request failed",{path:e.url,error:u.message}),E(t,e,{e:"Internal server error"},500)}):E(t,e,p)}catch(n){const s=Y(n);if(s!==null){const o=J("http",s);t.setHeader("Retry-After",String(Math.ceil(o.error.retry_after_ms/1e3))),E(t,e,o,503);return}D.error("Fast API request failed",{path:e.url,error:n.message}),E(t,e,{e:"Internal server error"},500)}}),V=()=>{F.close(()=>{try{O?.close()}catch{}process.exit(0)})};process.on("SIGINT",V),process.on("SIGTERM",V);import{fileURLToPath as De}from"url";const ke=De(import.meta.url);if(process.argv[1]===ke){const e=process.env.WYRM_BIND_HOST||"127.0.0.1";F.listen(j,e,()=>{D.info("Wyrm Fast API started",{port:j,host:e}),console.log(`\u{F115D} Wyrm Fast API on ${e}:${j}`),console.log(` Auth: ${k().requireAuth?"required":"disabled"}`),g&&console.log(" Read-only: writes + off-box egress blocked (safe to expose)"),e!=="127.0.0.1"&&e!=="::1"&&e!=="localhost"&&(D.warn("Wyrm Fast API bound to a NON-loopback host \u2014 reachable beyond localhost; ensure auth is required",{host:e}),console.log(` \u26A0 bound to ${e}: reachable beyond localhost \u2014 make sure auth is required`),g||console.log(" \u26A0 not in read-only mode; set WYRM_UI_READONLY=1 for a public bind"))})}export{F as server};
|
package/dist/metrics.js
ADDED
|
@@ -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};
|
package/dist/migrations.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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}},
|
|
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
|
|
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
|
-
`),
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ('${
|
|
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
|
|
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 ('${
|
|
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
|
|
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
|
|
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};
|