@the-open-engine/zeroshot 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/CHANGELOG.md +539 -0
  2. package/LICENSE +21 -0
  3. package/README.md +508 -0
  4. package/cli/commands/inspect-render.js +135 -0
  5. package/cli/commands/inspect.js +294 -0
  6. package/cli/commands/providers.js +149 -0
  7. package/cli/index.js +5431 -0
  8. package/cli/lib/first-run.js +211 -0
  9. package/cli/lib/update-checker.js +281 -0
  10. package/cli/message-formatter-utils.js +75 -0
  11. package/cli/message-formatters-normal.js +275 -0
  12. package/cli/message-formatters-watch.js +185 -0
  13. package/cluster-templates/base-templates/debug-workflow.json +422 -0
  14. package/cluster-templates/base-templates/full-workflow.json +727 -0
  15. package/cluster-templates/base-templates/heavy-validation.json +272 -0
  16. package/cluster-templates/base-templates/quick-validation.json +285 -0
  17. package/cluster-templates/base-templates/single-worker.json +71 -0
  18. package/cluster-templates/base-templates/worker-validator.json +230 -0
  19. package/cluster-templates/conductor-bootstrap.json +130 -0
  20. package/docker/zeroshot-cluster/Dockerfile +153 -0
  21. package/docker/zeroshot-cluster/pre-baked-deps.json +28 -0
  22. package/lib/agent-cli-provider/adapters/claude-parser.d.ts +3 -0
  23. package/lib/agent-cli-provider/adapters/claude-parser.d.ts.map +1 -0
  24. package/lib/agent-cli-provider/adapters/claude-parser.js +122 -0
  25. package/lib/agent-cli-provider/adapters/claude-parser.js.map +1 -0
  26. package/lib/agent-cli-provider/adapters/claude-recovery.d.ts +15 -0
  27. package/lib/agent-cli-provider/adapters/claude-recovery.d.ts.map +1 -0
  28. package/lib/agent-cli-provider/adapters/claude-recovery.js +165 -0
  29. package/lib/agent-cli-provider/adapters/claude-recovery.js.map +1 -0
  30. package/lib/agent-cli-provider/adapters/claude.d.ts +3 -0
  31. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -0
  32. package/lib/agent-cli-provider/adapters/claude.js +181 -0
  33. package/lib/agent-cli-provider/adapters/claude.js.map +1 -0
  34. package/lib/agent-cli-provider/adapters/codex-parser.d.ts +3 -0
  35. package/lib/agent-cli-provider/adapters/codex-parser.d.ts.map +1 -0
  36. package/lib/agent-cli-provider/adapters/codex-parser.js +192 -0
  37. package/lib/agent-cli-provider/adapters/codex-parser.js.map +1 -0
  38. package/lib/agent-cli-provider/adapters/codex.d.ts +3 -0
  39. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -0
  40. package/lib/agent-cli-provider/adapters/codex.js +151 -0
  41. package/lib/agent-cli-provider/adapters/codex.js.map +1 -0
  42. package/lib/agent-cli-provider/adapters/common.d.ts +28 -0
  43. package/lib/agent-cli-provider/adapters/common.d.ts.map +1 -0
  44. package/lib/agent-cli-provider/adapters/common.js +71 -0
  45. package/lib/agent-cli-provider/adapters/common.js.map +1 -0
  46. package/lib/agent-cli-provider/adapters/gemini.d.ts +3 -0
  47. package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -0
  48. package/lib/agent-cli-provider/adapters/gemini.js +176 -0
  49. package/lib/agent-cli-provider/adapters/gemini.js.map +1 -0
  50. package/lib/agent-cli-provider/adapters/index.d.ts +9 -0
  51. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -0
  52. package/lib/agent-cli-provider/adapters/index.js +95 -0
  53. package/lib/agent-cli-provider/adapters/index.js.map +1 -0
  54. package/lib/agent-cli-provider/adapters/opencode.d.ts +3 -0
  55. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -0
  56. package/lib/agent-cli-provider/adapters/opencode.js +228 -0
  57. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -0
  58. package/lib/agent-cli-provider/contract-actions.d.ts +5 -0
  59. package/lib/agent-cli-provider/contract-actions.d.ts.map +1 -0
  60. package/lib/agent-cli-provider/contract-actions.js +140 -0
  61. package/lib/agent-cli-provider/contract-actions.js.map +1 -0
  62. package/lib/agent-cli-provider/contract-env.d.ts +11 -0
  63. package/lib/agent-cli-provider/contract-env.d.ts.map +1 -0
  64. package/lib/agent-cli-provider/contract-env.js +113 -0
  65. package/lib/agent-cli-provider/contract-env.js.map +1 -0
  66. package/lib/agent-cli-provider/contract-envelope.d.ts +49 -0
  67. package/lib/agent-cli-provider/contract-envelope.d.ts.map +1 -0
  68. package/lib/agent-cli-provider/contract-envelope.js +52 -0
  69. package/lib/agent-cli-provider/contract-envelope.js.map +1 -0
  70. package/lib/agent-cli-provider/contract-errors.d.ts +22 -0
  71. package/lib/agent-cli-provider/contract-errors.d.ts.map +1 -0
  72. package/lib/agent-cli-provider/contract-errors.js +71 -0
  73. package/lib/agent-cli-provider/contract-errors.js.map +1 -0
  74. package/lib/agent-cli-provider/contract-fallback.d.ts +7 -0
  75. package/lib/agent-cli-provider/contract-fallback.d.ts.map +1 -0
  76. package/lib/agent-cli-provider/contract-fallback.js +102 -0
  77. package/lib/agent-cli-provider/contract-fallback.js.map +1 -0
  78. package/lib/agent-cli-provider/contract-invoke.d.ts +5 -0
  79. package/lib/agent-cli-provider/contract-invoke.d.ts.map +1 -0
  80. package/lib/agent-cli-provider/contract-invoke.js +89 -0
  81. package/lib/agent-cli-provider/contract-invoke.js.map +1 -0
  82. package/lib/agent-cli-provider/contract-options.d.ts +3 -0
  83. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -0
  84. package/lib/agent-cli-provider/contract-options.js +140 -0
  85. package/lib/agent-cli-provider/contract-options.js.map +1 -0
  86. package/lib/agent-cli-provider/contract-parse.d.ts +15 -0
  87. package/lib/agent-cli-provider/contract-parse.d.ts.map +1 -0
  88. package/lib/agent-cli-provider/contract-parse.js +81 -0
  89. package/lib/agent-cli-provider/contract-parse.js.map +1 -0
  90. package/lib/agent-cli-provider/contract-support.d.ts +18 -0
  91. package/lib/agent-cli-provider/contract-support.d.ts.map +1 -0
  92. package/lib/agent-cli-provider/contract-support.js +154 -0
  93. package/lib/agent-cli-provider/contract-support.js.map +1 -0
  94. package/lib/agent-cli-provider/contract.d.ts +13 -0
  95. package/lib/agent-cli-provider/contract.d.ts.map +1 -0
  96. package/lib/agent-cli-provider/contract.js +30 -0
  97. package/lib/agent-cli-provider/contract.js.map +1 -0
  98. package/lib/agent-cli-provider/env-safety.d.ts +11 -0
  99. package/lib/agent-cli-provider/env-safety.d.ts.map +1 -0
  100. package/lib/agent-cli-provider/env-safety.js +83 -0
  101. package/lib/agent-cli-provider/env-safety.js.map +1 -0
  102. package/lib/agent-cli-provider/errors.d.ts +5 -0
  103. package/lib/agent-cli-provider/errors.d.ts.map +1 -0
  104. package/lib/agent-cli-provider/errors.js +115 -0
  105. package/lib/agent-cli-provider/errors.js.map +1 -0
  106. package/lib/agent-cli-provider/executable.d.ts +3 -0
  107. package/lib/agent-cli-provider/executable.d.ts.map +1 -0
  108. package/lib/agent-cli-provider/executable.js +24 -0
  109. package/lib/agent-cli-provider/executable.js.map +1 -0
  110. package/lib/agent-cli-provider/index.d.ts +8 -0
  111. package/lib/agent-cli-provider/index.d.ts.map +1 -0
  112. package/lib/agent-cli-provider/index.js +31 -0
  113. package/lib/agent-cli-provider/index.js.map +1 -0
  114. package/lib/agent-cli-provider/invoke-evidence.d.ts +4 -0
  115. package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -0
  116. package/lib/agent-cli-provider/invoke-evidence.js +17 -0
  117. package/lib/agent-cli-provider/invoke-evidence.js.map +1 -0
  118. package/lib/agent-cli-provider/json.d.ts +16 -0
  119. package/lib/agent-cli-provider/json.d.ts.map +1 -0
  120. package/lib/agent-cli-provider/json.js +110 -0
  121. package/lib/agent-cli-provider/json.js.map +1 -0
  122. package/lib/agent-cli-provider/log-prefix.d.ts +2 -0
  123. package/lib/agent-cli-provider/log-prefix.d.ts.map +1 -0
  124. package/lib/agent-cli-provider/log-prefix.js +22 -0
  125. package/lib/agent-cli-provider/log-prefix.js.map +1 -0
  126. package/lib/agent-cli-provider/process-runner.d.ts +17 -0
  127. package/lib/agent-cli-provider/process-runner.d.ts.map +1 -0
  128. package/lib/agent-cli-provider/process-runner.js +89 -0
  129. package/lib/agent-cli-provider/process-runner.js.map +1 -0
  130. package/lib/agent-cli-provider/redaction.d.ts +9 -0
  131. package/lib/agent-cli-provider/redaction.d.ts.map +1 -0
  132. package/lib/agent-cli-provider/redaction.js +227 -0
  133. package/lib/agent-cli-provider/redaction.js.map +1 -0
  134. package/lib/agent-cli-provider/schema.d.ts +5 -0
  135. package/lib/agent-cli-provider/schema.d.ts.map +1 -0
  136. package/lib/agent-cli-provider/schema.js +136 -0
  137. package/lib/agent-cli-provider/schema.js.map +1 -0
  138. package/lib/agent-cli-provider/single-agent-runtime.d.ts +15 -0
  139. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -0
  140. package/lib/agent-cli-provider/single-agent-runtime.js +228 -0
  141. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -0
  142. package/lib/agent-cli-provider/types.d.ts +194 -0
  143. package/lib/agent-cli-provider/types.d.ts.map +1 -0
  144. package/lib/agent-cli-provider/types.js +12 -0
  145. package/lib/agent-cli-provider/types.js.map +1 -0
  146. package/lib/completion.js +174 -0
  147. package/lib/detached-startup.js +220 -0
  148. package/lib/docker-config.js +220 -0
  149. package/lib/git-remote-utils.js +165 -0
  150. package/lib/id-detector.js +55 -0
  151. package/lib/provider-defaults.js +62 -0
  152. package/lib/provider-detection.js +59 -0
  153. package/lib/provider-names.js +57 -0
  154. package/lib/repo-settings.js +69 -0
  155. package/lib/settings/claude-auth.js +78 -0
  156. package/lib/settings.js +542 -0
  157. package/lib/start-cluster.js +321 -0
  158. package/lib/stream-json-parser.js +67 -0
  159. package/package.json +162 -0
  160. package/scripts/fix-node-pty-permissions.js +75 -0
  161. package/scripts/record-demo.sh +279 -0
  162. package/scripts/setup-merge-queue.sh +170 -0
  163. package/scripts/test-install.sh +40 -0
  164. package/scripts/validate-templates.js +107 -0
  165. package/src/agent/agent-config.js +266 -0
  166. package/src/agent/agent-context-builder.js +189 -0
  167. package/src/agent/agent-context-sections.js +338 -0
  168. package/src/agent/agent-context-sources.js +147 -0
  169. package/src/agent/agent-hook-executor.js +721 -0
  170. package/src/agent/agent-input-injector.js +141 -0
  171. package/src/agent/agent-lifecycle.js +982 -0
  172. package/src/agent/agent-quality-gate-schema.js +93 -0
  173. package/src/agent/agent-quality-gates-context.js +51 -0
  174. package/src/agent/agent-stuck-detector.js +256 -0
  175. package/src/agent/agent-task-executor.js +2028 -0
  176. package/src/agent/agent-trigger-evaluator.js +67 -0
  177. package/src/agent/context-metrics.js +160 -0
  178. package/src/agent/context-pack-builder.js +367 -0
  179. package/src/agent/context-replay-policy.js +51 -0
  180. package/src/agent/guidance-queue.js +77 -0
  181. package/src/agent/output-extraction.js +367 -0
  182. package/src/agent/output-reformatter.js +175 -0
  183. package/src/agent/pr-verification.js +653 -0
  184. package/src/agent/rate-limit-backoff.js +82 -0
  185. package/src/agent/schema-utils.js +146 -0
  186. package/src/agent/validation-platform.js +35 -0
  187. package/src/agent-cli-provider/adapters/claude-parser.ts +133 -0
  188. package/src/agent-cli-provider/adapters/claude-recovery.ts +203 -0
  189. package/src/agent-cli-provider/adapters/claude.ts +247 -0
  190. package/src/agent-cli-provider/adapters/codex-parser.ts +211 -0
  191. package/src/agent-cli-provider/adapters/codex.ts +217 -0
  192. package/src/agent-cli-provider/adapters/common.ts +124 -0
  193. package/src/agent-cli-provider/adapters/gemini.ts +243 -0
  194. package/src/agent-cli-provider/adapters/index.ts +126 -0
  195. package/src/agent-cli-provider/adapters/opencode.ts +286 -0
  196. package/src/agent-cli-provider/contract-actions.ts +150 -0
  197. package/src/agent-cli-provider/contract-env.ts +111 -0
  198. package/src/agent-cli-provider/contract-envelope.ts +110 -0
  199. package/src/agent-cli-provider/contract-errors.ts +66 -0
  200. package/src/agent-cli-provider/contract-fallback.ts +121 -0
  201. package/src/agent-cli-provider/contract-invoke.ts +104 -0
  202. package/src/agent-cli-provider/contract-options.ts +173 -0
  203. package/src/agent-cli-provider/contract-parse.ts +94 -0
  204. package/src/agent-cli-provider/contract-support.ts +167 -0
  205. package/src/agent-cli-provider/contract.ts +56 -0
  206. package/src/agent-cli-provider/env-safety.ts +82 -0
  207. package/src/agent-cli-provider/errors.ts +122 -0
  208. package/src/agent-cli-provider/executable.ts +24 -0
  209. package/src/agent-cli-provider/index.ts +83 -0
  210. package/src/agent-cli-provider/invoke-evidence.ts +18 -0
  211. package/src/agent-cli-provider/json.ts +114 -0
  212. package/src/agent-cli-provider/log-prefix.ts +20 -0
  213. package/src/agent-cli-provider/process-runner.ts +145 -0
  214. package/src/agent-cli-provider/redaction.ts +282 -0
  215. package/src/agent-cli-provider/schema.ts +115 -0
  216. package/src/agent-cli-provider/single-agent-runtime.ts +311 -0
  217. package/src/agent-cli-provider/types.ts +237 -0
  218. package/src/agent-wrapper.js +615 -0
  219. package/src/agents/git-pusher-template.js +705 -0
  220. package/src/attach/attach-client.js +438 -0
  221. package/src/attach/attach-server.js +543 -0
  222. package/src/attach/index.js +37 -0
  223. package/src/attach/protocol.js +220 -0
  224. package/src/attach/ring-buffer.js +121 -0
  225. package/src/attach/send-input.js +88 -0
  226. package/src/attach/socket-discovery.js +267 -0
  227. package/src/claude-task-runner.js +661 -0
  228. package/src/config-router.js +89 -0
  229. package/src/config-validator.js +2202 -0
  230. package/src/copy-worker.js +43 -0
  231. package/src/guidance-topics.js +10 -0
  232. package/src/input-helpers.js +65 -0
  233. package/src/isolation-manager.js +1734 -0
  234. package/src/issue-providers/README.md +305 -0
  235. package/src/issue-providers/azure-devops-provider.js +307 -0
  236. package/src/issue-providers/base-provider.js +232 -0
  237. package/src/issue-providers/github-provider.js +210 -0
  238. package/src/issue-providers/gitlab-provider.js +262 -0
  239. package/src/issue-providers/index.js +196 -0
  240. package/src/issue-providers/jira-provider.js +260 -0
  241. package/src/ledger.js +692 -0
  242. package/src/lib/gc.js +232 -0
  243. package/src/lib/safe-exec.js +88 -0
  244. package/src/logic-engine.js +201 -0
  245. package/src/message-buffer.js +81 -0
  246. package/src/message-bus-bridge.js +144 -0
  247. package/src/message-bus.js +256 -0
  248. package/src/name-generator.js +232 -0
  249. package/src/orchestrator.js +3924 -0
  250. package/src/preflight.js +712 -0
  251. package/src/process-metrics.js +608 -0
  252. package/src/providers/anthropic/index.js +3 -0
  253. package/src/providers/base-provider.js +355 -0
  254. package/src/providers/capabilities.js +60 -0
  255. package/src/providers/google/index.js +3 -0
  256. package/src/providers/index.js +293 -0
  257. package/src/providers/openai/index.js +3 -0
  258. package/src/providers/opencode/index.js +3 -0
  259. package/src/quality-gates.js +143 -0
  260. package/src/schemas/sub-cluster.js +208 -0
  261. package/src/state-snapshot.js +398 -0
  262. package/src/state-snapshotter.js +142 -0
  263. package/src/status-footer.js +1026 -0
  264. package/src/sub-cluster-wrapper.js +693 -0
  265. package/src/task-runner.js +30 -0
  266. package/src/template-resolver.js +425 -0
  267. package/src/template-validation/index.js +338 -0
  268. package/src/template-validation/simulate-consensus-gates.js +324 -0
  269. package/src/template-validation/simulate-random-topology.js +541 -0
  270. package/src/template-validation/simulate-two-stage-validation.js +270 -0
  271. package/src/worktree-claude-config.js +135 -0
  272. package/src/worktree-tooling-env.js +150 -0
  273. package/task-lib/attachable-watcher.js +381 -0
  274. package/task-lib/commands/clean.js +50 -0
  275. package/task-lib/commands/episodes.js +105 -0
  276. package/task-lib/commands/get-log-path.js +23 -0
  277. package/task-lib/commands/kill.js +32 -0
  278. package/task-lib/commands/list.js +105 -0
  279. package/task-lib/commands/logs.js +439 -0
  280. package/task-lib/commands/resume.js +42 -0
  281. package/task-lib/commands/run.js +57 -0
  282. package/task-lib/commands/schedule.js +105 -0
  283. package/task-lib/commands/scheduler-cmd.js +96 -0
  284. package/task-lib/commands/schedules.js +148 -0
  285. package/task-lib/commands/status.js +44 -0
  286. package/task-lib/commands/unschedule.js +16 -0
  287. package/task-lib/completion.js +9 -0
  288. package/task-lib/config.js +11 -0
  289. package/task-lib/name-generator.js +230 -0
  290. package/task-lib/package.json +3 -0
  291. package/task-lib/provider-helper-runtime.js +29 -0
  292. package/task-lib/runner.js +190 -0
  293. package/task-lib/scheduler.js +252 -0
  294. package/task-lib/store.js +529 -0
  295. package/task-lib/watcher.js +305 -0
package/src/ledger.js ADDED
@@ -0,0 +1,692 @@
1
+ /**
2
+ * Ledger - Immutable event log for multi-agent coordination
3
+ *
4
+ * Provides:
5
+ * - SQLite-backed message storage with indexes
6
+ * - Query API for message retrieval
7
+ * - In-memory cache for recent queries
8
+ * - Subscription mechanism for real-time updates
9
+ */
10
+
11
+ const Database = require('better-sqlite3');
12
+ const EventEmitter = require('events');
13
+ const crypto = require('crypto');
14
+ const {
15
+ GUIDANCE_TOPICS,
16
+ USER_GUIDANCE_AGENT,
17
+ USER_GUIDANCE_CLUSTER,
18
+ } = require('./guidance-topics');
19
+
20
+ class Ledger extends EventEmitter {
21
+ constructor(dbPath = ':memory:') {
22
+ super();
23
+ this.dbPath = dbPath;
24
+ const busyTimeoutMs = (() => {
25
+ const raw = process.env.ZEROSHOT_SQLITE_BUSY_TIMEOUT_MS;
26
+ if (!raw) return 5000;
27
+ const value = Number(raw);
28
+ return Number.isFinite(value) && value >= 0 ? value : 5000;
29
+ })();
30
+
31
+ this.db = new Database(dbPath, { timeout: busyTimeoutMs });
32
+ this.cache = new Map(); // LRU cache for queries
33
+ this.cacheLimit = 1000;
34
+ this._closed = false; // Track closed state to prevent write-after-close
35
+ this._lastTimestamp = 0;
36
+ this._initSchema();
37
+ }
38
+
39
+ _initSchema() {
40
+ const journalMode = (process.env.ZEROSHOT_SQLITE_JOURNAL_MODE || 'WAL').trim().toUpperCase();
41
+ // Enable WAL mode for concurrent reads (default), but allow overrides for network filesystems.
42
+ this.db.pragma(`journal_mode = ${journalMode}`);
43
+ // Force synchronous writes so other processes see changes immediately
44
+ this.db.pragma('synchronous = NORMAL');
45
+ // Autocheckpoint trades latency for WAL growth; 1-page checkpoints are extremely slow on
46
+ // higher-latency disks (common in Kubernetes PVs). Default to SQLite-ish behavior (1000 pages),
47
+ // but allow override for niche correctness/debugging needs.
48
+ const walAutocheckpointPages = (() => {
49
+ const raw = process.env.ZEROSHOT_SQLITE_WAL_AUTOCHECKPOINT_PAGES;
50
+ if (!raw) return 1000;
51
+ const value = Number(raw);
52
+ return Number.isFinite(value) && value >= 0 ? Math.floor(value) : 1000;
53
+ })();
54
+ this.db.pragma(`wal_autocheckpoint = ${walAutocheckpointPages}`);
55
+
56
+ // Create messages table
57
+ this.db.exec(`
58
+ CREATE TABLE IF NOT EXISTS messages (
59
+ id TEXT PRIMARY KEY,
60
+ timestamp INTEGER NOT NULL,
61
+ topic TEXT NOT NULL,
62
+ sender TEXT NOT NULL,
63
+ receiver TEXT NOT NULL,
64
+ content_text TEXT,
65
+ content_data TEXT,
66
+ metadata TEXT,
67
+ cluster_id TEXT NOT NULL
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
71
+ CREATE INDEX IF NOT EXISTS idx_topic ON messages(topic);
72
+ CREATE INDEX IF NOT EXISTS idx_cluster_sender ON messages(cluster_id, sender);
73
+ CREATE INDEX IF NOT EXISTS idx_cluster_topic ON messages(cluster_id, topic);
74
+ CREATE INDEX IF NOT EXISTS idx_cluster_timestamp ON messages(cluster_id, timestamp);
75
+ `);
76
+
77
+ this._prepareStatements();
78
+ this._loadLastTimestamp();
79
+ }
80
+
81
+ _prepareStatements() {
82
+ this.stmts = {
83
+ insert: this.db.prepare(`
84
+ INSERT INTO messages (id, timestamp, topic, sender, receiver, content_text, content_data, metadata, cluster_id)
85
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
86
+ `),
87
+
88
+ queryBase: `SELECT * FROM messages WHERE cluster_id = ?`,
89
+
90
+ count: this.db.prepare(`SELECT COUNT(*) as count FROM messages WHERE cluster_id = ?`),
91
+
92
+ getAll: this.db.prepare(`SELECT * FROM messages WHERE cluster_id = ? ORDER BY timestamp ASC`),
93
+ };
94
+ }
95
+
96
+ _loadLastTimestamp() {
97
+ const row = this.db.prepare('SELECT MAX(timestamp) AS max_timestamp FROM messages').get();
98
+ if (row && Number.isFinite(row.max_timestamp)) {
99
+ this._lastTimestamp = row.max_timestamp;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Append a message to the ledger
105
+ * @param {Object} message - Message object
106
+ * @returns {Object} The appended message with generated ID
107
+ */
108
+ append(message) {
109
+ // Guard against write-after-close race condition
110
+ // This can happen when orchestrator closes ledger while agents are still publishing
111
+ if (this._closed) {
112
+ // Silent return - agent is being stopped, message loss is expected
113
+ return null;
114
+ }
115
+
116
+ const id = message.id || `msg_${crypto.randomBytes(16).toString('hex')}`;
117
+ const baseTimestamp = Math.max(Date.now(), this._lastTimestamp + 1);
118
+ const requestedTimestamp = typeof message.timestamp === 'number' ? message.timestamp : null;
119
+ const timestamp =
120
+ requestedTimestamp !== null ? Math.max(requestedTimestamp, baseTimestamp) : baseTimestamp;
121
+
122
+ const receiver = message.receiver || message.target_agent_id || 'broadcast';
123
+ const record = {
124
+ id,
125
+ timestamp,
126
+ topic: message.topic,
127
+ sender: message.sender,
128
+ receiver,
129
+ content_text: message.content?.text || null,
130
+ content_data: message.content?.data ? JSON.stringify(message.content.data) : null,
131
+ metadata: message.metadata ? JSON.stringify(message.metadata) : null,
132
+ cluster_id: message.cluster_id,
133
+ };
134
+
135
+ try {
136
+ this.stmts.insert.run(
137
+ record.id,
138
+ record.timestamp,
139
+ record.topic,
140
+ record.sender,
141
+ record.receiver,
142
+ record.content_text,
143
+ record.content_data,
144
+ record.metadata,
145
+ record.cluster_id
146
+ );
147
+
148
+ // Invalidate cache
149
+ this.cache.clear();
150
+
151
+ this._lastTimestamp = Math.max(this._lastTimestamp, timestamp);
152
+
153
+ // Emit event for subscriptions
154
+ const fullMessage = this._deserializeMessage(record);
155
+ this.emit('message', fullMessage);
156
+ this.emit(`topic:${message.topic}`, fullMessage);
157
+
158
+ return fullMessage;
159
+ } catch (error) {
160
+ throw new Error(`Failed to append message: ${error.message}`);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Append multiple messages atomically using a transaction
166
+ * All messages get contiguous timestamps and are committed together.
167
+ * If any insert fails, the entire batch is rolled back.
168
+ *
169
+ * Use this for task completion messages to prevent interleaving:
170
+ * - TOKEN_USAGE, TASK_COMPLETED, and hook messages published atomically
171
+ * - Other agents' messages cannot appear between them
172
+ *
173
+ * @param {Array<Object>} messages - Array of message objects
174
+ * @returns {Array<Object>} Array of appended messages with generated IDs
175
+ */
176
+ batchAppend(messages) {
177
+ if (!Array.isArray(messages) || messages.length === 0) {
178
+ return [];
179
+ }
180
+
181
+ // Guard against write-after-close race condition
182
+ if (this._closed) {
183
+ return [];
184
+ }
185
+
186
+ // Create transaction function - all inserts happen atomically
187
+ const insertMany = this.db.transaction((msgs) => {
188
+ const results = [];
189
+ const baseTimestamp = Math.max(Date.now(), this._lastTimestamp + 1);
190
+
191
+ for (let i = 0; i < msgs.length; i++) {
192
+ const message = msgs[i];
193
+ const id = message.id || `msg_${crypto.randomBytes(16).toString('hex')}`;
194
+ // Use incrementing timestamps to preserve order within batch
195
+ const timestamp = baseTimestamp + i;
196
+
197
+ const receiver = message.receiver || message.target_agent_id || 'broadcast';
198
+ const record = {
199
+ id,
200
+ timestamp,
201
+ topic: message.topic,
202
+ sender: message.sender,
203
+ receiver,
204
+ content_text: message.content?.text || null,
205
+ content_data: message.content?.data ? JSON.stringify(message.content.data) : null,
206
+ metadata: message.metadata ? JSON.stringify(message.metadata) : null,
207
+ cluster_id: message.cluster_id,
208
+ };
209
+
210
+ this.stmts.insert.run(
211
+ record.id,
212
+ record.timestamp,
213
+ record.topic,
214
+ record.sender,
215
+ record.receiver,
216
+ record.content_text,
217
+ record.content_data,
218
+ record.metadata,
219
+ record.cluster_id
220
+ );
221
+
222
+ results.push(this._deserializeMessage(record));
223
+ }
224
+
225
+ return { results, baseTimestamp };
226
+ });
227
+
228
+ try {
229
+ // Execute transaction (atomic - all or nothing)
230
+ const { results: appendedMessages, baseTimestamp } = insertMany(messages);
231
+
232
+ // Invalidate cache
233
+ this.cache.clear();
234
+
235
+ this._lastTimestamp = Math.max(this._lastTimestamp, baseTimestamp + messages.length - 1);
236
+
237
+ // Emit events for subscriptions AFTER transaction commits
238
+ // This ensures listeners see consistent state
239
+ for (const fullMessage of appendedMessages) {
240
+ this.emit('message', fullMessage);
241
+ this.emit(`topic:${fullMessage.topic}`, fullMessage);
242
+ }
243
+
244
+ return appendedMessages;
245
+ } catch (error) {
246
+ throw new Error(`Failed to batch append messages: ${error.message}`);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Query messages with filters
252
+ * @param {Object} criteria - Query criteria
253
+ * @returns {Array} Matching messages
254
+ */
255
+ query(criteria) {
256
+ const { cluster_id, topic, sender, receiver, since, until, limit, offset } = criteria;
257
+
258
+ if (!cluster_id) {
259
+ throw new Error('cluster_id is required for queries');
260
+ }
261
+
262
+ // Build query
263
+ const conditions = ['cluster_id = ?'];
264
+ const params = [cluster_id];
265
+
266
+ if (topic) {
267
+ conditions.push('topic = ?');
268
+ params.push(topic);
269
+ }
270
+
271
+ if (sender) {
272
+ conditions.push('sender = ?');
273
+ params.push(sender);
274
+ }
275
+
276
+ if (receiver) {
277
+ conditions.push('receiver = ?');
278
+ params.push(receiver);
279
+ }
280
+
281
+ if (since) {
282
+ conditions.push('timestamp >= ?');
283
+ params.push(typeof since === 'number' ? since : new Date(since).getTime());
284
+ }
285
+
286
+ if (until) {
287
+ conditions.push('timestamp <= ?');
288
+ params.push(typeof until === 'number' ? until : new Date(until).getTime());
289
+ }
290
+
291
+ // Defend against prototype pollution affecting default query ordering.
292
+ // Only treat `criteria.order` as set if it's an own property.
293
+ const orderValue = Object.prototype.hasOwnProperty.call(criteria, 'order')
294
+ ? criteria.order
295
+ : undefined;
296
+ const direction = String(orderValue ?? 'asc').toLowerCase() === 'desc' ? 'DESC' : 'ASC';
297
+ let sql = `SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY timestamp ${direction}`;
298
+
299
+ if (limit) {
300
+ sql += ` LIMIT ?`;
301
+ params.push(limit);
302
+ }
303
+
304
+ if (offset) {
305
+ sql += ` OFFSET ?`;
306
+ params.push(offset);
307
+ }
308
+
309
+ const stmt = this.db.prepare(sql);
310
+ const rows = stmt.all(...params);
311
+ return rows.map((row) => this._deserializeMessage(row));
312
+ }
313
+
314
+ /**
315
+ * Query guidance mailbox for cluster-wide + agent-specific guidance
316
+ * @param {Object} criteria - { cluster_id, target_agent_id, lastDeliveredAt, limit }
317
+ * @returns {Array} Guidance messages ordered by timestamp ASC
318
+ */
319
+ queryGuidanceMailbox(criteria) {
320
+ const { cluster_id, target_agent_id, lastDeliveredAt, limit } = criteria || {};
321
+
322
+ if (!cluster_id) {
323
+ throw new Error('cluster_id is required for guidance mailbox queries');
324
+ }
325
+
326
+ const guidanceTopics = new Set(GUIDANCE_TOPICS);
327
+ if (!guidanceTopics.has(USER_GUIDANCE_CLUSTER) || !guidanceTopics.has(USER_GUIDANCE_AGENT)) {
328
+ throw new Error('GUIDANCE_TOPICS must include USER_GUIDANCE_CLUSTER and USER_GUIDANCE_AGENT');
329
+ }
330
+
331
+ let sinceTimestamp = null;
332
+ if (lastDeliveredAt !== undefined && lastDeliveredAt !== null) {
333
+ const candidate =
334
+ typeof lastDeliveredAt === 'number' ? lastDeliveredAt : new Date(lastDeliveredAt).getTime();
335
+ if (!Number.isFinite(candidate)) {
336
+ throw new Error('lastDeliveredAt must be a number or valid date');
337
+ }
338
+ sinceTimestamp = candidate;
339
+ }
340
+
341
+ const params = [cluster_id, USER_GUIDANCE_CLUSTER];
342
+ let sql = 'SELECT * FROM messages WHERE cluster_id = ? AND (topic = ?';
343
+
344
+ if (target_agent_id) {
345
+ params.push(USER_GUIDANCE_AGENT, target_agent_id);
346
+ sql += ' OR (topic = ? AND receiver = ?)';
347
+ }
348
+
349
+ sql += ')';
350
+
351
+ if (sinceTimestamp !== null) {
352
+ params.push(sinceTimestamp);
353
+ sql += ' AND timestamp > ?';
354
+ }
355
+
356
+ sql += ' ORDER BY timestamp ASC';
357
+
358
+ if (limit) {
359
+ params.push(limit);
360
+ sql += ' LIMIT ?';
361
+ }
362
+
363
+ const stmt = this.db.prepare(sql);
364
+ const rows = stmt.all(...params);
365
+ return rows.map((row) => this._deserializeMessage(row));
366
+ }
367
+
368
+ /**
369
+ * Find the last message matching criteria
370
+ * @param {Object} criteria - Query criteria
371
+ * @returns {Object|null} Last matching message
372
+ */
373
+ findLast(criteria) {
374
+ const { cluster_id, topic, sender, receiver, since, until } = criteria;
375
+
376
+ if (!cluster_id) {
377
+ throw new Error('cluster_id is required for queries');
378
+ }
379
+
380
+ // Build query with DESC order
381
+ const conditions = ['cluster_id = ?'];
382
+ const params = [cluster_id];
383
+
384
+ if (topic) {
385
+ conditions.push('topic = ?');
386
+ params.push(topic);
387
+ }
388
+
389
+ if (sender) {
390
+ conditions.push('sender = ?');
391
+ params.push(sender);
392
+ }
393
+
394
+ if (receiver) {
395
+ conditions.push('receiver = ?');
396
+ params.push(receiver);
397
+ }
398
+
399
+ if (since) {
400
+ conditions.push('timestamp >= ?');
401
+ params.push(typeof since === 'number' ? since : new Date(since).getTime());
402
+ }
403
+
404
+ if (until) {
405
+ conditions.push('timestamp <= ?');
406
+ params.push(typeof until === 'number' ? until : new Date(until).getTime());
407
+ }
408
+
409
+ const sql = `SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY timestamp DESC LIMIT 1`;
410
+
411
+ const stmt = this.db.prepare(sql);
412
+ const row = stmt.get(...params);
413
+ return row ? this._deserializeMessage(row) : null;
414
+ }
415
+
416
+ /**
417
+ * Count messages matching criteria
418
+ * @param {Object} criteria - Query criteria
419
+ * @returns {Number} Message count
420
+ */
421
+ count(criteria) {
422
+ const { cluster_id, topic } = criteria;
423
+
424
+ if (!cluster_id) {
425
+ throw new Error('cluster_id is required for count');
426
+ }
427
+
428
+ let sql = 'SELECT COUNT(*) as count FROM messages WHERE cluster_id = ?';
429
+ const params = [cluster_id];
430
+
431
+ if (topic) {
432
+ sql += ' AND topic = ?';
433
+ params.push(topic);
434
+ }
435
+
436
+ const stmt = this.db.prepare(sql);
437
+ const result = stmt.get(...params);
438
+ return result.count;
439
+ }
440
+
441
+ /**
442
+ * Get messages since a specific timestamp
443
+ * @param {Object} params - { cluster_id, timestamp }
444
+ * @returns {Array} Messages since timestamp
445
+ */
446
+ since(params) {
447
+ return this.query({
448
+ cluster_id: params.cluster_id,
449
+ since: params.timestamp,
450
+ });
451
+ }
452
+
453
+ /**
454
+ * Get all messages for a cluster
455
+ * @param {String} cluster_id - Cluster ID
456
+ * @returns {Array} All messages
457
+ */
458
+ getAll(cluster_id) {
459
+ const rows = this.stmts.getAll.all(cluster_id);
460
+ return rows.map((row) => this._deserializeMessage(row));
461
+ }
462
+
463
+ /**
464
+ * Get aggregated token usage by agent role
465
+ * Queries TOKEN_USAGE messages and sums tokens per role
466
+ * @param {String} cluster_id - Cluster ID
467
+ * @returns {Object} Token usage aggregated by role
468
+ * Example: {
469
+ * implementation: { inputTokens: 5000, outputTokens: 2000, totalCostUsd: 0.05, count: 3 },
470
+ * validator: { inputTokens: 3000, outputTokens: 1500, totalCostUsd: 0.03, count: 2 },
471
+ * _total: { inputTokens: 8000, outputTokens: 3500, totalCostUsd: 0.08, count: 5 }
472
+ * }
473
+ */
474
+ getTokensByRole(cluster_id) {
475
+ if (!cluster_id) {
476
+ throw new Error('cluster_id is required for getTokensByRole');
477
+ }
478
+
479
+ // Query all TOKEN_USAGE messages for this cluster
480
+ const sql = `SELECT * FROM messages WHERE cluster_id = ? AND topic = 'TOKEN_USAGE' ORDER BY timestamp ASC`;
481
+ const stmt = this.db.prepare(sql);
482
+ const rows = stmt.all(cluster_id);
483
+
484
+ const byRole = {};
485
+ const total = {
486
+ inputTokens: 0,
487
+ outputTokens: 0,
488
+ cacheReadInputTokens: 0,
489
+ cacheCreationInputTokens: 0,
490
+ totalCostUsd: 0,
491
+ count: 0,
492
+ };
493
+
494
+ for (const row of rows) {
495
+ const message = this._deserializeMessage(row);
496
+ const data = message.content?.data || {};
497
+ const role = data.role || 'unknown';
498
+
499
+ // Initialize role bucket if needed
500
+ if (!byRole[role]) {
501
+ byRole[role] = {
502
+ inputTokens: 0,
503
+ outputTokens: 0,
504
+ cacheReadInputTokens: 0,
505
+ cacheCreationInputTokens: 0,
506
+ totalCostUsd: 0,
507
+ count: 0,
508
+ };
509
+ }
510
+
511
+ // Aggregate tokens for this role
512
+ byRole[role].inputTokens += data.inputTokens || 0;
513
+ byRole[role].outputTokens += data.outputTokens || 0;
514
+ byRole[role].cacheReadInputTokens += data.cacheReadInputTokens || 0;
515
+ byRole[role].cacheCreationInputTokens += data.cacheCreationInputTokens || 0;
516
+ byRole[role].totalCostUsd += data.totalCostUsd || 0;
517
+ byRole[role].count += 1;
518
+
519
+ // Aggregate totals
520
+ total.inputTokens += data.inputTokens || 0;
521
+ total.outputTokens += data.outputTokens || 0;
522
+ total.cacheReadInputTokens += data.cacheReadInputTokens || 0;
523
+ total.cacheCreationInputTokens += data.cacheCreationInputTokens || 0;
524
+ total.totalCostUsd += data.totalCostUsd || 0;
525
+ total.count += 1;
526
+ }
527
+
528
+ // Add total as special _total key
529
+ byRole._total = total;
530
+
531
+ return byRole;
532
+ }
533
+
534
+ /**
535
+ * Subscribe to new messages
536
+ * @param {Function} callback - Called with each new message
537
+ * @returns {Function} Unsubscribe function
538
+ */
539
+ subscribe(callback) {
540
+ this.on('message', callback);
541
+ return () => this.off('message', callback);
542
+ }
543
+
544
+ /**
545
+ * Poll for new messages (cross-process support)
546
+ * @param {String} clusterId - Cluster ID to poll (null for all clusters)
547
+ * @param {Function} callback - Called with each new message
548
+ * @param {Number} intervalMs - Poll interval (default 500ms)
549
+ * @param {Number} initialCount - Number of messages to show initially (default 300)
550
+ * @returns {Function} Stop polling function
551
+ */
552
+ pollForMessages(clusterId, callback, intervalMs = 500, initialCount = 300) {
553
+ let lastTimestamp = 0;
554
+ let lastMessageIds = new Set();
555
+ let isFirstPoll = true;
556
+
557
+ const poll = () => {
558
+ try {
559
+ let sql, params;
560
+
561
+ if (isFirstPoll) {
562
+ // First poll: get last N messages by count
563
+ if (clusterId) {
564
+ sql =
565
+ 'SELECT * FROM (SELECT * FROM messages WHERE cluster_id = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC';
566
+ params = [clusterId, initialCount];
567
+ } else {
568
+ sql =
569
+ 'SELECT * FROM (SELECT * FROM messages ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC';
570
+ params = [initialCount];
571
+ }
572
+ isFirstPoll = false;
573
+ } else {
574
+ // Subsequent polls: get messages since last timestamp
575
+ if (clusterId) {
576
+ sql =
577
+ 'SELECT * FROM messages WHERE cluster_id = ? AND timestamp >= ? ORDER BY timestamp ASC';
578
+ params = [clusterId, lastTimestamp - 1000]; // 1s buffer for race conditions
579
+ } else {
580
+ sql = 'SELECT * FROM messages WHERE timestamp >= ? ORDER BY timestamp ASC';
581
+ params = [lastTimestamp - 1000];
582
+ }
583
+ }
584
+
585
+ const stmt = this.db.prepare(sql);
586
+ const rows = stmt.all(...params);
587
+
588
+ for (const row of rows) {
589
+ // Skip already-seen messages
590
+ if (lastMessageIds.has(row.id)) continue;
591
+
592
+ lastMessageIds.add(row.id);
593
+ const message = this._deserializeMessage(row);
594
+ callback(message);
595
+
596
+ // Update timestamp high-water mark
597
+ if (row.timestamp > lastTimestamp) {
598
+ lastTimestamp = row.timestamp;
599
+ }
600
+ }
601
+
602
+ // Prune old message IDs to prevent memory leak
603
+ if (lastMessageIds.size > 10000) {
604
+ const idsArray = Array.from(lastMessageIds);
605
+ lastMessageIds = new Set(idsArray.slice(-5000));
606
+ }
607
+ } catch (error) {
608
+ // DB busy is expected during concurrent access - log but continue polling
609
+ // Other errors indicate real bugs and should be visible
610
+ console.error(`[Ledger] pollForMessages error (will retry): ${error.message}`);
611
+ }
612
+ };
613
+
614
+ // Initial poll
615
+ poll();
616
+
617
+ // Set up interval
618
+ const intervalId = setInterval(poll, intervalMs);
619
+
620
+ // Return stop function
621
+ return () => clearInterval(intervalId);
622
+ }
623
+
624
+ /**
625
+ * Subscribe to specific topic
626
+ * @param {String} topic - Topic to subscribe to
627
+ * @param {Function} callback - Called with matching messages
628
+ * @returns {Function} Unsubscribe function
629
+ */
630
+ subscribeTopic(topic, callback) {
631
+ const event = `topic:${topic}`;
632
+ this.on(event, callback);
633
+ return () => this.off(event, callback);
634
+ }
635
+
636
+ /**
637
+ * Deserialize a database row into a message object
638
+ * @private
639
+ */
640
+ _deserializeMessage(row) {
641
+ const message = {
642
+ id: row.id,
643
+ timestamp: row.timestamp,
644
+ topic: row.topic,
645
+ sender: row.sender,
646
+ receiver: row.receiver,
647
+ cluster_id: row.cluster_id,
648
+ };
649
+
650
+ if (row.content_text || row.content_data) {
651
+ message.content = {};
652
+ if (row.content_text) {
653
+ message.content.text = row.content_text;
654
+ }
655
+ if (row.content_data) {
656
+ try {
657
+ message.content.data = JSON.parse(row.content_data);
658
+ } catch {
659
+ message.content.data = null;
660
+ }
661
+ }
662
+ }
663
+
664
+ if (row.metadata) {
665
+ try {
666
+ message.metadata = JSON.parse(row.metadata);
667
+ } catch {
668
+ message.metadata = null;
669
+ }
670
+ }
671
+
672
+ return message;
673
+ }
674
+
675
+ /**
676
+ * Close the database connection
677
+ */
678
+ close() {
679
+ this._closed = true; // Set flag BEFORE closing to prevent race conditions
680
+ this.db.close();
681
+ }
682
+
683
+ /**
684
+ * Clear all messages (for testing)
685
+ */
686
+ clear() {
687
+ this.db.exec('DELETE FROM messages');
688
+ this.cache.clear();
689
+ }
690
+ }
691
+
692
+ module.exports = Ledger;