@kontextmind/kxm 0.6.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 (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,637 @@
1
+ import { resolve } from "node:path";
2
+ import type { DatabaseSync } from "node:sqlite";
3
+ import type { AgentRecord, MessageRecord } from "./protocol.ts";
4
+ import type { ContextItem } from "./context.ts";
5
+ import type { WorkflowJournalEntry, WorkflowRun } from "./workflow.ts";
6
+ import {
7
+ openDatabase,
8
+ withDatabaseTransaction,
9
+ type DatabaseMigrationStep,
10
+ type DatabaseSchemaSpec,
11
+ } from "./database.ts";
12
+
13
+ export const HUB_STORE_SCHEMA_VERSION = 3;
14
+
15
+ export const HUB_STORE_TABLES: Readonly<Record<string, readonly string[]>> = Object.freeze({
16
+ agents: Object.freeze(["id", "record"]),
17
+ messages: Object.freeze(["id", "record"]),
18
+ consumer_cursors: Object.freeze(["agent_id", "cursor"]),
19
+ agent_sequences: Object.freeze(["agent_id", "next_seq"]),
20
+ workflow_runs: Object.freeze(["id", "definition_id", "delivery_id", "record"]),
21
+ workflow_journal: Object.freeze(["id", "run_id", "category", "area", "record"]),
22
+ context_items: Object.freeze(["id", "project", "kind", "record"]),
23
+ });
24
+
25
+ export const HUB_STORE_SCHEMA_V3 = `
26
+ CREATE TABLE IF NOT EXISTS agents (
27
+ id TEXT PRIMARY KEY,
28
+ record TEXT NOT NULL
29
+ ) STRICT;
30
+ CREATE TABLE IF NOT EXISTS messages (
31
+ id TEXT PRIMARY KEY,
32
+ record TEXT NOT NULL
33
+ ) STRICT;
34
+ CREATE UNIQUE INDEX IF NOT EXISTS messages_from_idempotency
35
+ ON messages(
36
+ json_extract(record, '$.from'),
37
+ json_extract(record, '$.idempotencyKey')
38
+ ) WHERE json_extract(record, '$.idempotencyKey') IS NOT NULL;
39
+ CREATE INDEX IF NOT EXISTS messages_to_seq
40
+ ON messages(
41
+ json_extract(record, '$.to'),
42
+ COALESCE(json_extract(record, '$.seq'), 0)
43
+ );
44
+ CREATE TABLE IF NOT EXISTS consumer_cursors (
45
+ agent_id TEXT PRIMARY KEY,
46
+ cursor INTEGER NOT NULL
47
+ ) STRICT;
48
+ CREATE TABLE IF NOT EXISTS agent_sequences (
49
+ agent_id TEXT PRIMARY KEY,
50
+ next_seq INTEGER NOT NULL
51
+ ) STRICT;
52
+ CREATE TABLE IF NOT EXISTS workflow_runs (
53
+ id TEXT PRIMARY KEY,
54
+ definition_id TEXT NOT NULL,
55
+ delivery_id TEXT NOT NULL,
56
+ record TEXT NOT NULL,
57
+ UNIQUE(definition_id, delivery_id)
58
+ ) STRICT;
59
+ CREATE TABLE IF NOT EXISTS workflow_journal (
60
+ id TEXT PRIMARY KEY,
61
+ run_id TEXT NOT NULL,
62
+ category TEXT NOT NULL,
63
+ area TEXT NOT NULL,
64
+ record TEXT NOT NULL
65
+ ) STRICT;
66
+ CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
67
+ CREATE TABLE IF NOT EXISTS context_items (
68
+ id TEXT PRIMARY KEY,
69
+ project TEXT NOT NULL,
70
+ kind TEXT NOT NULL,
71
+ record TEXT NOT NULL
72
+ ) STRICT;
73
+ CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
74
+ `;
75
+
76
+ export const HUB_STORE_MIGRATIONS: readonly DatabaseMigrationStep[] = Object.freeze([
77
+ {
78
+ fromVersion: 1,
79
+ toVersion: 2,
80
+ migrate: (db: DatabaseSync) => {
81
+ db.exec(`
82
+ CREATE TABLE IF NOT EXISTS agents (
83
+ id TEXT PRIMARY KEY,
84
+ record TEXT NOT NULL
85
+ ) STRICT;
86
+ CREATE TABLE IF NOT EXISTS messages (
87
+ id TEXT PRIMARY KEY,
88
+ record TEXT NOT NULL
89
+ ) STRICT;
90
+ CREATE TABLE IF NOT EXISTS workflow_runs (
91
+ id TEXT PRIMARY KEY,
92
+ definition_id TEXT NOT NULL,
93
+ delivery_id TEXT NOT NULL,
94
+ record TEXT NOT NULL,
95
+ UNIQUE(definition_id, delivery_id)
96
+ ) STRICT;
97
+ CREATE TABLE IF NOT EXISTS workflow_journal (
98
+ id TEXT PRIMARY KEY,
99
+ run_id TEXT NOT NULL,
100
+ category TEXT NOT NULL,
101
+ area TEXT NOT NULL,
102
+ record TEXT NOT NULL
103
+ ) STRICT;
104
+ CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
105
+ `);
106
+ },
107
+ },
108
+ {
109
+ fromVersion: 2,
110
+ toVersion: 3,
111
+ migrate: (db: DatabaseSync) => {
112
+ db.exec(`
113
+ CREATE TABLE IF NOT EXISTS agents (
114
+ id TEXT PRIMARY KEY,
115
+ record TEXT NOT NULL
116
+ ) STRICT;
117
+ CREATE TABLE IF NOT EXISTS messages (
118
+ id TEXT PRIMARY KEY,
119
+ record TEXT NOT NULL
120
+ ) STRICT;
121
+ CREATE TABLE IF NOT EXISTS workflow_runs (
122
+ id TEXT PRIMARY KEY,
123
+ definition_id TEXT NOT NULL,
124
+ delivery_id TEXT NOT NULL,
125
+ record TEXT NOT NULL,
126
+ UNIQUE(definition_id, delivery_id)
127
+ ) STRICT;
128
+ CREATE TABLE IF NOT EXISTS workflow_journal (
129
+ id TEXT PRIMARY KEY,
130
+ run_id TEXT NOT NULL,
131
+ category TEXT NOT NULL,
132
+ area TEXT NOT NULL,
133
+ record TEXT NOT NULL
134
+ ) STRICT;
135
+ CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
136
+ CREATE TABLE IF NOT EXISTS context_items (
137
+ id TEXT PRIMARY KEY,
138
+ project TEXT NOT NULL,
139
+ kind TEXT NOT NULL,
140
+ record TEXT NOT NULL
141
+ ) STRICT;
142
+ CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
143
+ CREATE UNIQUE INDEX IF NOT EXISTS messages_from_idempotency
144
+ ON messages(
145
+ json_extract(record, '$.from'),
146
+ json_extract(record, '$.idempotencyKey')
147
+ ) WHERE json_extract(record, '$.idempotencyKey') IS NOT NULL;
148
+ CREATE INDEX IF NOT EXISTS messages_to_seq
149
+ ON messages(
150
+ json_extract(record, '$.to'),
151
+ COALESCE(json_extract(record, '$.seq'), 0)
152
+ );
153
+ CREATE TABLE IF NOT EXISTS consumer_cursors (
154
+ agent_id TEXT PRIMARY KEY,
155
+ cursor INTEGER NOT NULL
156
+ ) STRICT;
157
+ CREATE TABLE IF NOT EXISTS agent_sequences (
158
+ agent_id TEXT PRIMARY KEY,
159
+ next_seq INTEGER NOT NULL
160
+ ) STRICT;
161
+ `);
162
+ },
163
+ },
164
+ ]);
165
+
166
+ export const HUB_STORE_SCHEMA_SPEC: DatabaseSchemaSpec = Object.freeze({
167
+ schema: HUB_STORE_SCHEMA_V3,
168
+ version: HUB_STORE_SCHEMA_VERSION,
169
+ tables: HUB_STORE_TABLES,
170
+ migrations: HUB_STORE_MIGRATIONS,
171
+ });
172
+
173
+ export interface StoredAgent extends AgentRecord {
174
+ key: string;
175
+ }
176
+
177
+ class MessageMap extends Map<string, MessageRecord> {
178
+ private readonly store: MeshStore;
179
+
180
+ constructor(store: MeshStore) {
181
+ super();
182
+ this.store = store;
183
+ }
184
+
185
+ override get(id: string): MessageRecord | undefined {
186
+ const cached = super.get(id);
187
+ if (cached) return cached;
188
+ return this.store.loadMessageFromDb(id);
189
+ }
190
+
191
+ override has(id: string): boolean {
192
+ if (super.has(id)) return true;
193
+ return this.store.hasMessage(id);
194
+ }
195
+
196
+ override delete(id: string): boolean {
197
+ this.store.deleteMessage(id);
198
+ return super.delete(id);
199
+ }
200
+
201
+ override get size(): number {
202
+ return this.store.countMessages();
203
+ }
204
+
205
+ get cachedSize(): number {
206
+ return super.size;
207
+ }
208
+ }
209
+
210
+ export class MeshStore {
211
+ readonly agents = new Map<string, StoredAgent>();
212
+ readonly messages: MessageMap;
213
+ readonly workflowRuns = new Map<string, WorkflowRun>();
214
+ readonly journal = new Map<string, WorkflowJournalEntry>();
215
+ readonly contextItems = new Map<string, ContextItem>();
216
+ readonly path?: string;
217
+ private readonly database?: DatabaseSync;
218
+ private readonly agentSequences = new Map<string, number>();
219
+ private readonly consumerCursors = new Map<string, number>();
220
+
221
+ constructor(path?: string) {
222
+ this.messages = new MessageMap(this);
223
+ if (!path) return;
224
+ this.path = path === ":memory:" ? path : resolve(path);
225
+ this.database = openDatabase(this.path, "hub database", HUB_STORE_SCHEMA_SPEC);
226
+ this.load();
227
+ }
228
+
229
+ get persistent(): boolean {
230
+ return Boolean(this.database && this.path !== ":memory:");
231
+ }
232
+
233
+ saveAgent(agent: StoredAgent): void {
234
+ this.agents.set(agent.id, agent);
235
+ this.database?.prepare(`
236
+ INSERT INTO agents (id, record) VALUES (?, ?)
237
+ ON CONFLICT(id) DO UPDATE SET record = excluded.record
238
+ `).run(agent.id, JSON.stringify(agent));
239
+ }
240
+
241
+ saveMessage(message: MessageRecord): void {
242
+ Map.prototype.set.call(this.messages, message.id, message);
243
+ this.database?.prepare(`
244
+ INSERT INTO messages (id, record) VALUES (?, ?)
245
+ ON CONFLICT(id) DO UPDATE SET record = excluded.record
246
+ `).run(message.id, JSON.stringify(message));
247
+ }
248
+
249
+ deleteMessage(messageId: string): void {
250
+ Map.prototype.delete.call(this.messages, messageId);
251
+ this.database?.prepare("DELETE FROM messages WHERE id = ?").run(messageId);
252
+ }
253
+
254
+ loadMessageFromDb(id: string): MessageRecord | undefined {
255
+ if (!this.database) return undefined;
256
+ const row = this.database.prepare("SELECT record FROM messages WHERE id = ?").get(id) as { record: string } | undefined;
257
+ if (!row) return undefined;
258
+ try {
259
+ const msg = JSON.parse(row.record) as MessageRecord;
260
+ Map.prototype.set.call(this.messages, msg.id, msg);
261
+ return msg;
262
+ } catch {
263
+ return undefined;
264
+ }
265
+ }
266
+
267
+ hasMessage(id: string): boolean {
268
+ if (Map.prototype.has.call(this.messages, id)) return true;
269
+ if (!this.database) return false;
270
+ const row = this.database.prepare("SELECT 1 AS ok FROM messages WHERE id = ?").get(id) as { ok?: number } | undefined;
271
+ return Boolean(row?.ok);
272
+ }
273
+
274
+ countMessages(): number {
275
+ if (!this.database) return [...Map.prototype.keys.call(this.messages)].length;
276
+ const row = this.database.prepare("SELECT COUNT(*) AS total FROM messages").get() as { total: number } | undefined;
277
+ return Number(row?.total ?? 0);
278
+ }
279
+
280
+ nextAgentSequence(agentId: string): number {
281
+ if (!this.database) {
282
+ const current = this.agentSequences.get(agentId) ?? 0;
283
+ const next = current + 1;
284
+ this.agentSequences.set(agentId, next);
285
+ return next;
286
+ }
287
+ this.database.exec("BEGIN IMMEDIATE");
288
+ try {
289
+ const row = this.database.prepare("SELECT next_seq FROM agent_sequences WHERE agent_id = ?").get(agentId) as { next_seq: number } | undefined;
290
+ let next = row?.next_seq;
291
+ if (next === undefined) {
292
+ const maxRow = this.database.prepare(`
293
+ SELECT COALESCE(MAX(json_extract(record, '$.seq')), 0) AS max_seq
294
+ FROM messages WHERE json_extract(record, '$.to') = ?
295
+ `).get(agentId) as { max_seq: number } | undefined;
296
+ next = Number(maxRow?.max_seq ?? 0) + 1;
297
+ }
298
+ this.database.prepare(`
299
+ INSERT INTO agent_sequences (agent_id, next_seq) VALUES (?, ?)
300
+ ON CONFLICT(agent_id) DO UPDATE SET next_seq = excluded.next_seq
301
+ `).run(agentId, next + 1);
302
+ this.database.exec("COMMIT");
303
+ this.agentSequences.set(agentId, next + 1);
304
+ return next;
305
+ } catch (err) {
306
+ try { this.database.exec("ROLLBACK"); } catch { /* rollback */ }
307
+ throw err;
308
+ }
309
+ }
310
+
311
+ getConsumerCursor(agentId: string): number {
312
+ if (!this.database) {
313
+ return this.consumerCursors.get(agentId) ?? 0;
314
+ }
315
+ const row = this.database.prepare("SELECT cursor FROM consumer_cursors WHERE agent_id = ?").get(agentId) as { cursor: number } | undefined;
316
+ const cursor = Number(row?.cursor ?? 0);
317
+ this.consumerCursors.set(agentId, cursor);
318
+ return cursor;
319
+ }
320
+
321
+ advanceCursor(agentId: string, seq: number): number {
322
+ if (!this.database) {
323
+ const current = this.consumerCursors.get(agentId) ?? 0;
324
+ const next = Math.max(current, seq);
325
+ this.consumerCursors.set(agentId, next);
326
+ return next;
327
+ }
328
+ this.database.prepare(`
329
+ INSERT INTO consumer_cursors (agent_id, cursor) VALUES (?, ?)
330
+ ON CONFLICT(agent_id) DO UPDATE SET cursor = MAX(consumer_cursors.cursor, excluded.cursor)
331
+ `).run(agentId, seq);
332
+ return this.getConsumerCursor(agentId);
333
+ }
334
+
335
+ getPendingMessages(agentId: string, cursor = 0): MessageRecord[] {
336
+ if (!this.database) {
337
+ return [...Map.prototype.values.call(this.messages)]
338
+ .filter((m) => m.to === agentId && (m.status === "queued" || m.status === "delivered") && (m.seq ?? 0) > cursor)
339
+ .sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
340
+ }
341
+ const rows = this.database.prepare(`
342
+ SELECT record FROM messages
343
+ WHERE json_extract(record, '$.to') = ?
344
+ AND json_extract(record, '$.status') IN ('queued', 'delivered')
345
+ AND COALESCE(json_extract(record, '$.seq'), 0) > ?
346
+ ORDER BY COALESCE(json_extract(record, '$.seq'), 0) ASC
347
+ `).all(agentId, cursor) as Array<{ record: string }>;
348
+ const result: MessageRecord[] = [];
349
+ for (const row of rows) {
350
+ try {
351
+ const msg = JSON.parse(row.record) as MessageRecord;
352
+ Map.prototype.set.call(this.messages, msg.id, msg);
353
+ result.push(msg);
354
+ } catch { /* skip */ }
355
+ }
356
+ return result;
357
+ }
358
+
359
+ findMessageByIdempotency(fromId: string, idempotencyKey: string): MessageRecord | undefined {
360
+ for (const m of Map.prototype.values.call(this.messages)) {
361
+ if (m.from === fromId && m.idempotencyKey === idempotencyKey) return m;
362
+ }
363
+ if (!this.database) return undefined;
364
+ const row = this.database.prepare(`
365
+ SELECT record FROM messages
366
+ WHERE json_extract(record, '$.from') = ? AND json_extract(record, '$.idempotencyKey') = ?
367
+ `).get(fromId, idempotencyKey) as { record: string } | undefined;
368
+ if (!row) return undefined;
369
+ try {
370
+ const msg = JSON.parse(row.record) as MessageRecord;
371
+ Map.prototype.set.call(this.messages, msg.id, msg);
372
+ return msg;
373
+ } catch {
374
+ return undefined;
375
+ }
376
+ }
377
+
378
+ getOpenMessages(project: string): MessageRecord[] {
379
+ if (!this.database) {
380
+ return [...Map.prototype.values.call(this.messages)]
381
+ .filter((m) => m.project === project && (m.status === "queued" || m.status === "delivered"))
382
+ .sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
383
+ }
384
+ const rows = this.database.prepare(`
385
+ SELECT record FROM messages
386
+ WHERE json_extract(record, '$.project') = ?
387
+ AND json_extract(record, '$.status') IN ('queued', 'delivered')
388
+ ORDER BY json_extract(record, '$.createdAt') DESC
389
+ `).all(project) as Array<{ record: string }>;
390
+ const result: MessageRecord[] = [];
391
+ for (const row of rows) {
392
+ try {
393
+ const msg = JSON.parse(row.record) as MessageRecord;
394
+ Map.prototype.set.call(this.messages, msg.id, msg);
395
+ result.push(msg);
396
+ } catch { /* skip */ }
397
+ }
398
+ return result;
399
+ }
400
+
401
+ getExpiringMessages(nowMs = Date.now()): MessageRecord[] {
402
+ const nowIso = new Date(nowMs).toISOString();
403
+ if (!this.database) {
404
+ return [...Map.prototype.values.call(this.messages)]
405
+ .filter((m) => (m.status === "queued" || m.status === "delivered") && Date.parse(m.expiresAt) <= nowMs);
406
+ }
407
+ const rows = this.database.prepare(`
408
+ SELECT record FROM messages
409
+ WHERE json_extract(record, '$.status') IN ('queued', 'delivered')
410
+ AND json_extract(record, '$.expiresAt') <= ?
411
+ `).all(nowIso) as Array<{ record: string }>;
412
+ const result: MessageRecord[] = [];
413
+ for (const row of rows) {
414
+ try {
415
+ const msg = JSON.parse(row.record) as MessageRecord;
416
+ Map.prototype.set.call(this.messages, msg.id, msg);
417
+ result.push(msg);
418
+ } catch { /* skip */ }
419
+ }
420
+ return result;
421
+ }
422
+
423
+ deleteWorkflowRun(runId: string): void {
424
+ this.workflowRuns.delete(runId);
425
+ this.database?.prepare("DELETE FROM workflow_runs WHERE id = ?").run(runId);
426
+ }
427
+
428
+ deleteJournalEntry(id: string): void {
429
+ this.journal.delete(id);
430
+ this.database?.prepare("DELETE FROM workflow_journal WHERE id = ?").run(id);
431
+ }
432
+
433
+ deleteContextItem(id: string): void {
434
+ this.contextItems.delete(id);
435
+ this.database?.prepare("DELETE FROM context_items WHERE id = ?").run(id);
436
+ }
437
+
438
+ sweepRetention(
439
+ messageRetentionMs: number,
440
+ runRetentionMs = 7 * 86_400_000,
441
+ nowMs = Date.now(),
442
+ ): {
443
+ purgedMessages: MessageRecord[];
444
+ purgedRuns: string[];
445
+ purgedJournal: string[];
446
+ purgedContextItems: string[];
447
+ } {
448
+ const messageCutoffMs = nowMs - messageRetentionMs;
449
+ const messageCutoffIso = new Date(messageCutoffMs).toISOString();
450
+ const runCutoffMs = nowMs - runRetentionMs;
451
+ const purgedMessages: MessageRecord[] = [];
452
+ const purgedRuns: string[] = [];
453
+ const purgedJournal: string[] = [];
454
+ const purgedContextItems: string[] = [];
455
+
456
+ // 1. Messages
457
+ if (!this.database) {
458
+ for (const m of [...Map.prototype.values.call(this.messages)]) {
459
+ const terminal = m.status === "replied" || m.status === "cancelled" || m.status === "expired" || m.status === "error";
460
+ if (!terminal) continue;
461
+ const terminalAt = m.repliedAt ?? m.cancelledAt ?? m.expiresAt ?? m.createdAt;
462
+ if (Date.parse(terminalAt) <= messageCutoffMs) {
463
+ Map.prototype.delete.call(this.messages, m.id);
464
+ purgedMessages.push(m);
465
+ }
466
+ }
467
+ } else {
468
+ const rows = this.database.prepare(`
469
+ SELECT record FROM messages
470
+ WHERE json_extract(record, '$.status') IN ('replied', 'cancelled', 'expired', 'error')
471
+ AND COALESCE(json_extract(record, '$.repliedAt'), json_extract(record, '$.cancelledAt'), json_extract(record, '$.expiresAt'), json_extract(record, '$.createdAt')) <= ?
472
+ `).all(messageCutoffIso) as Array<{ record: string }>;
473
+ for (const row of rows) {
474
+ try {
475
+ const msg = JSON.parse(row.record) as MessageRecord;
476
+ Map.prototype.delete.call(this.messages, msg.id);
477
+ this.database.prepare("DELETE FROM messages WHERE id = ?").run(msg.id);
478
+ purgedMessages.push(msg);
479
+ } catch { /* skip */ }
480
+ }
481
+ }
482
+
483
+ // 2. Runs & their journal entries
484
+ const terminalRunsToPurge: string[] = [];
485
+ for (const run of this.workflowRuns.values()) {
486
+ const terminal = run.status === "completed" || run.status === "failed";
487
+ if (!terminal) continue;
488
+ const terminalAt = run.updatedAt ?? run.createdAt;
489
+ if (Date.parse(terminalAt) <= runCutoffMs) {
490
+ terminalRunsToPurge.push(run.id);
491
+ }
492
+ }
493
+ for (const runId of terminalRunsToPurge) {
494
+ this.deleteWorkflowRun(runId);
495
+ purgedRuns.push(runId);
496
+ const toDeleteJournal: string[] = [];
497
+ for (const entry of this.journal.values()) {
498
+ if (entry.runId === runId) toDeleteJournal.push(entry.id);
499
+ }
500
+ for (const jid of toDeleteJournal) {
501
+ this.deleteJournalEntry(jid);
502
+ purgedJournal.push(jid);
503
+ }
504
+ }
505
+
506
+ // 3. Orphan journal entries
507
+ const orphanJournal: string[] = [];
508
+ for (const entry of this.journal.values()) {
509
+ if (Date.parse(entry.createdAt) <= runCutoffMs && !this.workflowRuns.has(entry.runId)) {
510
+ orphanJournal.push(entry.id);
511
+ }
512
+ }
513
+ for (const jid of orphanJournal) {
514
+ this.deleteJournalEntry(jid);
515
+ purgedJournal.push(jid);
516
+ }
517
+
518
+ // 4. Context items
519
+ const contextItemsToPurge: string[] = [];
520
+ for (const item of this.contextItems.values()) {
521
+ const terminal = item.status === "superseded" || item.status === "rejected" || (item.validUntil !== undefined && Date.parse(item.validUntil) <= runCutoffMs);
522
+ if (!terminal) continue;
523
+ const terminalAt = item.validUntil ?? item.observedAt;
524
+ if (terminalAt && Date.parse(terminalAt) <= runCutoffMs) {
525
+ contextItemsToPurge.push(item.id);
526
+ }
527
+ }
528
+ for (const id of contextItemsToPurge) {
529
+ this.deleteContextItem(id);
530
+ purgedContextItems.push(id);
531
+ }
532
+
533
+ return { purgedMessages, purgedRuns, purgedJournal, purgedContextItems };
534
+ }
535
+
536
+ saveWorkflowRun(run: WorkflowRun): void {
537
+ this.workflowRuns.set(run.id, run);
538
+ this.database?.prepare(`
539
+ INSERT INTO workflow_runs (id, definition_id, delivery_id, record) VALUES (?, ?, ?, ?)
540
+ ON CONFLICT(id) DO UPDATE SET record = excluded.record
541
+ `).run(run.id, run.definitionId, run.deliveryId, JSON.stringify(run));
542
+ }
543
+
544
+ saveJournalEntry(entry: WorkflowJournalEntry): void {
545
+ this.journal.set(entry.id, entry);
546
+ this.database?.prepare(`
547
+ INSERT INTO workflow_journal (id, run_id, category, area, record) VALUES (?, ?, ?, ?, ?)
548
+ ON CONFLICT(id) DO UPDATE SET record = excluded.record
549
+ `).run(entry.id, entry.runId, entry.category, entry.area, JSON.stringify(entry));
550
+ }
551
+
552
+ saveContextItem(item: ContextItem): void {
553
+ this.contextItems.set(item.id, item);
554
+ this.database?.prepare(`
555
+ INSERT INTO context_items (id, project, kind, record) VALUES (?, ?, ?, ?)
556
+ ON CONFLICT(id) DO UPDATE SET record = excluded.record
557
+ `).run(item.id, item.project, item.kind, JSON.stringify(item));
558
+ }
559
+
560
+ getContextItem(id: string, project?: string): ContextItem | undefined {
561
+ const item = this.contextItems.get(id);
562
+ if (!item) return undefined;
563
+ if (project !== undefined && item.project !== project) return undefined;
564
+ return item;
565
+ }
566
+
567
+ listContextItems(project: string, kinds?: ContextItem["kind"][]): ContextItem[] {
568
+ const wanted = kinds ? new Set(kinds) : undefined;
569
+ return [...this.contextItems.values()]
570
+ .filter((item) => item.project === project)
571
+ .filter((item) => wanted === undefined || wanted.has(item.kind))
572
+ .sort((left, right) => left.id.localeCompare(right.id));
573
+ }
574
+
575
+ saveWorkflowTransition(
576
+ run: WorkflowRun,
577
+ message?: MessageRecord,
578
+ entry?: WorkflowJournalEntry,
579
+ ): void {
580
+ if (this.database) {
581
+ withDatabaseTransaction(this.database, () => {
582
+ if (message) {
583
+ this.database!.prepare(`
584
+ INSERT INTO messages (id, record) VALUES (?, ?)
585
+ ON CONFLICT(id) DO UPDATE SET record = excluded.record
586
+ `).run(message.id, JSON.stringify(message));
587
+ }
588
+ if (entry) {
589
+ this.database!.prepare(`
590
+ INSERT INTO workflow_journal (id, run_id, category, area, record) VALUES (?, ?, ?, ?, ?)
591
+ ON CONFLICT(id) DO UPDATE SET record = excluded.record
592
+ `).run(entry.id, entry.runId, entry.category, entry.area, JSON.stringify(entry));
593
+ }
594
+ this.database!.prepare(`
595
+ INSERT INTO workflow_runs (id, definition_id, delivery_id, record) VALUES (?, ?, ?, ?)
596
+ ON CONFLICT(id) DO UPDATE SET record = excluded.record
597
+ `).run(run.id, run.definitionId, run.deliveryId, JSON.stringify(run));
598
+ });
599
+ }
600
+ if (message) Map.prototype.set.call(this.messages, message.id, message);
601
+ if (entry) this.journal.set(entry.id, entry);
602
+ this.workflowRuns.set(run.id, run);
603
+ }
604
+
605
+ healthy(): boolean {
606
+ if (!this.database) return true;
607
+ return this.database.prepare("SELECT 1 AS ok").get() !== undefined;
608
+ }
609
+
610
+ close(): void {
611
+ this.database?.close();
612
+ }
613
+
614
+ private load(): void {
615
+ if (!this.database) return;
616
+ const agentRows = this.database.prepare("SELECT record FROM agents").all() as Array<{ record: string }>;
617
+ const workflowRows = this.database.prepare("SELECT record FROM workflow_runs").all() as Array<{ record: string }>;
618
+ const journalRows = this.database.prepare("SELECT record FROM workflow_journal").all() as Array<{ record: string }>;
619
+ const contextRows = this.database.prepare("SELECT record FROM context_items").all() as Array<{ record: string }>;
620
+ for (const row of agentRows) {
621
+ const agent = JSON.parse(row.record) as StoredAgent;
622
+ this.agents.set(agent.id, agent);
623
+ }
624
+ for (const row of workflowRows) {
625
+ const run = JSON.parse(row.record) as WorkflowRun;
626
+ this.workflowRuns.set(run.id, run);
627
+ }
628
+ for (const row of journalRows) {
629
+ const entry = JSON.parse(row.record) as WorkflowJournalEntry;
630
+ this.journal.set(entry.id, entry);
631
+ }
632
+ for (const row of contextRows) {
633
+ const item = JSON.parse(row.record) as ContextItem;
634
+ this.contextItems.set(item.id, item);
635
+ }
636
+ }
637
+ }