context-mode 1.0.157 → 1.0.159
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/adapters/codex/index.d.ts +4 -1
- package/build/adapters/codex/index.js +237 -45
- package/build/session/db.d.ts +35 -0
- package/build/session/db.js +71 -0
- package/build/session/error-classifier.d.ts +87 -0
- package/build/session/error-classifier.js +303 -0
- package/cli.bundle.mjs +222 -202
- package/hooks/session-db.bundle.mjs +23 -6
- package/hooks/session-loaders.mjs +310 -6
- package/hooks/sessionstart.mjs +52 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +145 -125
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{createRequire as oe}from"node:module";import{existsSync as ie,unlinkSync as P,renameSync as ae}from"node:fs";import{tmpdir as ce}from"node:os";import{join as ue}from"node:path";var A=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){let t="",r=null;for(let a=0;a<e.length;a++){let i=e[a];if(r)t+=i,i===r&&(r=null);else if(i==="'"||i==='"')t+=i,r=i;else if(i===";"){let c=t.trim();c&&this.#e.prepare(c).run(),t=""}else t+=i}let s=t.trim();return s&&this.#e.prepare(s).run(),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>{let s=t.get(...r);return s===null?void 0:s},all:(...r)=>t.all(...r),iterate:(...r)=>t.iterate(...r)}}transaction(e){return this.#e.transaction(e)}close(){this.#e.close()}},w=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){return this.#e.exec(e),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>t.get(...r),all:(...r)=>t.all(...r),iterate:(...r)=>typeof t.iterate=="function"?t.iterate(...r):t.all(...r)[Symbol.iterator]()}}transaction(e){return(...t)=>{this.#e.exec("BEGIN");try{let r=e(...t);return this.#e.exec("COMMIT"),r}catch(r){throw this.#e.exec("ROLLBACK"),r}}}close(){this.#e.close()}},
|
|
2
|
-
`))}function K(n){let e=process.env[l];if(e===void 0)return{kind:"unset"};let t=e.trim();if(!t)return{kind:"ignored-empty",ignoredEnvVar:l,ignoredReason:"empty"};if(!
|
|
3
|
-
`)}function Oe(n){return n.ignoredEnvVar&&n.ignoredReason==="empty"?`Ignored empty ${n.ignoredEnvVar}; using adapter default.`:null}function J(){return`Set ${l} to a writable absolute path.`}function Ce(n){if(!n||typeof n!="object")return null;let e=n.path;return typeof e=="string"&&e.length>0?e:null}var m;function
|
|
1
|
+
import{createRequire as oe}from"node:module";import{existsSync as ie,unlinkSync as P,renameSync as ae}from"node:fs";import{tmpdir as ce}from"node:os";import{join as ue}from"node:path";var A=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){let t="",r=null;for(let a=0;a<e.length;a++){let i=e[a];if(r)t+=i,i===r&&(r=null);else if(i==="'"||i==='"')t+=i,r=i;else if(i===";"){let c=t.trim();c&&this.#e.prepare(c).run(),t=""}else t+=i}let s=t.trim();return s&&this.#e.prepare(s).run(),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>{let s=t.get(...r);return s===null?void 0:s},all:(...r)=>t.all(...r),iterate:(...r)=>t.iterate(...r)}}transaction(e){return this.#e.transaction(e)}close(){this.#e.close()}},w=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){return this.#e.exec(e),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>t.get(...r),all:(...r)=>t.all(...r),iterate:(...r)=>typeof t.iterate=="function"?t.iterate(...r):t.all(...r)[Symbol.iterator]()}}transaction(e){return(...t)=>{this.#e.exec("BEGIN");try{let r=e(...t);return this.#e.exec("COMMIT"),r}catch(r){throw this.#e.exec("ROLLBACK"),r}}}close(){this.#e.close()}},g=null;function de(n){let e=null;try{return e=new n(":memory:"),e.exec("CREATE VIRTUAL TABLE __fts5_probe USING fts5(x)"),!0}catch{return!1}finally{try{e?.close()}catch{}}}function le(n,e){let t=e!==void 0?e:globalThis.Bun;if(typeof t<"u"&&t!==null)return!0;let r=n??process.versions,[s,a]=(r.node??"0.0.0").split("."),i=Number(s),c=Number(a);return!Number.isFinite(i)||!Number.isFinite(c)?!1:i>22||i===22&&c>=5}function Ee(){if(!g){let n=oe(import.meta.url);if(globalThis.Bun){let e=n(["bun","sqlite"].join(":")).Database;g=function(r,s){let a=new e(r,{readonly:s?.readonly,create:!0}),i=new A(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}}else if(le()){let e=null;try{({DatabaseSync:e}=n(["node","sqlite"].join(":")))}catch{e=null}e&&de(e)?g=function(r,s){let a=new e(r,{readOnly:s?.readonly??!1}),i=new w(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}:g=n("better-sqlite3")}else g=n("better-sqlite3")}return g}function F(n){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");try{n.pragma("mmap_size = 268435456")}catch{}}function k(n){if(!ie(n))for(let e of["-wal","-shm"])try{P(n+e)}catch{}}function ge(n){for(let e of["","-wal","-shm"])try{P(n+e)}catch{}}function x(n){try{n.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{n.close()}catch{}}function B(n="context-mode"){return ue(ce(),`${n}-${process.pid}.db`)}function me(n,e=[100,500,2e3]){let t;for(let r=0;r<=e.length;r++)try{return n()}catch(s){let a=s instanceof Error?s.message:String(s);if(!a.includes("SQLITE_BUSY")&&!a.includes("database is locked"))throw s;if(t=s instanceof Error?s:new Error(a),r<e.length){let i=e[r],c=Date.now();for(;Date.now()-c<i;);}}throw new Error(`SQLITE_BUSY: database is locked after ${e.length} retries. Original error: ${t?.message}`)}function _e(n){return n.includes("SQLITE_CORRUPT")||n.includes("SQLITE_NOTADB")||n.includes("database disk image is malformed")||n.includes("file is not a database")}function pe(n){let e=Date.now();for(let t of["","-wal","-shm"])try{ae(n+t,`${n}${t}.corrupt-${e}`)}catch{}}var y=Symbol.for("__context_mode_live_dbs_v3__"),C=(()=>{let n=globalThis;return n[y]||(n[y]=new Set,process.on("exit",()=>{for(let e of n[y])x(e);n[y].clear()})),n[y]})(),v=class{#e;#t;constructor(e){let t=Ee();this.#e=e,k(e);let r;try{r=new t(e,{timeout:3e4}),F(r)}catch(s){let a=s instanceof Error?s.message:String(s);if(_e(a)){pe(e),k(e);try{r=new t(e,{timeout:3e4}),F(r)}catch(i){throw new Error(`Failed to create fresh DB after renaming corrupt file: ${i instanceof Error?i.message:String(i)}`)}}else throw s}this.#t=r,C.add(this.#t),this.initSchema(),this.prepareStatements()}get db(){return this.#t}get dbPath(){return this.#e}close(){C.delete(this.#t),x(this.#t)}withRetry(e){return me(e)}cleanup(){C.delete(this.#t),x(this.#t),ge(this.#e)}};import{createHash as S}from"node:crypto";import{execFileSync as ye}from"node:child_process";import{accessSync as Se,constants as fe,existsSync as D,mkdirSync as he,realpathSync as Te,renameSync as I}from"node:fs";import{homedir as q}from"node:os";import{dirname as ve,isAbsolute as G,join as E,resolve as _}from"node:path";var l="CONTEXT_MODE_DIR",Y="sessions",j="content",f=class extends Error{kind;path;overrideEnvVar;ignoredEnvVar;ignoredReason;constructor(e,t,r=l,s,a,i={}){super(a??Le(e,t,i),{cause:s}),this.name="StorageDirectoryError",this.kind=e,this.path=t,this.overrideEnvVar=r,this.ignoredEnvVar=i.ignoredEnvVar,this.ignoredReason=i.ignoredReason}},b=new Map;function qe(n){let e=n.env??process.env,t=n.legacySessionDirEnv,r=t?e[t]?.trim():void 0;return r&&t?(n.onLegacySessionDir?.(t,r),r):E(Re(n.configDir,n.configDirEnv,e),"context-mode","sessions")}function Re(n,e,t){let r=e?t[e]:void 0;return r&&r.trim()!==""?V(r.trim()):V(n,q())}function V(n,e){return n.startsWith("~")?_(q(),n.replace(/^~[/\\]?/,"")):G(n)?_(n):e?_(e,n):_(n)}function be(n,e,t){return new f(n,e,l,void 0,[`Invalid ${l} for context-mode ${n} directory: ${t}`,J()].join(`
|
|
2
|
+
`))}function K(n){let e=process.env[l];if(e===void 0)return{kind:"unset"};let t=e.trim();if(!t)return{kind:"ignored-empty",ignoredEnvVar:l,ignoredReason:"empty"};if(!G(t))throw be(n,t,`${l} must be an absolute path.`);return{kind:"override",root:_(t)}}function De(n){return n.kind==="ignored-empty"?{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason}:{}}function z(n,e){let t=K(n);return t.kind!=="override"?null:{kind:n,path:E(t.root,e),envVar:l,source:"override"}}function Ne(n,e,t){return{kind:n,path:_(e()),envVar:null,source:"default",...t}}function Q(n){let e=K("session");return e.kind==="override"?{kind:"session",path:E(e.root,Y),envVar:l,source:"override"}:Ne("session",n,De(e))}function Ge(n){let e=z("content",j);if(e)return e;let t=Q(n);return{kind:"content",path:E(ve(t.path),j),envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function Ye(n){let e=z("stats",Y);if(e)return e;let t=Q(n);return{kind:"stats",path:t.path,envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function Ke(n){return n.message}function ze(n){return n.source==="override"&&n.envVar?`via ${n.envVar}`:n.ignoredEnvVar&&n.ignoredReason==="empty"?`default; ignored empty ${n.ignoredEnvVar}`:"default"}function Qe(){b.clear()}function Je(n){let e=[n.kind,n.path,n.source,n.envVar??"",n.ignoredEnvVar??"",n.ignoredReason??""].join("\0"),t=b.get(e);if(t instanceof f)throw t;if(t===n.path)return t;try{return he(n.path,{recursive:!0}),Se(n.path,fe.W_OK),b.set(e,n.path),n.path}catch(r){let s=new f(n.kind,Ce(r)??n.path,l,r,void 0,{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason});throw b.set(e,s),s}}function Le(n,e,t={}){return[`context-mode ${n} directory is not writable: ${e}`,Oe(t),J()].filter(Boolean).join(`
|
|
3
|
+
`)}function Oe(n){return n.ignoredEnvVar&&n.ignoredReason==="empty"?`Ignored empty ${n.ignoredEnvVar}; using adapter default.`:null}function J(){return`Set ${l} to a writable absolute path.`}function Ce(n){if(!n||typeof n!="object")return null;let e=n.path;return typeof e=="string"&&e.length>0?e:null}var m;function h(n){let e=n.replace(/\\/g,"/");return/^\/+$/.test(e)?"/":/^[A-Za-z]:\/+$/.test(e)?`${e.slice(0,2)}/`:e.replace(/\/+$/,"")}function H(n){let e=n;try{e=Te.native(n)}catch{}let t=h(e);return process.platform==="win32"||process.platform==="darwin"?t.toLowerCase():t}function Z(n,e){return ye("git",["-C",n,...e],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function Ae(n){let e=Z(n,["rev-parse","--show-toplevel"]);return e.length>0?h(e):null}function we(n){let e=Z(n,["worktree","list","--porcelain"]).split(/\r?\n/).find(t=>t.startsWith("worktree "))?.replace("worktree ","")?.trim();return e?h(e):null}function xe(n=process.cwd()){let e=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(m&&m.projectDir===n&&m.envSuffix===e)return m.suffix;let t="";if(e!==void 0)t=e?`__${e}`:"";else try{let r=Ae(n),s=we(n);if(r&&s){let a=H(r),i=H(s);a!==i&&(t=`__${S("sha256").update(a).digest("hex").slice(0,8)}`)}}catch{}return m={projectDir:n,envSuffix:e,suffix:t},t}function Ze(){m=void 0}function ee(n){return S("sha256").update(h(n)).digest("hex").slice(0,16)}function te(n){let e=h(n),t=process.platform==="darwin"||process.platform==="win32"?e.toLowerCase():e;return S("sha256").update(t).digest("hex").slice(0,16)}function et(n){let{projectDir:e,contentDir:t}=n,r=te(e),s=E(t,`${r}.db`);if(D(s))return s;let a=ee(e);if(a===r)return s;let i=E(t,`${a}.db`);if(D(i))try{I(i,s);for(let c of["-wal","-shm"])try{I(i+c,s+c)}catch{}}catch{}return s}function tt(n){return Ie({...n,ext:".db"})}function Ie(n){let{projectDir:e,sessionsDir:t,ext:r}=n,s=n.suffix??xe(e),a=te(e),i=E(t,`${a}${s}${r}`);if(D(i))return i;let c=ee(e);if(c===a)return i;let d=E(t,`${c}${s}${r}`);if(D(d))try{I(d,i)}catch{}return i}var W=1e3,X=5;function R(n){let e=Number(n);return!Number.isFinite(e)||e<=0?0:Math.floor(e)}var o={insertEvent:"insertEvent",getEvents:"getEvents",getEventsByType:"getEventsByType",getEventsByPriority:"getEventsByPriority",getEventsByTypeAndPriority:"getEventsByTypeAndPriority",getEventCount:"getEventCount",getLatestAttributedProject:"getLatestAttributedProject",checkDuplicate:"checkDuplicate",evictLowestPriority:"evictLowestPriority",updateMetaLastEvent:"updateMetaLastEvent",ensureSession:"ensureSession",getSessionStats:"getSessionStats",getSessionRollup:"getSessionRollup",getMaxFileEdits:"getMaxFileEdits",incrementCompactCount:"incrementCompactCount",upsertResume:"upsertResume",getResume:"getResume",markResumeConsumed:"markResumeConsumed",claimLatestUnconsumedResume:"claimLatestUnconsumedResume",deleteEvents:"deleteEvents",deleteMeta:"deleteMeta",deleteResume:"deleteResume",getOldSessions:"getOldSessions",searchEvents:"searchEvents",incrementToolCall:"incrementToolCall",getToolCallTotals:"getToolCallTotals",getToolCallByTool:"getToolCallByTool",getEventBytesSummary:"getEventBytesSummary"},Ue=[["project_dir","TEXT NOT NULL DEFAULT ''"],["attribution_source","TEXT NOT NULL DEFAULT 'unknown'"],["attribution_confidence","REAL NOT NULL DEFAULT 0"],["bytes_avoided","INTEGER NOT NULL DEFAULT 0"],["bytes_returned","INTEGER NOT NULL DEFAULT 0"]];function ne(n){let e=n.pragma("table_xinfo(session_events)"),t=new Set(e.map(s=>s.name)),r=!1;for(let[s,a]of Ue)t.has(s)||(n.exec(`ALTER TABLE session_events ADD COLUMN ${s} ${a}`),r=!0);return r&&n.exec("CREATE INDEX IF NOT EXISTS idx_session_events_project ON session_events(session_id, project_dir)"),r}function nt(n,e){let t=null;try{t=new e(n),ne(t)}catch{}finally{try{t?.close()}catch{}}}var $=class extends v{constructor(e){super(e?.dbPath??B("session"))}stmt(e){return this.stmts.get(e)}initSchema(){try{let t=this.db.pragma("table_xinfo(session_events)").find(r=>r.name==="data_hash");t&&t.hidden!==0&&this.db.exec("DROP TABLE session_events")}catch{}this.db.exec(`
|
|
4
4
|
CREATE TABLE IF NOT EXISTS session_events (
|
|
5
5
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
6
|
session_id TEXT NOT NULL,
|
|
@@ -87,7 +87,24 @@ import{createRequire as oe}from"node:module";import{existsSync as ie,unlinkSync
|
|
|
87
87
|
)`),e(o.updateMetaLastEvent,`UPDATE session_meta
|
|
88
88
|
SET last_event_at = datetime('now'), event_count = event_count + 1
|
|
89
89
|
WHERE session_id = ?`),e(o.ensureSession,"INSERT OR IGNORE INTO session_meta (session_id, project_dir) VALUES (?, ?)"),e(o.getSessionStats,`SELECT session_id, project_dir, started_at, last_event_at, event_count, compact_count
|
|
90
|
-
FROM session_meta WHERE session_id = ?`),e(o.
|
|
90
|
+
FROM session_meta WHERE session_id = ?`),e(o.getSessionRollup,`SELECT
|
|
91
|
+
COUNT(*) AS tool_calls,
|
|
92
|
+
COALESCE(SUM(CASE WHEN category = 'error' THEN 1 ELSE 0 END), 0) AS errors,
|
|
93
|
+
COUNT(DISTINCT type) AS unique_tools,
|
|
94
|
+
COUNT(DISTINCT CASE WHEN category = 'file' THEN data END) AS unique_files,
|
|
95
|
+
CASE WHEN SUM(CASE WHEN category = 'git' THEN 1 ELSE 0 END) > 0 THEN 1 ELSE 0 END AS has_commit,
|
|
96
|
+
CAST(COALESCE((MAX(strftime('%s', created_at)) - MIN(strftime('%s', created_at))) / 60.0, 0) AS INTEGER) AS duration_min,
|
|
97
|
+
COALESCE(SUM(CASE WHEN type = 'external_ref' THEN 1 ELSE 0 END), 0) AS sources_indexed,
|
|
98
|
+
CAST(COALESCE(SUM(bytes_avoided) / 1024.0, 0) AS INTEGER) AS total_chunks,
|
|
99
|
+
COALESCE(SUM(CASE WHEN type IN ('file_search', 'file_glob') THEN 1 ELSE 0 END), 0) AS search_queries
|
|
100
|
+
FROM session_events
|
|
101
|
+
WHERE session_id = ?`),e(o.getMaxFileEdits,`SELECT COALESCE(MAX(c), 0) AS max_file_edits
|
|
102
|
+
FROM (
|
|
103
|
+
SELECT COUNT(*) AS c
|
|
104
|
+
FROM session_events
|
|
105
|
+
WHERE session_id = ? AND category = 'file' AND type IN ('file_edit', 'file_write')
|
|
106
|
+
GROUP BY data
|
|
107
|
+
)`),e(o.incrementCompactCount,"UPDATE session_meta SET compact_count = compact_count + 1 WHERE session_id = ?"),e(o.upsertResume,`INSERT INTO session_resume (session_id, snapshot, event_count)
|
|
91
108
|
VALUES (?, ?, ?)
|
|
92
109
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
93
110
|
snapshot = excluded.snapshot,
|
|
@@ -118,6 +135,6 @@ import{createRequire as oe}from"node:module";import{existsSync as ie,unlinkSync
|
|
|
118
135
|
FROM tool_calls WHERE session_id = ?`),e(o.getToolCallByTool,`SELECT tool, calls, bytes_returned
|
|
119
136
|
FROM tool_calls WHERE session_id = ? ORDER BY calls DESC`),e(o.getEventBytesSummary,`SELECT COALESCE(SUM(bytes_avoided), 0) AS bytes_avoided,
|
|
120
137
|
COALESCE(SUM(bytes_returned), 0) AS bytes_returned
|
|
121
|
-
FROM session_events WHERE session_id = ?`)}insertEvent(e,t,r="PostToolUse",s,a){let i=
|
|
138
|
+
FROM session_events WHERE session_id = ?`)}insertEvent(e,t,r="PostToolUse",s,a){let i=S("sha256").update(t.data).digest("hex").slice(0,16).toUpperCase(),c=String(s?.projectDir??t.project_dir??this._getSessionProjectDir(e)).trim(),d=String(s?.source??t.attribution_source??"unknown"),u=Number(s?.confidence??t.attribution_confidence??0),T=Number.isFinite(u)?Math.max(0,Math.min(1,u)):0,p=R(a?.bytesAvoided),N=R(a?.bytesReturned),L=this.db.transaction(()=>{if(this.stmt(o.checkDuplicate).get(e,X,t.type,i))return;this.stmt(o.getEventCount).get(e).cnt>=W&&this.stmt(o.evictLowestPriority).run(e),this.stmt(o.insertEvent).run(e,t.type,t.category,t.priority,t.data,c,d,T,p,N,r,i),this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>L())}bulkInsertEvents(e,t,r="PostToolUse",s,a){if(!t||t.length===0)return;if(t.length===1){this.insertEvent(e,t[0],r,s?.[0],a?.[0]);return}let i=t.map((d,u)=>{let T=S("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),p=s?.[u],N=String(p?.projectDir??d.project_dir??this._getSessionProjectDir(e)??"").trim(),L=String(p?.source??d.attribution_source??"unknown"),O=Number(p?.confidence??d.attribution_confidence??0),U=Number.isFinite(O)?Math.max(0,Math.min(1,O)):0,M=a?.[u],re=R(M?.bytesAvoided),se=R(M?.bytesReturned);return{event:d,dataHash:T,projectDir:N,attributionSource:L,attributionConfidence:U,bytesAvoided:re,bytesReturned:se}}),c=this.db.transaction(()=>{let d=this.stmt(o.getEventCount).get(e).cnt;for(let u of i)this.stmt(o.checkDuplicate).get(e,X,u.event.type,u.dataHash)||(d>=W?this.stmt(o.evictLowestPriority).run(e):d++,this.stmt(o.insertEvent).run(e,u.event.type,u.event.category,u.event.priority,u.event.data,u.projectDir,u.attributionSource,u.attributionConfidence,u.bytesAvoided,u.bytesReturned,r,u.dataHash));this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>c())}getEvents(e,t){let r=t?.limit??1e3,s=t?.type,a=t?.minPriority;return s&&a!==void 0?this.stmt(o.getEventsByTypeAndPriority).all(e,s,a,r):s?this.stmt(o.getEventsByType).all(e,s,r):a!==void 0?this.stmt(o.getEventsByPriority).all(e,a,r):this.stmt(o.getEvents).all(e,r)}getEventCount(e){return this.stmt(o.getEventCount).get(e).cnt}getEventBytesSummary(e){let t=this.stmt(o.getEventBytesSummary).get(e);return{bytesAvoided:Number(t?.bytes_avoided??0),bytesReturned:Number(t?.bytes_returned??0)}}getLatestAttributedProjectDir(e){return this.stmt(o.getLatestAttributedProject).get(e)?.project_dir||null}_getSessionProjectDir(e){try{return this.db.prepare("SELECT project_dir FROM session_meta WHERE session_id = ?").get(e)?.project_dir||""}catch{return""}}searchEvents(e,t,r,s){try{let a=e.replace(/[%_]/g,c=>"\\"+c),i=s??null;return this.stmt(o.searchEvents).all(r,a,a,i,i,t)}catch{return[]}}getSessionIdsForProject(e){try{return this.db.prepare(`SELECT DISTINCT session_id
|
|
122
139
|
FROM session_events
|
|
123
|
-
WHERE project_dir = ?`).all(e).map(r=>r.session_id)}catch{return[]}}ensureSession(e,t){this.stmt(o.ensureSession).run(e,t)}getSessionStats(e){return this.stmt(o.getSessionStats).get(e)??null}incrementCompactCount(e){this.stmt(o.incrementCompactCount).run(e)}upsertResume(e,t,r){this.stmt(o.upsertResume).run(e,t,r??0)}getResume(e){return this.stmt(o.getResume).get(e)??null}markResumeConsumed(e){this.stmt(o.markResumeConsumed).run(e)}claimLatestUnconsumedResume(e){let t=this.stmt(o.claimLatestUnconsumedResume).get(e);return t?{sessionId:t.session_id,snapshot:t.snapshot}:null}getLatestSessionId(){try{return this.db.prepare("SELECT session_id FROM session_meta ORDER BY started_at DESC LIMIT 1").get()?.session_id??null}catch{return null}}incrementToolCall(e,t,r=0){let s=Number.isFinite(r)&&r>0?Math.round(r):0;try{this.stmt(o.incrementToolCall).run(e,t,s)}catch{}}getToolCallStats(e){try{let t=this.stmt(o.getToolCallTotals).get(e),r=this.stmt(o.getToolCallByTool).all(e),s={};for(let a of r)s[a.tool]={calls:a.calls,bytesReturned:a.bytes_returned};return{totalCalls:t?.calls??0,totalBytesReturned:t?.bytes_returned??0,byTool:s}}catch{return{totalCalls:0,totalBytesReturned:0,byTool:{}}}}deleteSession(e){this.db.transaction(()=>{this.stmt(o.deleteEvents).run(e),this.stmt(o.deleteResume).run(e),this.stmt(o.deleteMeta).run(e)})()}cleanupOldSessions(e=7){let t=`-${e}`,r=this.stmt(o.getOldSessions).all(t);for(let{session_id:s}of r)this.deleteSession(s);return r.length}pruneOrphanedEvents(){let e=this.db.prepare("DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)").run();return Number(e.changes??0)}};export{$ as SessionDB,
|
|
140
|
+
WHERE project_dir = ?`).all(e).map(r=>r.session_id)}catch{return[]}}ensureSession(e,t){this.stmt(o.ensureSession).run(e,t)}getSessionStats(e){return this.stmt(o.getSessionStats).get(e)??null}getSessionRollup(e){let t=this.stmt(o.getSessionRollup).get(e),r=this.stmt(o.getMaxFileEdits).get(e),s=this.getSessionStats(e),a=(t?.tool_calls??0)>0?t?.unique_files??0:0,i=t?.errors??0,c=Math.min(a,i);return{tool_calls:t?.tool_calls??0,errors:t?.errors??0,unique_tools:t?.unique_tools??0,unique_files:t?.unique_files??0,max_file_edits:r?.max_file_edits??0,has_commit:t?.has_commit??0,edit_test_cycles:c,duration_min:t?.duration_min??0,compact_count:s?.compact_count??0,sources_indexed:t?.sources_indexed??0,total_chunks:t?.total_chunks??0,search_queries:t?.search_queries??0}}incrementCompactCount(e){this.stmt(o.incrementCompactCount).run(e)}upsertResume(e,t,r){this.stmt(o.upsertResume).run(e,t,r??0)}getResume(e){return this.stmt(o.getResume).get(e)??null}markResumeConsumed(e){this.stmt(o.markResumeConsumed).run(e)}claimLatestUnconsumedResume(e){let t=this.stmt(o.claimLatestUnconsumedResume).get(e);return t?{sessionId:t.session_id,snapshot:t.snapshot}:null}getLatestSessionId(){try{return this.db.prepare("SELECT session_id FROM session_meta ORDER BY started_at DESC LIMIT 1").get()?.session_id??null}catch{return null}}incrementToolCall(e,t,r=0){let s=Number.isFinite(r)&&r>0?Math.round(r):0;try{this.stmt(o.incrementToolCall).run(e,t,s)}catch{}}getToolCallStats(e){try{let t=this.stmt(o.getToolCallTotals).get(e),r=this.stmt(o.getToolCallByTool).all(e),s={};for(let a of r)s[a.tool]={calls:a.calls,bytesReturned:a.bytes_returned};return{totalCalls:t?.calls??0,totalBytesReturned:t?.bytes_returned??0,byTool:s}}catch{return{totalCalls:0,totalBytesReturned:0,byTool:{}}}}deleteSession(e){this.db.transaction(()=>{this.stmt(o.deleteEvents).run(e),this.stmt(o.deleteResume).run(e),this.stmt(o.deleteMeta).run(e)})()}cleanupOldSessions(e=7){let t=`-${e}`,r=this.stmt(o.getOldSessions).all(t);for(let{session_id:s}of r)this.deleteSession(s);return r.length}pruneOrphanedEvents(){let e=this.db.prepare("DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)").run();return Number(e.changes??0)}};export{$ as SessionDB,f as StorageDirectoryError,Ze as _resetWorktreeSuffixCacheForTests,ne as applyMissingSessionEventsColumns,Qe as clearStorageDirectoryCheckCacheForTests,ze as describeStorageDirectorySource,nt as ensureSessionEventsSchema,Je as ensureWritableStorageDir,Ke as formatStorageDirectoryError,xe as getWorktreeSuffix,te as hashProjectDirCanonical,ee as hashProjectDirLegacy,h as normalizeWorktreePath,Ge as resolveContentStorageDir,et as resolveContentStorePath,qe as resolveDefaultSessionDir,tt as resolveSessionDbPath,Ie as resolveSessionPath,Q as resolveSessionStorageDir,Ye as resolveStatsStorageDir};
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* Fallback: if bundles are missing (marketplace installs), try build/session/*.js.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { join } from "node:path";
|
|
9
|
+
import { join, resolve as resolvePath } from "node:path";
|
|
10
10
|
import { pathToFileURL } from "node:url";
|
|
11
|
-
import { existsSync } from "node:fs";
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
12
13
|
|
|
13
14
|
import { hasPlatformConfig, maybeForward } from "./platform-bridge.mjs";
|
|
14
15
|
import { detectPlatformFromEnv } from "./core/platform-detect.mjs";
|
|
@@ -106,13 +107,316 @@ export function attributeAndInsertEvents(db, sessionId, events, input, projectDi
|
|
|
106
107
|
// the unconfigured-user path costs at most one syscall per minute.
|
|
107
108
|
if (hasPlatformConfig()) {
|
|
108
109
|
const platform = detectPlatformFromEnv();
|
|
110
|
+
// Session-wide rollup snapshot — stamped onto every outgoing event so
|
|
111
|
+
// the analytics engine sees the seed.ts shape (tool_calls, errors,
|
|
112
|
+
// unique_tools, ...). Defensive call: older SessionDB bundles that
|
|
113
|
+
// predate v1.0.158 won't have getSessionRollup; fall back to null
|
|
114
|
+
// and the bridge will still pass the per-event facts through.
|
|
115
|
+
const rollup = typeof db.getSessionRollup === "function"
|
|
116
|
+
? db.getSessionRollup(sessionId)
|
|
117
|
+
: null;
|
|
118
|
+
|
|
119
|
+
// v1.0.159: Bash metadata shared across all events from this hook fire.
|
|
120
|
+
// A single Bash tool call may emit multiple canonical events (a `git
|
|
121
|
+
// pull` produces type=git AND type=cwd) — they all share the same
|
|
122
|
+
// command_type / command_tool / exit_code / duration_bucket. Hook
|
|
123
|
+
// metadata (latency, exit_code) is also per-call, not per-event.
|
|
124
|
+
const bashMeta = deriveBashMetadata(input);
|
|
125
|
+
// v1.0.159: latency_ms read from the PreToolUse timestamp stamp.
|
|
126
|
+
// PreToolUse writes ms-precision Date.now() to a tmp file, PostToolUse
|
|
127
|
+
// reads + computes delta + cleans up. Failure → undefined (no field
|
|
128
|
+
// surfaces on the wire; Zod is optional).
|
|
129
|
+
const latencyMs = readLatencyMs(sessionId, input?.tool_name);
|
|
130
|
+
|
|
109
131
|
for (let i = 0; i < events.length; i++) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
const enriched = enrichEventForPlatform(events[i], attributions[i]);
|
|
133
|
+
const withBash = bashMeta ? { ...enriched, ...bashMeta } : enriched;
|
|
134
|
+
const withLatency = latencyMs !== undefined
|
|
135
|
+
? { ...withBash, latency_ms: latencyMs, duration_bucket: bucketizeDuration(latencyMs) }
|
|
136
|
+
: withBash;
|
|
137
|
+
const payload = rollup ? { ...withLatency, ...rollup } : withLatency;
|
|
138
|
+
maybeForward({ ...payload, session_id: sessionId }, platform);
|
|
114
139
|
}
|
|
115
140
|
}
|
|
116
141
|
|
|
117
142
|
return attributions;
|
|
118
143
|
}
|
|
144
|
+
|
|
145
|
+
// ── Per-event enrichment (seed.ts shape parity) ──────────────────────────
|
|
146
|
+
//
|
|
147
|
+
// Each canonical event from session-extract carries only {type, category, data}.
|
|
148
|
+
// The platform's events table has 35 columns; the engine's aggregate SQL reads
|
|
149
|
+
// most of them. This helper derives the per-event-derivable subset directly
|
|
150
|
+
// from the event's own facts — no I/O, no classifier dependency, no allocation
|
|
151
|
+
// beyond the spread. Aggregates (tool_calls, errors, ...) come from the
|
|
152
|
+
// session rollup stamp in the caller.
|
|
153
|
+
//
|
|
154
|
+
// PRD-context-as-a-service §5.4 ABI: bridge stays a dumb pipe. This enrichment
|
|
155
|
+
// runs BEFORE maybeForward so the body envelope spreads the enriched event
|
|
156
|
+
// unchanged.
|
|
157
|
+
function enrichEventForPlatform(event, attribution) {
|
|
158
|
+
const error = event?.category === "error" ? 1 : 0;
|
|
159
|
+
const dataStr = typeof event?.data === "string" ? event.data : "";
|
|
160
|
+
|
|
161
|
+
const enriched = {
|
|
162
|
+
...event,
|
|
163
|
+
...attribution,
|
|
164
|
+
error,
|
|
165
|
+
// session_* are open-string passthroughs (ADR-0001) — let the platform
|
|
166
|
+
// do forensic queries on the raw shape without forcing the wide→narrow
|
|
167
|
+
// category derivation to ever round-trip.
|
|
168
|
+
session_category: event?.category,
|
|
169
|
+
session_type: event?.type,
|
|
170
|
+
session_data: dataStr.length > 0 ? dataStr.slice(0, 500) : undefined,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Error events: surface the message + classify
|
|
174
|
+
if (error === 1) {
|
|
175
|
+
enriched.error_message = dataStr.slice(0, 1000);
|
|
176
|
+
const cls = classifyError(dataStr);
|
|
177
|
+
enriched.error_category = cls.error_category;
|
|
178
|
+
enriched.error_tool = cls.error_tool;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// blocker_status: derive from the canonical event TYPE, not lexical
|
|
182
|
+
// pattern-matching on prose. session-extract already identifies blocker
|
|
183
|
+
// states semantically (type='blocker' when the agent signals stuck;
|
|
184
|
+
// type='blocker_resolved' on recovery). Regex on error_message would
|
|
185
|
+
// false-positive on the millions of error texts in the wild — we let
|
|
186
|
+
// the extractor's structural judgment be the source of truth.
|
|
187
|
+
if (event?.type === "blocker") enriched.blocker_status = "open";
|
|
188
|
+
else if (event?.type === "blocker_resolved") enriched.blocker_status = "resolved";
|
|
189
|
+
|
|
190
|
+
// Git events: surface commit message + mark has_commit at the event level
|
|
191
|
+
// (rollup-level has_commit comes from the session-wide stamp; both win
|
|
192
|
+
// when set — `{...enriched, ...rollup}` order keeps rollup authoritative
|
|
193
|
+
// for non-git events while git events stay marked).
|
|
194
|
+
if (event?.category === "git" && dataStr.length > 0) {
|
|
195
|
+
enriched.commit_message = dataStr.slice(0, 500);
|
|
196
|
+
enriched.has_commit = 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// File events: ship the file path as the single-item array shape the
|
|
200
|
+
// platform schema expects (Zod: z.array(z.string()).max(20))
|
|
201
|
+
if (event?.category === "file" && dataStr.length > 0) {
|
|
202
|
+
enriched.file_paths = [dataStr.slice(0, 500)];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return enriched;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Inline error classifier — seed.ts ERROR_CATEGORIES parity ────────────
|
|
209
|
+
//
|
|
210
|
+
// Mirrors src/session/error-classifier.ts's 10-category table for runtime
|
|
211
|
+
// callers (this is a .mjs hook file; the TS classifier ships bundled but
|
|
212
|
+
// the bundle import path costs an extra ~20ms on first hook fire and an
|
|
213
|
+
// extra disk read per hook subprocess. Inline keeps the hot path fast.)
|
|
214
|
+
// If the table ever drifts from error-classifier.ts, the classifier test
|
|
215
|
+
// suite (tests/session/classifier.test.ts) is the canonical source — sync
|
|
216
|
+
// the patterns there first, then mirror here.
|
|
217
|
+
function classifyError(message) {
|
|
218
|
+
const m = String(message ?? "").toLowerCase();
|
|
219
|
+
if (!m) return { error_category: "unknown", error_tool: "Bash" };
|
|
220
|
+
|
|
221
|
+
// Order matters: timeout + git_conflict checked BEFORE test_failed so
|
|
222
|
+
// "test timed out" and "CONFLICT … fail" land in the right bucket.
|
|
223
|
+
if (/etimedout|timed out|timeout|deadline exceeded/.test(m)) return { error_category: "timeout", error_tool: "Bash" };
|
|
224
|
+
if (/conflict.*(merge|rebase|git)|merge conflict|^conflict/.test(m)) return { error_category: "git_conflict", error_tool: "Bash" };
|
|
225
|
+
if (/enoent|no such file|cannot find module|filenotfounderror/.test(m)) return { error_category: "file_not_found", error_tool: "Read" };
|
|
226
|
+
if (/command not found|: not found|exit code 127/.test(m)) return { error_category: "command_not_found", error_tool: "Bash" };
|
|
227
|
+
if (/old_string|could not find string|matches multiple/.test(m)) return { error_category: "edit_match_failed", error_tool: "Edit" };
|
|
228
|
+
if (/eacces|permission denied|operation not permitted|eperm/.test(m)) return { error_category: "permission_denied", error_tool: "Bash" };
|
|
229
|
+
if (/syntaxerror|error ts\d+|unexpected token|parse error/.test(m)) return { error_category: "syntax_error", error_tool: "Bash" };
|
|
230
|
+
if (/typeerror|referenceerror|rangeerror|traceback|nullpointer/.test(m)) return { error_category: "runtime_error", error_tool: "Bash" };
|
|
231
|
+
if (/test failed|fail |tests failed|assertion/.test(m)) return { error_category: "test_failed", error_tool: "Bash" };
|
|
232
|
+
return { error_category: "unknown", error_tool: "Bash" };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Bash metadata derivation — algorithmic, not enumerative ──────────────
|
|
236
|
+
//
|
|
237
|
+
// A single Bash tool call may emit MULTIPLE canonical events (a `git pull`
|
|
238
|
+
// produces type='git' AND type='cwd'). The platform's command_metadata
|
|
239
|
+
// describes the BASH CALL, not the per-event derivative — so all events
|
|
240
|
+
// from one PostToolUse fire carry the same shape. Non-Bash tool calls
|
|
241
|
+
// return null and the per-event fields stay undefined (Zod optional drops
|
|
242
|
+
// them silently — no NULL noise on the wire).
|
|
243
|
+
//
|
|
244
|
+
// DESIGN: tool ecosystems contain millions of CLI binaries but converge on
|
|
245
|
+
// a tiny canonical verb set (test/build/install/lint/format/run/start/
|
|
246
|
+
// deploy/...). The classifier scans for these verbs at canonical token
|
|
247
|
+
// positions — agnostic of which package manager / language / framework.
|
|
248
|
+
// New tools without a registry change automatically classify correctly as
|
|
249
|
+
// long as they use the verbs (which is the dominant ecosystem convention).
|
|
250
|
+
// This was originally regex-table enumeration; the table never converges.
|
|
251
|
+
const CANONICAL_VERBS = new Set([
|
|
252
|
+
"test", "build", "install", "lint", "format", "run", "start",
|
|
253
|
+
"deploy", "compile", "bundle", "watch", "serve", "publish",
|
|
254
|
+
]);
|
|
255
|
+
// Runners that wrap the actual executable — strip them so command_tool
|
|
256
|
+
// reflects the real binary the user invoked (`bunx pytest` → "pytest",
|
|
257
|
+
// not "bunx"). NODE_ENV=production npm run build → "npm".
|
|
258
|
+
const COMMAND_RUNNERS = new Set([
|
|
259
|
+
"sudo", "doas", "env", "exec", "time",
|
|
260
|
+
"npx", "pnpx", "bunx", "pnpm", "yarn", "bun",
|
|
261
|
+
]);
|
|
262
|
+
const ENV_ASSIGN_RE = /^[A-Z_][A-Z0-9_]*=/;
|
|
263
|
+
|
|
264
|
+
// Tools whose NAME directly implies their type (no subcommand needed).
|
|
265
|
+
// Curated minimum — covers the dominant test/lint/format/build/db/http/
|
|
266
|
+
// deploy invocations across ecosystems. New ecosystem tools land in
|
|
267
|
+
// "other" until added — preferred to a noisy heuristic that misclassifies.
|
|
268
|
+
// Lookup is O(1); contrast with the original regex-table approach which
|
|
269
|
+
// scaled to no boundary and still missed unknowns.
|
|
270
|
+
const CANONICAL_TOOLS = new Map([
|
|
271
|
+
// test runners
|
|
272
|
+
["pytest", "test"], ["jest", "test"], ["vitest", "test"], ["mocha", "test"],
|
|
273
|
+
["ava", "test"], ["jasmine", "test"], ["rspec", "test"], ["junit", "test"],
|
|
274
|
+
["tap", "test"], ["karma", "test"],
|
|
275
|
+
// linters
|
|
276
|
+
["eslint", "lint"], ["tslint", "lint"], ["ruff", "lint"], ["rubocop", "lint"],
|
|
277
|
+
["pylint", "lint"], ["flake8", "lint"], ["clippy", "lint"], ["staticcheck", "lint"],
|
|
278
|
+
["mypy", "lint"], ["shellcheck", "lint"],
|
|
279
|
+
// formatters
|
|
280
|
+
["prettier", "format"], ["black", "format"], ["gofmt", "format"], ["rustfmt", "format"],
|
|
281
|
+
["autopep8", "format"], ["yapf", "format"],
|
|
282
|
+
// bundlers / builders
|
|
283
|
+
["webpack", "build"], ["vite", "build"], ["rollup", "build"], ["esbuild", "build"],
|
|
284
|
+
["parcel", "build"], ["tsc", "build"], ["swc", "build"], ["turbo", "build"],
|
|
285
|
+
// deploy / infra
|
|
286
|
+
["docker", "deploy"], ["kubectl", "deploy"], ["terraform", "deploy"], ["pulumi", "deploy"],
|
|
287
|
+
["ansible", "deploy"], ["helm", "deploy"], ["aws", "deploy"], ["gcloud", "deploy"], ["az", "deploy"],
|
|
288
|
+
// databases
|
|
289
|
+
["psql", "database"], ["mysql", "database"], ["sqlite3", "database"],
|
|
290
|
+
["redis-cli", "database"], ["mongosh", "database"], ["mongo", "database"],
|
|
291
|
+
// http
|
|
292
|
+
["curl", "http"], ["wget", "http"], ["httpie", "http"], ["http", "http"],
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
function deriveBashMetadata(input) {
|
|
296
|
+
if (input?.tool_name !== "Bash") return null;
|
|
297
|
+
const cmd = String(input?.tool_input?.command ?? "").trim();
|
|
298
|
+
if (!cmd) return { command_type: "other", command_tool: "Bash" };
|
|
299
|
+
|
|
300
|
+
const tokens = cmd.split(/\s+/);
|
|
301
|
+
const command_tool = extractCommandTool(tokens);
|
|
302
|
+
const command_type = classifyCommandType(tokens, command_tool);
|
|
303
|
+
const exit_code = inferExitCode(input?.tool_response);
|
|
304
|
+
return { command_type, command_tool, exit_code };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Strip env-assign prefixes (`FOO=bar`), then strip runner shells,
|
|
308
|
+
// then return the basename of the executable token.
|
|
309
|
+
function extractCommandTool(tokens) {
|
|
310
|
+
let i = 0;
|
|
311
|
+
// Skip env assignments
|
|
312
|
+
while (i < tokens.length && ENV_ASSIGN_RE.test(tokens[i])) i++;
|
|
313
|
+
// Skip runner shells
|
|
314
|
+
while (i < tokens.length && COMMAND_RUNNERS.has(tokens[i].toLowerCase())) {
|
|
315
|
+
i++;
|
|
316
|
+
// Skip subcommands like `pnpm dlx`, `pnpm exec`, `bun run`
|
|
317
|
+
if (i < tokens.length && /^(dlx|exec|run|x)$/i.test(tokens[i])) i++;
|
|
318
|
+
}
|
|
319
|
+
if (i >= tokens.length) return tokens[0] || "Bash";
|
|
320
|
+
const exe = tokens[i];
|
|
321
|
+
// basename of path-like executables (`/usr/local/bin/foo` → "foo")
|
|
322
|
+
const base = exe.split(/[/\\]/).pop() || "Bash";
|
|
323
|
+
// Strip shell quoting if present
|
|
324
|
+
return base.replace(/^['"]|['"]$/g, "");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Type classification — priority order:
|
|
328
|
+
// 1. Tool name implies type (curated CANONICAL_TOOLS map)
|
|
329
|
+
// 2. Canonical verb at subcommand position (`npm test`, `cargo build`)
|
|
330
|
+
// 3. Argument-shape heuristics (test/ dir, .test.ts suffix, --prod flag)
|
|
331
|
+
// 4. Tool-level fallback (git → git, make → build)
|
|
332
|
+
// 5. "other"
|
|
333
|
+
function classifyCommandType(tokens, command_tool) {
|
|
334
|
+
const toolLc = (command_tool || "").toLowerCase();
|
|
335
|
+
|
|
336
|
+
// 1. Tool name itself names the type
|
|
337
|
+
const fromTool = CANONICAL_TOOLS.get(toolLc);
|
|
338
|
+
if (fromTool) return fromTool;
|
|
339
|
+
|
|
340
|
+
// Skip env + runners to find subcommand position
|
|
341
|
+
const lower = tokens.map((t) => t.toLowerCase());
|
|
342
|
+
let start = 0;
|
|
343
|
+
while (start < lower.length && ENV_ASSIGN_RE.test(tokens[start])) start++;
|
|
344
|
+
while (start < lower.length && COMMAND_RUNNERS.has(lower[start])) {
|
|
345
|
+
start++;
|
|
346
|
+
if (start < lower.length && /^(dlx|exec|run|x)$/.test(lower[start])) start++;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 2. Canonical verb scan within next 4 tokens
|
|
350
|
+
const horizon = Math.min(lower.length, start + 4);
|
|
351
|
+
for (let i = start; i < horizon; i++) {
|
|
352
|
+
if (CANONICAL_VERBS.has(lower[i])) return lower[i];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 3. Argument-shape heuristics
|
|
356
|
+
const tail = tokens.slice(start).join(" ");
|
|
357
|
+
if (/\btests?[/\\]|\bspec[/\\]|__tests__|\.(test|spec)\.[mc]?[jt]sx?\b|test_[\w-]+\.py\b|_test\.go\b/.test(tail)) return "test";
|
|
358
|
+
if (/--(prod|production|release|optimize)\b/.test(tail)) return "build";
|
|
359
|
+
if (/\bDockerfile\b|docker-compose/.test(tail)) return "deploy";
|
|
360
|
+
|
|
361
|
+
// 4. Tool-level fallback for tools whose mere presence implies the type
|
|
362
|
+
if (toolLc === "git") return "git";
|
|
363
|
+
if (toolLc === "make" || toolLc === "ninja" || toolLc === "cmake") return "build";
|
|
364
|
+
|
|
365
|
+
return "other";
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Exit code best-effort inference from tool_response. Hook stdin does
|
|
369
|
+
// not carry the actual exit code on CC; we read the shape of the output
|
|
370
|
+
// for signals. Engine treats exit_code as soft signal (Anomaly #3 — no
|
|
371
|
+
// pattern in patterns.ts reads it today), so probabilistic stamps are
|
|
372
|
+
// adequate. Captures named exit code when explicit.
|
|
373
|
+
function inferExitCode(response) {
|
|
374
|
+
const r = String(response ?? "");
|
|
375
|
+
if (!r) return 0;
|
|
376
|
+
// Explicit exit-code marker (some wrappers emit "exit status 137" etc.)
|
|
377
|
+
const explicit = r.match(/\bexit (?:status|code)\s+(\d+)\b/i);
|
|
378
|
+
if (explicit) return Number(explicit[1]);
|
|
379
|
+
// "command not found" → POSIX standard 127
|
|
380
|
+
if (/^bash:.*: (?:command not found|No such file)/m.test(r)) return 127;
|
|
381
|
+
// Heuristic non-zero indicators (line-anchored to avoid false positives
|
|
382
|
+
// inside narrative text from successful commands).
|
|
383
|
+
if (/^(?:Error: |Traceback|FAIL\b|✗|✘)/m.test(r)) return 1;
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Latency timing — reads PreToolUse marker ────────────────────────────
|
|
388
|
+
//
|
|
389
|
+
// PreToolUse already writes `${tmpdir}/context-mode-latency-${sessionId}-
|
|
390
|
+
// ${toolName}.txt` with the start timestamp (pretooluse.mjs:177). We
|
|
391
|
+
// piggyback on that marker — read + compute delta, do NOT unlink (the
|
|
392
|
+
// downstream slow-tool event emission in posttooluse.mjs:128-152 manages
|
|
393
|
+
// the unlink lifecycle). Failure modes (missing marker, parse error,
|
|
394
|
+
// negative delta, sanity-out-of-range) all return undefined — Zod's
|
|
395
|
+
// optional handling drops the field silently. No NULL noise on the wire.
|
|
396
|
+
function readLatencyMs(sessionId, toolName) {
|
|
397
|
+
if (!sessionId || !toolName) return undefined;
|
|
398
|
+
const markerPath = resolvePath(
|
|
399
|
+
tmpdir(),
|
|
400
|
+
`context-mode-latency-${sessionId}-${toolName}.txt`,
|
|
401
|
+
);
|
|
402
|
+
try {
|
|
403
|
+
const start = parseInt(readFileSync(markerPath, "utf8").trim(), 10);
|
|
404
|
+
if (!Number.isFinite(start) || start <= 0) return undefined;
|
|
405
|
+
const delta = Date.now() - start;
|
|
406
|
+
if (delta < 0 || delta > 24 * 3600 * 1000) return undefined;
|
|
407
|
+
return delta;
|
|
408
|
+
} catch {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Duration bucket ──────────────────────────────────────────────────────
|
|
414
|
+
//
|
|
415
|
+
// Open-string label the platform Zod schema accepts (max 20 chars). Three
|
|
416
|
+
// buckets cover the seed.ts shape: <5s | 5-30s | 30s+.
|
|
417
|
+
function bucketizeDuration(ms) {
|
|
418
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms < 0) return undefined;
|
|
419
|
+
if (ms < 5_000) return "<5s";
|
|
420
|
+
if (ms < 30_000) return "5-30s";
|
|
421
|
+
return "30s+";
|
|
422
|
+
}
|
package/hooks/sessionstart.mjs
CHANGED
|
@@ -30,6 +30,7 @@ await runHook(async () => {
|
|
|
30
30
|
readStdin,
|
|
31
31
|
parseStdin,
|
|
32
32
|
getSessionId,
|
|
33
|
+
getInputProjectDir,
|
|
33
34
|
getSessionDBPath,
|
|
34
35
|
getSessionEventsPath,
|
|
35
36
|
getCleanupFlagPath,
|
|
@@ -38,7 +39,7 @@ await runHook(async () => {
|
|
|
38
39
|
const { writeSessionEventsFile, buildSessionDirective, getSessionEvents } = await import(
|
|
39
40
|
"./session-directive.mjs"
|
|
40
41
|
);
|
|
41
|
-
const { createSessionLoaders } = await import("./session-loaders.mjs");
|
|
42
|
+
const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
|
|
42
43
|
const { join, dirname } = await import("node:path");
|
|
43
44
|
const { fileURLToPath } = await import("node:url");
|
|
44
45
|
const { readFileSync, unlinkSync, readdirSync, rmSync, lstatSync } = await import("node:fs");
|
|
@@ -49,7 +50,40 @@ await runHook(async () => {
|
|
|
49
50
|
|
|
50
51
|
// Resolve absolute path for imports (fileURLToPath for Windows compat)
|
|
51
52
|
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
52
|
-
const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
|
|
53
|
+
const { loadSessionDB, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
|
|
54
|
+
|
|
55
|
+
// Emit a `session_start` canonical event at the boundary of each session
|
|
56
|
+
// lifecycle transition (startup / resume / compact). The platform's insight
|
|
57
|
+
// engine joins on `category='session_start'` to compute per-session
|
|
58
|
+
// aggregates (~60 of 180 patterns depend on this anchor row). Bridge
|
|
59
|
+
// forwards via attributeAndInsertEvents which also stamps the rollup
|
|
60
|
+
// snapshot — safe for the FIRST event of a fresh session.
|
|
61
|
+
async function emitSessionStartLifecycle(db, sessionId, source, projectDir, input) {
|
|
62
|
+
try {
|
|
63
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
64
|
+
const lifecycleEvent = {
|
|
65
|
+
type: "session_start",
|
|
66
|
+
category: "session_start",
|
|
67
|
+
data: JSON.stringify({
|
|
68
|
+
source,
|
|
69
|
+
project_dir: projectDir,
|
|
70
|
+
started_at: Math.floor(Date.now() / 1000),
|
|
71
|
+
}),
|
|
72
|
+
priority: 1,
|
|
73
|
+
};
|
|
74
|
+
attributeAndInsertEvents(
|
|
75
|
+
db,
|
|
76
|
+
sessionId,
|
|
77
|
+
[lifecycleEvent],
|
|
78
|
+
input,
|
|
79
|
+
projectDir,
|
|
80
|
+
"SessionStart",
|
|
81
|
+
resolveProjectAttributions,
|
|
82
|
+
);
|
|
83
|
+
} catch {
|
|
84
|
+
// Best-effort — lifecycle emission failure MUST NOT block session start.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
53
87
|
|
|
54
88
|
// Self-heal a partial plugin cache install before anything else
|
|
55
89
|
// touches the cache dir. The Algo-D4 boot gate and the #604
|
|
@@ -202,6 +236,13 @@ await runHook(async () => {
|
|
|
202
236
|
} catch { /* best-effort */ }
|
|
203
237
|
}
|
|
204
238
|
|
|
239
|
+
// Emit lifecycle anchor BEFORE close — engine joins on
|
|
240
|
+
// category='session_start' to compute per-session aggregates.
|
|
241
|
+
// Cross-platform projectDir via getInputProjectDir (covers cursor's
|
|
242
|
+
// workspace_roots[], codex/gemini/qwen's *_PROJECT_DIR env vars,
|
|
243
|
+
// CC's CLAUDE_PROJECT_DIR, falls back to input.cwd and process.cwd).
|
|
244
|
+
const projectDirCompact = getInputProjectDir(input);
|
|
245
|
+
await emitSessionStartLifecycle(db, sessionId, "compact", projectDirCompact, input);
|
|
205
246
|
db.close();
|
|
206
247
|
} else if (source === "resume") {
|
|
207
248
|
// User invoked --continue, --resume, or /resume — clear cleanup flag so
|
|
@@ -234,6 +275,10 @@ await runHook(async () => {
|
|
|
234
275
|
}
|
|
235
276
|
}
|
|
236
277
|
|
|
278
|
+
const projectDirResume = getInputProjectDir(input);
|
|
279
|
+
if (sessionId) {
|
|
280
|
+
await emitSessionStartLifecycle(db, sessionId, "resume", projectDirResume, input);
|
|
281
|
+
}
|
|
237
282
|
db.close();
|
|
238
283
|
} else if (source === "startup") {
|
|
239
284
|
// Fresh session (no --continue) — clean slate, capture CLAUDE.md rules.
|
|
@@ -294,6 +339,11 @@ await runHook(async () => {
|
|
|
294
339
|
} catch { /* file doesn't exist — skip */ }
|
|
295
340
|
}
|
|
296
341
|
|
|
342
|
+
// Lifecycle anchor for a fresh session — emits BEFORE the CLAUDE.md
|
|
343
|
+
// rule events have been forwarded so the `session_start` row lands
|
|
344
|
+
// as the very first row the platform sees for this session.
|
|
345
|
+
await emitSessionStartLifecycle(db, sessionId, "startup", projectDir, input);
|
|
346
|
+
|
|
297
347
|
db.close();
|
|
298
348
|
|
|
299
349
|
// Age-gated lazy cleanup of old plugin cache version dirs (#181).
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.159",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.159",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP plugin that saves 98% of your context window. Works with Claude Code, Gemini CLI, VS Code Copilot, OpenCode, and Codex CLI. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
|
|
6
6
|
"author": "Mert Koseoğlu",
|