@wopr-network/defcon 1.12.0 → 1.14.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 (30) hide show
  1. package/README.md +44 -0
  2. package/dist/src/engine/engine.js +12 -0
  3. package/dist/src/execution/cli.js +19 -2
  4. package/dist/src/repositories/drizzle/domain-event.repo.d.ts +1 -0
  5. package/dist/src/repositories/drizzle/domain-event.repo.js +4 -1
  6. package/dist/src/repositories/drizzle/entity-snapshot.repo.d.ts +14 -0
  7. package/dist/src/repositories/drizzle/entity-snapshot.repo.js +68 -0
  8. package/dist/src/repositories/drizzle/entity.repo.d.ts +1 -0
  9. package/dist/src/repositories/drizzle/entity.repo.js +13 -0
  10. package/dist/src/repositories/drizzle/index.d.ts +1 -0
  11. package/dist/src/repositories/drizzle/index.js +1 -0
  12. package/dist/src/repositories/drizzle/schema.d.ts +329 -0
  13. package/dist/src/repositories/drizzle/schema.js +23 -0
  14. package/dist/src/repositories/event-sourced/entity.repo.d.ts +30 -0
  15. package/dist/src/repositories/event-sourced/entity.repo.js +88 -0
  16. package/dist/src/repositories/event-sourced/index.d.ts +3 -0
  17. package/dist/src/repositories/event-sourced/index.js +3 -0
  18. package/dist/src/repositories/event-sourced/invocation.repo.d.ts +20 -0
  19. package/dist/src/repositories/event-sourced/invocation.repo.js +61 -0
  20. package/dist/src/repositories/event-sourced/replay.d.ts +11 -0
  21. package/dist/src/repositories/event-sourced/replay.js +150 -0
  22. package/dist/src/repositories/interfaces.d.ts +13 -0
  23. package/drizzle/0015_clean_redwing.sql +11 -0
  24. package/drizzle/0016_hesitant_bill_hollister.sql +22 -0
  25. package/drizzle/0017_hesitant_bill_hollister.sql +22 -0
  26. package/drizzle/0018_volatile_reavers.sql +1 -0
  27. package/drizzle/meta/0016_snapshot.json +1316 -0
  28. package/drizzle/meta/0018_snapshot.json +1351 -0
  29. package/drizzle/meta/_journal.json +15 -1
  30. package/package.json +1 -1
package/README.md CHANGED
@@ -204,6 +204,50 @@ npx defcon run --flow my-pipeline
204
204
 
205
205
  Same flow. Same gates. Same escalation. The only difference is who's turning the crank — your agent or DEFCON's runner. Either way, the work doesn't advance until the evidence says it should.
206
206
 
207
+ ## The Deeper Truth: Defcon Is a Prompt Engineering State Machine
208
+
209
+ The manifesto above tells you why gates matter. Here's the insight that changes how you think about everything else.
210
+
211
+ Defcon is not an orchestration engine that happens to give prompts to agents. **Defcon is a prompt engineering state machine.** Every state is a prompt. Every transition is a context transformation. Every gate is a deterministic filter that decides what prompt the agent gets next — or whether it gets one at all.
212
+
213
+ The flow definition is the engineering artifact. Not the agent code. Not the model selection. The flow.
214
+
215
+ ### Context Assembly Is the Contract
216
+
217
+ An agent invocation is expensive. An agent invocation where the agent spends tool calls reading its own issue, checking CI status, or finding the PR it's supposed to review — that is a flow engineering defect. The onEnter hook should have assembled that context before the agent fired.
218
+
219
+ **Every tool call an agent makes to gather context is a failure of the flow definition to provide it.**
220
+
221
+ When the architect calls Linear to read the issue description — that's already in `entity.refs.linear.description`. Put it in the prompt template. When the coder calls `gh pr list` — that's a missing onEnter hook. When the reviewer runs `gh pr checks` — the gate already verified this.
222
+
223
+ The measure of a well-engineered state is: **can the agent complete its job with zero context-gathering tool calls?** Every tool call should be *work* — writing code, posting comments, running tests — never reconnaissance.
224
+
225
+ ### Gates Are Prompt Qualification
226
+
227
+ A gate doesn't just verify that work is done. A gate verifies that **the next state's context can be assembled completely**.
228
+
229
+ `review-bots-ready` waiting for CI and bot comments isn't patience. It ensures the reviewer's prompt will contain: green CI, all bot findings, full diff. Without the gate, the reviewer either polls (burning tokens) or reviews without full information (wrong answer, another loop).
230
+
231
+ The cost of a gate is milliseconds of shell execution. The cost of a skipped gate is a full review/fix cycle — potentially minutes and dollars.
232
+
233
+ ### The 1:2.8 Ratio Is Physics
234
+
235
+ For every 1 coder invocation, there are approximately 2.8 reviewer/fixer invocations. This is not a pipeline inefficiency. It is the actual shape of software.
236
+
237
+ The coder produces a first approximation. Reality pushes back: CI failures, static analysis findings, edge cases the spec didn't anticipate, style violations the linter catches. 70% of the engineering work happens after the code is written. You cannot prompt-engineer your way out of this. You cannot pre-load enough context to get one-shot correctness. The iteration is load-bearing.
238
+
239
+ The design question is not "how do we reduce the review/fix loop." It is: **given that ~2.8 cycles is the physics, how do we make each cycle as cheap and fast as possible?**
240
+
241
+ Every gate that catches a problem before an agent runs saves a full cycle. Every onEnter hook that assembles complete context means the agent spends its tokens on reasoning instead of discovery. Every failure prompt that tells the agent exactly what went wrong reduces the chance of another loop.
242
+
243
+ ### Flow Engineering Is 90% of the Work
244
+
245
+ The promise is big: software that ships with 100% overhead reduction. But 90% of the engineering work to get there is flow engineering — designing states, writing hooks, placing gates, and crafting prompts. The agent is the easy part. The agent is a commodity. The flow is the competitive advantage.
246
+
247
+ A poorly written failure prompt extends the loop. A gate that fires too early sends an under-qualified prompt to the reviewer. A missing onEnter hook makes the agent reconstruct context with tool calls instead of reasoning. The flow definition IS the quality of the system.
248
+
249
+ ---
250
+
207
251
  ## The Engine
208
252
 
209
253
  A **flow** is a state machine. Entities enter it and move through states. At each state an agent does work. At each boundary a deterministic gate verifies the output. Transitions fire on signals — not parsed natural language, not regex, but typed strings agents emit via tool call. The entire definition lives in a database and can be mutated at runtime.
@@ -145,6 +145,18 @@ export class Engine {
145
145
  // 6b. Execute onEnter hook if defined on the new state
146
146
  const newStateDef = flow.states.find((s) => s.name === toState);
147
147
  if (newStateDef?.onEnter) {
148
+ // Clear stale onEnter artifact keys so the hook re-runs on state re-entry.
149
+ // Only the keys belonging to THIS state's onEnter are removed; other artifacts are preserved.
150
+ const keysToRemove = [...newStateDef.onEnter.artifacts, "onEnter_error"];
151
+ const currentArtifacts = updated.artifacts ?? {};
152
+ const hasStaleKeys = keysToRemove.some((k) => currentArtifacts[k] !== undefined);
153
+ if (hasStaleKeys) {
154
+ await this.entityRepo.removeArtifactKeys(entityId, keysToRemove);
155
+ // Refresh in-memory entity so executeOnEnter sees cleared artifacts
156
+ const refreshed = await this.entityRepo.get(entityId);
157
+ if (refreshed)
158
+ updated = refreshed;
159
+ }
148
160
  const onEnterResult = await executeOnEnter(newStateDef.onEnter, updated, this.entityRepo);
149
161
  if (onEnterResult.skipped) {
150
162
  await emitter.emit({
@@ -19,6 +19,7 @@ import { buildConfigFromEnv, isLitestreamEnabled, LitestreamManager } from "../l
19
19
  import { withTransaction } from "../main.js";
20
20
  import { DrizzleDomainEventRepository } from "../repositories/drizzle/domain-event.repo.js";
21
21
  import { DrizzleEntityRepository } from "../repositories/drizzle/entity.repo.js";
22
+ import { DrizzleEntitySnapshotRepository } from "../repositories/drizzle/entity-snapshot.repo.js";
22
23
  import { DrizzleEventRepository } from "../repositories/drizzle/event.repo.js";
23
24
  import { DrizzleFlowRepository } from "../repositories/drizzle/flow.repo.js";
24
25
  import { DrizzleGateRepository } from "../repositories/drizzle/gate.repo.js";
@@ -26,6 +27,8 @@ import { DrizzleInvocationRepository } from "../repositories/drizzle/invocation.
26
27
  import * as schema from "../repositories/drizzle/schema.js";
27
28
  import { entities, entityHistory, flowDefinitions, flowVersions, gateDefinitions, gateResults, invocations, stateDefinitions, transitionRules, } from "../repositories/drizzle/schema.js";
28
29
  import { DrizzleTransitionLogRepository } from "../repositories/drizzle/transition-log.repo.js";
30
+ import { EventSourcedEntityRepository } from "../repositories/event-sourced/entity.repo.js";
31
+ import { EventSourcedInvocationRepository } from "../repositories/event-sourced/invocation.repo.js";
29
32
  import { UiSseAdapter } from "../ui/sse.js";
30
33
  import { WebSocketBroadcaster } from "../ws/broadcast.js";
31
34
  import { createMcpServer, startStdioServer } from "./mcp-server.js";
@@ -171,12 +174,26 @@ program
171
174
  if (litestreamMgr) {
172
175
  litestreamMgr.start();
173
176
  }
174
- const entityRepo = new DrizzleEntityRepository(db);
177
+ const mutableEntityRepo = new DrizzleEntityRepository(db);
175
178
  const flowRepo = new DrizzleFlowRepository(db);
176
- const invocationRepo = new DrizzleInvocationRepository(db);
179
+ const mutableInvocationRepo = new DrizzleInvocationRepository(db);
177
180
  const gateRepo = new DrizzleGateRepository(db);
178
181
  const transitionLogRepo = new DrizzleTransitionLogRepository(db);
179
182
  const domainEventRepo = new DrizzleDomainEventRepository(db);
183
+ const useEventSourced = process.env.DEFCON_EVENT_SOURCED === "true";
184
+ const snapshotInterval = parseInt(process.env.DEFCON_SNAPSHOT_INTERVAL ?? "10", 10);
185
+ let entityRepo;
186
+ let invocationRepo;
187
+ if (useEventSourced) {
188
+ const snapshotRepo = new DrizzleEntitySnapshotRepository(db);
189
+ entityRepo = new EventSourcedEntityRepository(mutableEntityRepo, domainEventRepo, snapshotRepo, snapshotInterval);
190
+ invocationRepo = new EventSourcedInvocationRepository(mutableInvocationRepo, domainEventRepo);
191
+ process.stderr.write("[defcon] Event-sourced repositories enabled\n");
192
+ }
193
+ else {
194
+ entityRepo = mutableEntityRepo;
195
+ invocationRepo = mutableInvocationRepo;
196
+ }
180
197
  const eventEmitter = new EventEmitter();
181
198
  eventEmitter.register({
182
199
  emit: async (event) => {
@@ -11,6 +11,7 @@ export declare class DrizzleDomainEventRepository implements IDomainEventReposit
11
11
  list(entityId: string, opts?: {
12
12
  type?: string;
13
13
  limit?: number;
14
+ minSequence?: number;
14
15
  }): Promise<DomainEvent[]>;
15
16
  }
16
17
  export {};
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { and, eq, sql } from "drizzle-orm";
2
+ import { and, eq, gt, sql } from "drizzle-orm";
3
3
  import { domainEvents } from "./schema.js";
4
4
  export class DrizzleDomainEventRepository {
5
5
  db;
@@ -62,6 +62,9 @@ export class DrizzleDomainEventRepository {
62
62
  if (opts?.type) {
63
63
  conditions.push(eq(domainEvents.type, opts.type));
64
64
  }
65
+ if (opts?.minSequence !== undefined) {
66
+ conditions.push(gt(domainEvents.sequence, opts.minSequence));
67
+ }
65
68
  const rows = this.db
66
69
  .select()
67
70
  .from(domainEvents)
@@ -0,0 +1,14 @@
1
+ import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
2
+ import type { Entity, IEntitySnapshotRepository } from "../interfaces.js";
3
+ import type * as schema from "./schema.js";
4
+ type Db = BetterSQLite3Database<typeof schema>;
5
+ export declare class DrizzleEntitySnapshotRepository implements IEntitySnapshotRepository {
6
+ private readonly db;
7
+ constructor(db: Db);
8
+ save(entityId: string, sequence: number, state: Entity): Promise<void>;
9
+ loadLatest(entityId: string): Promise<{
10
+ sequence: number;
11
+ state: Entity;
12
+ } | null>;
13
+ }
14
+ export {};
@@ -0,0 +1,68 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { desc, eq } from "drizzle-orm";
3
+ import { entitySnapshots } from "./schema.js";
4
+ export class DrizzleEntitySnapshotRepository {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ }
9
+ async save(entityId, sequence, state) {
10
+ const id = randomUUID();
11
+ this.db
12
+ .insert(entitySnapshots)
13
+ .values({
14
+ id,
15
+ entityId,
16
+ sequence,
17
+ state: state.state,
18
+ flowId: state.flowId,
19
+ refs: state.refs,
20
+ artifacts: state.artifacts,
21
+ claimedBy: state.claimedBy,
22
+ claimedAt: state.claimedAt?.getTime() ?? null,
23
+ flowVersion: state.flowVersion,
24
+ priority: state.priority,
25
+ affinityWorkerId: state.affinityWorkerId,
26
+ affinityRole: state.affinityRole,
27
+ affinityExpiresAt: state.affinityExpiresAt?.getTime() ?? null,
28
+ createdAt: state.createdAt.getTime(),
29
+ updatedAt: state.updatedAt.getTime(),
30
+ snapshotAt: Date.now(),
31
+ parentEntityId: state.parentEntityId,
32
+ })
33
+ .onConflictDoNothing()
34
+ .run();
35
+ }
36
+ async loadLatest(entityId) {
37
+ const rows = this.db
38
+ .select()
39
+ .from(entitySnapshots)
40
+ .where(eq(entitySnapshots.entityId, entityId))
41
+ .orderBy(desc(entitySnapshots.sequence))
42
+ .limit(1)
43
+ .all();
44
+ if (rows.length === 0)
45
+ return null;
46
+ const row = rows[0];
47
+ return {
48
+ sequence: row.sequence,
49
+ state: {
50
+ id: entityId,
51
+ flowId: row.flowId,
52
+ state: row.state,
53
+ refs: row.refs,
54
+ artifacts: row.artifacts,
55
+ claimedBy: row.claimedBy,
56
+ claimedAt: row.claimedAt ? new Date(row.claimedAt) : null,
57
+ flowVersion: row.flowVersion ?? 1,
58
+ priority: row.priority ?? 0,
59
+ createdAt: new Date(row.createdAt ?? 0),
60
+ updatedAt: new Date(row.updatedAt ?? 0),
61
+ affinityWorkerId: row.affinityWorkerId ?? null,
62
+ affinityRole: row.affinityRole ?? null,
63
+ affinityExpiresAt: row.affinityExpiresAt ? new Date(row.affinityExpiresAt) : null,
64
+ parentEntityId: row.parentEntityId ?? null,
65
+ },
66
+ };
67
+ }
68
+ }
@@ -12,6 +12,7 @@ export declare class DrizzleEntityRepository implements IEntityRepository {
12
12
  hasAnyInFlowAndState(flowId: string, stateNames: string[]): Promise<boolean>;
13
13
  transition(id: string, toState: string, _trigger: string, artifacts?: Partial<Artifacts>, _invocationId?: string | null): Promise<Entity>;
14
14
  updateArtifacts(id: string, artifacts: Partial<Artifacts>): Promise<void>;
15
+ removeArtifactKeys(id: string, keys: string[]): Promise<void>;
15
16
  claim(flowId: string, state: string, agentId: string): Promise<Entity | null>;
16
17
  claimById(entityId: string, agentId: string): Promise<Entity | null>;
17
18
  release(entityId: string, agentId: string): Promise<void>;
@@ -101,6 +101,19 @@ export class DrizzleEntityRepository {
101
101
  .set({ artifacts: { ...existing, ...artifacts }, updatedAt: Date.now() })
102
102
  .where(eq(entities.id, id));
103
103
  }
104
+ async removeArtifactKeys(id, keys) {
105
+ if (keys.length === 0)
106
+ return;
107
+ const rows = await this.db.select().from(entities).where(eq(entities.id, id)).limit(1);
108
+ if (rows.length === 0)
109
+ throw new NotFoundError(`Entity not found: ${id}`);
110
+ const existing = rows[0].artifacts ?? {};
111
+ const cleaned = { ...existing };
112
+ for (const key of keys) {
113
+ delete cleaned[key];
114
+ }
115
+ await this.db.update(entities).set({ artifacts: cleaned, updatedAt: Date.now() }).where(eq(entities.id, id));
116
+ }
104
117
  async claim(flowId, state, agentId) {
105
118
  return this.db.transaction((tx) => {
106
119
  const rows = tx
@@ -1,5 +1,6 @@
1
1
  export { DrizzleDomainEventRepository } from "./domain-event.repo.js";
2
2
  export { DrizzleEntityRepository } from "./entity.repo.js";
3
+ export { DrizzleEntitySnapshotRepository } from "./entity-snapshot.repo.js";
3
4
  export { DrizzleEventRepository } from "./event.repo.js";
4
5
  export { DrizzleFlowRepository } from "./flow.repo.js";
5
6
  export { DrizzleGateRepository } from "./gate.repo.js";
@@ -1,6 +1,7 @@
1
1
  // Drizzle ORM implementations of repository interfaces
2
2
  export { DrizzleDomainEventRepository } from "./domain-event.repo.js";
3
3
  export { DrizzleEntityRepository } from "./entity.repo.js";
4
+ export { DrizzleEntitySnapshotRepository } from "./entity-snapshot.repo.js";
4
5
  export { DrizzleEventRepository } from "./event.repo.js";
5
6
  export { DrizzleFlowRepository } from "./flow.repo.js";
6
7
  export { DrizzleGateRepository } from "./gate.repo.js";
@@ -2149,3 +2149,332 @@ export declare const domainEvents: import("drizzle-orm/sqlite-core").SQLiteTable
2149
2149
  };
2150
2150
  dialect: "sqlite";
2151
2151
  }>;
2152
+ export declare const entitySnapshots: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
2153
+ name: "entity_snapshots";
2154
+ schema: undefined;
2155
+ columns: {
2156
+ id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2157
+ name: "id";
2158
+ tableName: "entity_snapshots";
2159
+ dataType: "string";
2160
+ columnType: "SQLiteText";
2161
+ data: string;
2162
+ driverParam: string;
2163
+ notNull: true;
2164
+ hasDefault: false;
2165
+ isPrimaryKey: true;
2166
+ isAutoincrement: false;
2167
+ hasRuntimeDefault: false;
2168
+ enumValues: [string, ...string[]];
2169
+ baseColumn: never;
2170
+ identity: undefined;
2171
+ generated: undefined;
2172
+ }, {}, {
2173
+ length: number | undefined;
2174
+ }>;
2175
+ entityId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2176
+ name: "entity_id";
2177
+ tableName: "entity_snapshots";
2178
+ dataType: "string";
2179
+ columnType: "SQLiteText";
2180
+ data: string;
2181
+ driverParam: string;
2182
+ notNull: true;
2183
+ hasDefault: false;
2184
+ isPrimaryKey: false;
2185
+ isAutoincrement: false;
2186
+ hasRuntimeDefault: false;
2187
+ enumValues: [string, ...string[]];
2188
+ baseColumn: never;
2189
+ identity: undefined;
2190
+ generated: undefined;
2191
+ }, {}, {
2192
+ length: number | undefined;
2193
+ }>;
2194
+ sequence: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2195
+ name: "sequence";
2196
+ tableName: "entity_snapshots";
2197
+ dataType: "number";
2198
+ columnType: "SQLiteInteger";
2199
+ data: number;
2200
+ driverParam: number;
2201
+ notNull: true;
2202
+ hasDefault: false;
2203
+ isPrimaryKey: false;
2204
+ isAutoincrement: false;
2205
+ hasRuntimeDefault: false;
2206
+ enumValues: undefined;
2207
+ baseColumn: never;
2208
+ identity: undefined;
2209
+ generated: undefined;
2210
+ }, {}, {}>;
2211
+ state: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2212
+ name: "state";
2213
+ tableName: "entity_snapshots";
2214
+ dataType: "string";
2215
+ columnType: "SQLiteText";
2216
+ data: string;
2217
+ driverParam: string;
2218
+ notNull: true;
2219
+ hasDefault: false;
2220
+ isPrimaryKey: false;
2221
+ isAutoincrement: false;
2222
+ hasRuntimeDefault: false;
2223
+ enumValues: [string, ...string[]];
2224
+ baseColumn: never;
2225
+ identity: undefined;
2226
+ generated: undefined;
2227
+ }, {}, {
2228
+ length: number | undefined;
2229
+ }>;
2230
+ flowId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2231
+ name: "flow_id";
2232
+ tableName: "entity_snapshots";
2233
+ dataType: "string";
2234
+ columnType: "SQLiteText";
2235
+ data: string;
2236
+ driverParam: string;
2237
+ notNull: true;
2238
+ hasDefault: false;
2239
+ isPrimaryKey: false;
2240
+ isAutoincrement: false;
2241
+ hasRuntimeDefault: false;
2242
+ enumValues: [string, ...string[]];
2243
+ baseColumn: never;
2244
+ identity: undefined;
2245
+ generated: undefined;
2246
+ }, {}, {
2247
+ length: number | undefined;
2248
+ }>;
2249
+ refs: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2250
+ name: "refs";
2251
+ tableName: "entity_snapshots";
2252
+ dataType: "json";
2253
+ columnType: "SQLiteTextJson";
2254
+ data: unknown;
2255
+ driverParam: string;
2256
+ notNull: false;
2257
+ hasDefault: false;
2258
+ isPrimaryKey: false;
2259
+ isAutoincrement: false;
2260
+ hasRuntimeDefault: false;
2261
+ enumValues: undefined;
2262
+ baseColumn: never;
2263
+ identity: undefined;
2264
+ generated: undefined;
2265
+ }, {}, {}>;
2266
+ artifacts: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2267
+ name: "artifacts";
2268
+ tableName: "entity_snapshots";
2269
+ dataType: "json";
2270
+ columnType: "SQLiteTextJson";
2271
+ data: unknown;
2272
+ driverParam: string;
2273
+ notNull: false;
2274
+ hasDefault: false;
2275
+ isPrimaryKey: false;
2276
+ isAutoincrement: false;
2277
+ hasRuntimeDefault: false;
2278
+ enumValues: undefined;
2279
+ baseColumn: never;
2280
+ identity: undefined;
2281
+ generated: undefined;
2282
+ }, {}, {}>;
2283
+ claimedBy: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2284
+ name: "claimed_by";
2285
+ tableName: "entity_snapshots";
2286
+ dataType: "string";
2287
+ columnType: "SQLiteText";
2288
+ data: string;
2289
+ driverParam: string;
2290
+ notNull: false;
2291
+ hasDefault: false;
2292
+ isPrimaryKey: false;
2293
+ isAutoincrement: false;
2294
+ hasRuntimeDefault: false;
2295
+ enumValues: [string, ...string[]];
2296
+ baseColumn: never;
2297
+ identity: undefined;
2298
+ generated: undefined;
2299
+ }, {}, {
2300
+ length: number | undefined;
2301
+ }>;
2302
+ claimedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2303
+ name: "claimed_at";
2304
+ tableName: "entity_snapshots";
2305
+ dataType: "number";
2306
+ columnType: "SQLiteInteger";
2307
+ data: number;
2308
+ driverParam: number;
2309
+ notNull: false;
2310
+ hasDefault: false;
2311
+ isPrimaryKey: false;
2312
+ isAutoincrement: false;
2313
+ hasRuntimeDefault: false;
2314
+ enumValues: undefined;
2315
+ baseColumn: never;
2316
+ identity: undefined;
2317
+ generated: undefined;
2318
+ }, {}, {}>;
2319
+ flowVersion: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2320
+ name: "flow_version";
2321
+ tableName: "entity_snapshots";
2322
+ dataType: "number";
2323
+ columnType: "SQLiteInteger";
2324
+ data: number;
2325
+ driverParam: number;
2326
+ notNull: false;
2327
+ hasDefault: false;
2328
+ isPrimaryKey: false;
2329
+ isAutoincrement: false;
2330
+ hasRuntimeDefault: false;
2331
+ enumValues: undefined;
2332
+ baseColumn: never;
2333
+ identity: undefined;
2334
+ generated: undefined;
2335
+ }, {}, {}>;
2336
+ priority: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2337
+ name: "priority";
2338
+ tableName: "entity_snapshots";
2339
+ dataType: "number";
2340
+ columnType: "SQLiteInteger";
2341
+ data: number;
2342
+ driverParam: number;
2343
+ notNull: false;
2344
+ hasDefault: true;
2345
+ isPrimaryKey: false;
2346
+ isAutoincrement: false;
2347
+ hasRuntimeDefault: false;
2348
+ enumValues: undefined;
2349
+ baseColumn: never;
2350
+ identity: undefined;
2351
+ generated: undefined;
2352
+ }, {}, {}>;
2353
+ affinityWorkerId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2354
+ name: "affinity_worker_id";
2355
+ tableName: "entity_snapshots";
2356
+ dataType: "string";
2357
+ columnType: "SQLiteText";
2358
+ data: string;
2359
+ driverParam: string;
2360
+ notNull: false;
2361
+ hasDefault: false;
2362
+ isPrimaryKey: false;
2363
+ isAutoincrement: false;
2364
+ hasRuntimeDefault: false;
2365
+ enumValues: [string, ...string[]];
2366
+ baseColumn: never;
2367
+ identity: undefined;
2368
+ generated: undefined;
2369
+ }, {}, {
2370
+ length: number | undefined;
2371
+ }>;
2372
+ affinityRole: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2373
+ name: "affinity_role";
2374
+ tableName: "entity_snapshots";
2375
+ dataType: "string";
2376
+ columnType: "SQLiteText";
2377
+ data: string;
2378
+ driverParam: string;
2379
+ notNull: false;
2380
+ hasDefault: false;
2381
+ isPrimaryKey: false;
2382
+ isAutoincrement: false;
2383
+ hasRuntimeDefault: false;
2384
+ enumValues: [string, ...string[]];
2385
+ baseColumn: never;
2386
+ identity: undefined;
2387
+ generated: undefined;
2388
+ }, {}, {
2389
+ length: number | undefined;
2390
+ }>;
2391
+ affinityExpiresAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2392
+ name: "affinity_expires_at";
2393
+ tableName: "entity_snapshots";
2394
+ dataType: "number";
2395
+ columnType: "SQLiteInteger";
2396
+ data: number;
2397
+ driverParam: number;
2398
+ notNull: false;
2399
+ hasDefault: false;
2400
+ isPrimaryKey: false;
2401
+ isAutoincrement: false;
2402
+ hasRuntimeDefault: false;
2403
+ enumValues: undefined;
2404
+ baseColumn: never;
2405
+ identity: undefined;
2406
+ generated: undefined;
2407
+ }, {}, {}>;
2408
+ createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2409
+ name: "created_at";
2410
+ tableName: "entity_snapshots";
2411
+ dataType: "number";
2412
+ columnType: "SQLiteInteger";
2413
+ data: number;
2414
+ driverParam: number;
2415
+ notNull: false;
2416
+ hasDefault: false;
2417
+ isPrimaryKey: false;
2418
+ isAutoincrement: false;
2419
+ hasRuntimeDefault: false;
2420
+ enumValues: undefined;
2421
+ baseColumn: never;
2422
+ identity: undefined;
2423
+ generated: undefined;
2424
+ }, {}, {}>;
2425
+ updatedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2426
+ name: "updated_at";
2427
+ tableName: "entity_snapshots";
2428
+ dataType: "number";
2429
+ columnType: "SQLiteInteger";
2430
+ data: number;
2431
+ driverParam: number;
2432
+ notNull: false;
2433
+ hasDefault: false;
2434
+ isPrimaryKey: false;
2435
+ isAutoincrement: false;
2436
+ hasRuntimeDefault: false;
2437
+ enumValues: undefined;
2438
+ baseColumn: never;
2439
+ identity: undefined;
2440
+ generated: undefined;
2441
+ }, {}, {}>;
2442
+ snapshotAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2443
+ name: "snapshot_at";
2444
+ tableName: "entity_snapshots";
2445
+ dataType: "number";
2446
+ columnType: "SQLiteInteger";
2447
+ data: number;
2448
+ driverParam: number;
2449
+ notNull: true;
2450
+ hasDefault: false;
2451
+ isPrimaryKey: false;
2452
+ isAutoincrement: false;
2453
+ hasRuntimeDefault: false;
2454
+ enumValues: undefined;
2455
+ baseColumn: never;
2456
+ identity: undefined;
2457
+ generated: undefined;
2458
+ }, {}, {}>;
2459
+ parentEntityId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2460
+ name: "parent_entity_id";
2461
+ tableName: "entity_snapshots";
2462
+ dataType: "string";
2463
+ columnType: "SQLiteText";
2464
+ data: string;
2465
+ driverParam: string;
2466
+ notNull: false;
2467
+ hasDefault: false;
2468
+ isPrimaryKey: false;
2469
+ isAutoincrement: false;
2470
+ hasRuntimeDefault: false;
2471
+ enumValues: [string, ...string[]];
2472
+ baseColumn: never;
2473
+ identity: undefined;
2474
+ generated: undefined;
2475
+ }, {}, {
2476
+ length: number | undefined;
2477
+ }>;
2478
+ };
2479
+ dialect: "sqlite";
2480
+ }>;
@@ -173,3 +173,26 @@ export const domainEvents = sqliteTable("domain_events", {
173
173
  entitySeqIdx: uniqueIndex("domain_events_entity_seq_idx").on(table.entityId, table.sequence),
174
174
  typeIdx: index("domain_events_type_idx").on(table.type, table.emittedAt),
175
175
  }));
176
+ export const entitySnapshots = sqliteTable("entity_snapshots", {
177
+ id: text("id").primaryKey(),
178
+ entityId: text("entity_id").notNull(),
179
+ sequence: integer("sequence").notNull(),
180
+ state: text("state").notNull(),
181
+ flowId: text("flow_id").notNull(),
182
+ refs: text("refs", { mode: "json" }),
183
+ artifacts: text("artifacts", { mode: "json" }),
184
+ claimedBy: text("claimed_by"),
185
+ claimedAt: integer("claimed_at"),
186
+ flowVersion: integer("flow_version"),
187
+ priority: integer("priority").default(0),
188
+ affinityWorkerId: text("affinity_worker_id"),
189
+ affinityRole: text("affinity_role"),
190
+ affinityExpiresAt: integer("affinity_expires_at"),
191
+ createdAt: integer("created_at"),
192
+ updatedAt: integer("updated_at"),
193
+ snapshotAt: integer("snapshot_at").notNull(),
194
+ parentEntityId: text("parent_entity_id"),
195
+ }, (table) => ({
196
+ entitySeqUnique: uniqueIndex("entity_snapshots_entity_seq_idx").on(table.entityId, table.sequence),
197
+ entityLatestIdx: index("entity_snapshots_entity_latest_idx").on(table.entityId, table.snapshotAt),
198
+ }));
@@ -0,0 +1,30 @@
1
+ import type { Artifacts, Entity, IDomainEventRepository, IEntityRepository, IEntitySnapshotRepository, Refs } from "../interfaces.js";
2
+ export declare class EventSourcedEntityRepository implements IEntityRepository {
3
+ private readonly mutable;
4
+ private readonly domainEvents;
5
+ private readonly snapshots;
6
+ private readonly snapshotInterval;
7
+ constructor(mutable: IEntityRepository, domainEvents: IDomainEventRepository, snapshots: IEntitySnapshotRepository, snapshotInterval?: number);
8
+ create(flowId: string, initialState: string, refs?: Refs, flowVersion?: number): Promise<Entity>;
9
+ get(id: string): Promise<Entity | null>;
10
+ findByFlowAndState(flowId: string, state: string, limit?: number): Promise<Entity[]>;
11
+ hasAnyInFlowAndState(flowId: string, stateNames: string[]): Promise<boolean>;
12
+ transition(id: string, toState: string, trigger: string, artifacts?: Partial<Artifacts>): Promise<Entity>;
13
+ updateArtifacts(id: string, artifacts: Partial<Artifacts>): Promise<void>;
14
+ removeArtifactKeys(id: string, keys: string[]): Promise<void>;
15
+ claim(flowId: string, state: string, agentId: string): Promise<Entity | null>;
16
+ claimById(entityId: string, agentId: string): Promise<Entity | null>;
17
+ release(entityId: string, agentId: string): Promise<void>;
18
+ reapExpired(ttlMs: number): Promise<string[]>;
19
+ setAffinity(entityId: string, workerId: string, role: string, expiresAt: Date): Promise<void>;
20
+ clearExpiredAffinity(): Promise<string[]>;
21
+ appendSpawnedChild(parentId: string, entry: {
22
+ childId: string;
23
+ childFlow: string;
24
+ spawnedAt: string;
25
+ }): Promise<void>;
26
+ findByParentId(parentEntityId: string): Promise<Entity[]>;
27
+ cancelEntity(entityId: string): Promise<void>;
28
+ resetEntity(entityId: string, targetState: string): Promise<Entity>;
29
+ updateFlowVersion(entityId: string, version: number): Promise<void>;
30
+ }