@wopr-network/defcon 1.6.0 → 1.6.1

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.
@@ -38,6 +38,8 @@ export interface EngineDeps {
38
38
  adapters: Map<string, unknown>;
39
39
  eventEmitter: IEventBusAdapter;
40
40
  logger?: Logger;
41
+ /** Optional transaction wrapper from the database layer. When provided, processSignal runs inside a single transaction and events are flushed only after successful commit. */
42
+ withTransaction?: <T>(fn: () => T | Promise<T>) => Promise<T>;
41
43
  }
42
44
  export declare class Engine {
43
45
  private entityRepo;
@@ -48,6 +50,7 @@ export declare class Engine {
48
50
  readonly adapters: Map<string, unknown>;
49
51
  private eventEmitter;
50
52
  private readonly logger;
53
+ private readonly withTransactionFn;
51
54
  private drainingWorkers;
52
55
  constructor(deps: EngineDeps);
53
56
  drainWorker(workerId: string): void;
@@ -56,6 +59,7 @@ export declare class Engine {
56
59
  listDrainingWorkers(): string[];
57
60
  emit(event: import("./event-types.js").EngineEvent): Promise<void>;
58
61
  processSignal(entityId: string, signal: string, artifacts?: Artifacts, triggeringInvocationId?: string): Promise<ProcessSignalResult>;
62
+ private _processSignalInner;
59
63
  /**
60
64
  * Evaluate a gate and return a routing decision:
61
65
  * - `proceed` — gate passed, continue to transition.toState
@@ -17,6 +17,7 @@ export class Engine {
17
17
  adapters;
18
18
  eventEmitter;
19
19
  logger;
20
+ withTransactionFn;
20
21
  drainingWorkers = new Set();
21
22
  constructor(deps) {
22
23
  this.entityRepo = deps.entityRepo;
@@ -27,6 +28,7 @@ export class Engine {
27
28
  this.adapters = deps.adapters;
28
29
  this.eventEmitter = deps.eventEmitter;
29
30
  this.logger = deps.logger ?? consoleLogger;
31
+ this.withTransactionFn = deps.withTransaction ?? null;
30
32
  }
31
33
  drainWorker(workerId) {
32
34
  this.drainingWorkers.add(workerId);
@@ -44,6 +46,28 @@ export class Engine {
44
46
  await this.eventEmitter.emit(event);
45
47
  }
46
48
  async processSignal(entityId, signal, artifacts, triggeringInvocationId) {
49
+ const pendingEvents = [];
50
+ const bufferingEmitter = {
51
+ emit: async (event) => {
52
+ pendingEvents.push(event);
53
+ },
54
+ };
55
+ if (this.withTransactionFn) {
56
+ // Run DB writes inside a transaction; events are buffered and only
57
+ // flushed to real subscribers after successful COMMIT.
58
+ const result = await this.withTransactionFn(() => this._processSignalInner(entityId, signal, artifacts, triggeringInvocationId, bufferingEmitter));
59
+ for (const event of pendingEvents) {
60
+ await this.eventEmitter.emit(event);
61
+ }
62
+ return result;
63
+ }
64
+ const result = await this._processSignalInner(entityId, signal, artifacts, triggeringInvocationId, bufferingEmitter);
65
+ for (const event of pendingEvents) {
66
+ await this.eventEmitter.emit(event);
67
+ }
68
+ return result;
69
+ }
70
+ async _processSignalInner(entityId, signal, artifacts, triggeringInvocationId, emitter = this.eventEmitter) {
47
71
  // 1. Load entity
48
72
  const entity = await this.entityRepo.get(entityId);
49
73
  if (!entity)
@@ -98,7 +122,7 @@ export class Engine {
98
122
  // Keep the in-memory entity in sync so buildInvocation sees the cleared failures
99
123
  updated = { ...updated, artifacts: { ...updated.artifacts, gate_failures: [] } };
100
124
  // 6. Emit transition event
101
- await this.eventEmitter.emit({
125
+ await emitter.emit({
102
126
  type: "entity.transitioned",
103
127
  entityId,
104
128
  flowId: flow.id,
@@ -118,7 +142,7 @@ export class Engine {
118
142
  if (newStateDef?.onEnter) {
119
143
  const onEnterResult = await executeOnEnter(newStateDef.onEnter, updated, this.entityRepo);
120
144
  if (onEnterResult.skipped) {
121
- await this.eventEmitter.emit({
145
+ await emitter.emit({
122
146
  type: "onEnter.skipped",
123
147
  entityId,
124
148
  state: toState,
@@ -126,7 +150,7 @@ export class Engine {
126
150
  });
127
151
  }
128
152
  else if (onEnterResult.error) {
129
- await this.eventEmitter.emit({
153
+ await emitter.emit({
130
154
  type: "onEnter.failed",
131
155
  entityId,
132
156
  state: toState,
@@ -151,7 +175,7 @@ export class Engine {
151
175
  };
152
176
  }
153
177
  else {
154
- await this.eventEmitter.emit({
178
+ await emitter.emit({
155
179
  type: "onEnter.completed",
156
180
  entityId,
157
181
  state: toState,
@@ -179,7 +203,7 @@ export class Engine {
179
203
  ? { systemPrompt: build.systemPrompt, userContent: build.userContent }
180
204
  : undefined, newStateDef.agentRole ?? null);
181
205
  result.invocationId = invocation.id;
182
- await this.eventEmitter.emit({
206
+ await emitter.emit({
183
207
  type: "invocation.created",
184
208
  entityId,
185
209
  invocationId: invocation.id,
@@ -202,7 +226,7 @@ export class Engine {
202
226
  const spawned = await executeSpawn({ spawnFlow }, updated, this.flowRepo, this.entityRepo, this.logger);
203
227
  if (spawned) {
204
228
  result.spawned = [spawned.id];
205
- await this.eventEmitter.emit({
229
+ await emitter.emit({
206
230
  type: "flow.spawned",
207
231
  entityId,
208
232
  flowId: flow.id,
@@ -14,6 +14,7 @@ import { loadSeed } from "../config/seed-loader.js";
14
14
  import { resolveCorsOrigin } from "../cors.js";
15
15
  import { Engine } from "../engine/engine.js";
16
16
  import { EventEmitter } from "../engine/event-emitter.js";
17
+ import { withTransaction } from "../main.js";
17
18
  import { DrizzleEntityRepository } from "../repositories/drizzle/entity.repo.js";
18
19
  import { DrizzleEventRepository } from "../repositories/drizzle/event.repo.js";
19
20
  import { DrizzleFlowRepository } from "../repositories/drizzle/flow.repo.js";
@@ -173,6 +174,7 @@ program
173
174
  transitionLogRepo,
174
175
  adapters: new Map(),
175
176
  eventEmitter,
177
+ withTransaction: (fn) => withTransaction(sqlite, fn),
176
178
  });
177
179
  const deps = {
178
180
  entities: entityRepo,
@@ -5,6 +5,12 @@ export declare function createDatabase(dbPath?: string): {
5
5
  db: ReturnType<typeof drizzle<typeof schema>>;
6
6
  sqlite: Database.Database;
7
7
  };
8
+ /**
9
+ * Wraps `fn` in a BEGIN/COMMIT/ROLLBACK transaction on `sqlite`.
10
+ * If already inside a transaction, runs `fn` directly (allows nested calls).
11
+ * Supports both synchronous and Promise-returning `fn`.
12
+ */
13
+ export declare function withTransaction<T>(sqlite: Database.Database, fn: () => T | Promise<T>): Promise<T>;
8
14
  export declare function runMigrations(db: ReturnType<typeof drizzle>, migrationsFolder?: string): void;
9
15
  export declare function bootstrap(dbPath?: string): {
10
16
  db: ReturnType<typeof drizzle>;
package/dist/src/main.js CHANGED
@@ -10,6 +10,26 @@ export function createDatabase(dbPath = DB_PATH) {
10
10
  const db = drizzle(sqlite, { schema });
11
11
  return { db, sqlite };
12
12
  }
13
+ /**
14
+ * Wraps `fn` in a BEGIN/COMMIT/ROLLBACK transaction on `sqlite`.
15
+ * If already inside a transaction, runs `fn` directly (allows nested calls).
16
+ * Supports both synchronous and Promise-returning `fn`.
17
+ */
18
+ export async function withTransaction(sqlite, fn) {
19
+ if (sqlite.inTransaction) {
20
+ return fn();
21
+ }
22
+ sqlite.exec("BEGIN");
23
+ try {
24
+ const result = await fn();
25
+ sqlite.exec("COMMIT");
26
+ return result;
27
+ }
28
+ catch (err) {
29
+ sqlite.exec("ROLLBACK");
30
+ throw err;
31
+ }
32
+ }
13
33
  export function runMigrations(db, migrationsFolder = "./drizzle") {
14
34
  migrate(db, { migrationsFolder });
15
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/defcon",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@9.15.4",
6
6
  "engines": {