@wopr-network/defcon 1.0.3 → 1.1.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.
@@ -11,6 +11,21 @@ function extractBearerToken(header) {
11
11
  return undefined;
12
12
  return header.slice(7).trim() || undefined;
13
13
  }
14
+ function requireAdminToken(deps, req) {
15
+ const configuredToken = deps.adminToken?.trim() || undefined;
16
+ if (!configuredToken)
17
+ return null; // open mode
18
+ const callerToken = extractBearerToken(req.authorization);
19
+ if (!callerToken) {
20
+ return { status: 401, body: { error: "Unauthorized: admin endpoints require authentication." } };
21
+ }
22
+ const hashA = createHash("sha256").update(configuredToken.trim()).digest();
23
+ const hashB = createHash("sha256").update(callerToken.trim()).digest();
24
+ if (!timingSafeEqual(hashA, hashB)) {
25
+ return { status: 401, body: { error: "Unauthorized: admin endpoints require authentication." } };
26
+ }
27
+ return null;
28
+ }
14
29
  function requireWorkerToken(deps, req) {
15
30
  const configuredToken = deps.workerToken?.trim() || undefined; // treat "" and whitespace-only as unset
16
31
  if (!configuredToken)
@@ -198,6 +213,67 @@ export function createHttpServer(deps) {
198
213
  router.add("DELETE", "/api/flows/:id", async () => {
199
214
  return { status: 501, body: { error: "Flow deletion not implemented" } };
200
215
  });
216
+ // --- Admin: Pause/Resume Flow ---
217
+ router.add("POST", "/api/admin/flows/:flow/pause", async (req) => {
218
+ const authErr = requireAdminToken(deps, req);
219
+ if (authErr)
220
+ return authErr;
221
+ const callerToken = extractBearerToken(req.authorization);
222
+ const result = await callToolHandler(deps.mcpDeps, "admin.flow.pause", { flow_name: req.params.flow }, { adminToken: deps.adminToken, callerToken });
223
+ return mcpResultToApi(result);
224
+ });
225
+ router.add("POST", "/api/admin/flows/:flow/resume", async (req) => {
226
+ const authErr = requireAdminToken(deps, req);
227
+ if (authErr)
228
+ return authErr;
229
+ const callerToken = extractBearerToken(req.authorization);
230
+ const result = await callToolHandler(deps.mcpDeps, "admin.flow.resume", { flow_name: req.params.flow }, { adminToken: deps.adminToken, callerToken });
231
+ return mcpResultToApi(result);
232
+ });
233
+ // --- Admin: Cancel Entity ---
234
+ router.add("POST", "/api/admin/entities/:id/cancel", async (req) => {
235
+ const authErr = requireAdminToken(deps, req);
236
+ if (authErr)
237
+ return authErr;
238
+ const callerToken = extractBearerToken(req.authorization);
239
+ const result = await callToolHandler(deps.mcpDeps, "admin.entity.cancel", { entity_id: req.params.id }, { adminToken: deps.adminToken, callerToken });
240
+ return mcpResultToApi(result);
241
+ });
242
+ // --- Admin: Reset Entity ---
243
+ router.add("POST", "/api/admin/entities/:id/reset", async (req) => {
244
+ const authErr = requireAdminToken(deps, req);
245
+ if (authErr)
246
+ return authErr;
247
+ const callerToken = extractBearerToken(req.authorization);
248
+ const result = await callToolHandler(deps.mcpDeps, "admin.entity.reset", { entity_id: req.params.id, target_state: req.body?.target_state }, { adminToken: deps.adminToken, callerToken });
249
+ return mcpResultToApi(result);
250
+ });
251
+ // --- Admin: Drain/Undrain Worker ---
252
+ router.add("POST", "/api/admin/workers/:workerId/drain", async (req) => {
253
+ const authErr = requireAdminToken(deps, req);
254
+ if (authErr)
255
+ return authErr;
256
+ const callerToken = extractBearerToken(req.authorization);
257
+ const result = await callToolHandler(deps.mcpDeps, "admin.worker.drain", { worker_id: req.params.workerId }, { adminToken: deps.adminToken, callerToken });
258
+ return mcpResultToApi(result);
259
+ });
260
+ router.add("POST", "/api/admin/workers/:workerId/undrain", async (req) => {
261
+ const authErr = requireAdminToken(deps, req);
262
+ if (authErr)
263
+ return authErr;
264
+ const callerToken = extractBearerToken(req.authorization);
265
+ const result = await callToolHandler(deps.mcpDeps, "admin.worker.undrain", { worker_id: req.params.workerId }, { adminToken: deps.adminToken, callerToken });
266
+ return mcpResultToApi(result);
267
+ });
268
+ // --- Admin: Rerun Gate ---
269
+ router.add("POST", "/api/admin/entities/:id/gates/:gateName/rerun", async (req) => {
270
+ const authErr = requireAdminToken(deps, req);
271
+ if (authErr)
272
+ return authErr;
273
+ const callerToken = extractBearerToken(req.authorization);
274
+ const result = await callToolHandler(deps.mcpDeps, "admin.gate.rerun", { entity_id: req.params.id, gate_name: req.params.gateName }, { adminToken: deps.adminToken, callerToken });
275
+ return mcpResultToApi(result);
276
+ });
201
277
  // --- HTTP server ---
202
278
  const server = http.createServer(async (req, res) => {
203
279
  // CORS
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Wire types for defcon's REST and MCP APIs.
3
- * Exported for consumers (e.g. norad) to import instead of duplicating.
3
+ * Exported for consumers (e.g. radar) to import instead of duplicating.
4
4
  */
5
5
  export type ClaimResponse = {
6
6
  next_action: "check_back";
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Wire types for defcon's REST and MCP APIs.
3
- * Exported for consumers (e.g. norad) to import instead of duplicating.
3
+ * Exported for consumers (e.g. radar) to import instead of duplicating.
4
4
  */
5
5
  export {};
@@ -48,7 +48,12 @@ export declare class Engine {
48
48
  readonly adapters: Map<string, unknown>;
49
49
  private eventEmitter;
50
50
  private readonly logger;
51
+ private drainingWorkers;
51
52
  constructor(deps: EngineDeps);
53
+ drainWorker(workerId: string): void;
54
+ undrainWorker(workerId: string): void;
55
+ isDraining(workerId: string): boolean;
56
+ listDrainingWorkers(): string[];
52
57
  processSignal(entityId: string, signal: string, artifacts?: Artifacts, triggeringInvocationId?: string): Promise<ProcessSignalResult>;
53
58
  createEntity(flowName: string, refs?: Record<string, {
54
59
  adapter: string;
@@ -16,6 +16,7 @@ export class Engine {
16
16
  adapters;
17
17
  eventEmitter;
18
18
  logger;
19
+ drainingWorkers = new Set();
19
20
  constructor(deps) {
20
21
  this.entityRepo = deps.entityRepo;
21
22
  this.flowRepo = deps.flowRepo;
@@ -26,6 +27,18 @@ export class Engine {
26
27
  this.eventEmitter = deps.eventEmitter;
27
28
  this.logger = deps.logger ?? consoleLogger;
28
29
  }
30
+ drainWorker(workerId) {
31
+ this.drainingWorkers.add(workerId);
32
+ }
33
+ undrainWorker(workerId) {
34
+ this.drainingWorkers.delete(workerId);
35
+ }
36
+ isDraining(workerId) {
37
+ return this.drainingWorkers.has(workerId);
38
+ }
39
+ listDrainingWorkers() {
40
+ return Array.from(this.drainingWorkers);
41
+ }
29
42
  async processSignal(entityId, signal, artifacts, triggeringInvocationId) {
30
43
  // 1. Load entity
31
44
  const entity = await this.entityRepo.get(entityId);
@@ -304,6 +317,10 @@ export class Engine {
304
317
  return entity;
305
318
  }
306
319
  async claimWork(role, flowName, worker_id) {
320
+ // Skip draining workers entirely
321
+ if (worker_id && this.drainingWorkers.has(worker_id)) {
322
+ return "all_claimed";
323
+ }
307
324
  // 1. Find candidate flows filtered by discipline
308
325
  let flows;
309
326
  if (flowName) {
@@ -316,6 +333,8 @@ export class Engine {
316
333
  const allFlows = await this.flowRepo.listAll();
317
334
  flows = allFlows.filter((f) => f.discipline === null || f.discipline === role);
318
335
  }
336
+ // Filter out paused flows
337
+ flows = flows.filter((f) => !f.paused);
319
338
  if (flows.length === 0)
320
339
  return null;
321
340
  const candidates = [];
@@ -114,3 +114,20 @@ export declare const AdminFlowRestoreSchema: z.ZodObject<{
114
114
  flow_name: z.ZodString;
115
115
  version: z.ZodNumber;
116
116
  }, z.core.$strip>;
117
+ export declare const AdminFlowPauseSchema: z.ZodObject<{
118
+ flow_name: z.ZodString;
119
+ }, z.core.$strip>;
120
+ export declare const AdminEntityCancelSchema: z.ZodObject<{
121
+ entity_id: z.ZodString;
122
+ }, z.core.$strip>;
123
+ export declare const AdminEntityResetSchema: z.ZodObject<{
124
+ entity_id: z.ZodString;
125
+ target_state: z.ZodString;
126
+ }, z.core.$strip>;
127
+ export declare const AdminWorkerDrainSchema: z.ZodObject<{
128
+ worker_id: z.ZodString;
129
+ }, z.core.$strip>;
130
+ export declare const AdminGateRerunSchema: z.ZodObject<{
131
+ entity_id: z.ZodString;
132
+ gate_name: z.ZodString;
133
+ }, z.core.$strip>;
@@ -123,3 +123,20 @@ export const AdminFlowRestoreSchema = z.object({
123
123
  flow_name: z.string().min(1),
124
124
  version: z.number().int().min(1),
125
125
  });
126
+ export const AdminFlowPauseSchema = z.object({
127
+ flow_name: z.string().min(1),
128
+ });
129
+ export const AdminEntityCancelSchema = z.object({
130
+ entity_id: z.string().min(1),
131
+ });
132
+ export const AdminEntityResetSchema = z.object({
133
+ entity_id: z.string().min(1),
134
+ target_state: z.string().min(1),
135
+ });
136
+ export const AdminWorkerDrainSchema = z.object({
137
+ worker_id: z.string().min(1),
138
+ });
139
+ export const AdminGateRerunSchema = z.object({
140
+ entity_id: z.string().min(1),
141
+ gate_name: z.string().min(1),
142
+ });
@@ -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 { AdminFlowCreateSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, } from "./admin-schemas.js";
9
+ import { AdminEntityCancelSchema, AdminEntityResetSchema, AdminFlowCreateSchema, AdminFlowPauseSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminGateRerunSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, AdminWorkerDrainSchema, } from "./admin-schemas.js";
10
10
  import { FlowClaimSchema, FlowFailSchema, FlowGetPromptSchema, FlowReportSchema, FlowSeedSchema, QueryEntitiesSchema, QueryEntitySchema, QueryFlowSchema, QueryInvocationsSchema, } from "./tool-schemas.js";
11
11
  function getSystemDefaultGateTimeoutMs() {
12
12
  const parsed = parseInt(process.env.DEFCON_DEFAULT_GATE_TIMEOUT_MS ?? "", 10);
@@ -337,6 +337,49 @@ const TOOL_DEFINITIONS = [
337
337
  required: ["flow"],
338
338
  },
339
339
  },
340
+ {
341
+ name: "admin.flow.pause",
342
+ description: "Pause a flow — claimWork() will skip paused flows until resumed.",
343
+ inputSchema: { type: "object", properties: { flow_name: { type: "string" } }, required: ["flow_name"] },
344
+ },
345
+ {
346
+ name: "admin.flow.resume",
347
+ description: "Resume a previously paused flow.",
348
+ inputSchema: { type: "object", properties: { flow_name: { type: "string" } }, required: ["flow_name"] },
349
+ },
350
+ {
351
+ name: "admin.entity.cancel",
352
+ description: "Cancel an entity — fails active invocation, moves to 'cancelled' terminal state.",
353
+ inputSchema: { type: "object", properties: { entity_id: { type: "string" } }, required: ["entity_id"] },
354
+ },
355
+ {
356
+ name: "admin.entity.reset",
357
+ description: "Reset an entity to a target state — clears claimedBy.",
358
+ inputSchema: {
359
+ type: "object",
360
+ properties: { entity_id: { type: "string" }, target_state: { type: "string" } },
361
+ required: ["entity_id", "target_state"],
362
+ },
363
+ },
364
+ {
365
+ name: "admin.worker.drain",
366
+ description: "Mark a worker as draining — claimWork() will skip it.",
367
+ inputSchema: { type: "object", properties: { worker_id: { type: "string" } }, required: ["worker_id"] },
368
+ },
369
+ {
370
+ name: "admin.worker.undrain",
371
+ description: "Remove draining flag from a worker.",
372
+ inputSchema: { type: "object", properties: { worker_id: { type: "string" } }, required: ["worker_id"] },
373
+ },
374
+ {
375
+ name: "admin.gate.rerun",
376
+ description: "Clear a gate result for an entity and release it so the gate can be re-evaluated.",
377
+ inputSchema: {
378
+ type: "object",
379
+ properties: { entity_id: { type: "string" }, gate_name: { type: "string" } },
380
+ required: ["entity_id", "gate_name"],
381
+ },
382
+ },
340
383
  ];
341
384
  export function createMcpServer(deps, opts) {
342
385
  const server = new Server({ name: "defcon", version: "0.1.0" }, { capabilities: { tools: {} } });
@@ -413,6 +456,20 @@ export async function callToolHandler(deps, name, safeArgs, opts) {
413
456
  return await handleAdminFlowRestore(deps, safeArgs);
414
457
  case "admin.entity.create":
415
458
  return await handleAdminEntityCreate(deps, safeArgs);
459
+ case "admin.flow.pause":
460
+ return await handleAdminFlowPause(deps, safeArgs);
461
+ case "admin.flow.resume":
462
+ return await handleAdminFlowResume(deps, safeArgs);
463
+ case "admin.entity.cancel":
464
+ return await handleAdminEntityCancel(deps, safeArgs);
465
+ case "admin.entity.reset":
466
+ return await handleAdminEntityReset(deps, safeArgs);
467
+ case "admin.worker.drain":
468
+ return await handleAdminWorkerDrain(deps, safeArgs);
469
+ case "admin.worker.undrain":
470
+ return await handleAdminWorkerUndrain(deps, safeArgs);
471
+ case "admin.gate.rerun":
472
+ return await handleAdminGateRerun(deps, safeArgs);
416
473
  default:
417
474
  return errorResult(`Unknown tool: ${name}`);
418
475
  }
@@ -707,6 +764,106 @@ async function handleAdminEntityCreate(deps, args) {
707
764
  }
708
765
  return jsonResult(result);
709
766
  }
767
+ async function handleAdminFlowPause(deps, args) {
768
+ const v = validateInput(AdminFlowPauseSchema, args);
769
+ if (!v.ok)
770
+ return v.result;
771
+ const flow = await deps.flows.getByName(v.data.flow_name);
772
+ if (!flow)
773
+ return errorResult(`Flow not found: ${v.data.flow_name}`);
774
+ await deps.flows.update(flow.id, { paused: true });
775
+ emitDefinitionChanged(deps.eventRepo, flow.id, "admin.flow.pause", { name: v.data.flow_name });
776
+ return jsonResult({ paused: true, flow: v.data.flow_name });
777
+ }
778
+ async function handleAdminFlowResume(deps, args) {
779
+ const v = validateInput(AdminFlowPauseSchema, args);
780
+ if (!v.ok)
781
+ return v.result;
782
+ const flow = await deps.flows.getByName(v.data.flow_name);
783
+ if (!flow)
784
+ return errorResult(`Flow not found: ${v.data.flow_name}`);
785
+ await deps.flows.update(flow.id, { paused: false });
786
+ emitDefinitionChanged(deps.eventRepo, flow.id, "admin.flow.resume", { name: v.data.flow_name });
787
+ return jsonResult({ paused: false, flow: v.data.flow_name });
788
+ }
789
+ async function handleAdminEntityCancel(deps, args) {
790
+ const v = validateInput(AdminEntityCancelSchema, args);
791
+ if (!v.ok)
792
+ return v.result;
793
+ const entity = await deps.entities.get(v.data.entity_id);
794
+ if (!entity)
795
+ return errorResult(`Entity not found: ${v.data.entity_id}`);
796
+ const invocations = await deps.invocations.findByEntity(v.data.entity_id);
797
+ for (const inv of invocations) {
798
+ if (inv.completedAt === null && inv.failedAt === null) {
799
+ await deps.invocations.fail(inv.id, "Cancelled by admin");
800
+ }
801
+ }
802
+ await deps.entities.cancelEntity(v.data.entity_id);
803
+ return jsonResult({ cancelled: true, entity_id: v.data.entity_id });
804
+ }
805
+ async function handleAdminEntityReset(deps, args) {
806
+ const v = validateInput(AdminEntityResetSchema, args);
807
+ if (!v.ok)
808
+ return v.result;
809
+ const entity = await deps.entities.get(v.data.entity_id);
810
+ if (!entity)
811
+ return errorResult(`Entity not found: ${v.data.entity_id}`);
812
+ const flow = await deps.flows.get(entity.flowId);
813
+ if (!flow)
814
+ return errorResult(`Flow not found for entity: ${v.data.entity_id}`);
815
+ const targetState = flow.states.find((s) => s.name === v.data.target_state);
816
+ if (!targetState)
817
+ return errorResult(`State '${v.data.target_state}' not found in flow '${flow.name}'`);
818
+ const invocations = await deps.invocations.findByEntity(v.data.entity_id);
819
+ for (const inv of invocations) {
820
+ if (inv.completedAt === null && inv.failedAt === null) {
821
+ await deps.invocations.fail(inv.id, "Reset by admin");
822
+ }
823
+ }
824
+ const updated = await deps.entities.resetEntity(v.data.entity_id, v.data.target_state);
825
+ return jsonResult({ reset: true, entity_id: v.data.entity_id, state: updated.state });
826
+ }
827
+ async function handleAdminWorkerDrain(deps, args) {
828
+ const v = validateInput(AdminWorkerDrainSchema, args);
829
+ if (!v.ok)
830
+ return v.result;
831
+ if (!deps.engine)
832
+ return errorResult("Engine not available");
833
+ deps.engine.drainWorker(v.data.worker_id);
834
+ return jsonResult({ draining: true, worker_id: v.data.worker_id });
835
+ }
836
+ async function handleAdminWorkerUndrain(deps, args) {
837
+ const v = validateInput(AdminWorkerDrainSchema, args);
838
+ if (!v.ok)
839
+ return v.result;
840
+ if (!deps.engine)
841
+ return errorResult("Engine not available");
842
+ deps.engine.undrainWorker(v.data.worker_id);
843
+ return jsonResult({ draining: false, worker_id: v.data.worker_id });
844
+ }
845
+ async function handleAdminGateRerun(deps, args) {
846
+ const v = validateInput(AdminGateRerunSchema, args);
847
+ if (!v.ok)
848
+ return v.result;
849
+ const entity = await deps.entities.get(v.data.entity_id);
850
+ if (!entity)
851
+ return errorResult(`Entity not found: ${v.data.entity_id}`);
852
+ const gate = await deps.gates.getByName(v.data.gate_name);
853
+ if (!gate)
854
+ return errorResult(`Gate not found: ${v.data.gate_name}`);
855
+ await deps.gates.clearResult(v.data.entity_id, gate.id);
856
+ const invocations = await deps.invocations.findByEntity(v.data.entity_id);
857
+ for (const inv of invocations) {
858
+ if (inv.completedAt === null && inv.failedAt === null) {
859
+ await deps.invocations.fail(inv.id, "Gate rerun by admin");
860
+ }
861
+ }
862
+ if (entity.claimedBy) {
863
+ await deps.entities.release(entity.id, entity.claimedBy);
864
+ }
865
+ return jsonResult({ rerun: true, entity_id: v.data.entity_id, gate: v.data.gate_name });
866
+ }
710
867
  /** Start the MCP server on stdio transport. */
711
868
  export async function startStdioServer(deps, opts) {
712
869
  const server = createMcpServer(deps, opts);
@@ -23,5 +23,7 @@ export declare class DrizzleEntityRepository implements IEntityRepository {
23
23
  reapExpired(ttlMs: number): Promise<string[]>;
24
24
  setAffinity(entityId: string, workerId: string, role: string, expiresAt: Date): Promise<void>;
25
25
  clearExpiredAffinity(): Promise<string[]>;
26
+ cancelEntity(entityId: string): Promise<void>;
27
+ resetEntity(entityId: string, targetState: string): Promise<Entity>;
26
28
  }
27
29
  export {};
@@ -188,4 +188,21 @@ export class DrizzleEntityRepository {
188
188
  .all();
189
189
  return rows.map((r) => r.id);
190
190
  }
191
+ async cancelEntity(entityId) {
192
+ await this.db
193
+ .update(entities)
194
+ .set({ state: "cancelled", claimedBy: null, claimedAt: null, updatedAt: Date.now() })
195
+ .where(eq(entities.id, entityId));
196
+ }
197
+ async resetEntity(entityId, targetState) {
198
+ const now = Date.now();
199
+ await this.db
200
+ .update(entities)
201
+ .set({ state: targetState, claimedBy: null, claimedAt: null, updatedAt: now })
202
+ .where(eq(entities.id, entityId));
203
+ const rows = await this.db.select().from(entities).where(eq(entities.id, entityId)).limit(1);
204
+ if (rows.length === 0)
205
+ throw new NotFoundError(`Entity not found: ${entityId}`);
206
+ return this.toEntity(rows[0]);
207
+ }
191
208
  }
@@ -47,6 +47,7 @@ function rowToFlow(r, states, transitions) {
47
47
  discipline: r.discipline ?? null,
48
48
  defaultModelTier: r.defaultModelTier ?? null,
49
49
  timeoutPrompt: r.timeoutPrompt ?? null,
50
+ paused: !!r.paused,
50
51
  createdAt: toDate(r.createdAt),
51
52
  updatedAt: toDate(r.updatedAt),
52
53
  states,
@@ -91,6 +92,7 @@ export class DrizzleFlowRepository {
91
92
  discipline: input.discipline ?? null,
92
93
  defaultModelTier: input.defaultModelTier ?? null,
93
94
  timeoutPrompt: input.timeoutPrompt ?? null,
95
+ paused: input.paused ? 1 : 0,
94
96
  createdAt: now,
95
97
  updatedAt: now,
96
98
  };
@@ -148,6 +150,8 @@ export class DrizzleFlowRepository {
148
150
  updateValues.defaultModelTier = changes.defaultModelTier;
149
151
  if (changes.timeoutPrompt !== undefined)
150
152
  updateValues.timeoutPrompt = changes.timeoutPrompt;
153
+ if (changes.paused !== undefined)
154
+ updateValues.paused = changes.paused ? 1 : 0;
151
155
  this.db.update(flowDefinitions).set(updateValues).where(eq(flowDefinitions.id, id)).run();
152
156
  const updated = this.db.select().from(flowDefinitions).where(eq(flowDefinitions.id, id)).all();
153
157
  return this.hydrateFlow(updated[0]);
@@ -12,5 +12,6 @@ export declare class DrizzleGateRepository implements IGateRepository {
12
12
  record(entityId: string, gateId: string, passed: boolean, output: string): Promise<GateResult>;
13
13
  update(id: string, changes: Partial<Pick<Gate, "command" | "functionRef" | "apiConfig" | "timeoutMs" | "failurePrompt" | "timeoutPrompt">>): Promise<Gate>;
14
14
  resultsFor(entityId: string): Promise<GateResult[]>;
15
+ clearResult(entityId: string, gateId: string): Promise<void>;
15
16
  }
16
17
  export {};
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { asc, eq, sql } from "drizzle-orm";
2
+ import { and, asc, eq, sql } from "drizzle-orm";
3
3
  import { InternalError } from "../../errors.js";
4
4
  import { gateDefinitions, gateResults } from "./schema.js";
5
5
  function toGate(row) {
@@ -96,4 +96,10 @@ export class DrizzleGateRepository {
96
96
  .all();
97
97
  return rows.map(toGateResult);
98
98
  }
99
+ async clearResult(entityId, gateId) {
100
+ this.db
101
+ .delete(gateResults)
102
+ .where(and(eq(gateResults.entityId, entityId), eq(gateResults.gateId, gateId)))
103
+ .run();
104
+ }
99
105
  }
@@ -256,6 +256,23 @@ export declare const flowDefinitions: import("drizzle-orm/sqlite-core").SQLiteTa
256
256
  }, {}, {
257
257
  length: number | undefined;
258
258
  }>;
259
+ paused: import("drizzle-orm/sqlite-core").SQLiteColumn<{
260
+ name: "paused";
261
+ tableName: "flow_definitions";
262
+ dataType: "number";
263
+ columnType: "SQLiteInteger";
264
+ data: number;
265
+ driverParam: number;
266
+ notNull: false;
267
+ hasDefault: true;
268
+ isPrimaryKey: false;
269
+ isAutoincrement: false;
270
+ hasRuntimeDefault: false;
271
+ enumValues: undefined;
272
+ baseColumn: never;
273
+ identity: undefined;
274
+ generated: undefined;
275
+ }, {}, {}>;
259
276
  createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
260
277
  name: "created_at";
261
278
  tableName: "flow_definitions";
@@ -15,6 +15,7 @@ export const flowDefinitions = sqliteTable("flow_definitions", {
15
15
  discipline: text("discipline"),
16
16
  defaultModelTier: text("default_model_tier"),
17
17
  timeoutPrompt: text("timeout_prompt"),
18
+ paused: integer("paused").default(0),
18
19
  createdAt: integer("created_at"),
19
20
  updatedAt: integer("updated_at"),
20
21
  });
@@ -130,6 +130,7 @@ export interface Flow {
130
130
  discipline: string | null;
131
131
  defaultModelTier: string | null;
132
132
  timeoutPrompt: string | null;
133
+ paused: boolean;
133
134
  createdAt: Date | null;
134
135
  updatedAt: Date | null;
135
136
  states: State[];
@@ -159,6 +160,7 @@ export interface CreateFlowInput {
159
160
  discipline?: string;
160
161
  defaultModelTier?: string;
161
162
  timeoutPrompt?: string;
163
+ paused?: boolean;
162
164
  }
163
165
  /** Input for adding a state to a flow */
164
166
  export interface CreateStateInput {
@@ -225,6 +227,10 @@ export interface IEntityRepository {
225
227
  childFlow: string;
226
228
  spawnedAt: string;
227
229
  }): Promise<void>;
230
+ /** Move entity to 'cancelled' terminal state and clear claimedBy/claimedAt. */
231
+ cancelEntity(entityId: string): Promise<void>;
232
+ /** Move entity to targetState and clear claimedBy/claimedAt. Returns the updated entity. */
233
+ resetEntity(entityId: string, targetState: string): Promise<Entity>;
228
234
  }
229
235
  /** Fields that can be updated on a flow's top-level definition */
230
236
  export type UpdateFlowInput = Partial<Omit<Flow, "id" | "states" | "transitions" | "createdAt" | "updatedAt">>;
@@ -318,4 +324,6 @@ export interface IGateRepository {
318
324
  resultsFor(entityId: string): Promise<GateResult[]>;
319
325
  /** Update mutable fields on a gate definition. */
320
326
  update(id: string, changes: Partial<Pick<Gate, "command" | "functionRef" | "apiConfig" | "timeoutMs" | "failurePrompt" | "timeoutPrompt">>): Promise<Gate>;
327
+ /** Delete the gate result for a specific entity+gate combination. */
328
+ clearResult(entityId: string, gateId: string): Promise<void>;
321
329
  }
@@ -0,0 +1,19 @@
1
+ PRAGMA foreign_keys=OFF;--> statement-breakpoint
2
+ CREATE TABLE `__new_gate_definitions` (
3
+ `id` text PRIMARY KEY NOT NULL,
4
+ `name` text NOT NULL,
5
+ `type` text NOT NULL,
6
+ `command` text,
7
+ `function_ref` text,
8
+ `api_config` text,
9
+ `timeout_ms` integer,
10
+ `failure_prompt` text,
11
+ `timeout_prompt` text
12
+ );
13
+ --> statement-breakpoint
14
+ INSERT INTO `__new_gate_definitions`("id", "name", "type", "command", "function_ref", "api_config", "timeout_ms", "failure_prompt", "timeout_prompt") SELECT "id", "name", "type", "command", "function_ref", "api_config", "timeout_ms", "failure_prompt", "timeout_prompt" FROM `gate_definitions`;--> statement-breakpoint
15
+ DROP TABLE `gate_definitions`;--> statement-breakpoint
16
+ ALTER TABLE `__new_gate_definitions` RENAME TO `gate_definitions`;--> statement-breakpoint
17
+ PRAGMA foreign_keys=ON;--> statement-breakpoint
18
+ CREATE UNIQUE INDEX `gate_definitions_name_unique` ON `gate_definitions` (`name`);--> statement-breakpoint
19
+ ALTER TABLE `flow_definitions` ADD `paused` integer DEFAULT 0;