agentix-cli 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +17 -0
  2. package/apps/mac-voice/Sources/AgentXVoice/AgentClient.swift +17 -5
  3. package/apps/mac-voice/Sources/AgentXVoice/Config.swift +6 -1
  4. package/apps/mac-voice/Sources/AgentXVoice/main.swift +5 -2
  5. package/dist/{App-EXB4HP3B.js → App-KE4SURSS.js} +2 -2
  6. package/dist/{ChatApp-XMNE3SPH.js → ChatApp-NNHIKXO2.js} +2 -2
  7. package/dist/{agent-ORYXXL5K.js → agent-RY7Z3IYY.js} +2 -2
  8. package/dist/{board-dashboard-2B3HYIXD.js → board-dashboard-RUOASCSQ.js} +6 -6
  9. package/dist/board-dashboard-RUOASCSQ.js.map +1 -0
  10. package/dist/{chunk-DLKRPK5M.js → chunk-4BNQSUWV.js} +7 -2
  11. package/dist/{chunk-DLKRPK5M.js.map → chunk-4BNQSUWV.js.map} +1 -1
  12. package/dist/{chunk-26OP3X5B.js → chunk-4NBWF4TL.js} +2 -2
  13. package/dist/{chunk-P7MR53U7.js → chunk-4TITGMPX.js} +2 -2
  14. package/dist/{chunk-XM7ZQW4F.js → chunk-5ALS7SVV.js} +2 -2
  15. package/dist/{chunk-3ZHMIWSV.js → chunk-5XQCGPQJ.js} +2 -2
  16. package/dist/{chunk-DVNIFP7S.js → chunk-6VKCUMCN.js} +2 -2
  17. package/dist/{chunk-GCHUE3TN.js → chunk-7WDJW37G.js} +2 -2
  18. package/dist/{chunk-FRHSQVFN.js → chunk-AQASR6KR.js} +3 -3
  19. package/dist/chunk-AQASR6KR.js.map +1 -0
  20. package/dist/{chunk-M5R3PWW5.js → chunk-B5XHX44Y.js} +2 -2
  21. package/dist/{chunk-4ZX4E7P3.js → chunk-BYD55JXH.js} +8 -8
  22. package/dist/chunk-BYD55JXH.js.map +1 -0
  23. package/dist/{chunk-DIV66W3N.js → chunk-EMI3VJLH.js} +10 -10
  24. package/dist/chunk-EMI3VJLH.js.map +1 -0
  25. package/dist/chunk-HRBKSZ34.js +2 -0
  26. package/dist/chunk-HRBKSZ34.js.map +1 -0
  27. package/dist/{chunk-MLLEOFX2.js → chunk-IXUEJBHD.js} +2 -2
  28. package/dist/{chunk-LDILAHMN.js → chunk-JVNFRS26.js} +2 -2
  29. package/dist/{chunk-GIK7S3OY.js → chunk-N2XXKFJ6.js} +7 -7
  30. package/dist/chunk-N2XXKFJ6.js.map +1 -0
  31. package/dist/{chunk-DJQA2CWD.js → chunk-N6EWZGIX.js} +6 -6
  32. package/dist/{chunk-DJQA2CWD.js.map → chunk-N6EWZGIX.js.map} +1 -1
  33. package/dist/{chunk-HEQPHUTR.js → chunk-NLZD2QKF.js} +2 -2
  34. package/dist/chunk-OHA3J5ZV.js +243 -0
  35. package/dist/chunk-OHA3J5ZV.js.map +1 -0
  36. package/dist/{chunk-P5JXQRBH.js → chunk-OTW5MPFT.js} +2 -2
  37. package/dist/{chunk-WMI5B36E.js → chunk-OUZ7YVQZ.js} +2 -2
  38. package/dist/{chunk-GAGVFSZM.js → chunk-P2ZYT5D6.js} +2 -2
  39. package/dist/chunk-P2ZYT5D6.js.map +1 -0
  40. package/dist/{chunk-2WPTP5HO.js → chunk-PZCTCCC5.js} +2 -2
  41. package/dist/chunk-Q7C3X3R6.js +61 -0
  42. package/dist/chunk-Q7C3X3R6.js.map +1 -0
  43. package/dist/{chunk-3WLO2UKI.js → chunk-QDE75R4U.js} +2 -2
  44. package/dist/{chunk-ZUZESPCH.js → chunk-TTL37CS5.js} +64 -64
  45. package/dist/chunk-TTL37CS5.js.map +1 -0
  46. package/dist/{chunk-2GSEEKQK.js → chunk-TUNQFPVA.js} +2 -2
  47. package/dist/{chunk-SL657MIM.js → chunk-WQLBJ2O5.js} +2 -2
  48. package/dist/{chunk-OZX7HVHC.js → chunk-Y6I3BYWV.js} +172 -165
  49. package/dist/chunk-Y6I3BYWV.js.map +1 -0
  50. package/dist/{chunk-SFLF64Q3.js → chunk-Z3IP66SL.js} +2 -2
  51. package/dist/cli.js +1 -1
  52. package/dist/client-JZTH46N2.js +2 -0
  53. package/dist/{compaction-U5SAYTX6.js → compaction-PIZINFHR.js} +2 -2
  54. package/dist/{config-ROQ4VEU3.js → config-R47ITLIY.js} +2 -2
  55. package/dist/config-mutator-T6JGXJ26.js +2 -0
  56. package/dist/context-planner-FJMU5WAT.js +17 -0
  57. package/dist/context-planner-FJMU5WAT.js.map +1 -0
  58. package/dist/doctor-2DMGAWMW.js +2 -0
  59. package/dist/failure-candidates-ARRMFXRY.js +8 -0
  60. package/dist/failure-candidates-ARRMFXRY.js.map +1 -0
  61. package/dist/index.d.ts +15 -1
  62. package/dist/index.js +1 -1
  63. package/dist/memory-extract-OWTI6MN5.js +2 -0
  64. package/dist/processes-KUJJ7XE7.js +2 -0
  65. package/dist/program-KY5ZYPFN.js +2 -0
  66. package/dist/promote-57YNOVXV.js +2 -0
  67. package/dist/proposals-TX2HBBYV.js +2 -0
  68. package/dist/query-ZT7WXY2F.js +2 -0
  69. package/dist/registry-LLAFJJN4.js +2 -0
  70. package/dist/{request-planner-FAFNFVFX.js → request-planner-PEAYYXAP.js} +2 -2
  71. package/dist/{review-candidates-2XKJ2R6U.js → review-candidates-W7NIHNAI.js} +2 -2
  72. package/dist/{routing-PUPVGJ77.js → routing-7LCO7UJ7.js} +2 -2
  73. package/dist/schedule-tool-ZD3VBUBW.js +2 -0
  74. package/dist/scheduler-GC3FQKXY.js +2 -0
  75. package/dist/{seat-2MW3DEXX.js → seat-HR6QMAR2.js} +2 -2
  76. package/dist/{select-Q3AGLQQX.js → select-4XYRIZ5L.js} +2 -2
  77. package/dist/{surface-usage-FM2ZHGYE.js → surface-usage-V2BX4SBD.js} +2 -2
  78. package/dist/{tool-IVUNSAXY.js → tool-PAQ47HV2.js} +2 -2
  79. package/dist/wiki-EREMPSAE.js +2 -0
  80. package/package.json +1 -1
  81. package/dist/board-dashboard-2B3HYIXD.js.map +0 -1
  82. package/dist/chunk-4ZX4E7P3.js.map +0 -1
  83. package/dist/chunk-7DXE264B.js +0 -2
  84. package/dist/chunk-7DXE264B.js.map +0 -1
  85. package/dist/chunk-DIV66W3N.js.map +0 -1
  86. package/dist/chunk-FRHSQVFN.js.map +0 -1
  87. package/dist/chunk-GAGVFSZM.js.map +0 -1
  88. package/dist/chunk-GIK7S3OY.js.map +0 -1
  89. package/dist/chunk-OZX7HVHC.js.map +0 -1
  90. package/dist/chunk-RYLTCOYE.js +0 -241
  91. package/dist/chunk-RYLTCOYE.js.map +0 -1
  92. package/dist/chunk-WZGABQ72.js +0 -59
  93. package/dist/chunk-WZGABQ72.js.map +0 -1
  94. package/dist/chunk-ZUZESPCH.js.map +0 -1
  95. package/dist/client-6HCY7UMW.js +0 -2
  96. package/dist/config-mutator-ML4BAOFT.js +0 -2
  97. package/dist/context-planner-AQHJMVY2.js +0 -17
  98. package/dist/context-planner-AQHJMVY2.js.map +0 -1
  99. package/dist/doctor-3HWGAPET.js +0 -2
  100. package/dist/memory-extract-YZI2QZVR.js +0 -2
  101. package/dist/processes-XWRGOLMT.js +0 -2
  102. package/dist/program-JLZOHWIU.js +0 -2
  103. package/dist/promote-YGNTKEXI.js +0 -2
  104. package/dist/proposals-MWMEXRDW.js +0 -2
  105. package/dist/query-V76JDSOC.js +0 -2
  106. package/dist/registry-DKN4HEI3.js +0 -2
  107. package/dist/schedule-tool-6J7AHAHH.js +0 -2
  108. package/dist/scheduler-ZHQBSSRH.js +0 -2
  109. package/dist/wiki-FPX2YBKS.js +0 -2
  110. /package/dist/{App-EXB4HP3B.js.map → App-KE4SURSS.js.map} +0 -0
  111. /package/dist/{ChatApp-XMNE3SPH.js.map → ChatApp-NNHIKXO2.js.map} +0 -0
  112. /package/dist/{agent-ORYXXL5K.js.map → agent-RY7Z3IYY.js.map} +0 -0
  113. /package/dist/{chunk-26OP3X5B.js.map → chunk-4NBWF4TL.js.map} +0 -0
  114. /package/dist/{chunk-P7MR53U7.js.map → chunk-4TITGMPX.js.map} +0 -0
  115. /package/dist/{chunk-XM7ZQW4F.js.map → chunk-5ALS7SVV.js.map} +0 -0
  116. /package/dist/{chunk-3ZHMIWSV.js.map → chunk-5XQCGPQJ.js.map} +0 -0
  117. /package/dist/{chunk-DVNIFP7S.js.map → chunk-6VKCUMCN.js.map} +0 -0
  118. /package/dist/{chunk-GCHUE3TN.js.map → chunk-7WDJW37G.js.map} +0 -0
  119. /package/dist/{chunk-M5R3PWW5.js.map → chunk-B5XHX44Y.js.map} +0 -0
  120. /package/dist/{chunk-MLLEOFX2.js.map → chunk-IXUEJBHD.js.map} +0 -0
  121. /package/dist/{chunk-LDILAHMN.js.map → chunk-JVNFRS26.js.map} +0 -0
  122. /package/dist/{chunk-HEQPHUTR.js.map → chunk-NLZD2QKF.js.map} +0 -0
  123. /package/dist/{chunk-P5JXQRBH.js.map → chunk-OTW5MPFT.js.map} +0 -0
  124. /package/dist/{chunk-WMI5B36E.js.map → chunk-OUZ7YVQZ.js.map} +0 -0
  125. /package/dist/{chunk-2WPTP5HO.js.map → chunk-PZCTCCC5.js.map} +0 -0
  126. /package/dist/{chunk-3WLO2UKI.js.map → chunk-QDE75R4U.js.map} +0 -0
  127. /package/dist/{chunk-2GSEEKQK.js.map → chunk-TUNQFPVA.js.map} +0 -0
  128. /package/dist/{chunk-SL657MIM.js.map → chunk-WQLBJ2O5.js.map} +0 -0
  129. /package/dist/{chunk-SFLF64Q3.js.map → chunk-Z3IP66SL.js.map} +0 -0
  130. /package/dist/{client-6HCY7UMW.js.map → client-JZTH46N2.js.map} +0 -0
  131. /package/dist/{compaction-U5SAYTX6.js.map → compaction-PIZINFHR.js.map} +0 -0
  132. /package/dist/{config-ROQ4VEU3.js.map → config-R47ITLIY.js.map} +0 -0
  133. /package/dist/{config-mutator-ML4BAOFT.js.map → config-mutator-T6JGXJ26.js.map} +0 -0
  134. /package/dist/{doctor-3HWGAPET.js.map → doctor-2DMGAWMW.js.map} +0 -0
  135. /package/dist/{memory-extract-YZI2QZVR.js.map → memory-extract-OWTI6MN5.js.map} +0 -0
  136. /package/dist/{processes-XWRGOLMT.js.map → processes-KUJJ7XE7.js.map} +0 -0
  137. /package/dist/{program-JLZOHWIU.js.map → program-KY5ZYPFN.js.map} +0 -0
  138. /package/dist/{promote-YGNTKEXI.js.map → promote-57YNOVXV.js.map} +0 -0
  139. /package/dist/{proposals-MWMEXRDW.js.map → proposals-TX2HBBYV.js.map} +0 -0
  140. /package/dist/{query-V76JDSOC.js.map → query-ZT7WXY2F.js.map} +0 -0
  141. /package/dist/{registry-DKN4HEI3.js.map → registry-LLAFJJN4.js.map} +0 -0
  142. /package/dist/{request-planner-FAFNFVFX.js.map → request-planner-PEAYYXAP.js.map} +0 -0
  143. /package/dist/{review-candidates-2XKJ2R6U.js.map → review-candidates-W7NIHNAI.js.map} +0 -0
  144. /package/dist/{routing-PUPVGJ77.js.map → routing-7LCO7UJ7.js.map} +0 -0
  145. /package/dist/{schedule-tool-6J7AHAHH.js.map → schedule-tool-ZD3VBUBW.js.map} +0 -0
  146. /package/dist/{scheduler-ZHQBSSRH.js.map → scheduler-GC3FQKXY.js.map} +0 -0
  147. /package/dist/{seat-2MW3DEXX.js.map → seat-HR6QMAR2.js.map} +0 -0
  148. /package/dist/{select-Q3AGLQQX.js.map → select-4XYRIZ5L.js.map} +0 -0
  149. /package/dist/{surface-usage-FM2ZHGYE.js.map → surface-usage-V2BX4SBD.js.map} +0 -0
  150. /package/dist/{tool-IVUNSAXY.js.map → tool-PAQ47HV2.js.map} +0 -0
  151. /package/dist/{wiki-FPX2YBKS.js.map → wiki-EREMPSAE.js.map} +0 -0
@@ -1,4 +1,4 @@
1
- import{createRequire as _}from"module";import{mkdirSync as d}from"fs";import{dirname as c,resolve as N}from"path";var L=_(import.meta.url),T,u;function o(t){let a=String(t?.message??t),s=process.versions.modules,e=`Run "pnpm rebuild better-sqlite3" with the same Node used by the daemon (${process.version}, modules ${s}).`;return/NODE_MODULE_VERSION|was compiled against|Module version mismatch|invalid ELF|mach-o/i.test(a)?`${a} ${e}`:`${a} ${e}`}function l(t=!1){if(u)return u;try{return u=L("better-sqlite3"),u}catch(a){return t||console.error(`[storage/sqlite] better-sqlite3 native binding failed to load: ${o(a)}`),null}}function k(t={}){if(t.disabled)return null;if(T)return T;let a=N(process.cwd(),t.path??".agentx/db.sqlite"),s=l(t.quiet);if(!s)return null;try{d(c(a),{recursive:!0});let r=new s(a);return r.pragma("journal_mode = WAL"),r.pragma("synchronous = NORMAL"),r.pragma("foreign_keys = ON"),R(r),T=r,T}catch(r){return t.quiet||console.error(`[storage/sqlite] openDb failed at ${a}: ${o(r)}`),null}}function R(t){t.exec(`
1
+ import{createRequire as d}from"module";import{mkdirSync as c}from"fs";import{dirname as N,resolve as L}from"path";var l=d(import.meta.url),T,u;function _(t){let a=String(t?.message??t),s=process.versions.modules,e=`Run "pnpm rebuild better-sqlite3" with the same Node used by the daemon (${process.version}, modules ${s}).`;return/NODE_MODULE_VERSION|was compiled against|Module version mismatch|invalid ELF|mach-o/i.test(a)?`${a} ${e}`:`${a} ${e}`}function R(t=!1){if(u)return u;try{return u=l("better-sqlite3"),u}catch(a){return t||console.error(`[storage/sqlite] better-sqlite3 native binding failed to load: ${_(a)}`),null}}function I(t={}){if(t.disabled)return null;if(T)return T;let a=L(process.cwd(),t.path??".agentx/db.sqlite"),s=R(t.quiet);if(!s)return null;try{c(N(a),{recursive:!0});let r=new s(a);return r.pragma("journal_mode = WAL"),r.pragma("synchronous = NORMAL"),r.pragma("foreign_keys = ON"),A(r),T=r,T}catch(r){return t.quiet||console.error(`[storage/sqlite] openDb failed at ${a}: ${_(r)}`),null}}function A(t){t.exec(`
2
2
  CREATE TABLE IF NOT EXISTS schema_version (
3
3
  v INTEGER PRIMARY KEY
4
4
  );
@@ -169,10 +169,10 @@ import{createRequire as _}from"module";import{mkdirSync as d}from"fs";import{dir
169
169
  `},{v:13,sql:`
170
170
  ALTER TABLE task_traces ADD COLUMN jev_arm TEXT;
171
171
  CREATE INDEX idx_task_traces_jev_arm ON task_traces(jev_arm, started_at);
172
- `}],r=t.transaction(e=>{t.exec(e.sql),t.prepare("INSERT INTO schema_version (v) VALUES (?)").run(e.v)});for(let e of s)e.v>a&&r(e);m(t,"task_traces",A)}function I(t,a,s){let r=Date.now(),n=t.prepare("SELECT COUNT(*) AS n FROM task_queue WHERE status = 'active'").get().n<s,E=null;return n||(E=t.prepare("SELECT COUNT(*) AS n FROM task_queue WHERE status = 'queued'").get().n+1),t.prepare(`
172
+ `}],r=t.transaction(e=>{t.exec(e.sql),t.prepare("INSERT INTO schema_version (v) VALUES (?)").run(e.v)});for(let e of s)e.v>a&&r(e);o(t,"task_traces",m),o(t,"task_traces",g)}function X(t,a,s){let r=Date.now(),n=t.prepare("SELECT COUNT(*) AS n FROM task_queue WHERE status = 'active'").get().n<s,E=null;return n||(E=t.prepare("SELECT COUNT(*) AS n FROM task_queue WHERE status = 'queued'").get().n+1),t.prepare(`
173
173
  INSERT INTO task_queue (id, conversation_id, agent_id, summary, status, position, created_at, started_at)
174
174
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
175
- `).run(a.id,a.conversationId,a.agentId,a.summary,n?"active":"queued",E,r,n?r:null),{id:a.id,status:n?"active":"queued",position:E}}function X(t,a,s="done"){let r=Date.now();t.prepare(`
175
+ `).run(a.id,a.conversationId,a.agentId,a.summary,n?"active":"queued",E,r,n?r:null),{id:a.id,status:n?"active":"queued",position:E}}function v(t,a,s="done"){let r=Date.now();t.prepare(`
176
176
  UPDATE task_queue SET status = ?, finished_at = ?, position = NULL WHERE id = ?
177
177
  `).run(s,r,a);let e=t.prepare(`
178
178
  SELECT id FROM task_queue WHERE status = 'queued' ORDER BY created_at ASC LIMIT 1
@@ -180,8 +180,8 @@ import{createRequire as _}from"module";import{mkdirSync as d}from"fs";import{dir
180
180
  UPDATE task_queue SET status = 'active', position = NULL, started_at = ? WHERE id = ?
181
181
  `).run(r,e.id);let n=t.prepare(`
182
182
  SELECT id FROM task_queue WHERE status = 'queued' ORDER BY created_at ASC
183
- `).all(),E=t.prepare("UPDATE task_queue SET position = ? WHERE id = ?");for(let i=0;i<n.length;i++)E.run(i+1,n[i].id)}}function v(t,a){let s=t.prepare("SELECT * FROM task_queue WHERE id = ?").get(a);return s?{id:s.id,conversationId:s.conversation_id,agentId:s.agent_id,summary:s.summary,status:s.status,position:s.position,createdAt:s.created_at,startedAt:s.started_at,finishedAt:s.finished_at}:null}function U(t,a,s=20){return t.prepare(`
183
+ `).all(),E=t.prepare("UPDATE task_queue SET position = ? WHERE id = ?");for(let i=0;i<n.length;i++)E.run(i+1,n[i].id)}}function U(t,a){let s=t.prepare("SELECT * FROM task_queue WHERE id = ?").get(a);return s?{id:s.id,conversationId:s.conversation_id,agentId:s.agent_id,summary:s.summary,status:s.status,position:s.position,createdAt:s.created_at,startedAt:s.started_at,finishedAt:s.finished_at}:null}function f(t,a,s=20){return t.prepare(`
184
184
  SELECT * FROM task_queue WHERE conversation_id = ?
185
185
  ORDER BY created_at DESC LIMIT ?
186
- `).all(a,s).map(e=>({id:e.id,conversationId:e.conversation_id,agentId:e.agent_id,summary:e.summary,status:e.status,position:e.position,createdAt:e.created_at,startedAt:e.started_at,finishedAt:e.finished_at}))}function f(t,a){let s=Date.now()-a*24*60*60*1e3,r=new Date(s).toISOString(),e=t.prepare("DELETE FROM task_history WHERE started_at < ?").run(r).changes??0,n=t.prepare("DELETE FROM rotations WHERE rotated_at < ?").run(r).changes??0,E=t.prepare("DELETE FROM route_traces WHERE at < ?").run(r).changes??0,i=t.prepare("DELETE FROM task_traces WHERE started_at < ?").run(s).changes??0;if(e+n+E+i>0)try{t.pragma("incremental_vacuum")}catch{}return{taskHistory:e,rotations:n,routeTraces:E,taskTraces:i}}var A=[["resume_origin","TEXT"],["resume_attempt","INTEGER NOT NULL DEFAULT 0"],["resumed_from","TEXT"],["resume_decision","TEXT"],["resume_reason","TEXT"]];function m(t,a,s){let r=new Set(t.prepare(`PRAGMA table_info(${a})`).all().map(e=>e.name));for(let[e,n]of s)r.has(e)||t.exec(`ALTER TABLE ${a} ADD COLUMN ${e} ${n}`)}export{k as a,I as b,X as c,v as d,U as e,f};
187
- //# sourceMappingURL=chunk-DJQA2CWD.js.map
186
+ `).all(a,s).map(e=>({id:e.id,conversationId:e.conversation_id,agentId:e.agent_id,summary:e.summary,status:e.status,position:e.position,createdAt:e.created_at,startedAt:e.started_at,finishedAt:e.finished_at}))}function C(t,a){let s=Date.now()-a*24*60*60*1e3,r=new Date(s).toISOString(),e=t.prepare("DELETE FROM task_history WHERE started_at < ?").run(r).changes??0,n=t.prepare("DELETE FROM rotations WHERE rotated_at < ?").run(r).changes??0,E=t.prepare("DELETE FROM route_traces WHERE at < ?").run(r).changes??0,i=t.prepare("DELETE FROM task_traces WHERE started_at < ?").run(s).changes??0;if(e+n+E+i>0)try{t.pragma("incremental_vacuum")}catch{}return{taskHistory:e,rotations:n,routeTraces:E,taskTraces:i}}var m=[["resume_origin","TEXT"],["resume_attempt","INTEGER NOT NULL DEFAULT 0"],["resumed_from","TEXT"],["resume_decision","TEXT"],["resume_reason","TEXT"]],g=[["num_turns","INTEGER"],["injected_context","TEXT"]];function o(t,a,s){let r=new Set(t.prepare(`PRAGMA table_info(${a})`).all().map(e=>e.name));for(let[e,n]of s)r.has(e)||t.exec(`ALTER TABLE ${a} ADD COLUMN ${e} ${n}`)}export{I as a,X as b,v as c,U as d,f as e,C as f};
187
+ //# sourceMappingURL=chunk-N6EWZGIX.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/storage/sqlite.ts"],"sourcesContent":["import type Database from \"better-sqlite3\"\nimport { createRequire } from \"module\"\nimport { existsSync, mkdirSync } from \"fs\"\nimport { dirname, resolve } from \"path\"\n\n// --- SQLite storage layer ---\n//\n// Move 2 of the architectural review (docs/architecture/hexabot-vs-agentx.md).\n// SQLite for the operational tables — task_history, usage, dedup, sessions —\n// where queryable, indexed access has user-visible value (dashboards,\n// retention, analytics). Workflow definitions, wiki, agent-memory, and\n// references stay as files: they're git-diffable, human-readable, and don't\n// need queries.\n//\n// Approach: subscribe to the event bus (src/events/bus.ts) and write to\n// SQLite from the subscribers. Existing JSON writes are not touched —\n// SQLite runs alongside as a parallel pipeline. Once we have a query API\n// the dashboard depends on, we can deprecate the JSON path. Until then,\n// SQLite is observability-grade only.\n//\n// Synchronous API: better-sqlite3 is sync, no callbacks. The daemon's\n// event loop is the single writer; multiple readers (dashboard) use WAL\n// mode for concurrent read while we write.\n\ntype DatabaseConstructor = typeof Database\n\nconst require = createRequire(import.meta.url)\nlet _db: Database.Database | undefined\nlet _Database: DatabaseConstructor | undefined\n\nfunction betterSqlite3Fix(error: any): string {\n const msg = String(error?.message ?? error)\n const abi = process.versions.modules\n const node = process.version\n const hint = `Run \"pnpm rebuild better-sqlite3\" with the same Node used by the daemon (${node}, modules ${abi}).`\n if (/NODE_MODULE_VERSION|was compiled against|Module version mismatch|invalid ELF|mach-o/i.test(msg)) {\n return `${msg} ${hint}`\n }\n return `${msg} ${hint}`\n}\n\nfunction loadDatabaseCtor(quiet = false): DatabaseConstructor | null {\n if (_Database) return _Database\n try {\n _Database = require(\"better-sqlite3\") as DatabaseConstructor\n return _Database\n } catch (e: any) {\n if (!quiet) {\n console.error(`[storage/sqlite] better-sqlite3 native binding failed to load: ${betterSqlite3Fix(e)}`)\n }\n return null\n }\n}\n\nexport interface OpenOptions {\n /** Resolved relative to cwd. Default: .agentx/db.sqlite */\n path?: string\n /** When true, SQLite writes are skipped (used by tests + opt-out). */\n disabled?: boolean\n /** Suppress the open-failure log. For best-effort callers whose whole\n * contract is \"do nothing if unavailable\" — telemetry, mainly. Without\n * this, a native-binding ABI mismatch prints a paragraph of node-gyp\n * advice on EVERY CLI invocation, which is worse than the thing it\n * is warning about. Load-bearing callers must never pass it. */\n quiet?: boolean\n}\n\n/**\n * Open the per-process SQLite database. Idempotent — repeated calls\n * return the same handle. The file is created on first open; the schema\n * is migrated forward to the latest version.\n *\n * Returns null when disabled — every subscriber should check for null\n * before issuing a write so the daemon stays runnable without SQLite if\n * someone's environment can't compile the native binding.\n */\nexport function openDb(opts: OpenOptions = {}): Database.Database | null {\n if (opts.disabled) return null\n if (_db) return _db\n const path = resolve(process.cwd(), opts.path ?? \".agentx/db.sqlite\")\n const Database = loadDatabaseCtor(opts.quiet)\n if (!Database) return null\n try {\n mkdirSync(dirname(path), { recursive: true })\n const db = new Database(path)\n // WAL keeps readers (dashboard) unblocked while the daemon writes.\n // Synchronous=NORMAL is the SQLite-recommended balance for WAL: the\n // db is durable on power-loss, but a crash within the last few ms can\n // lose the most recent commit. For our use (observability), that's fine.\n db.pragma(\"journal_mode = WAL\")\n db.pragma(\"synchronous = NORMAL\")\n db.pragma(\"foreign_keys = ON\")\n runMigrations(db)\n _db = db\n return _db\n } catch (e: any) {\n // Native binding missing, file unwritable, etc. Surface by default — the\n // alternative was a silent no-op, which makes the \"SQLite not opened\"\n // message in the daemon log impossible to debug. `quiet` callers have\n // explicitly opted out because doing nothing is their correct behavior.\n if (!opts.quiet) {\n console.error(`[storage/sqlite] openDb failed at ${path}: ${betterSqlite3Fix(e)}`)\n }\n return null\n }\n}\n\n/** Close the global handle. Test-only — production never closes. */\nexport function closeDb(): void {\n if (_db) {\n _db.close()\n _db = undefined\n }\n}\n\n/**\n * Per-version schema steps. New tables / columns go in a new migration —\n * never mutate an old one. Run on every open(); each step is idempotent\n * because it CREATE TABLE IF NOT EXISTS. SQLite has no concurrent\n * migration issue here (single writer process).\n */\nfunction runMigrations(db: Database.Database): void {\n db.exec(`\n CREATE TABLE IF NOT EXISTS schema_version (\n v INTEGER PRIMARY KEY\n );\n `)\n const current = (db\n .prepare(\"SELECT MAX(v) AS v FROM schema_version\")\n .get() as { v: number | null }).v ?? 0\n\n const steps: Array<{ v: number; sql: string }> = [\n {\n v: 1,\n sql: `\n CREATE TABLE task_history (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n channel TEXT,\n chat_id TEXT,\n status TEXT NOT NULL, -- 'ok' | 'error' | 'timeout'\n message_preview TEXT,\n error TEXT,\n duration_ms INTEGER,\n input_tokens INTEGER,\n output_tokens INTEGER,\n cache_read_tokens INTEGER,\n cache_create_tokens INTEGER,\n started_at TEXT NOT NULL,\n finished_at TEXT NOT NULL\n );\n CREATE INDEX idx_task_history_agent_started\n ON task_history(agent_id, started_at);\n CREATE INDEX idx_task_history_status\n ON task_history(status, started_at);\n `,\n },\n {\n v: 2,\n sql: `\n CREATE TABLE usage_daily (\n agent_id TEXT NOT NULL,\n model TEXT NOT NULL,\n day TEXT NOT NULL, -- 'YYYY-MM-DD' UTC\n input_tokens INTEGER DEFAULT 0,\n output_tokens INTEGER DEFAULT 0,\n cache_read_tokens INTEGER DEFAULT 0,\n cache_create_tokens INTEGER DEFAULT 0,\n tasks INTEGER DEFAULT 0,\n PRIMARY KEY (agent_id, model, day)\n );\n CREATE INDEX idx_usage_day ON usage_daily(day);\n `,\n },\n {\n v: 3,\n sql: `\n CREATE TABLE rotations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n agent_id TEXT NOT NULL,\n channel TEXT NOT NULL,\n chat_id TEXT NOT NULL,\n reason TEXT NOT NULL, -- 'stale' | 'tier-2' | 'max-turns'\n last_turn_input_tokens INTEGER,\n rotated_at TEXT NOT NULL\n );\n CREATE INDEX idx_rotations_agent_at\n ON rotations(agent_id, rotated_at);\n `,\n },\n {\n v: 4,\n sql: `\n CREATE TABLE route_traces (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n channel TEXT NOT NULL,\n chat_id TEXT NOT NULL,\n msg_id TEXT NOT NULL,\n account_id TEXT,\n kind TEXT NOT NULL, -- 'match' | 'drop'\n deciding_stage TEXT,\n agent_id TEXT,\n reason TEXT,\n at TEXT NOT NULL\n );\n CREATE INDEX idx_route_traces_channel_at\n ON route_traces(channel, at);\n `,\n },\n {\n // Move A — tier-2 token buckets on usage_daily. The per-request tier\n // split is decided at record time by TokenTracker (it's a function of\n // the live cumulative input, not the row totals), so we cannot\n // reconstruct it on read. Adding columns is additive and idempotent;\n // existing rows default to 0.\n v: 5,\n sql: `\n ALTER TABLE usage_daily ADD COLUMN tier2_input_tokens INTEGER DEFAULT 0;\n ALTER TABLE usage_daily ADD COLUMN tier2_output_tokens INTEGER DEFAULT 0;\n ALTER TABLE usage_daily ADD COLUMN tier2_cache_read_tokens INTEGER DEFAULT 0;\n ALTER TABLE usage_daily ADD COLUMN tier2_cache_create_tokens INTEGER DEFAULT 0;\n `,\n },\n {\n // task_traces — per-execution observability surface (improvement plan\n // #2). One row per executeTask invocation, keyed by a ULID task_id so\n // it's URL-safe + monotonically sortable. Workflow context columns let\n // pattern queries group runs by workflow (\"agent-managed workflow as\n // the program\" framing); intent_event_id cross-links into the rescue\n // plan's intent ledger so a dispatch decision joins to the execution\n // it produced. Tokens snapshot the final usage at finish time.\n v: 6,\n sql: `\n CREATE TABLE task_traces (\n task_id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n channel TEXT,\n chat_id TEXT,\n workflow_run_id TEXT,\n workflow_id TEXT,\n workflow_node_id TEXT,\n intent_event_id TEXT,\n intent_decided_by TEXT,\n resume_session_id TEXT,\n final_session_id TEXT,\n model TEXT,\n status TEXT NOT NULL,\n started_at INTEGER NOT NULL,\n finished_at INTEGER,\n duration_ms INTEGER,\n input_tokens INTEGER,\n output_tokens INTEGER,\n cache_read_tokens INTEGER,\n cache_create_tokens INTEGER,\n error TEXT,\n message_preview TEXT\n );\n CREATE INDEX idx_traces_agent_started ON task_traces(agent_id, started_at);\n CREATE INDEX idx_traces_workflow_run ON task_traces(workflow_run_id);\n CREATE INDEX idx_traces_intent_event ON task_traces(intent_event_id);\n CREATE INDEX idx_traces_status_started ON task_traces(status, started_at);\n `,\n },\n {\n // task_trace_steps — append-only step ledger inside one task. Sequence\n // numbers are 0-based and unique per task_id; ON DELETE CASCADE so\n // pruning task_traces removes children atomically. The (input|output)_\n // summary columns are TEXT — callers cap byte-size at the call site,\n // not in the schema, because the right cap depends on the step kind\n // (a tool_use's args vs a tool_result's stdout have very different\n // typical sizes).\n v: 7,\n sql: `\n CREATE TABLE task_trace_steps (\n task_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n name TEXT NOT NULL,\n action TEXT,\n status TEXT,\n input_summary TEXT,\n output_summary TEXT,\n error TEXT,\n ms INTEGER,\n started_at INTEGER NOT NULL,\n PRIMARY KEY (task_id, seq),\n FOREIGN KEY (task_id) REFERENCES task_traces(task_id) ON DELETE CASCADE\n );\n CREATE INDEX idx_trace_steps_name ON task_trace_steps(name, started_at);\n `,\n },\n {\n // task replay support (handoff item #11). Capture the full original\n // user message AND the agent's final response so 'agentx trace replay\n // <taskId> --diff' can show side-by-side without reconstructing from\n // the step ledger. message_preview stays for cheap listing UX (200\n // chars); original_message holds the untruncated input. final_response\n // mirrors the agent's reply text. Both NULLABLE — old rows have NULL\n // and the replay command falls back to the preview gracefully.\n v: 8,\n sql: `\n ALTER TABLE task_traces ADD COLUMN original_message TEXT;\n ALTER TABLE task_traces ADD COLUMN final_response TEXT;\n `,\n },\n {\n // task_queue — persistent chat-initiated work queue (Phase 2).\n // Each row represents a task an agent queued during a /chat turn.\n // status: 'active' = currently running; 'queued' = waiting;\n // 'done' = completed; 'error' = failed.\n // position is 1-based rank among queued rows (null when active/done/error).\n v: 9,\n sql: `\n CREATE TABLE task_queue (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n summary TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'queued',\n position INTEGER,\n created_at INTEGER NOT NULL,\n started_at INTEGER,\n finished_at INTEGER\n );\n CREATE INDEX idx_task_queue_status ON task_queue(status, created_at);\n CREATE INDEX idx_task_queue_convo ON task_queue(conversation_id, created_at);\n `,\n },\n {\n // guardrail_decisions — audit trail for the destructive-action guard\n // (PRD R7). One row per PreToolUse guard verdict, written by the\n // short-lived `agentx guard check` hook process. `verdict` is the\n // policy's decision (allow|deny|escalate|warn); `effective_action` is\n // what was actually enforced after the mode downgrade (in warn mode a\n // deny is recorded as verdict=deny, effective_action=allow). task_id is\n // a logical link to task_traces when the guard can resolve it (often\n // null — the hook doesn't always know the AgentX task id). `approver`\n // is reserved for Phase 2 escalation (human who approved/denied).\n v: 10,\n sql: `\n CREATE TABLE guardrail_decisions (\n id TEXT PRIMARY KEY,\n ts INTEGER NOT NULL,\n agent_id TEXT,\n task_id TEXT,\n tool TEXT NOT NULL,\n command TEXT,\n resolved_target TEXT,\n matched_rule TEXT,\n verdict TEXT NOT NULL,\n effective_action TEXT NOT NULL,\n mode TEXT,\n severity TEXT,\n approver TEXT,\n ms INTEGER\n );\n CREATE INDEX idx_guardrail_ts ON guardrail_decisions(ts);\n CREATE INDEX idx_guardrail_verdict ON guardrail_decisions(verdict, ts);\n CREATE INDEX idx_guardrail_agent ON guardrail_decisions(agent_id, ts);\n `,\n },\n {\n // Which operator surfaces actually get used. task_history records agent\n // dispatches and says nothing about which of the ~46 CLI commands or 16\n // dashboard pages anyone opens — the two biggest surfaces by count, and\n // the ones the surface-reduction work needs evidence for.\n //\n // Deliberately NOT folded into usage_daily: that table is keyed by\n // (agent_id, model, day) and is about token cost. Surface counts share\n // none of that shape, and mixing them would corrupt cost reporting.\n //\n // Names only — never arguments, paths, or payloads. This counts which\n // door was opened, not what was carried through it.\n v: 11,\n sql: `\n CREATE TABLE surface_usage (\n kind TEXT NOT NULL, -- 'cli' | 'page'\n name TEXT NOT NULL, -- 'guard log' | '/live'\n day TEXT NOT NULL, -- 'YYYY-MM-DD' UTC\n count INTEGER NOT NULL DEFAULT 0,\n last_at TEXT,\n PRIMARY KEY (kind, name, day)\n );\n CREATE INDEX idx_surface_day ON surface_usage(day);\n `,\n },\n {\n // Per-turn token accounting that a with/without comparison can trust.\n // task_traces only carried the tier-1 buckets, and tier-2 turns are\n // where 90%+ of prompt tokens land, so most big turns read as zero.\n // `resumed` is explicit rather than inferred from resume_session_id:\n // NULL means \"not recorded\" (older rows), 0 a fresh session.\n v: 12,\n sql: `\n ALTER TABLE task_traces ADD COLUMN tier2_input_tokens INTEGER;\n ALTER TABLE task_traces ADD COLUMN tier2_output_tokens INTEGER;\n ALTER TABLE task_traces ADD COLUMN tier2_cache_read_tokens INTEGER;\n ALTER TABLE task_traces ADD COLUMN tier2_cache_create_tokens INTEGER;\n ALTER TABLE task_traces ADD COLUMN resumed INTEGER;\n `,\n },\n {\n // Request-gate experiment arm: 'treatment' (asked Jev), 'holdout'\n // (randomly skipped it), or NULL when the gate was not active. The\n // with/without-Jev comparison groups turns on this column.\n v: 13,\n sql: `\n ALTER TABLE task_traces ADD COLUMN jev_arm TEXT;\n CREATE INDEX idx_task_traces_jev_arm ON task_traces(jev_arm, started_at);\n `,\n },\n ]\n\n const txn = db.transaction((step: { v: number; sql: string }) => {\n db.exec(step.sql)\n db.prepare(\"INSERT INTO schema_version (v) VALUES (?)\").run(step.v)\n })\n\n for (const step of steps) {\n if (step.v > current) txn(step)\n }\n\n // Columns added by name, not by version number. Several branches can each\n // claim the next migration number; a database that already recorded that\n // number from another branch would silently skip ours. Checking the\n // table itself makes these columns appear on every database.\n ensureColumns(db, \"task_traces\", RESUME_COLUMNS)\n}\n\n/** Schema version check for tests. */\nexport function getSchemaVersion(db: Database.Database): number {\n return (db.prepare(\"SELECT MAX(v) AS v FROM schema_version\").get() as { v: number | null }).v ?? 0\n}\n\n// --- task_queue helpers (Phase 2 chat-initiated work queue) ---\n\nexport interface TaskQueueEntry {\n id: string\n conversationId: string\n agentId: string\n summary: string\n status: \"active\" | \"queued\" | \"done\" | \"error\"\n position: number | null\n createdAt: number\n startedAt: number | null\n finishedAt: number | null\n}\n\nexport interface InsertTaskQueueResult {\n id: string\n status: \"active\" | \"queued\"\n position: number | null\n}\n\n/**\n * Insert a new task into the queue. Respects maxConcurrent — if the number\n * of 'active' rows is below the limit the new task is inserted as 'active';\n * otherwise it is 'queued' with a 1-based position.\n */\nexport function insertTaskQueue(\n db: Database.Database,\n entry: { id: string; conversationId: string; agentId: string; summary: string },\n maxConcurrent: number,\n): InsertTaskQueueResult {\n const now = Date.now()\n const activeCount = (db\n .prepare(\"SELECT COUNT(*) AS n FROM task_queue WHERE status = 'active'\")\n .get() as { n: number }).n\n\n const isActive = activeCount < maxConcurrent\n let position: number | null = null\n if (!isActive) {\n const queuedCount = (db\n .prepare(\"SELECT COUNT(*) AS n FROM task_queue WHERE status = 'queued'\")\n .get() as { n: number }).n\n position = queuedCount + 1\n }\n\n db.prepare(`\n INSERT INTO task_queue (id, conversation_id, agent_id, summary, status, position, created_at, started_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `).run(\n entry.id,\n entry.conversationId,\n entry.agentId,\n entry.summary,\n isActive ? \"active\" : \"queued\",\n position,\n now,\n isActive ? now : null,\n )\n\n return { id: entry.id, status: isActive ? \"active\" : \"queued\", position }\n}\n\n/**\n * Mark a task done/error and promote the next queued task (if any) to active.\n */\nexport function completeTaskQueue(\n db: Database.Database,\n id: string,\n status: \"done\" | \"error\" = \"done\",\n): void {\n const now = Date.now()\n db.prepare(`\n UPDATE task_queue SET status = ?, finished_at = ?, position = NULL WHERE id = ?\n `).run(status, now, id)\n\n // Promote oldest queued task to active\n const next = db.prepare(`\n SELECT id FROM task_queue WHERE status = 'queued' ORDER BY created_at ASC LIMIT 1\n `).get() as { id: string } | undefined\n if (next) {\n db.prepare(`\n UPDATE task_queue SET status = 'active', position = NULL, started_at = ? WHERE id = ?\n `).run(now, next.id)\n // Resequence remaining queued positions\n const remaining = db.prepare(`\n SELECT id FROM task_queue WHERE status = 'queued' ORDER BY created_at ASC\n `).all() as Array<{ id: string }>\n const upd = db.prepare(\"UPDATE task_queue SET position = ? WHERE id = ?\")\n for (let i = 0; i < remaining.length; i++) {\n upd.run(i + 1, remaining[i].id)\n }\n }\n}\n\n/** Look up a single task_queue row. */\nexport function getTaskQueue(\n db: Database.Database,\n id: string,\n): TaskQueueEntry | null {\n const row = db.prepare(\"SELECT * FROM task_queue WHERE id = ?\").get(id) as Record<string, unknown> | undefined\n if (!row) return null\n return {\n id: row.id as string,\n conversationId: row.conversation_id as string,\n agentId: row.agent_id as string,\n summary: row.summary as string,\n status: row.status as TaskQueueEntry[\"status\"],\n position: row.position as number | null,\n createdAt: row.created_at as number,\n startedAt: row.started_at as number | null,\n finishedAt: row.finished_at as number | null,\n }\n}\n\n/** List task_queue rows for a conversation, newest first. */\nexport function listTaskQueueByConversation(\n db: Database.Database,\n conversationId: string,\n limit = 20,\n): TaskQueueEntry[] {\n const rows = db.prepare(`\n SELECT * FROM task_queue WHERE conversation_id = ?\n ORDER BY created_at DESC LIMIT ?\n `).all(conversationId, limit) as Array<Record<string, unknown>>\n return rows.map((row) => ({\n id: row.id as string,\n conversationId: row.conversation_id as string,\n agentId: row.agent_id as string,\n summary: row.summary as string,\n status: row.status as TaskQueueEntry[\"status\"],\n position: row.position as number | null,\n createdAt: row.created_at as number,\n startedAt: row.started_at as number | null,\n finishedAt: row.finished_at as number | null,\n }))\n}\n\nexport interface PruneResult {\n taskHistory: number\n rotations: number\n routeTraces: number\n taskTraces: number\n}\n\n/**\n * Drop rows older than `retentionDays` from the operational tables that grow\n * unbounded over time. `usage_daily` is bounded by (agent, model, day) and\n * doesn't need pruning; the caller decides whether to keep aggregates beyond\n * the retention window.\n *\n * Mirrors the file-based `pruneTaskHistory` sweep that runs at daemon\n * startup. Cheap — three indexed deletes plus an incremental_vacuum.\n */\nexport function pruneSqliteTables(\n db: Database.Database,\n retentionDays: number,\n): PruneResult {\n const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000\n const cutoffIso = new Date(cutoffMs).toISOString()\n\n const taskHistory = (db\n .prepare(`DELETE FROM task_history WHERE started_at < ?`)\n .run(cutoffIso).changes ?? 0) as number\n const rotations = (db\n .prepare(`DELETE FROM rotations WHERE rotated_at < ?`)\n .run(cutoffIso).changes ?? 0) as number\n const routeTraces = (db\n .prepare(`DELETE FROM route_traces WHERE at < ?`)\n .run(cutoffIso).changes ?? 0) as number\n // task_traces uses ms-epoch INTEGERs (not ISO strings) on started_at.\n // task_trace_steps cascades automatically via ON DELETE CASCADE.\n const taskTraces = (db\n .prepare(`DELETE FROM task_traces WHERE started_at < ?`)\n .run(cutoffMs).changes ?? 0) as number\n\n // Reclaim the freelist pages so the file shrinks. WAL writes still\n // checkpoint independently.\n if (taskHistory + rotations + routeTraces + taskTraces > 0) {\n try { db.pragma(\"incremental_vacuum\") } catch { /* best-effort */ }\n }\n\n return { taskHistory, rotations, routeTraces, taskTraces }\n}\n\n/** Resume after restart (#103): how to re-enter a run (JSON), its attempt\n * number, the cut-off run it continues, and the boot decision made for it. */\nconst RESUME_COLUMNS: Array<[string, string]> = [\n [\"resume_origin\", \"TEXT\"],\n [\"resume_attempt\", \"INTEGER NOT NULL DEFAULT 0\"],\n [\"resumed_from\", \"TEXT\"],\n [\"resume_decision\", \"TEXT\"],\n [\"resume_reason\", \"TEXT\"],\n]\n\n/** Add any of `columns` the table lacks. Idempotent. */\nexport function ensureColumns(db: Database.Database, table: string, columns: Array<[string, string]>): void {\n const have = new Set((db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>).map((c) => c.name))\n for (const [name, type] of columns) {\n if (!have.has(name)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`)\n }\n}\n"],"mappings":"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","names":["createRequire","mkdirSync","dirname","resolve","require","_db","_Database","betterSqlite3Fix","error","msg","abi","hint","loadDatabaseCtor","quiet","e","openDb","opts","path","Database","db","runMigrations","runMigrations","db","current","steps","txn","step","ensureColumns","RESUME_COLUMNS","insertTaskQueue","db","entry","maxConcurrent","now","isActive","position","completeTaskQueue","id","status","next","remaining","upd","getTaskQueue","row","listTaskQueueByConversation","conversationId","limit","pruneSqliteTables","retentionDays","cutoffMs","cutoffIso","taskHistory","rotations","routeTraces","taskTraces","RESUME_COLUMNS","ensureColumns","table","columns","have","c","name","type"]}
1
+ {"version":3,"sources":["../src/storage/sqlite.ts"],"sourcesContent":["import type Database from \"better-sqlite3\"\nimport { createRequire } from \"module\"\nimport { existsSync, mkdirSync } from \"fs\"\nimport { dirname, resolve } from \"path\"\n\n// --- SQLite storage layer ---\n//\n// Move 2 of the architectural review (docs/architecture/hexabot-vs-agentx.md).\n// SQLite for the operational tables — task_history, usage, dedup, sessions —\n// where queryable, indexed access has user-visible value (dashboards,\n// retention, analytics). Workflow definitions, wiki, agent-memory, and\n// references stay as files: they're git-diffable, human-readable, and don't\n// need queries.\n//\n// Approach: subscribe to the event bus (src/events/bus.ts) and write to\n// SQLite from the subscribers. Existing JSON writes are not touched —\n// SQLite runs alongside as a parallel pipeline. Once we have a query API\n// the dashboard depends on, we can deprecate the JSON path. Until then,\n// SQLite is observability-grade only.\n//\n// Synchronous API: better-sqlite3 is sync, no callbacks. The daemon's\n// event loop is the single writer; multiple readers (dashboard) use WAL\n// mode for concurrent read while we write.\n\ntype DatabaseConstructor = typeof Database\n\nconst require = createRequire(import.meta.url)\nlet _db: Database.Database | undefined\nlet _Database: DatabaseConstructor | undefined\n\nfunction betterSqlite3Fix(error: any): string {\n const msg = String(error?.message ?? error)\n const abi = process.versions.modules\n const node = process.version\n const hint = `Run \"pnpm rebuild better-sqlite3\" with the same Node used by the daemon (${node}, modules ${abi}).`\n if (/NODE_MODULE_VERSION|was compiled against|Module version mismatch|invalid ELF|mach-o/i.test(msg)) {\n return `${msg} ${hint}`\n }\n return `${msg} ${hint}`\n}\n\nfunction loadDatabaseCtor(quiet = false): DatabaseConstructor | null {\n if (_Database) return _Database\n try {\n _Database = require(\"better-sqlite3\") as DatabaseConstructor\n return _Database\n } catch (e: any) {\n if (!quiet) {\n console.error(`[storage/sqlite] better-sqlite3 native binding failed to load: ${betterSqlite3Fix(e)}`)\n }\n return null\n }\n}\n\nexport interface OpenOptions {\n /** Resolved relative to cwd. Default: .agentx/db.sqlite */\n path?: string\n /** When true, SQLite writes are skipped (used by tests + opt-out). */\n disabled?: boolean\n /** Suppress the open-failure log. For best-effort callers whose whole\n * contract is \"do nothing if unavailable\" — telemetry, mainly. Without\n * this, a native-binding ABI mismatch prints a paragraph of node-gyp\n * advice on EVERY CLI invocation, which is worse than the thing it\n * is warning about. Load-bearing callers must never pass it. */\n quiet?: boolean\n}\n\n/**\n * Open the per-process SQLite database. Idempotent — repeated calls\n * return the same handle. The file is created on first open; the schema\n * is migrated forward to the latest version.\n *\n * Returns null when disabled — every subscriber should check for null\n * before issuing a write so the daemon stays runnable without SQLite if\n * someone's environment can't compile the native binding.\n */\nexport function openDb(opts: OpenOptions = {}): Database.Database | null {\n if (opts.disabled) return null\n if (_db) return _db\n const path = resolve(process.cwd(), opts.path ?? \".agentx/db.sqlite\")\n const Database = loadDatabaseCtor(opts.quiet)\n if (!Database) return null\n try {\n mkdirSync(dirname(path), { recursive: true })\n const db = new Database(path)\n // WAL keeps readers (dashboard) unblocked while the daemon writes.\n // Synchronous=NORMAL is the SQLite-recommended balance for WAL: the\n // db is durable on power-loss, but a crash within the last few ms can\n // lose the most recent commit. For our use (observability), that's fine.\n db.pragma(\"journal_mode = WAL\")\n db.pragma(\"synchronous = NORMAL\")\n db.pragma(\"foreign_keys = ON\")\n runMigrations(db)\n _db = db\n return _db\n } catch (e: any) {\n // Native binding missing, file unwritable, etc. Surface by default — the\n // alternative was a silent no-op, which makes the \"SQLite not opened\"\n // message in the daemon log impossible to debug. `quiet` callers have\n // explicitly opted out because doing nothing is their correct behavior.\n if (!opts.quiet) {\n console.error(`[storage/sqlite] openDb failed at ${path}: ${betterSqlite3Fix(e)}`)\n }\n return null\n }\n}\n\n/** Close the global handle. Test-only — production never closes. */\nexport function closeDb(): void {\n if (_db) {\n _db.close()\n _db = undefined\n }\n}\n\n/**\n * Per-version schema steps. New tables / columns go in a new migration —\n * never mutate an old one. Run on every open(); each step is idempotent\n * because it CREATE TABLE IF NOT EXISTS. SQLite has no concurrent\n * migration issue here (single writer process).\n */\nfunction runMigrations(db: Database.Database): void {\n db.exec(`\n CREATE TABLE IF NOT EXISTS schema_version (\n v INTEGER PRIMARY KEY\n );\n `)\n const current = (db\n .prepare(\"SELECT MAX(v) AS v FROM schema_version\")\n .get() as { v: number | null }).v ?? 0\n\n const steps: Array<{ v: number; sql: string }> = [\n {\n v: 1,\n sql: `\n CREATE TABLE task_history (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n channel TEXT,\n chat_id TEXT,\n status TEXT NOT NULL, -- 'ok' | 'error' | 'timeout'\n message_preview TEXT,\n error TEXT,\n duration_ms INTEGER,\n input_tokens INTEGER,\n output_tokens INTEGER,\n cache_read_tokens INTEGER,\n cache_create_tokens INTEGER,\n started_at TEXT NOT NULL,\n finished_at TEXT NOT NULL\n );\n CREATE INDEX idx_task_history_agent_started\n ON task_history(agent_id, started_at);\n CREATE INDEX idx_task_history_status\n ON task_history(status, started_at);\n `,\n },\n {\n v: 2,\n sql: `\n CREATE TABLE usage_daily (\n agent_id TEXT NOT NULL,\n model TEXT NOT NULL,\n day TEXT NOT NULL, -- 'YYYY-MM-DD' UTC\n input_tokens INTEGER DEFAULT 0,\n output_tokens INTEGER DEFAULT 0,\n cache_read_tokens INTEGER DEFAULT 0,\n cache_create_tokens INTEGER DEFAULT 0,\n tasks INTEGER DEFAULT 0,\n PRIMARY KEY (agent_id, model, day)\n );\n CREATE INDEX idx_usage_day ON usage_daily(day);\n `,\n },\n {\n v: 3,\n sql: `\n CREATE TABLE rotations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n agent_id TEXT NOT NULL,\n channel TEXT NOT NULL,\n chat_id TEXT NOT NULL,\n reason TEXT NOT NULL, -- 'stale' | 'tier-2' | 'max-turns'\n last_turn_input_tokens INTEGER,\n rotated_at TEXT NOT NULL\n );\n CREATE INDEX idx_rotations_agent_at\n ON rotations(agent_id, rotated_at);\n `,\n },\n {\n v: 4,\n sql: `\n CREATE TABLE route_traces (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n channel TEXT NOT NULL,\n chat_id TEXT NOT NULL,\n msg_id TEXT NOT NULL,\n account_id TEXT,\n kind TEXT NOT NULL, -- 'match' | 'drop'\n deciding_stage TEXT,\n agent_id TEXT,\n reason TEXT,\n at TEXT NOT NULL\n );\n CREATE INDEX idx_route_traces_channel_at\n ON route_traces(channel, at);\n `,\n },\n {\n // Move A — tier-2 token buckets on usage_daily. The per-request tier\n // split is decided at record time by TokenTracker (it's a function of\n // the live cumulative input, not the row totals), so we cannot\n // reconstruct it on read. Adding columns is additive and idempotent;\n // existing rows default to 0.\n v: 5,\n sql: `\n ALTER TABLE usage_daily ADD COLUMN tier2_input_tokens INTEGER DEFAULT 0;\n ALTER TABLE usage_daily ADD COLUMN tier2_output_tokens INTEGER DEFAULT 0;\n ALTER TABLE usage_daily ADD COLUMN tier2_cache_read_tokens INTEGER DEFAULT 0;\n ALTER TABLE usage_daily ADD COLUMN tier2_cache_create_tokens INTEGER DEFAULT 0;\n `,\n },\n {\n // task_traces — per-execution observability surface (improvement plan\n // #2). One row per executeTask invocation, keyed by a ULID task_id so\n // it's URL-safe + monotonically sortable. Workflow context columns let\n // pattern queries group runs by workflow (\"agent-managed workflow as\n // the program\" framing); intent_event_id cross-links into the rescue\n // plan's intent ledger so a dispatch decision joins to the execution\n // it produced. Tokens snapshot the final usage at finish time.\n v: 6,\n sql: `\n CREATE TABLE task_traces (\n task_id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n channel TEXT,\n chat_id TEXT,\n workflow_run_id TEXT,\n workflow_id TEXT,\n workflow_node_id TEXT,\n intent_event_id TEXT,\n intent_decided_by TEXT,\n resume_session_id TEXT,\n final_session_id TEXT,\n model TEXT,\n status TEXT NOT NULL,\n started_at INTEGER NOT NULL,\n finished_at INTEGER,\n duration_ms INTEGER,\n input_tokens INTEGER,\n output_tokens INTEGER,\n cache_read_tokens INTEGER,\n cache_create_tokens INTEGER,\n error TEXT,\n message_preview TEXT\n );\n CREATE INDEX idx_traces_agent_started ON task_traces(agent_id, started_at);\n CREATE INDEX idx_traces_workflow_run ON task_traces(workflow_run_id);\n CREATE INDEX idx_traces_intent_event ON task_traces(intent_event_id);\n CREATE INDEX idx_traces_status_started ON task_traces(status, started_at);\n `,\n },\n {\n // task_trace_steps — append-only step ledger inside one task. Sequence\n // numbers are 0-based and unique per task_id; ON DELETE CASCADE so\n // pruning task_traces removes children atomically. The (input|output)_\n // summary columns are TEXT — callers cap byte-size at the call site,\n // not in the schema, because the right cap depends on the step kind\n // (a tool_use's args vs a tool_result's stdout have very different\n // typical sizes).\n v: 7,\n sql: `\n CREATE TABLE task_trace_steps (\n task_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n name TEXT NOT NULL,\n action TEXT,\n status TEXT,\n input_summary TEXT,\n output_summary TEXT,\n error TEXT,\n ms INTEGER,\n started_at INTEGER NOT NULL,\n PRIMARY KEY (task_id, seq),\n FOREIGN KEY (task_id) REFERENCES task_traces(task_id) ON DELETE CASCADE\n );\n CREATE INDEX idx_trace_steps_name ON task_trace_steps(name, started_at);\n `,\n },\n {\n // task replay support (handoff item #11). Capture the full original\n // user message AND the agent's final response so 'agentx trace replay\n // <taskId> --diff' can show side-by-side without reconstructing from\n // the step ledger. message_preview stays for cheap listing UX (200\n // chars); original_message holds the untruncated input. final_response\n // mirrors the agent's reply text. Both NULLABLE — old rows have NULL\n // and the replay command falls back to the preview gracefully.\n v: 8,\n sql: `\n ALTER TABLE task_traces ADD COLUMN original_message TEXT;\n ALTER TABLE task_traces ADD COLUMN final_response TEXT;\n `,\n },\n {\n // task_queue — persistent chat-initiated work queue (Phase 2).\n // Each row represents a task an agent queued during a /chat turn.\n // status: 'active' = currently running; 'queued' = waiting;\n // 'done' = completed; 'error' = failed.\n // position is 1-based rank among queued rows (null when active/done/error).\n v: 9,\n sql: `\n CREATE TABLE task_queue (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n summary TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'queued',\n position INTEGER,\n created_at INTEGER NOT NULL,\n started_at INTEGER,\n finished_at INTEGER\n );\n CREATE INDEX idx_task_queue_status ON task_queue(status, created_at);\n CREATE INDEX idx_task_queue_convo ON task_queue(conversation_id, created_at);\n `,\n },\n {\n // guardrail_decisions — audit trail for the destructive-action guard\n // (PRD R7). One row per PreToolUse guard verdict, written by the\n // short-lived `agentx guard check` hook process. `verdict` is the\n // policy's decision (allow|deny|escalate|warn); `effective_action` is\n // what was actually enforced after the mode downgrade (in warn mode a\n // deny is recorded as verdict=deny, effective_action=allow). task_id is\n // a logical link to task_traces when the guard can resolve it (often\n // null — the hook doesn't always know the AgentX task id). `approver`\n // is reserved for Phase 2 escalation (human who approved/denied).\n v: 10,\n sql: `\n CREATE TABLE guardrail_decisions (\n id TEXT PRIMARY KEY,\n ts INTEGER NOT NULL,\n agent_id TEXT,\n task_id TEXT,\n tool TEXT NOT NULL,\n command TEXT,\n resolved_target TEXT,\n matched_rule TEXT,\n verdict TEXT NOT NULL,\n effective_action TEXT NOT NULL,\n mode TEXT,\n severity TEXT,\n approver TEXT,\n ms INTEGER\n );\n CREATE INDEX idx_guardrail_ts ON guardrail_decisions(ts);\n CREATE INDEX idx_guardrail_verdict ON guardrail_decisions(verdict, ts);\n CREATE INDEX idx_guardrail_agent ON guardrail_decisions(agent_id, ts);\n `,\n },\n {\n // Which operator surfaces actually get used. task_history records agent\n // dispatches and says nothing about which of the ~46 CLI commands or 16\n // dashboard pages anyone opens — the two biggest surfaces by count, and\n // the ones the surface-reduction work needs evidence for.\n //\n // Deliberately NOT folded into usage_daily: that table is keyed by\n // (agent_id, model, day) and is about token cost. Surface counts share\n // none of that shape, and mixing them would corrupt cost reporting.\n //\n // Names only — never arguments, paths, or payloads. This counts which\n // door was opened, not what was carried through it.\n v: 11,\n sql: `\n CREATE TABLE surface_usage (\n kind TEXT NOT NULL, -- 'cli' | 'page'\n name TEXT NOT NULL, -- 'guard log' | '/live'\n day TEXT NOT NULL, -- 'YYYY-MM-DD' UTC\n count INTEGER NOT NULL DEFAULT 0,\n last_at TEXT,\n PRIMARY KEY (kind, name, day)\n );\n CREATE INDEX idx_surface_day ON surface_usage(day);\n `,\n },\n {\n // Per-turn token accounting that a with/without comparison can trust.\n // task_traces only carried the tier-1 buckets, and tier-2 turns are\n // where 90%+ of prompt tokens land, so most big turns read as zero.\n // `resumed` is explicit rather than inferred from resume_session_id:\n // NULL means \"not recorded\" (older rows), 0 a fresh session.\n v: 12,\n sql: `\n ALTER TABLE task_traces ADD COLUMN tier2_input_tokens INTEGER;\n ALTER TABLE task_traces ADD COLUMN tier2_output_tokens INTEGER;\n ALTER TABLE task_traces ADD COLUMN tier2_cache_read_tokens INTEGER;\n ALTER TABLE task_traces ADD COLUMN tier2_cache_create_tokens INTEGER;\n ALTER TABLE task_traces ADD COLUMN resumed INTEGER;\n `,\n },\n {\n // Request-gate experiment arm: 'treatment' (asked Jev), 'holdout'\n // (randomly skipped it), or NULL when the gate was not active. The\n // with/without-Jev comparison groups turns on this column.\n v: 13,\n sql: `\n ALTER TABLE task_traces ADD COLUMN jev_arm TEXT;\n CREATE INDEX idx_task_traces_jev_arm ON task_traces(jev_arm, started_at);\n `,\n },\n ]\n\n const txn = db.transaction((step: { v: number; sql: string }) => {\n db.exec(step.sql)\n db.prepare(\"INSERT INTO schema_version (v) VALUES (?)\").run(step.v)\n })\n\n for (const step of steps) {\n if (step.v > current) txn(step)\n }\n\n // Columns added by name, not by version number. Several branches can each\n // claim the next migration number; a database that already recorded that\n // number from another branch would silently skip ours. Checking the\n // table itself makes these columns appear on every database.\n ensureColumns(db, \"task_traces\", RESUME_COLUMNS)\n ensureColumns(db, \"task_traces\", LESSON_IMPACT_COLUMNS)\n}\n\n/** Schema version check for tests. */\nexport function getSchemaVersion(db: Database.Database): number {\n return (db.prepare(\"SELECT MAX(v) AS v FROM schema_version\").get() as { v: number | null }).v ?? 0\n}\n\n// --- task_queue helpers (Phase 2 chat-initiated work queue) ---\n\nexport interface TaskQueueEntry {\n id: string\n conversationId: string\n agentId: string\n summary: string\n status: \"active\" | \"queued\" | \"done\" | \"error\"\n position: number | null\n createdAt: number\n startedAt: number | null\n finishedAt: number | null\n}\n\nexport interface InsertTaskQueueResult {\n id: string\n status: \"active\" | \"queued\"\n position: number | null\n}\n\n/**\n * Insert a new task into the queue. Respects maxConcurrent — if the number\n * of 'active' rows is below the limit the new task is inserted as 'active';\n * otherwise it is 'queued' with a 1-based position.\n */\nexport function insertTaskQueue(\n db: Database.Database,\n entry: { id: string; conversationId: string; agentId: string; summary: string },\n maxConcurrent: number,\n): InsertTaskQueueResult {\n const now = Date.now()\n const activeCount = (db\n .prepare(\"SELECT COUNT(*) AS n FROM task_queue WHERE status = 'active'\")\n .get() as { n: number }).n\n\n const isActive = activeCount < maxConcurrent\n let position: number | null = null\n if (!isActive) {\n const queuedCount = (db\n .prepare(\"SELECT COUNT(*) AS n FROM task_queue WHERE status = 'queued'\")\n .get() as { n: number }).n\n position = queuedCount + 1\n }\n\n db.prepare(`\n INSERT INTO task_queue (id, conversation_id, agent_id, summary, status, position, created_at, started_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `).run(\n entry.id,\n entry.conversationId,\n entry.agentId,\n entry.summary,\n isActive ? \"active\" : \"queued\",\n position,\n now,\n isActive ? now : null,\n )\n\n return { id: entry.id, status: isActive ? \"active\" : \"queued\", position }\n}\n\n/**\n * Mark a task done/error and promote the next queued task (if any) to active.\n */\nexport function completeTaskQueue(\n db: Database.Database,\n id: string,\n status: \"done\" | \"error\" = \"done\",\n): void {\n const now = Date.now()\n db.prepare(`\n UPDATE task_queue SET status = ?, finished_at = ?, position = NULL WHERE id = ?\n `).run(status, now, id)\n\n // Promote oldest queued task to active\n const next = db.prepare(`\n SELECT id FROM task_queue WHERE status = 'queued' ORDER BY created_at ASC LIMIT 1\n `).get() as { id: string } | undefined\n if (next) {\n db.prepare(`\n UPDATE task_queue SET status = 'active', position = NULL, started_at = ? WHERE id = ?\n `).run(now, next.id)\n // Resequence remaining queued positions\n const remaining = db.prepare(`\n SELECT id FROM task_queue WHERE status = 'queued' ORDER BY created_at ASC\n `).all() as Array<{ id: string }>\n const upd = db.prepare(\"UPDATE task_queue SET position = ? WHERE id = ?\")\n for (let i = 0; i < remaining.length; i++) {\n upd.run(i + 1, remaining[i].id)\n }\n }\n}\n\n/** Look up a single task_queue row. */\nexport function getTaskQueue(\n db: Database.Database,\n id: string,\n): TaskQueueEntry | null {\n const row = db.prepare(\"SELECT * FROM task_queue WHERE id = ?\").get(id) as Record<string, unknown> | undefined\n if (!row) return null\n return {\n id: row.id as string,\n conversationId: row.conversation_id as string,\n agentId: row.agent_id as string,\n summary: row.summary as string,\n status: row.status as TaskQueueEntry[\"status\"],\n position: row.position as number | null,\n createdAt: row.created_at as number,\n startedAt: row.started_at as number | null,\n finishedAt: row.finished_at as number | null,\n }\n}\n\n/** List task_queue rows for a conversation, newest first. */\nexport function listTaskQueueByConversation(\n db: Database.Database,\n conversationId: string,\n limit = 20,\n): TaskQueueEntry[] {\n const rows = db.prepare(`\n SELECT * FROM task_queue WHERE conversation_id = ?\n ORDER BY created_at DESC LIMIT ?\n `).all(conversationId, limit) as Array<Record<string, unknown>>\n return rows.map((row) => ({\n id: row.id as string,\n conversationId: row.conversation_id as string,\n agentId: row.agent_id as string,\n summary: row.summary as string,\n status: row.status as TaskQueueEntry[\"status\"],\n position: row.position as number | null,\n createdAt: row.created_at as number,\n startedAt: row.started_at as number | null,\n finishedAt: row.finished_at as number | null,\n }))\n}\n\nexport interface PruneResult {\n taskHistory: number\n rotations: number\n routeTraces: number\n taskTraces: number\n}\n\n/**\n * Drop rows older than `retentionDays` from the operational tables that grow\n * unbounded over time. `usage_daily` is bounded by (agent, model, day) and\n * doesn't need pruning; the caller decides whether to keep aggregates beyond\n * the retention window.\n *\n * Mirrors the file-based `pruneTaskHistory` sweep that runs at daemon\n * startup. Cheap — three indexed deletes plus an incremental_vacuum.\n */\nexport function pruneSqliteTables(\n db: Database.Database,\n retentionDays: number,\n): PruneResult {\n const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000\n const cutoffIso = new Date(cutoffMs).toISOString()\n\n const taskHistory = (db\n .prepare(`DELETE FROM task_history WHERE started_at < ?`)\n .run(cutoffIso).changes ?? 0) as number\n const rotations = (db\n .prepare(`DELETE FROM rotations WHERE rotated_at < ?`)\n .run(cutoffIso).changes ?? 0) as number\n const routeTraces = (db\n .prepare(`DELETE FROM route_traces WHERE at < ?`)\n .run(cutoffIso).changes ?? 0) as number\n // task_traces uses ms-epoch INTEGERs (not ISO strings) on started_at.\n // task_trace_steps cascades automatically via ON DELETE CASCADE.\n const taskTraces = (db\n .prepare(`DELETE FROM task_traces WHERE started_at < ?`)\n .run(cutoffMs).changes ?? 0) as number\n\n // Reclaim the freelist pages so the file shrinks. WAL writes still\n // checkpoint independently.\n if (taskHistory + rotations + routeTraces + taskTraces > 0) {\n try { db.pragma(\"incremental_vacuum\") } catch { /* best-effort */ }\n }\n\n return { taskHistory, rotations, routeTraces, taskTraces }\n}\n\n/** Resume after restart (#103): how to re-enter a run (JSON), its attempt\n * number, the cut-off run it continues, and the boot decision made for it. */\nconst RESUME_COLUMNS: Array<[string, string]> = [\n [\"resume_origin\", \"TEXT\"],\n [\"resume_attempt\", \"INTEGER NOT NULL DEFAULT 0\"],\n [\"resumed_from\", \"TEXT\"],\n [\"resume_decision\", \"TEXT\"],\n [\"resume_reason\", \"TEXT\"],\n]\n\n/** Lesson impact (#98): the memory facts and procedures injected into a turn,\n * as JSON {memory: ids, procedures: ids, wiki: bool}, plus the runtime's turn\n * count. `agentx trace lessons` compares a repeated task before and after\n * each lesson on these columns. */\nconst LESSON_IMPACT_COLUMNS: Array<[string, string]> = [\n [\"num_turns\", \"INTEGER\"],\n [\"injected_context\", \"TEXT\"],\n]\n\n/** Add any of `columns` the table lacks. Idempotent. */\nexport function ensureColumns(db: Database.Database, table: string, columns: Array<[string, string]>): void {\n const have = new Set((db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>).map((c) => c.name))\n for (const [name, type] of columns) {\n if (!have.has(name)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`)\n }\n}\n"],"mappings":"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","names":["createRequire","mkdirSync","dirname","resolve","require","_db","_Database","betterSqlite3Fix","error","msg","abi","hint","loadDatabaseCtor","quiet","e","openDb","opts","path","Database","db","runMigrations","runMigrations","db","current","steps","txn","step","ensureColumns","RESUME_COLUMNS","LESSON_IMPACT_COLUMNS","insertTaskQueue","db","entry","maxConcurrent","now","isActive","position","completeTaskQueue","id","status","next","remaining","upd","getTaskQueue","row","listTaskQueueByConversation","conversationId","limit","pruneSqliteTables","retentionDays","cutoffMs","cutoffIso","taskHistory","rotations","routeTraces","taskTraces","RESUME_COLUMNS","LESSON_IMPACT_COLUMNS","ensureColumns","table","columns","have","c","name","type"]}
@@ -1,5 +1,5 @@
1
- import{k as m,l as $,p as x}from"./chunk-GAGVFSZM.js";x();import{readFileSync as S,writeFileSync as P,existsSync as b}from"fs";import{resolve as f}from"path";function w(r){if(r)return f(r);let e=[f(process.cwd(),"agentx.json"),f(process.cwd(),".agentx/config.json")];for(let n of e)if(b(n))return n;return e[0]}function v(r){return JSON.parse(JSON.stringify(r))}async function O(r){try{let e=await fetch(`${r.replace(/\/$/,"")}/reload`,{method:"POST",signal:AbortSignal.timeout(3e3)});return e.ok?{ok:!0}:{ok:!1,reason:`HTTP ${e.status}`}}catch(e){return{ok:!1,reason:e?.cause?.code||e?.message||"unreachable"}}}async function J(r,e={}){let{validate:n=!0,dryRun:t=!1,reload:s=!0,configPath:u,daemonUrl:c}=e,o=w(u);if(!b(o))return{success:!1,path:o,error:`Config not found at ${o}. Run \`agentx init\` first.`};let d;try{d=S(o,"utf-8")}catch(a){return{success:!1,path:o,error:`Cannot read ${o}: ${a.message}`}}let g;try{g=JSON.parse(d)}catch(a){return{success:!1,path:o,error:`Invalid JSON in ${o}: ${a.message}`}}let i=v(g);try{await r(i)}catch(a){return{success:!1,path:o,error:`Mutator threw: ${a.message}`}}if(n){let a=$(i),l=m.safeParse(a);if(!l.success){let k=l.error.issues.map(y=>` ${y.path.join(".")||"<root>"}: ${y.message}`).join(`
1
+ import{k as m,l as $,p as x}from"./chunk-P2ZYT5D6.js";x();import{readFileSync as S,writeFileSync as P,existsSync as b}from"fs";import{resolve as f}from"path";function w(r){if(r)return f(r);let e=[f(process.cwd(),"agentx.json"),f(process.cwd(),".agentx/config.json")];for(let n of e)if(b(n))return n;return e[0]}function v(r){return JSON.parse(JSON.stringify(r))}async function O(r){try{let e=await fetch(`${r.replace(/\/$/,"")}/reload`,{method:"POST",signal:AbortSignal.timeout(3e3)});return e.ok?{ok:!0}:{ok:!1,reason:`HTTP ${e.status}`}}catch(e){return{ok:!1,reason:e?.cause?.code||e?.message||"unreachable"}}}async function J(r,e={}){let{validate:n=!0,dryRun:t=!1,reload:s=!0,configPath:u,daemonUrl:c}=e,o=w(u);if(!b(o))return{success:!1,path:o,error:`Config not found at ${o}. Run \`agentx init\` first.`};let d;try{d=S(o,"utf-8")}catch(a){return{success:!1,path:o,error:`Cannot read ${o}: ${a.message}`}}let g;try{g=JSON.parse(d)}catch(a){return{success:!1,path:o,error:`Invalid JSON in ${o}: ${a.message}`}}let i=v(g);try{await r(i)}catch(a){return{success:!1,path:o,error:`Mutator threw: ${a.message}`}}if(n){let a=$(i),l=m.safeParse(a);if(!l.success){let k=l.error.issues.map(y=>` ${y.path.join(".")||"<root>"}: ${y.message}`).join(`
2
2
  `);return{success:!1,path:o,error:`Validation failed:
3
3
  ${k}`,after:i}}}if(t)return{success:!0,path:o,after:i,dryRun:!0};try{P(o,JSON.stringify(i,null,2)+`
4
4
  `)}catch(a){return{success:!1,path:o,error:`Write failed: ${a.message}`,after:i}}let p=!1,h;if(s){let a=c||R(i),l=await O(a);l.ok?p=!0:h=l.reason}return{success:!0,path:o,after:i,reloaded:p,reloadSkipped:h}}function R(r){let e=r?.node?.bind;if(!e)return"http://127.0.0.1:18800";let[n,t]=e.split(":");return`http://${n==="0.0.0.0"||n==="::"?"127.0.0.1":n}:${t}`}function N(r,e,n){let t=e.split("."),s=r;for(let u=0;u<t.length-1;u++){let c=t[u];(s[c]===void 0||s[c]===null)&&(s[c]={}),s=s[c]}s[t[t.length-1]]=n}function j(r,e){let n=e.split("."),t=r;for(let s of n){if(t==null)return;t=t[s]}return t}function A(r,e){let n=e.split("."),t=r;for(let s=0;s<n.length-1;s++){if(t==null)return;t=t[n[s]]}t!=null&&delete t[n[n.length-1]]}export{w as a,J as b,N as c,j as d,A as e};
5
- //# sourceMappingURL=chunk-HEQPHUTR.js.map
5
+ //# sourceMappingURL=chunk-NLZD2QKF.js.map
@@ -0,0 +1,243 @@
1
+ import{q as _,r as W}from"./chunk-3SCPRYJB.js";import{b as Y,c as I,d as O}from"./chunk-AQASR6KR.js";import{b as F}from"./chunk-DP7RTNDM.js";import{e as E}from"./chunk-JVNFRS26.js";import{b as L}from"./chunk-64P4QHSZ.js";import{createHash as ee}from"crypto";import{existsSync as H,readdirSync as te,readFileSync as re,writeFileSync as ne}from"fs";import{resolve as P}from"path";function be(n,r,s,t,o,i,c=""){return Z(r,s,t,o,i,c)}function Z(n,r,s,t,o,i=""){let c=r?`
2
+ ## Worldview
3
+
4
+ ${r}
5
+ `:"",p=new Map;for(let g of s){let l=g.type||"untyped",d=p.get(l)||[];d.push(g),p.set(l,d)}let u=s.length===0?"":`
6
+ ## Existing Articles (the current catalog)
7
+
8
+ `+Array.from(p.entries()).sort().map(([g,l])=>`**${g}**
9
+ `+l.sort((d,e)=>d.title.localeCompare(e.title)).map(d=>`- [[${d.title}]] \u2014 ${d.path}`).join(`
10
+ `)).join(`
11
+
12
+ `)+`
13
+ `;return`You are compiling a personal wiki for the "${n}" agent in the Karpathy / Farzapedia pattern.
14
+
15
+ ## The Pattern (read this first \u2014 it shapes every decision below)
16
+
17
+ The wiki is a compounding knowledge base of interlinked markdown articles that a future LLM will read by walking the graph, NOT by bag-of-words search. Your job is to produce articles that another agent can navigate via \`type\` + wikilinks. If the wikilinks don't form a coherent graph, the wiki is useless regardless of how many tags you add.
18
+
19
+ **Three rules, in priority order:**
20
+
21
+ 1. **\`type\` is the organizational spine.** Every article has exactly one of: \`person | project | place | concept | event | decision | pattern\`. Choose the type BEFORE writing; if you can't pick one, the article probably isn't needed.
22
+ 2. **\`related\` is the navigation surface.** Every article cross-references 2\u20135 other articles via \`[[Article Title]]\` wikilinks in the body AND lists them in the \`related\` frontmatter field. Links the reader could follow to learn more.
23
+ 3. **Tags are a secondary hint, not a retrieval spine.** 2\u20134 specific tags max. Do NOT tag aggressively. Do NOT tag with dates unless the article is an \`event\`. Do NOT tag with generic terms ("deploy", "work") that will match half the corpus.
24
+
25
+ ## Path convention
26
+
27
+ Path reflects type: \`<type>s/<slug>.md\` where slug is a kebab-case title.
28
+
29
+ - People: \`people/jane-doe.md\`
30
+ - Projects: \`projects/globex-system-v2.md\`
31
+ - Concepts: \`concepts/staging-deployment.md\`
32
+ - Events: \`events/YYYY-MM-DD-<slug>.md\` (date in path)
33
+ - Decisions: \`decisions/<slug>.md\`
34
+ - Patterns: \`patterns/<slug>.md\`
35
+ - Places: \`places/<slug>.md\`
36
+
37
+ ## How to process entries
38
+
39
+ For each of the ${o} raw entries below, ask in this order:
40
+
41
+ 1. **Does it extend an existing article?** If a person/project/concept mentioned in the entry already has an article in the catalog, produce an UPDATE with the full merged content. Prefer merging over proliferating.
42
+ 2. **Does it deserve a new article?** Only if the subject is a persistent entity (a person, a project, a recurring concept, a specific event/decision) that future queries will need to find. Not every conversation deserves an article.
43
+ 3. **Does it belong in an existing \`event\` or \`decision\`?** Most work entries fold into one of these.
44
+ 4. **Can you skip it?** If the entry is small talk, a transient status ping, or already covered elsewhere \u2014 skip. The wiki is curated, not exhaustive.
45
+
46
+ ## Writing standards
47
+
48
+ - **Wikipedia-style, flat, factual, encyclopedic.** This is the agent's knowledge, not a diary. Write about the *role* of the entity in our work, not a product description.
49
+ - **Synthesize, don't quote.** At most 2 short quoted lines per article.
50
+ - **Organize by theme, not chronology.** An article about a person lists what they do, who they work with, and their actual contact details \u2014 not a log of every interaction. History goes last, and only where it still bears on the present.
51
+ - **Length: 20\u2013100 lines.** Articles exceeding 100 lines should split into multiple type-specific articles.
52
+ - **Every paragraph earns its place.** Cut narrative filler. If you can remove a sentence without losing a fact, remove it.
53
+
54
+ ## Build in layers
55
+
56
+ Facts are not equally load-bearing. Write them in this order, and never
57
+ spend length on a lower layer while a higher one is missing:
58
+
59
+ ${W()}
60
+
61
+ An article is a building, not a transcript. The common failure is to
62
+ furnish a slab: a person's page that records an invoice thread in detail
63
+ and never states their role, their organisation, or how to reach them.
64
+ That article cannot answer "who is this" or "how do I reach them" \u2014 the
65
+ two questions it exists for. History is the last layer, and it is the
66
+ only one you can safely leave thin.
67
+
68
+ ## Identity before history
69
+
70
+ An entity article must first establish **what the entity is**, and only then
71
+ what happened to it.
72
+
73
+ Before the narrative, every entity article carries an **Identity** section
74
+ with the fields for its type. State a field even when the entries do not
75
+ answer it, as \`unknown\`, so the gap is visible and a later pass can fill it.
76
+ A field silently omitted is indistinguishable from a field that does not
77
+ apply.
78
+
79
+ ${_()}
80
+
81
+ Fields in **bold** are the ones an article of that type is unusable without.
82
+ Every article of any type also states what it is, why it matters to our work,
83
+ and which named people, projects or systems it connects to.
84
+
85
+ Extract identifiers **verbatim** from the entries. A phone number, an
86
+ account number, a URL or a path is the single most useful thing an article
87
+ can carry, and it is exactly what summarising discards. Never paraphrase or
88
+ redact one that appears in the source; if a value appears only in part, say
89
+ so rather than dropping it.
90
+
91
+ ## Wikilink discipline
92
+
93
+ - Use \`[[Article Title]]\` **inside the body** every time another tracked entity is referenced.
94
+ - List those same targets in the \`related\` frontmatter field.
95
+ - If you reference something that has no article yet (\`[[New Thing]]\`), add "New Thing" to the output \`gaps\` array so the next pass can create it.
96
+
97
+ ## Article frontmatter (exact shape \u2014 do not invent fields)
98
+
99
+ \`\`\`yaml
100
+ ---
101
+ title: "Article Title"
102
+ type: person | project | place | concept | event | decision | pattern
103
+ related: ["Other Article", "Another Article"]
104
+ tags: ["2-4-specific-tags", "no-dates-unless-event"]
105
+ owner: ${n}
106
+ access: public | shared | private
107
+ created: YYYY-MM-DD
108
+ last_updated: YYYY-MM-DD
109
+ sources: ["entry-id-1", "entry-id-2"]
110
+ ---
111
+ \`\`\`
112
+
113
+ Access guidance: default \`public\`; \`private\` only for sensitive credentials or agent-specific learnings; \`shared\` with specific agent IDs when the article matters only to a subset.
114
+ ${c}${u}${i}
115
+ ## Gap Detection
116
+
117
+ After compiling, populate a \`gaps\` array: wikilink targets you referenced but for which no article exists yet. Be specific:
118
+
119
+ - Good: "Karim Rahmouni \u2014 coach for Tunis Padel club, mentioned twice but no \`people/\` article"
120
+ - Bad: "We need more content about deployments"
121
+
122
+ ## Output \u2014 valid JSON only, no markdown fencing
123
+
124
+ \`\`\`
125
+ {
126
+ "articles": [
127
+ {
128
+ "path": "projects/globex-system-v2.md",
129
+ "title": "Globex System V2",
130
+ "type": "project",
131
+ "related": ["Alex Rivera", "Laravel", "Staging Deployment"],
132
+ "tags": ["globex", "laravel", "react"],
133
+ "content": "Globex System V2 is [[Alex Rivera]]'s production Laravel + React app for \u2026",
134
+ "sources": ["entry-id-1", "entry-id-2"]
135
+ }
136
+ ],
137
+ "gaps": [
138
+ "Karim Rahmouni \u2014 Tunis Padel coach, mentioned in entries but no people/ article"
139
+ ]
140
+ }
141
+ \`\`\`
142
+
143
+ ENTRIES (${o}):
144
+
145
+ ${t}`}var U=2e3,Q=300;function D(n,r,s,t){let o=t?`
146
+ ## Worldview
147
+
148
+ ${t}
149
+ `:"",i=new Map;for(let l of s){let d=l.type||"untyped",e=i.get(d)||[];e.push(l),i.set(d,e)}let c=s.length>Q,p=s.length===0?"":`
150
+ ## Existing Articles (the current catalog${c?`, ${s.length} articles \u2014 titles only`:""})
151
+
152
+ `+Array.from(i.entries()).sort().map(([l,d])=>`**${l}**
153
+ `+d.sort((e,a)=>e.title.localeCompare(a.title)).map(e=>c?`- [[${e.title}]]`:`- [[${e.title}]] \u2014 ${e.path}`).join(`
154
+ `)).join(`
155
+
156
+ `)+`
157
+ `,u=r.reduce((l,d)=>l+d.candidates.length,0),g=r.map(l=>{let d=l.corroboratingAgents.length>1?`corroborated by: ${l.corroboratingAgents.join(", ")} (${l.corroboratingAgents.length} agents)`:`single agent: ${l.corroboratingAgents[0]}`;return l.candidates.map(e=>{let a=e.memory.body.length>U?e.memory.body.slice(0,U)+`
158
+ [\u2026 truncated]`:e.memory.body;return[`--- CANDIDATE ${e.stamp} ---`,`type: ${e.memory.type} | agent: ${e.agentId} | ${d} | confidence: ${l.confidence.toFixed(2)}`,`description: ${e.memory.description}`,a,"--- END CANDIDATE ---"].join(`
159
+ `)}).join(`
160
+
161
+ `)}).join(`
162
+
163
+ `);return`You are promoting per-agent experiential memories into the shared, authoritative AgentX wiki (owner: "${n}").
164
+
165
+ ## The Job
166
+
167
+ Memory is "what one agent learned"; the wiki is "what every agent should know." For each candidate memory below, decide: promote it into a wiki article, or skip it with a reason.
168
+
169
+ **The promotion test \u2014 apply in order, skip on the first "no":**
170
+
171
+ 1. **Durable?** Will this still be true in 3 months? Transient state, one-off task context, and dated status pings fail here.
172
+ 2. **Cross-agent?** Would a DIFFERENT agent benefit from knowing this? Facts about shared infrastructure, projects, people, and conventions pass; one agent's private workflow habits fail.
173
+ 3. **A fact, not a preference?** Personal style preferences private to one agent's operator relationship fail.
174
+ 4. **Never a secret.** Credentials, tokens, keys, and passwords are NEVER promoted \u2014 skip with reason "contains secret". Pointers to where a secret lives (e.g. "SSH config is in X") are fine; the secret itself is not.
175
+
176
+ Corroboration across agents (shown per candidate) is a confidence signal, not a requirement \u2014 a single-agent memory that passes the test above should be promoted.
177
+
178
+ Candidates stamped \`failure:\` are not memories: they are the same failure recurring across several sessions, with example runs. The recurrence is what makes them durable. Promote one only when the examples share a cause, as a \`pattern\` article saying what fails, why, and what to do instead (including a tool or configuration change, if that is the fix). Never copy task text or error output verbatim beyond what the lesson needs.
179
+
180
+ **Three rules for the articles you write, in priority order:**
181
+
182
+ 1. **\`type\` is the organizational spine.** Every article has exactly one of: \`person | project | place | concept | event | decision | pattern\`. Choose the type BEFORE writing; if you can't pick one, skip the memory instead.
183
+ 2. **\`related\` is the navigation surface.** Every article cross-references 2\u20135 other articles via \`[[Article Title]]\` wikilinks in the body AND lists them in the \`related\` output field. Links the reader could follow to learn more.
184
+ 3. **Tags are a secondary hint.** 2\u20134 specific tags max. No dates unless the article is an \`event\`. No generic terms ("deploy", "work").
185
+
186
+ ## Path convention
187
+
188
+ Path reflects type: \`<type>s/<slug>.md\` where slug is a kebab-case title.
189
+
190
+ - People: \`people/jane-doe.md\`
191
+ - Projects: \`projects/globex-system-v2.md\`
192
+ - Concepts: \`concepts/staging-deployment.md\`
193
+ - Events: \`events/YYYY-MM-DD-<slug>.md\` (date in path)
194
+ - Decisions: \`decisions/<slug>.md\`
195
+ - Patterns: \`patterns/<slug>.md\`
196
+ - Places: \`places/<slug>.md\`
197
+
198
+ ## Merge over create
199
+
200
+ If a candidate extends an article in the catalog below, emit an UPDATE at the EXISTING path with the full merged content. Prefer merging over proliferating. Several candidates about the same entity should fold into ONE article.
201
+
202
+ ## Writing standards
203
+
204
+ - **Wikipedia-style, flat, factual, encyclopedic.** Write about the entity's role in our work, not a diary of how the memory was learned.
205
+ - **Synthesize, don't quote.** At most 2 short quoted lines per article.
206
+ - **Length: 20\u2013100 lines.**
207
+ - **Every paragraph earns its place.**
208
+
209
+ ## Gap Detection
210
+
211
+ Populate a \`gaps\` array: wikilink targets you referenced but for which no article exists yet. Be specific ("Karim Rahmouni \u2014 coach mentioned twice, no people/ article"), not vague ("need more deploy content").
212
+ ${o}${p}
213
+ ## Output \u2014 valid JSON only, no markdown fencing
214
+
215
+ Reference candidates ONLY by their exact stamp strings (the \`memory:...\` line in each CANDIDATE header). Every candidate must appear in exactly one of \`articles[].promotedFrom\` or \`skipped\`. Do NOT emit frontmatter, owner, access, dates, or sources \u2014 the caller sets those.
216
+
217
+ \`\`\`
218
+ {
219
+ "articles": [
220
+ {
221
+ "path": "concepts/globex-staging-deploy.md",
222
+ "title": "Globex Staging Deploy",
223
+ "type": "concept",
224
+ "related": ["Globex System V2", "Staging Deployment"],
225
+ "tags": ["globex", "deploy"],
226
+ "content": "Deploys to [[Globex System V2]] staging go through \u2026",
227
+ "promotedFrom": ["memory:coder-agent/project_globex_deploy@2026-07-01T22:14:03.000Z"]
228
+ }
229
+ ],
230
+ "skipped": [
231
+ { "memory": "memory:cx/feedback_short_replies@2026-06-01T00:00:00.000Z", "reason": "agent-specific style preference, not a cross-agent fact" }
232
+ ],
233
+ "gaps": []
234
+ }
235
+ \`\`\`
236
+
237
+ CANDIDATES (${u} memories in ${r.length} clusters):
238
+
239
+ ${g}`}var x="memory-promoter",oe=["project","reference","feedback"],K="_memory-promotions.json",N="memory:",j="failure:";function J(n){let r=n.lastIndexOf("@");return r===-1?n:n.slice(0,r)}var se=["user","feedback","project","reference"];function q(n,r){return`${n}/${r.type}_${r.name}`}function ie(n,r){return`${N}${q(n,r)}@${r.updatedAt}`}function $(n){if(!n.startsWith(N))return null;let r=n.slice(N.length),s=r.indexOf("/"),t=r.lastIndexOf("@");if(s<=0||t<=s)return null;let o=r.slice(0,s),i=r.slice(s+1,t),c=r.slice(t+1);if(!c)return null;let p=i.indexOf("_");if(p<=0)return null;let u=i.slice(0,p),g=i.slice(p+1);return!se.includes(u)||!g?null:{agentId:o,type:u,name:g,updatedAt:c,key:`${o}/${i}`}}function ae(n,r){let s=new Set;for(let o of r){let i=$(o);i&&s.add(i.key)}let t=[];for(let o of n??[]){let i=$(o);i&&s.has(i.key)||t.includes(o)||t.push(o)}for(let o of r)t.includes(o)||t.push(o);return t}function ce(n=P(process.cwd(),".agentx")){let r=new F({baseDir:n});if(!H(r.baseDir))return[];let s=[];for(let t of te(r.baseDir,{withFileTypes:!0}))if(!(!t.isDirectory()||t.name.startsWith("_")||t.name.startsWith(".")))for(let o of r.list(t.name))s.push({agentId:t.name,memory:o});return s}var pe=new Set(["promoted","skipped","proposed","rejected"]);function z(n){let r=P(n,K);if(!H(r))return[];try{let s=JSON.parse(re(r,"utf-8"));return Array.isArray(s)?s.filter(t=>t&&typeof t.stamp=="string"&&pe.has(t.decision)):[]}catch{return[]}}function C(n,r){let s=[...z(n),...r],t=new Map;for(let o of s){let i=$(o.stamp),c=i?.key??o.stamp,p=t.get(c);(!p||!i||($(p.stamp)?.updatedAt??"")<=i.updatedAt)&&t.set(c,o)}ne(P(n,K),JSON.stringify([...t.values()],null,2))}function le(n){let r=new Map;for(let s of n){let t=`${s.memory.type}_${s.memory.name}`,o=r.get(t)??[];o.push(s),r.set(t,o)}return[...r.values()].map(s=>{let t=[...new Set(s.map(c=>c.agentId))].sort(),o=["project","reference"].includes(s[0].memory.type)?.05:0,i=Math.min(.95,.5+.15*(t.length-1)+o);return{candidates:s,corroboratingAgents:t,confidence:i}})}function B(n,r){let s=n.indexOf("{");if(s===-1)return{error:"no JSON object in response"};let t=0,o=-1,i=!1,c=!1;for(let e=s;e<n.length;e++){let a=n[e];if(c){c=!1;continue}if(a==="\\"){c=!0;continue}if(a==='"'){i=!i;continue}if(!i){if(a==="{")t++;else if(a==="}"&&(t--,t===0)){o=e+1;break}}}if(o===-1)return{error:"unbalanced JSON in response"};let p;try{p=JSON.parse(n.slice(s,o))}catch(e){return{error:`JSON parse error: ${e.message}`}}if(!p||typeof p!="object"||!Array.isArray(p.articles))return{error:"response JSON missing articles array"};let u=[],g=[];for(let e of p.articles){if(!e||typeof e.path!="string"||typeof e.title!="string"||typeof e.content!="string"){u.push("article missing path/title/content \u2014 dropped");continue}let a=Array.isArray(e.promotedFrom)?e.promotedFrom.filter(y=>typeof y=="string"):[],h=a.filter(y=>r.has(y));for(let y of a)r.has(y)||u.push(`article "${e.path}" references unknown stamp "${y}" \u2014 dropped`);if(h.length===0){u.push(`article "${e.path}" has no valid promotedFrom stamps \u2014 dropped`);continue}g.push({path:e.path,title:e.title,type:typeof e.type=="string"?e.type:void 0,related:Array.isArray(e.related)?e.related.filter(y=>typeof y=="string"):void 0,tags:Array.isArray(e.tags)?e.tags.filter(y=>typeof y=="string"):[],content:e.content,promotedFrom:h})}let l=[];for(let e of Array.isArray(p.skipped)?p.skipped:[]){let a=typeof e?.memory=="string"?e.memory:typeof e?.stamp=="string"?e.stamp:"";if(!r.has(a)){a&&u.push(`skip references unknown stamp "${a}" \u2014 dropped`);continue}l.push({stamp:a,reason:typeof e.reason=="string"?e.reason:""})}let d=Array.isArray(p.gaps)?p.gaps.filter(e=>typeof e=="string"):[];return{articles:g,skipped:l,gaps:d,warnings:u}}function de(n,r,s,t={}){let o=t.types??oe,i=t.now??Date.now(),c=t.max??20,p=new Map,u=new Set,g=e=>{u.add(e);let a=$(e);if(!a)return;let h=p.get(a.key);(!h||a.updatedAt>h)&&p.set(a.key,a.updatedAt)};for(let e of r.articles)for(let a of e.sources??[])g(a);for(let e of s)g(e.stamp);let l=new Set(s.filter(e=>e.decision==="proposed"&&e.stamp.startsWith(j)).map(e=>J(e.stamp))),d=[];for(let{agentId:e,memory:a,stamp:h,occurrences:y,sessions:w,tasks:b,failure:f}of n){if(!o.includes(a.type)||t.agentFilter&&e!==t.agentFilter)continue;if(t.sinceMs!==void 0){let T=Date.parse(a.updatedAt);if(!Number.isFinite(T)||T<i-t.sinceMs)continue}if(h&&u.has(h)||h?.startsWith(j)&&l.has(J(h)))continue;let v=q(e,a),k=p.get(v);k&&a.updatedAt<=k||d.push({agentId:e,memory:a,key:v,stamp:h??ie(e,a),occurrences:y,...w?.length?{sessions:w}:{},...b?.length?{tasks:b}:{},...f?{failure:f}:{}})}return d.sort((e,a)=>(a.occurrences??0)-(e.occurrences??0)||a.memory.updatedAt.localeCompare(e.memory.updatedAt)).slice(0,c)}var me="https://api.anthropic.com/v1/messages",ge="2023-06-01";async function G(n,r,s=null){let t=r.fetchImpl??fetch,o=r.timeoutMs??18e4,i=new AbortController,c=setTimeout(()=>i.abort(),o);try{if(r.viaAgent){let a=(r.daemonUrl??"http://127.0.0.1:18800").replace(/\/+$/,""),h=s?`Your previous response failed validation: ${s}
240
+ Re-emit the FULL corrected JSON object \u2014 same structure, fixed issues, valid JSON only.`:n,y=await t(`${a}/task`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agent:r.viaAgent,message:h,freshSession:s===null,context:{channel:"api",chatId:r.chatId??"memory-promote"}}),signal:i.signal});if(!y.ok){let f=await y.text().catch(()=>"");throw new Error(`agent /task ${y.status}: ${f.slice(0,500)}`)}let w=await y.json();if(w.error)throw new Error(`agent reported error: ${w.error}`);let b=String(w.content??"").trim();if(!b)throw new Error("agent returned empty content");return b}if(!r.model)throw new Error("callPromotionLlm: viaAgent or model is required");let p=process.env.ANTHROPIC_API_KEY;if(!p)throw new Error("ANTHROPIC_API_KEY not set; use --via <agentId> instead");let u=[{role:"user",content:n}];s&&(u.push({role:"assistant",content:"(previous attempt; will retry with corrections)"}),u.push({role:"user",content:`Your previous response failed validation: ${s}
241
+ Re-emit the FULL corrected JSON object.`}));let g=await t(me,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":p,"anthropic-version":ge},body:JSON.stringify({model:r.model,max_tokens:8e3,messages:u}),signal:i.signal});if(!g.ok){let a=await g.text().catch(()=>"");throw new Error(`anthropic API ${g.status}: ${a.slice(0,500)}`)}let l=await g.json(),e=(Array.isArray(l.content)?l.content:[]).filter(a=>a?.type==="text").map(a=>a.text).join(`
242
+ `).trim();if(!e)throw new Error("anthropic API returned no text content");return e}finally{clearTimeout(c)}}async function Se(n={}){let r=n.wikiDir??P(process.cwd(),".agentx","wiki"),s=n.memoryRoot??P(process.cwd(),".agentx"),t=n.log??(()=>{}),o=n.now??Date.now(),i={candidates:[],clusters:[],proposed:[],skipped:[],gaps:[],warnings:[],errors:[],dryRun:!n.commit},c=new E(r,(...m)=>t(m.map(String).join(" "))),p=c.rebuildIndex(),u=z(r),g=[...ce(s),...n.extraCandidates??[]];if(i.candidates=de(g,p,u,{types:n.types,sinceMs:n.sinceMs,agentFilter:n.agentFilter,now:o,max:n.max}),i.candidates.length===0)return t("no unpromoted memories in window"),i;if(i.clusters=le(i.candidates),i.dryRun)return i;let l=p.articles.map(m=>({title:m.title,path:m.path,type:m.type})),d=c.getWorldview()??"",e=n.budgetTokens??ue,a=i.clusters,h=D(x,a,l,d);for(;R(h)>e&&a.length>1;)a=a.slice(0,-1),h=D(x,a,l,d);if(R(h)>e)return i.errors.push(`judge prompt (~${R(h)} tokens) exceeds the ${e}-token budget even with one cluster`),i;a.length<i.clusters.length&&i.warnings.push(`token budget: judged ${a.length} of ${i.clusters.length} clusters; the rest wait for the next run`);let y=a.flatMap(m=>m.candidates),w=new Set(y.map(m=>m.stamp)),b=n.chatId??`memory-promote-${new Date(o).toISOString().slice(0,10)}`,f=B(await G(h,{...n,chatId:b}),w);if("error"in f&&(t(`parse failed (${f.error}) \u2014 retrying with feedback`),f=B(await G(h,{...n,chatId:b},f.error),w)),"error"in f)return i.errors.push(`LLM response unusable after retry: ${f.error}`),i;i.warnings.push(...f.warnings),i.gaps=f.gaps;let v=new Date(o).toISOString(),k=[],T=new Map(y.map(m=>[m.stamp,m]));for(let m of f.articles){let A=c.readArticle(m.path);if(A&&!c.canWrite(A.meta,x)){i.errors.push(`write denied for "${m.path}" (owned by ${A.meta.owner??"?"})`);continue}let M=fe(m.promotedFrom,T),S={id:Y(o,m.path),createdAt:v,status:"pending",article:m,evidence:M,...A?{replaces:{lastUpdated:A.meta.lastUpdated,contentHash:V(A.content)}}:{}};I(r,S),i.proposed.push({id:S.id,path:m.path,title:m.title,type:m.type,stamps:m.promotedFrom,agents:M.agents,occurrences:M.occurrences});for(let X of m.promotedFrom)k.push({stamp:X,decision:"proposed",article:m.path,proposal:S.id,at:v})}for(let m of f.skipped)k.push({stamp:m.stamp,decision:"skipped",reason:m.reason,at:v}),i.skipped.push(m);return k.length&&C(r,k),i}var ue=6e4;function R(n){return Math.ceil(n.length/4)}function V(n){return ee("sha256").update(n).digest("hex").slice(0,16)}var he=600;function ye(n){return n.startsWith("review:")?"review":n.startsWith(j)?"failure":"memory"}function fe(n,r){let s=[];for(let t of n){let o=r.get(t);o&&s.push({stamp:t,kind:ye(t),agentId:o.agentId,type:o.memory.type,name:o.memory.name,description:o.memory.description,...o.occurrences?{occurrences:o.occurrences}:{},...o.sessions?.length?{sessions:o.sessions}:{},...o.tasks?.length?{tasks:o.tasks}:{},...o.failure?{failure:o.failure}:{},...o.memory.author?{author:o.memory.author}:{},...o.memory.taskId?{taskId:o.memory.taskId}:{},updatedAt:o.memory.updatedAt,excerpt:o.memory.body.slice(0,he)})}return{agents:[...new Set(s.map(t=>t.agentId))].sort(),occurrences:Math.max(1,...s.map(t=>t.occurrences??1)),sources:s}}function Ee(n,r,s={}){let t=O(n,r);if(!t)return{ok:!1,error:`no proposal "${r}"`};if(t.status!=="pending")return{ok:!1,error:`proposal "${r}" is already ${t.status}`};let o=s.log??(()=>{}),i=new E(n,(...a)=>o(a.map(String).join(" "))),{article:c}=t,p=i.readArticle(c.path);if(!s.force){if(p&&!t.replaces)return{ok:!1,error:`"${c.path}" was created after this proposal; review it, then approve with --force`};if(p&&t.replaces&&V(p.content)!==t.replaces.contentHash)return{ok:!1,error:`"${c.path}" changed since this proposal; review it, then approve with --force`}}let u=s.now??Date.now(),g=new Date(u).toISOString().slice(0,10);if(!i.writeArticle(c.path,{title:c.title,type:L(c.type)?c.type:void 0,related:c.related,tags:c.tags,owner:x,access:"public",created:p?.meta.created??g,lastUpdated:g,sources:ae(p?.meta.sources,c.promotedFrom)},c.content,x))return{ok:!1,error:`write denied for "${c.path}" (owned by ${p?.meta.owner??"?"})`};let d=new Date(u).toISOString();C(n,c.promotedFrom.map(a=>({stamp:a,decision:"promoted",article:c.path,proposal:r,at:d}))),i.rebuildIndex();let e={...t,status:"approved",decidedBy:s.by??"operator",decidedAt:d};return I(n,e),{ok:!0,proposal:e}}function Oe(n,r,s={}){let t=O(n,r);if(!t)return{ok:!1,error:`no proposal "${r}"`};if(t.status!=="pending")return{ok:!1,error:`proposal "${r}" is already ${t.status}`};let o=new Date(s.now??Date.now()).toISOString();C(n,t.article.promotedFrom.map(c=>({stamp:c,decision:"rejected",article:t.article.path,proposal:r,reason:s.reason,at:o})));let i={...t,status:"rejected",decidedBy:s.by??"operator",decidedAt:o,...s.reason?{reason:s.reason}:{}};return I(n,i),{ok:!0,proposal:i}}export{be as a,x as b,oe as c,K as d,q as e,ie as f,$ as g,ae as h,ce as i,z as j,C as k,le as l,B as m,de as n,G as o,Se as p,R as q,ye as r,fe as s,Ee as t,Oe as u};
243
+ //# sourceMappingURL=chunk-OHA3J5ZV.js.map