@wopr-network/defcon 1.6.0 → 1.7.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/dist/src/engine/engine.d.ts +5 -0
- package/dist/src/engine/engine.js +57 -15
- package/dist/src/execution/admin-schemas.d.ts +3 -0
- package/dist/src/execution/admin-schemas.js +3 -0
- package/dist/src/execution/cli.js +2 -0
- package/dist/src/execution/mcp-server.js +34 -1
- package/dist/src/main.d.ts +6 -0
- package/dist/src/main.js +20 -0
- package/dist/src/repositories/drizzle/entity.repo.d.ts +2 -1
- package/dist/src/repositories/drizzle/entity.repo.js +8 -2
- package/dist/src/repositories/drizzle/flow.repo.d.ts +1 -0
- package/dist/src/repositories/drizzle/flow.repo.js +65 -0
- package/dist/src/repositories/interfaces.d.ts +5 -1
- package/package.json +1 -1
|
@@ -28,6 +28,7 @@ export interface EngineStatus {
|
|
|
28
28
|
flows: Record<string, Record<string, number>>;
|
|
29
29
|
activeInvocations: number;
|
|
30
30
|
pendingClaims: number;
|
|
31
|
+
versionDistribution: Record<string, Record<string, number>>;
|
|
31
32
|
}
|
|
32
33
|
export interface EngineDeps {
|
|
33
34
|
entityRepo: IEntityRepository;
|
|
@@ -38,6 +39,8 @@ export interface EngineDeps {
|
|
|
38
39
|
adapters: Map<string, unknown>;
|
|
39
40
|
eventEmitter: IEventBusAdapter;
|
|
40
41
|
logger?: Logger;
|
|
42
|
+
/** Optional transaction wrapper from the database layer. When provided, processSignal runs inside a single transaction and events are flushed only after successful commit. */
|
|
43
|
+
withTransaction?: <T>(fn: () => T | Promise<T>) => Promise<T>;
|
|
41
44
|
}
|
|
42
45
|
export declare class Engine {
|
|
43
46
|
private entityRepo;
|
|
@@ -48,6 +51,7 @@ export declare class Engine {
|
|
|
48
51
|
readonly adapters: Map<string, unknown>;
|
|
49
52
|
private eventEmitter;
|
|
50
53
|
private readonly logger;
|
|
54
|
+
private readonly withTransactionFn;
|
|
51
55
|
private drainingWorkers;
|
|
52
56
|
constructor(deps: EngineDeps);
|
|
53
57
|
drainWorker(workerId: string): void;
|
|
@@ -56,6 +60,7 @@ export declare class Engine {
|
|
|
56
60
|
listDrainingWorkers(): string[];
|
|
57
61
|
emit(event: import("./event-types.js").EngineEvent): Promise<void>;
|
|
58
62
|
processSignal(entityId: string, signal: string, artifacts?: Artifacts, triggeringInvocationId?: string): Promise<ProcessSignalResult>;
|
|
63
|
+
private _processSignalInner;
|
|
59
64
|
/**
|
|
60
65
|
* Evaluate a gate and return a routing decision:
|
|
61
66
|
* - `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,14 +46,36 @@ 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)
|
|
50
74
|
throw new NotFoundError(`Entity "${entityId}" not found`);
|
|
51
|
-
// 2. Load flow
|
|
52
|
-
const flow = await this.flowRepo.
|
|
75
|
+
// 2. Load flow at entity's pinned version
|
|
76
|
+
const flow = await this.flowRepo.getAtVersion(entity.flowId, entity.flowVersion);
|
|
53
77
|
if (!flow)
|
|
54
|
-
throw new NotFoundError(`Flow "${entity.flowId}" not found`);
|
|
78
|
+
throw new NotFoundError(`Flow "${entity.flowId}" version ${entity.flowVersion} not found`);
|
|
55
79
|
// 3. Find transition
|
|
56
80
|
const transition = findTransition(flow, entity.state, signal, { entity }, true, this.logger);
|
|
57
81
|
if (!transition)
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
229
|
+
await emitter.emit({
|
|
206
230
|
type: "flow.spawned",
|
|
207
231
|
entityId,
|
|
208
232
|
flowId: flow.id,
|
|
@@ -301,7 +325,7 @@ export class Engine {
|
|
|
301
325
|
const flow = await this.flowRepo.getByName(flowName);
|
|
302
326
|
if (!flow)
|
|
303
327
|
throw new NotFoundError(`Flow "${flowName}" not found`);
|
|
304
|
-
let entity = await this.entityRepo.create(flow.id, flow.initialState, refs);
|
|
328
|
+
let entity = await this.entityRepo.create(flow.id, flow.initialState, refs, flow.version);
|
|
305
329
|
// Store any caller-supplied payload as initial artifacts so prompt templates
|
|
306
330
|
// can access refs like {{entity.artifacts.refs.linear.id}}.
|
|
307
331
|
if (payload && Object.keys(payload).length > 0) {
|
|
@@ -492,9 +516,11 @@ export class Engine {
|
|
|
492
516
|
this.logger.warn(`[engine] setAffinity failed for entity ${claimed.id} worker ${worker_id} — continuing:`, err);
|
|
493
517
|
}
|
|
494
518
|
}
|
|
495
|
-
const
|
|
519
|
+
const versionedFlow = await this.flowRepo.getAtVersion(claimed.flowId, claimed.flowVersion);
|
|
520
|
+
const effectiveFlow = versionedFlow ?? flow;
|
|
521
|
+
const state = effectiveFlow.states.find((s) => s.name === pending.stage);
|
|
496
522
|
const build = state
|
|
497
|
-
? await this.buildPromptForEntity(state, claimed,
|
|
523
|
+
? await this.buildPromptForEntity(state, claimed, effectiveFlow)
|
|
498
524
|
: { prompt: pending.prompt, context: null };
|
|
499
525
|
await this.eventEmitter.emit({
|
|
500
526
|
type: "entity.claimed",
|
|
@@ -519,12 +545,14 @@ export class Engine {
|
|
|
519
545
|
const claimed = await this.entityRepo.claim(flow.id, state.name, worker_id ?? `agent:${role}`);
|
|
520
546
|
if (!claimed)
|
|
521
547
|
continue;
|
|
522
|
-
const
|
|
548
|
+
const claimedVersionedFlow = await this.flowRepo.getAtVersion(claimed.flowId, claimed.flowVersion);
|
|
549
|
+
const claimedEffectiveFlow = claimedVersionedFlow ?? flow;
|
|
550
|
+
const canCreate = await this.checkConcurrency(claimedEffectiveFlow, claimed);
|
|
523
551
|
if (!canCreate) {
|
|
524
552
|
await this.entityRepo.release(claimed.id, worker_id ?? `agent:${role}`);
|
|
525
553
|
continue;
|
|
526
554
|
}
|
|
527
|
-
const build = await this.buildPromptForEntity(state, claimed,
|
|
555
|
+
const build = await this.buildPromptForEntity(state, claimed, claimedEffectiveFlow);
|
|
528
556
|
const invocation = await this.invocationRepo.create(claimed.id, state.name, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
529
557
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
530
558
|
: undefined, state.agentRole ?? null);
|
|
@@ -623,7 +651,21 @@ export class Engine {
|
|
|
623
651
|
activeInvocations += active;
|
|
624
652
|
pendingClaims += pending;
|
|
625
653
|
}
|
|
626
|
-
|
|
654
|
+
const versionDistribution = {};
|
|
655
|
+
for (const flow of allFlows) {
|
|
656
|
+
const versionCounts = {};
|
|
657
|
+
for (const state of flow.states) {
|
|
658
|
+
const stateEntities = await this.entityRepo.findByFlowAndState(flow.id, state.name);
|
|
659
|
+
for (const e of stateEntities) {
|
|
660
|
+
const vKey = String(e.flowVersion);
|
|
661
|
+
versionCounts[vKey] = (versionCounts[vKey] ?? 0) + 1;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (Object.keys(versionCounts).length > 0) {
|
|
665
|
+
versionDistribution[flow.id] = versionCounts;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return { flows: statusData, activeInvocations, pendingClaims, versionDistribution };
|
|
627
669
|
}
|
|
628
670
|
startReaper(intervalMs, entityTtlMs = 60_000) {
|
|
629
671
|
let tickInFlight = false;
|
|
@@ -124,6 +124,9 @@ export declare const AdminEntityResetSchema: z.ZodObject<{
|
|
|
124
124
|
entity_id: z.ZodString;
|
|
125
125
|
target_state: z.ZodString;
|
|
126
126
|
}, z.core.$strip>;
|
|
127
|
+
export declare const AdminEntityMigrateSchema: z.ZodObject<{
|
|
128
|
+
entity_id: z.ZodString;
|
|
129
|
+
}, z.core.$strip>;
|
|
127
130
|
export declare const AdminWorkerDrainSchema: z.ZodObject<{
|
|
128
131
|
worker_id: z.ZodString;
|
|
129
132
|
}, z.core.$strip>;
|
|
@@ -133,6 +133,9 @@ export const AdminEntityResetSchema = z.object({
|
|
|
133
133
|
entity_id: z.string().min(1),
|
|
134
134
|
target_state: z.string().min(1),
|
|
135
135
|
});
|
|
136
|
+
export const AdminEntityMigrateSchema = z.object({
|
|
137
|
+
entity_id: z.string().min(1),
|
|
138
|
+
});
|
|
136
139
|
export const AdminWorkerDrainSchema = z.object({
|
|
137
140
|
worker_id: z.string().min(1),
|
|
138
141
|
});
|
|
@@ -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,
|
|
@@ -6,7 +6,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
6
6
|
import { DEFAULT_TIMEOUT_PROMPT } from "../engine/constants.js";
|
|
7
7
|
import { isTerminal } from "../engine/state-machine.js";
|
|
8
8
|
import { consoleLogger } from "../logger.js";
|
|
9
|
-
import { AdminEntityCancelSchema, AdminEntityResetSchema, AdminFlowCreateSchema, AdminFlowPauseSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminGateRerunSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, AdminWorkerDrainSchema, } from "./admin-schemas.js";
|
|
9
|
+
import { AdminEntityCancelSchema, AdminEntityMigrateSchema, AdminEntityResetSchema, AdminFlowCreateSchema, AdminFlowPauseSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminGateRerunSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, AdminWorkerDrainSchema, } from "./admin-schemas.js";
|
|
10
10
|
import { handleFlowClaim } from "./handlers/flow.js";
|
|
11
11
|
import { FlowFailSchema, FlowGetPromptSchema, FlowReportSchema, FlowSeedSchema, QueryEntitiesSchema, QueryEntitySchema, QueryFlowSchema, QueryInvocationsSchema, } from "./tool-schemas.js";
|
|
12
12
|
function getSystemDefaultGateTimeoutMs() {
|
|
@@ -362,6 +362,11 @@ const TOOL_DEFINITIONS = [
|
|
|
362
362
|
required: ["entity_id", "target_state"],
|
|
363
363
|
},
|
|
364
364
|
},
|
|
365
|
+
{
|
|
366
|
+
name: "admin.entity.migrate",
|
|
367
|
+
description: "Migrate an entity to the latest flow version. Validates that the entity's current state exists in the new version.",
|
|
368
|
+
inputSchema: { type: "object", properties: { entity_id: { type: "string" } }, required: ["entity_id"] },
|
|
369
|
+
},
|
|
365
370
|
{
|
|
366
371
|
name: "admin.worker.drain",
|
|
367
372
|
description: "Mark a worker as draining — claimWork() will skip it.",
|
|
@@ -465,6 +470,8 @@ export async function callToolHandler(deps, name, safeArgs, opts) {
|
|
|
465
470
|
return await handleAdminEntityCancel(deps, safeArgs);
|
|
466
471
|
case "admin.entity.reset":
|
|
467
472
|
return await handleAdminEntityReset(deps, safeArgs);
|
|
473
|
+
case "admin.entity.migrate":
|
|
474
|
+
return await handleAdminEntityMigrate(deps, safeArgs);
|
|
468
475
|
case "admin.worker.drain":
|
|
469
476
|
return await handleAdminWorkerDrain(deps, safeArgs);
|
|
470
477
|
case "admin.worker.undrain":
|
|
@@ -804,6 +811,32 @@ async function handleAdminEntityReset(deps, args) {
|
|
|
804
811
|
});
|
|
805
812
|
return jsonResult({ reset: true, entity_id: v.data.entity_id, state: updated.state });
|
|
806
813
|
}
|
|
814
|
+
async function handleAdminEntityMigrate(deps, args) {
|
|
815
|
+
const v = validateInput(AdminEntityMigrateSchema, args);
|
|
816
|
+
if (!v.ok)
|
|
817
|
+
return v.result;
|
|
818
|
+
const { entity_id: entityId } = v.data;
|
|
819
|
+
const entity = await deps.entities.get(entityId);
|
|
820
|
+
if (!entity)
|
|
821
|
+
return errorResult(`Entity not found: ${entityId}`);
|
|
822
|
+
const flow = await deps.flows.get(entity.flowId);
|
|
823
|
+
if (!flow)
|
|
824
|
+
return errorResult(`Flow not found: ${entity.flowId}`);
|
|
825
|
+
if (entity.flowVersion === flow.version) {
|
|
826
|
+
return jsonResult({
|
|
827
|
+
migrated: false,
|
|
828
|
+
message: "Entity is already on the latest version",
|
|
829
|
+
flowVersion: flow.version,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
const stateExists = flow.states.some((s) => s.name === entity.state);
|
|
833
|
+
if (!stateExists) {
|
|
834
|
+
return errorResult(`Cannot migrate entity ${entityId}: current state '${entity.state}' does not exist in flow '${flow.name}' version ${flow.version}`);
|
|
835
|
+
}
|
|
836
|
+
await deps.entities.updateFlowVersion(entityId, flow.version);
|
|
837
|
+
const updated = await deps.entities.get(entityId);
|
|
838
|
+
return jsonResult(updated);
|
|
839
|
+
}
|
|
807
840
|
async function handleAdminWorkerDrain(deps, args) {
|
|
808
841
|
const v = validateInput(AdminWorkerDrainSchema, args);
|
|
809
842
|
if (!v.ok)
|
package/dist/src/main.d.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -6,7 +6,7 @@ export declare class DrizzleEntityRepository implements IEntityRepository {
|
|
|
6
6
|
private db;
|
|
7
7
|
constructor(db: Db);
|
|
8
8
|
private toEntity;
|
|
9
|
-
create(flowId: string, initialState: string, refs?: Refs): Promise<Entity>;
|
|
9
|
+
create(flowId: string, initialState: string, refs?: Refs, flowVersion?: number): Promise<Entity>;
|
|
10
10
|
get(id: string): Promise<Entity | null>;
|
|
11
11
|
findByFlowAndState(flowId: string, state: string, limit?: number): Promise<Entity[]>;
|
|
12
12
|
hasAnyInFlowAndState(flowId: string, stateNames: string[]): Promise<boolean>;
|
|
@@ -25,5 +25,6 @@ export declare class DrizzleEntityRepository implements IEntityRepository {
|
|
|
25
25
|
clearExpiredAffinity(): Promise<string[]>;
|
|
26
26
|
cancelEntity(entityId: string): Promise<void>;
|
|
27
27
|
resetEntity(entityId: string, targetState: string): Promise<Entity>;
|
|
28
|
+
updateFlowVersion(entityId: string, version: number): Promise<void>;
|
|
28
29
|
}
|
|
29
30
|
export {};
|
|
@@ -24,7 +24,7 @@ export class DrizzleEntityRepository {
|
|
|
24
24
|
affinityExpiresAt: row.affinityExpiresAt ? new Date(row.affinityExpiresAt) : null,
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
-
async create(flowId, initialState, refs) {
|
|
27
|
+
async create(flowId, initialState, refs, flowVersion) {
|
|
28
28
|
const now = Date.now();
|
|
29
29
|
const id = crypto.randomUUID();
|
|
30
30
|
const row = {
|
|
@@ -35,7 +35,7 @@ export class DrizzleEntityRepository {
|
|
|
35
35
|
artifacts: null,
|
|
36
36
|
claimedBy: null,
|
|
37
37
|
claimedAt: null,
|
|
38
|
-
flowVersion: 1,
|
|
38
|
+
flowVersion: flowVersion ?? 1,
|
|
39
39
|
createdAt: now,
|
|
40
40
|
updatedAt: now,
|
|
41
41
|
affinityWorkerId: null,
|
|
@@ -205,4 +205,10 @@ export class DrizzleEntityRepository {
|
|
|
205
205
|
throw new NotFoundError(`Entity not found: ${entityId}`);
|
|
206
206
|
return this.toEntity(rows[0]);
|
|
207
207
|
}
|
|
208
|
+
async updateFlowVersion(entityId, version) {
|
|
209
|
+
await this.db
|
|
210
|
+
.update(entities)
|
|
211
|
+
.set({ flowVersion: version, updatedAt: Date.now() })
|
|
212
|
+
.where(eq(entities.id, entityId));
|
|
213
|
+
}
|
|
208
214
|
}
|
|
@@ -9,6 +9,7 @@ export declare class DrizzleFlowRepository implements IFlowRepository {
|
|
|
9
9
|
create(input: CreateFlowInput): Promise<Flow>;
|
|
10
10
|
get(id: string): Promise<Flow | null>;
|
|
11
11
|
getByName(name: string): Promise<Flow | null>;
|
|
12
|
+
getAtVersion(flowId: string, version: number): Promise<Flow | null>;
|
|
12
13
|
list(): Promise<Flow[]>;
|
|
13
14
|
listAll(): Promise<Flow[]>;
|
|
14
15
|
update(id: string, changes: UpdateFlowInput): Promise<Flow>;
|
|
@@ -116,6 +116,67 @@ export class DrizzleFlowRepository {
|
|
|
116
116
|
return null;
|
|
117
117
|
return this.hydrateFlow(rows[0]);
|
|
118
118
|
}
|
|
119
|
+
async getAtVersion(flowId, version) {
|
|
120
|
+
const current = await this.get(flowId);
|
|
121
|
+
if (!current)
|
|
122
|
+
return null;
|
|
123
|
+
if (current.version === version)
|
|
124
|
+
return current;
|
|
125
|
+
const rows = this.db
|
|
126
|
+
.select()
|
|
127
|
+
.from(flowVersions)
|
|
128
|
+
.where(and(eq(flowVersions.flowId, flowId), eq(flowVersions.version, version)))
|
|
129
|
+
.all();
|
|
130
|
+
if (rows.length === 0)
|
|
131
|
+
return null;
|
|
132
|
+
const snap = rows[0].snapshot;
|
|
133
|
+
return {
|
|
134
|
+
id: flowId,
|
|
135
|
+
name: snap.name ?? current.name,
|
|
136
|
+
description: snap.description ?? null,
|
|
137
|
+
entitySchema: snap.entitySchema ?? null,
|
|
138
|
+
initialState: snap.initialState ?? current.initialState,
|
|
139
|
+
maxConcurrent: snap.maxConcurrent ?? 0,
|
|
140
|
+
maxConcurrentPerRepo: snap.maxConcurrentPerRepo ?? 0,
|
|
141
|
+
affinityWindowMs: snap.affinityWindowMs ?? 300000,
|
|
142
|
+
claimRetryAfterMs: snap.claimRetryAfterMs ?? null,
|
|
143
|
+
gateTimeoutMs: snap.gateTimeoutMs ?? null,
|
|
144
|
+
version,
|
|
145
|
+
createdBy: snap.createdBy ?? null,
|
|
146
|
+
discipline: snap.discipline ?? null,
|
|
147
|
+
defaultModelTier: snap.defaultModelTier ?? null,
|
|
148
|
+
timeoutPrompt: snap.timeoutPrompt ?? null,
|
|
149
|
+
paused: current.paused,
|
|
150
|
+
createdAt: snap.createdAt ? new Date(snap.createdAt) : null,
|
|
151
|
+
updatedAt: snap.updatedAt ? new Date(snap.updatedAt) : null,
|
|
152
|
+
states: (snap.states ?? []).map((s) => ({
|
|
153
|
+
id: s.id,
|
|
154
|
+
flowId,
|
|
155
|
+
name: s.name,
|
|
156
|
+
agentRole: s.agentRole ?? null,
|
|
157
|
+
modelTier: s.modelTier ?? null,
|
|
158
|
+
mode: s.mode ?? "passive",
|
|
159
|
+
promptTemplate: s.promptTemplate ?? null,
|
|
160
|
+
constraints: s.constraints ?? null,
|
|
161
|
+
onEnter: s.onEnter ?? null,
|
|
162
|
+
onExit: s.onExit ?? null,
|
|
163
|
+
retryAfterMs: s.retryAfterMs ?? null,
|
|
164
|
+
})),
|
|
165
|
+
transitions: (snap.transitions ?? []).map((t) => ({
|
|
166
|
+
id: t.id,
|
|
167
|
+
flowId,
|
|
168
|
+
fromState: t.fromState,
|
|
169
|
+
toState: t.toState,
|
|
170
|
+
trigger: t.trigger,
|
|
171
|
+
gateId: t.gateId ?? null,
|
|
172
|
+
condition: t.condition ?? null,
|
|
173
|
+
priority: t.priority ?? 0,
|
|
174
|
+
spawnFlow: t.spawnFlow ?? null,
|
|
175
|
+
spawnTemplate: t.spawnTemplate ?? null,
|
|
176
|
+
createdAt: t.createdAt ? new Date(t.createdAt) : null,
|
|
177
|
+
})),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
119
180
|
async list() {
|
|
120
181
|
const rows = this.db.select().from(flowDefinitions).all();
|
|
121
182
|
return rows.map((row) => this.hydrateFlow(row));
|
|
@@ -306,6 +367,10 @@ export class DrizzleFlowRepository {
|
|
|
306
367
|
createdAt: now,
|
|
307
368
|
})
|
|
308
369
|
.run();
|
|
370
|
+
tx.update(flowDefinitions)
|
|
371
|
+
.set({ version: version + 1, updatedAt: now })
|
|
372
|
+
.where(eq(flowDefinitions.id, flowId))
|
|
373
|
+
.run();
|
|
309
374
|
return version;
|
|
310
375
|
});
|
|
311
376
|
return {
|
|
@@ -224,7 +224,7 @@ export interface CreateGateInput {
|
|
|
224
224
|
/** Data-access contract for entity lifecycle operations. */
|
|
225
225
|
export interface IEntityRepository {
|
|
226
226
|
/** Create a new entity in the given flow's initial state. */
|
|
227
|
-
create(flowId: string, initialState: string, refs?: Refs): Promise<Entity>;
|
|
227
|
+
create(flowId: string, initialState: string, refs?: Refs, flowVersion?: number): Promise<Entity>;
|
|
228
228
|
/** Get an entity by ID, or null if not found. */
|
|
229
229
|
get(id: string): Promise<Entity | null>;
|
|
230
230
|
/** Find entities in a given flow and state, up to an optional limit. */
|
|
@@ -259,6 +259,8 @@ export interface IEntityRepository {
|
|
|
259
259
|
cancelEntity(entityId: string): Promise<void>;
|
|
260
260
|
/** Move entity to targetState and clear claimedBy/claimedAt. Returns the updated entity. */
|
|
261
261
|
resetEntity(entityId: string, targetState: string): Promise<Entity>;
|
|
262
|
+
/** Update an entity's pinned flow version. */
|
|
263
|
+
updateFlowVersion(entityId: string, version: number): Promise<void>;
|
|
262
264
|
}
|
|
263
265
|
/** Fields that can be updated on a flow's top-level definition */
|
|
264
266
|
export type UpdateFlowInput = Partial<Omit<Flow, "id" | "states" | "transitions" | "createdAt" | "updatedAt">>;
|
|
@@ -276,6 +278,8 @@ export interface IFlowRepository {
|
|
|
276
278
|
get(id: string): Promise<Flow | null>;
|
|
277
279
|
/** Get a flow by unique name. Returns null if not found. */
|
|
278
280
|
getByName(name: string): Promise<Flow | null>;
|
|
281
|
+
/** Load a flow definition at a specific version. Returns the snapshot if version < current, or live flow if version === current. */
|
|
282
|
+
getAtVersion(flowId: string, version: number): Promise<Flow | null>;
|
|
279
283
|
/** Update a flow's top-level fields. */
|
|
280
284
|
update(id: string, changes: UpdateFlowInput): Promise<Flow>;
|
|
281
285
|
/** Add a state definition to a flow. */
|