@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.
- package/README.md +44 -0
- package/dist/src/engine/engine.js +12 -0
- package/dist/src/execution/cli.js +19 -2
- package/dist/src/repositories/drizzle/domain-event.repo.d.ts +1 -0
- package/dist/src/repositories/drizzle/domain-event.repo.js +4 -1
- package/dist/src/repositories/drizzle/entity-snapshot.repo.d.ts +14 -0
- package/dist/src/repositories/drizzle/entity-snapshot.repo.js +68 -0
- package/dist/src/repositories/drizzle/entity.repo.d.ts +1 -0
- package/dist/src/repositories/drizzle/entity.repo.js +13 -0
- package/dist/src/repositories/drizzle/index.d.ts +1 -0
- package/dist/src/repositories/drizzle/index.js +1 -0
- package/dist/src/repositories/drizzle/schema.d.ts +329 -0
- package/dist/src/repositories/drizzle/schema.js +23 -0
- package/dist/src/repositories/event-sourced/entity.repo.d.ts +30 -0
- package/dist/src/repositories/event-sourced/entity.repo.js +88 -0
- package/dist/src/repositories/event-sourced/index.d.ts +3 -0
- package/dist/src/repositories/event-sourced/index.js +3 -0
- package/dist/src/repositories/event-sourced/invocation.repo.d.ts +20 -0
- package/dist/src/repositories/event-sourced/invocation.repo.js +61 -0
- package/dist/src/repositories/event-sourced/replay.d.ts +11 -0
- package/dist/src/repositories/event-sourced/replay.js +150 -0
- package/dist/src/repositories/interfaces.d.ts +13 -0
- package/drizzle/0015_clean_redwing.sql +11 -0
- package/drizzle/0016_hesitant_bill_hollister.sql +22 -0
- package/drizzle/0017_hesitant_bill_hollister.sql +22 -0
- package/drizzle/0018_volatile_reavers.sql +1 -0
- package/drizzle/meta/0016_snapshot.json +1316 -0
- package/drizzle/meta/0018_snapshot.json +1351 -0
- package/drizzle/meta/_journal.json +15 -1
- 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
|
|
177
|
+
const mutableEntityRepo = new DrizzleEntityRepository(db);
|
|
175
178
|
const flowRepo = new DrizzleFlowRepository(db);
|
|
176
|
-
const
|
|
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) => {
|
|
@@ -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
|
+
}
|