botinabox 1.4.2 → 1.6.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/index.js CHANGED
@@ -774,6 +774,147 @@ var MessagePipeline = class {
774
774
  }
775
775
  };
776
776
 
777
+ // src/core/chat/triage-router.ts
778
+ var TriageRouter = class {
779
+ constructor(db, hooks, config) {
780
+ this.db = db;
781
+ this.hooks = hooks;
782
+ this.rules = config.rules;
783
+ this.fallbackAgent = config.fallbackAgent;
784
+ this.llmFallback = config.llmFallback ?? true;
785
+ this.persist = config.persist ?? true;
786
+ this.compiledRules = this.rules.sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50)).map((rule) => ({
787
+ rule,
788
+ regexes: (rule.patterns ?? []).map((p) => new RegExp(p, "i")),
789
+ keywordSet: new Set((rule.keywords ?? []).map((k) => k.toLowerCase()))
790
+ }));
791
+ }
792
+ db;
793
+ hooks;
794
+ rules;
795
+ fallbackAgent;
796
+ llmFallback;
797
+ persist;
798
+ compiledRules;
799
+ /**
800
+ * Route an inbound message to the best agent.
801
+ * Returns the agent slug and the routing decision.
802
+ */
803
+ async route(msg) {
804
+ const body = msg.body.toLowerCase();
805
+ const words = new Set(body.split(/\s+/));
806
+ for (const { rule, regexes, keywordSet } of this.compiledRules) {
807
+ for (const keyword of keywordSet) {
808
+ if (words.has(keyword) || body.includes(keyword)) {
809
+ const decision2 = this.buildDecision(
810
+ rule.agentSlug,
811
+ `keyword: '${keyword}'`,
812
+ "deterministic",
813
+ msg
814
+ );
815
+ await this.logDecision(decision2);
816
+ return { agentSlug: rule.agentSlug, decision: decision2 };
817
+ }
818
+ }
819
+ for (const regex of regexes) {
820
+ if (regex.test(msg.body)) {
821
+ const decision2 = this.buildDecision(
822
+ rule.agentSlug,
823
+ `pattern: ${regex.source}`,
824
+ "deterministic",
825
+ msg
826
+ );
827
+ await this.logDecision(decision2);
828
+ return { agentSlug: rule.agentSlug, decision: decision2 };
829
+ }
830
+ }
831
+ }
832
+ if (this.llmFallback) {
833
+ const agentSlugs = this.rules.map((r) => r.agentSlug);
834
+ const classified = await this.classifyWithLLM(msg, agentSlugs);
835
+ if (classified) {
836
+ const decision2 = this.buildDecision(
837
+ classified.agentSlug,
838
+ `llm: ${classified.reason}`,
839
+ "llm",
840
+ msg
841
+ );
842
+ await this.logDecision(decision2);
843
+ return { agentSlug: classified.agentSlug, decision: decision2 };
844
+ }
845
+ }
846
+ const decision = this.buildDecision(
847
+ this.fallbackAgent,
848
+ "fallback: no rule matched",
849
+ "deterministic",
850
+ msg
851
+ );
852
+ await this.logDecision(decision);
853
+ return { agentSlug: this.fallbackAgent, decision };
854
+ }
855
+ /**
856
+ * Query the ownership chain for a given message or channel.
857
+ */
858
+ async getDecisionHistory(filter) {
859
+ const rows = await this.db.query("activity_log", {
860
+ where: { event_type: "triage_decision" }
861
+ });
862
+ let decisions = rows.map((r) => {
863
+ try {
864
+ return JSON.parse(r["payload"]);
865
+ } catch {
866
+ return null;
867
+ }
868
+ }).filter((d) => d !== null);
869
+ if (filter?.channel) {
870
+ decisions = decisions.filter((d) => d.channel === filter.channel);
871
+ }
872
+ decisions.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
873
+ if (filter?.limit) {
874
+ decisions = decisions.slice(0, filter.limit);
875
+ }
876
+ return decisions;
877
+ }
878
+ /**
879
+ * LLM classification — emits a hook for external LLM integration.
880
+ * Returns agent slug + reason, or undefined if LLM is unavailable.
881
+ */
882
+ async classifyWithLLM(msg, agentSlugs) {
883
+ const result = {};
884
+ await this.hooks.emit("triage.classify", {
885
+ message: msg,
886
+ candidates: agentSlugs,
887
+ respond: (slug, reason) => {
888
+ result.agentSlug = slug;
889
+ result.reason = reason;
890
+ }
891
+ });
892
+ if (result.agentSlug && result.reason) {
893
+ return { agentSlug: result.agentSlug, reason: result.reason };
894
+ }
895
+ return void 0;
896
+ }
897
+ buildDecision(target, reason, method, msg) {
898
+ return {
899
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
900
+ source: "triage",
901
+ target: target ?? "none",
902
+ reason,
903
+ method,
904
+ messageId: msg.id,
905
+ channel: msg.channel
906
+ };
907
+ }
908
+ async logDecision(decision) {
909
+ if (!this.persist) return;
910
+ await this.db.insert("activity_log", {
911
+ event_type: "triage_decision",
912
+ payload: JSON.stringify(decision)
913
+ });
914
+ await this.hooks.emit("triage.routed", { decision });
915
+ }
916
+ };
917
+
777
918
  // src/core/chat/session-key.ts
778
919
  var SessionKey = class _SessionKey {
779
920
  constructor(agentId, channel, scope) {
@@ -1639,6 +1780,52 @@ function defineCoreTables(db) {
1639
1780
  "CREATE UNIQUE INDEX IF NOT EXISTS idx_secrets_name_env ON secrets(name, environment, org_id) WHERE deleted_at IS NULL"
1640
1781
  ]
1641
1782
  });
1783
+ db.define("feedback", {
1784
+ columns: {
1785
+ id: "TEXT PRIMARY KEY",
1786
+ agent_id: "TEXT NOT NULL",
1787
+ task_id: "TEXT",
1788
+ issue: "TEXT NOT NULL",
1789
+ root_cause: "TEXT",
1790
+ severity: "TEXT NOT NULL DEFAULT 'medium'",
1791
+ repeatable: "INTEGER NOT NULL DEFAULT 0",
1792
+ accuracy_score: "REAL",
1793
+ efficiency_score: "REAL",
1794
+ tags: "TEXT NOT NULL DEFAULT '[]'",
1795
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
1796
+ },
1797
+ tableConstraints: [
1798
+ "CREATE INDEX IF NOT EXISTS idx_feedback_agent ON feedback(agent_id, created_at)",
1799
+ "CREATE INDEX IF NOT EXISTS idx_feedback_issue ON feedback(issue)"
1800
+ ]
1801
+ });
1802
+ db.define("playbooks", {
1803
+ columns: {
1804
+ id: "TEXT PRIMARY KEY",
1805
+ pattern: "TEXT NOT NULL",
1806
+ rule: "TEXT NOT NULL",
1807
+ feedback_ids: "TEXT NOT NULL DEFAULT '[]'",
1808
+ project_scoped: "INTEGER NOT NULL DEFAULT 1",
1809
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1810
+ updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1811
+ deleted_at: "TEXT"
1812
+ },
1813
+ tableConstraints: [
1814
+ "CREATE INDEX IF NOT EXISTS idx_playbooks_pattern ON playbooks(pattern)"
1815
+ ]
1816
+ });
1817
+ db.define("agent_playbooks", {
1818
+ columns: {
1819
+ agent_id: "TEXT NOT NULL",
1820
+ playbook_id: "TEXT NOT NULL",
1821
+ assigned_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
1822
+ },
1823
+ primaryKey: ["agent_id", "playbook_id"],
1824
+ tableConstraints: [
1825
+ "FOREIGN KEY (agent_id) REFERENCES agents(id)",
1826
+ "FOREIGN KEY (playbook_id) REFERENCES playbooks(id)"
1827
+ ]
1828
+ });
1642
1829
  }
1643
1830
 
1644
1831
  // src/core/data/core-migrations.ts
@@ -1704,12 +1891,55 @@ function defineCoreEntityContexts(db) {
1704
1891
  `# ${a.name}`,
1705
1892
  "",
1706
1893
  a.role ? `**Role:** ${a.role}` : null,
1894
+ a.adapter ? `**Adapter:** ${a.adapter}` : null,
1707
1895
  a.status ? `**Status:** ${a.status}` : null,
1708
1896
  a.cwd ? `**Working Directory:** ${a.cwd}` : null,
1709
1897
  a.reports_to ? `**Reports To:** ${a.reports_to}` : null,
1710
1898
  ""
1711
1899
  ].filter(Boolean).join("\n");
1712
1900
  }
1901
+ },
1902
+ "SKILLS.md": {
1903
+ source: {
1904
+ type: "manyToMany",
1905
+ junctionTable: "agent_skills",
1906
+ localKey: "agent_id",
1907
+ remoteKey: "skill_id",
1908
+ remoteTable: "skills"
1909
+ },
1910
+ omitIfEmpty: true,
1911
+ render: (rows) => {
1912
+ if (!rows.length) return "";
1913
+ const lines = [`# Skills (${rows.length})`, ""];
1914
+ for (const s of rows) {
1915
+ lines.push(`## ${s.name}`);
1916
+ if (s.category) lines.push(`**Category:** ${s.category}`);
1917
+ if (s.description) lines.push("", s.description);
1918
+ if (s.definition) lines.push("", "```", s.definition, "```");
1919
+ lines.push("");
1920
+ }
1921
+ return lines.join("\n");
1922
+ }
1923
+ },
1924
+ "PLAYBOOKS.md": {
1925
+ source: {
1926
+ type: "manyToMany",
1927
+ junctionTable: "agent_playbooks",
1928
+ localKey: "agent_id",
1929
+ remoteKey: "playbook_id",
1930
+ remoteTable: "playbooks"
1931
+ },
1932
+ omitIfEmpty: true,
1933
+ render: (rows) => {
1934
+ if (!rows.length) return "";
1935
+ const lines = [`# Playbooks (${rows.length})`, ""];
1936
+ for (const pb of rows) {
1937
+ lines.push(`## ${pb.pattern ?? pb.name ?? "Unnamed"}`);
1938
+ if (pb.rule) lines.push("", pb.rule);
1939
+ lines.push("");
1940
+ }
1941
+ return lines.join("\n");
1942
+ }
1713
1943
  }
1714
1944
  }
1715
1945
  });
@@ -2222,6 +2452,26 @@ ${r.rule_text}`
2222
2452
  return `# Project Rules
2223
2453
 
2224
2454
  ${lines.join("\n\n")}
2455
+ `;
2456
+ },
2457
+ omitIfEmpty: true
2458
+ }
2459
+ } : {},
2460
+ ...opts.files ? {
2461
+ "FILES.md": {
2462
+ source: {
2463
+ type: "hasMany",
2464
+ table: "file",
2465
+ foreignKey: "project_id"
2466
+ },
2467
+ render: (rows) => {
2468
+ if (!rows.length) return "";
2469
+ const lines = rows.map(
2470
+ (r) => `- [${r.name}](files/${r.name}/)${r.mime_type ? ` (${r.mime_type})` : ""}`
2471
+ );
2472
+ return `# Files
2473
+
2474
+ ${lines.join("\n")}
2225
2475
  `;
2226
2476
  },
2227
2477
  omitIfEmpty: true
@@ -2243,7 +2493,8 @@ ${lines.join("\n\n")}
2243
2493
  const agent = r.from_agent ? ` [${r.from_agent}]` : "";
2244
2494
  const body = r.body ?? "";
2245
2495
  const preview = truncateAtWord(body, 150);
2246
- return `- ${dir} **${ts}**${agent} ${preview}`;
2496
+ const link = `[${ts}](messages/${r.id}/)`;
2497
+ return `- ${dir} **${link}**${agent} ${preview}`;
2247
2498
  });
2248
2499
  return `# Messages
2249
2500
 
@@ -3076,6 +3327,13 @@ var RunManager = class {
3076
3327
  // agentId → runId
3077
3328
  orphanTimer = null;
3078
3329
  staleThresholdMs;
3330
+ circuitBreaker;
3331
+ /**
3332
+ * Attach a CircuitBreaker to prevent retries on broken agents.
3333
+ */
3334
+ setCircuitBreaker(cb) {
3335
+ this.circuitBreaker = cb;
3336
+ }
3079
3337
  isLocked(agentId) {
3080
3338
  return this.locks.has(agentId);
3081
3339
  }
@@ -3113,11 +3371,15 @@ var RunManager = class {
3113
3371
  this.locks.delete(agentId);
3114
3372
  const taskId = run["task_id"];
3115
3373
  if (!succeeded) {
3374
+ if (this.circuitBreaker) {
3375
+ await this.circuitBreaker.recordFailure(agentId, result.output);
3376
+ }
3116
3377
  const task = await this.db.get("tasks", { id: taskId });
3117
3378
  if (task) {
3118
3379
  const retryCount = task["retry_count"] ?? 0;
3119
3380
  const maxRetries = task["max_retries"] ?? 0;
3120
- if (retryCount < maxRetries) {
3381
+ const circuitOpen = this.circuitBreaker ? !this.circuitBreaker.canExecute(agentId) : false;
3382
+ if (retryCount < maxRetries && !circuitOpen) {
3121
3383
  const maxBackoff = this.config?.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
3122
3384
  const backoffMs = Math.min(BASE_BACKOFF_MS * Math.pow(2, retryCount), maxBackoff);
3123
3385
  const nextRetryAt = new Date(Date.now() + backoffMs).toISOString();
@@ -3136,6 +3398,9 @@ var RunManager = class {
3136
3398
  }
3137
3399
  }
3138
3400
  } else {
3401
+ if (this.circuitBreaker) {
3402
+ await this.circuitBreaker.recordSuccess(agentId);
3403
+ }
3139
3404
  await this.db.update("tasks", { id: taskId }, {
3140
3405
  status: "done",
3141
3406
  result: result.output,
@@ -4075,6 +4340,803 @@ var CliExecutionAdapter = class {
4075
4340
  }
4076
4341
  };
4077
4342
 
4343
+ // src/core/orchestrator/adapters/deterministic-adapter.ts
4344
+ var DEFAULT_TIMEOUT_MS = 3e4;
4345
+ var DeterministicAdapter = class {
4346
+ type = "deterministic";
4347
+ async execute(ctx) {
4348
+ const cwd = ctx.agent.cwd ?? process.cwd();
4349
+ let config = { command: "echo" };
4350
+ if (ctx.agent.adapter_config) {
4351
+ try {
4352
+ config = JSON.parse(ctx.agent.adapter_config);
4353
+ } catch {
4354
+ throw new Error("Invalid adapter_config for deterministic adapter");
4355
+ }
4356
+ }
4357
+ if (!config.command) {
4358
+ throw new Error("Deterministic adapter requires a command");
4359
+ }
4360
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4361
+ const payload = JSON.stringify({
4362
+ taskId: ctx.task.title,
4363
+ description: ctx.task.description ?? "",
4364
+ context: ctx.task.context ?? ""
4365
+ });
4366
+ const args = [...config.args ?? []];
4367
+ if (config.inputMode === "arg") {
4368
+ args.push(payload);
4369
+ }
4370
+ const child = spawnProcess(config.command, args, {
4371
+ cwd,
4372
+ extraEnv: config.env
4373
+ });
4374
+ if (config.inputMode !== "arg" && child.stdin) {
4375
+ child.stdin.write(payload);
4376
+ child.stdin.end();
4377
+ }
4378
+ const stdoutChunks = [];
4379
+ child.stdout?.on("data", (chunk) => {
4380
+ stdoutChunks.push(chunk);
4381
+ ctx.onLog?.("stdout", chunk.toString("utf8"));
4382
+ });
4383
+ child.stderr?.on("data", (chunk) => {
4384
+ ctx.onLog?.("stderr", chunk.toString("utf8"));
4385
+ });
4386
+ const abortHandler = () => {
4387
+ if (child.pid != null) killProcessGroup(child.pid);
4388
+ };
4389
+ ctx.abortSignal?.addEventListener("abort", abortHandler);
4390
+ const timeout = setTimeout(() => {
4391
+ if (child.pid != null) killProcessGroup(child.pid);
4392
+ }, timeoutMs);
4393
+ const exitCode = await new Promise((resolve) => {
4394
+ child.on("close", (code) => resolve(code ?? 1));
4395
+ child.on("error", () => resolve(1));
4396
+ });
4397
+ clearTimeout(timeout);
4398
+ ctx.abortSignal?.removeEventListener("abort", abortHandler);
4399
+ const output = Buffer.concat(stdoutChunks).toString("utf8");
4400
+ return { output, exitCode };
4401
+ }
4402
+ };
4403
+
4404
+ // src/core/orchestrator/loop-detector.ts
4405
+ var LoopType = /* @__PURE__ */ ((LoopType2) => {
4406
+ LoopType2["SELF_LOOP"] = "self_loop";
4407
+ LoopType2["PING_PONG"] = "ping_pong";
4408
+ LoopType2["BLOCKED_REENTRY"] = "blocked_reentry";
4409
+ return LoopType2;
4410
+ })(LoopType || {});
4411
+ var DEFAULT_WINDOW = 10;
4412
+ var DEFAULT_PING_PONG_THRESHOLD = 2;
4413
+ var LoopDetector = class {
4414
+ constructor(db, config) {
4415
+ this.db = db;
4416
+ this.windowSize = config?.windowSize ?? DEFAULT_WINDOW;
4417
+ this.pingPongThreshold = config?.pingPongThreshold ?? DEFAULT_PING_PONG_THRESHOLD;
4418
+ }
4419
+ db;
4420
+ windowSize;
4421
+ pingPongThreshold;
4422
+ /**
4423
+ * Check for loops before creating a followup task.
4424
+ * Returns a LoopDetection if a loop pattern is found, undefined otherwise.
4425
+ */
4426
+ async check(sourceAgentId, targetAgentId, taskId, chainOriginId) {
4427
+ const selfLoop = this.checkSelfLoop(sourceAgentId, targetAgentId, taskId);
4428
+ if (selfLoop) return selfLoop;
4429
+ const blocked = await this.checkBlockedReentry(targetAgentId, taskId, chainOriginId);
4430
+ if (blocked) return blocked;
4431
+ const pingPong = await this.checkPingPong(sourceAgentId, targetAgentId, chainOriginId);
4432
+ if (pingPong) return pingPong;
4433
+ return void 0;
4434
+ }
4435
+ /**
4436
+ * Check if an agent is routing to itself.
4437
+ */
4438
+ checkSelfLoop(sourceAgentId, targetAgentId, taskId) {
4439
+ if (sourceAgentId === targetAgentId) {
4440
+ return {
4441
+ type: "self_loop" /* SELF_LOOP */,
4442
+ agents: [sourceAgentId],
4443
+ taskId,
4444
+ message: `Self-loop detected: agent ${sourceAgentId} is routing to itself`
4445
+ };
4446
+ }
4447
+ return void 0;
4448
+ }
4449
+ /**
4450
+ * Check if a previously blocked task is being re-entered.
4451
+ */
4452
+ async checkBlockedReentry(targetAgentId, taskId, chainOriginId) {
4453
+ const originId = chainOriginId ?? taskId;
4454
+ const chainTasks = await this.db.query("tasks", {
4455
+ where: { chain_origin_id: originId }
4456
+ });
4457
+ const blockedInChain = chainTasks.filter(
4458
+ (t) => (t["status"] === "blocked" || t["status"] === "failed") && t["assignee_id"] === targetAgentId
4459
+ );
4460
+ if (blockedInChain.length > 0) {
4461
+ return {
4462
+ type: "blocked_reentry" /* BLOCKED_REENTRY */,
4463
+ agents: [targetAgentId],
4464
+ taskId,
4465
+ chainOriginId: originId,
4466
+ message: `Blocked re-entry: agent ${targetAgentId} already has a blocked/failed task in chain ${originId}`
4467
+ };
4468
+ }
4469
+ return void 0;
4470
+ }
4471
+ /**
4472
+ * Check for A→B→A→B ping-pong by scanning recent tasks in the chain.
4473
+ */
4474
+ async checkPingPong(sourceAgentId, targetAgentId, chainOriginId) {
4475
+ if (!chainOriginId) return void 0;
4476
+ const chainTasks = await this.db.query("tasks", {
4477
+ where: { chain_origin_id: chainOriginId }
4478
+ });
4479
+ const sorted = chainTasks.sort((a, b) => {
4480
+ const depthDiff = (a["chain_depth"] ?? 0) - (b["chain_depth"] ?? 0);
4481
+ if (depthDiff !== 0) return depthDiff;
4482
+ return (a["created_at"] ?? "").localeCompare(b["created_at"] ?? "");
4483
+ }).slice(-this.windowSize);
4484
+ const agentSequence = sorted.map((t) => t["assignee_id"]).filter(Boolean);
4485
+ agentSequence.push(targetAgentId);
4486
+ if (agentSequence.length >= this.pingPongThreshold * 2) {
4487
+ const tail = agentSequence.slice(-this.pingPongThreshold * 2);
4488
+ const a = tail[0];
4489
+ const b = tail[1];
4490
+ if (a && b && a !== b) {
4491
+ let isPingPong = true;
4492
+ for (let i = 0; i < tail.length; i++) {
4493
+ if (tail[i] !== (i % 2 === 0 ? a : b)) {
4494
+ isPingPong = false;
4495
+ break;
4496
+ }
4497
+ }
4498
+ if (isPingPong) {
4499
+ return {
4500
+ type: "ping_pong" /* PING_PONG */,
4501
+ agents: [a, b],
4502
+ taskId: sorted[sorted.length - 1]?.["id"] ?? "",
4503
+ chainOriginId,
4504
+ message: `Ping-pong detected: agents ${a} and ${b} are bouncing tasks in chain ${chainOriginId}`
4505
+ };
4506
+ }
4507
+ }
4508
+ }
4509
+ return void 0;
4510
+ }
4511
+ };
4512
+
4513
+ // src/core/orchestrator/circuit-breaker.ts
4514
+ var BreakerState = /* @__PURE__ */ ((BreakerState2) => {
4515
+ BreakerState2["CLOSED"] = "closed";
4516
+ BreakerState2["OPEN"] = "open";
4517
+ BreakerState2["HALF_OPEN"] = "half_open";
4518
+ return BreakerState2;
4519
+ })(BreakerState || {});
4520
+ var DEFAULT_FAILURE_THRESHOLD = 3;
4521
+ var DEFAULT_RESET_TIMEOUT_MS = 5 * 60 * 1e3;
4522
+ var CircuitBreaker = class {
4523
+ constructor(db, hooks, config) {
4524
+ this.db = db;
4525
+ this.hooks = hooks;
4526
+ this.failureThreshold = config?.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
4527
+ this.resetTimeoutMs = config?.resetTimeoutMs ?? DEFAULT_RESET_TIMEOUT_MS;
4528
+ this.persist = config?.persist ?? true;
4529
+ }
4530
+ db;
4531
+ hooks;
4532
+ breakers = /* @__PURE__ */ new Map();
4533
+ failureThreshold;
4534
+ resetTimeoutMs;
4535
+ persist;
4536
+ /**
4537
+ * Check if an agent is allowed to execute.
4538
+ * Returns true if execution is allowed, false if circuit is open.
4539
+ */
4540
+ canExecute(agentId) {
4541
+ const breaker = this.breakers.get(agentId);
4542
+ if (!breaker) return true;
4543
+ switch (breaker.state) {
4544
+ case "closed" /* CLOSED */:
4545
+ return true;
4546
+ case "open" /* OPEN */: {
4547
+ const elapsed = Date.now() - (breaker.trippedAt ?? 0);
4548
+ if (elapsed >= this.resetTimeoutMs) {
4549
+ breaker.state = "half_open" /* HALF_OPEN */;
4550
+ return true;
4551
+ }
4552
+ return false;
4553
+ }
4554
+ case "half_open" /* HALF_OPEN */:
4555
+ return true;
4556
+ }
4557
+ }
4558
+ /**
4559
+ * Record a successful execution. Resets the breaker to CLOSED.
4560
+ */
4561
+ async recordSuccess(agentId) {
4562
+ const breaker = this.breakers.get(agentId);
4563
+ if (!breaker) return;
4564
+ const previousState = breaker.state;
4565
+ breaker.state = "closed" /* CLOSED */;
4566
+ breaker.failureCount = 0;
4567
+ if (previousState === "half_open" /* HALF_OPEN */) {
4568
+ await this.logEvent(agentId, "circuit_recovered", {
4569
+ previousState
4570
+ });
4571
+ await this.hooks.emit("circuit_breaker.recovered", {
4572
+ agentId,
4573
+ previousState
4574
+ });
4575
+ }
4576
+ }
4577
+ /**
4578
+ * Record a failed execution. Increments failure count and may trip breaker.
4579
+ */
4580
+ async recordFailure(agentId, reason) {
4581
+ let breaker = this.breakers.get(agentId);
4582
+ if (!breaker) {
4583
+ breaker = {
4584
+ state: "closed" /* CLOSED */,
4585
+ failureCount: 0,
4586
+ lastFailureAt: Date.now()
4587
+ };
4588
+ this.breakers.set(agentId, breaker);
4589
+ }
4590
+ breaker.failureCount++;
4591
+ breaker.lastFailureAt = Date.now();
4592
+ if (breaker.state === "half_open" /* HALF_OPEN */) {
4593
+ await this.trip(agentId, reason ?? "Probe execution failed during half-open state");
4594
+ return;
4595
+ }
4596
+ if (breaker.failureCount >= this.failureThreshold) {
4597
+ await this.trip(agentId, reason ?? `Failure threshold reached (${this.failureThreshold})`);
4598
+ }
4599
+ }
4600
+ /**
4601
+ * Trip the breaker to OPEN state and escalate to human.
4602
+ */
4603
+ async trip(agentId, reason) {
4604
+ let breaker = this.breakers.get(agentId);
4605
+ if (!breaker) {
4606
+ breaker = {
4607
+ state: "closed" /* CLOSED */,
4608
+ failureCount: 0,
4609
+ lastFailureAt: Date.now()
4610
+ };
4611
+ this.breakers.set(agentId, breaker);
4612
+ }
4613
+ breaker.state = "open" /* OPEN */;
4614
+ breaker.trippedAt = Date.now();
4615
+ await this.logEvent(agentId, "circuit_tripped", {
4616
+ reason,
4617
+ failureCount: breaker.failureCount
4618
+ });
4619
+ await this.hooks.emit("circuit_breaker.tripped", {
4620
+ agentId,
4621
+ reason,
4622
+ failureCount: breaker.failureCount,
4623
+ action: "escalate_to_human"
4624
+ });
4625
+ }
4626
+ /**
4627
+ * Manually reset a breaker (e.g. after human review).
4628
+ */
4629
+ async reset(agentId) {
4630
+ this.breakers.delete(agentId);
4631
+ await this.logEvent(agentId, "circuit_reset", {});
4632
+ await this.hooks.emit("circuit_breaker.reset", { agentId });
4633
+ }
4634
+ /**
4635
+ * Get the current state of a breaker.
4636
+ */
4637
+ getState(agentId) {
4638
+ return this.breakers.get(agentId)?.state ?? "closed" /* CLOSED */;
4639
+ }
4640
+ /**
4641
+ * Get failure count for an agent.
4642
+ */
4643
+ getFailureCount(agentId) {
4644
+ return this.breakers.get(agentId)?.failureCount ?? 0;
4645
+ }
4646
+ async logEvent(agentId, eventType, payload) {
4647
+ if (!this.persist) return;
4648
+ await this.db.insert("activity_log", {
4649
+ agent_id: agentId,
4650
+ event_type: eventType,
4651
+ payload: JSON.stringify(payload)
4652
+ });
4653
+ }
4654
+ };
4655
+
4656
+ // src/core/orchestrator/learning-pipeline.ts
4657
+ var DEFAULT_PLAYBOOK_THRESHOLD = 3;
4658
+ var DEFAULT_SKILL_THRESHOLD = 3;
4659
+ var LearningPipeline = class {
4660
+ constructor(db, hooks, config) {
4661
+ this.db = db;
4662
+ this.hooks = hooks;
4663
+ this.playbookThreshold = config?.playbookThreshold ?? DEFAULT_PLAYBOOK_THRESHOLD;
4664
+ this.skillThreshold = config?.skillThreshold ?? DEFAULT_SKILL_THRESHOLD;
4665
+ this.autoPromote = config?.autoPromote ?? false;
4666
+ }
4667
+ db;
4668
+ hooks;
4669
+ playbookThreshold;
4670
+ skillThreshold;
4671
+ autoPromote;
4672
+ // --- Feedback Layer ---
4673
+ /**
4674
+ * Capture a structured feedback record from an execution.
4675
+ */
4676
+ async captureFeedback(entry) {
4677
+ const row = await this.db.insert("feedback", {
4678
+ agent_id: entry.agentId,
4679
+ task_id: entry.taskId,
4680
+ issue: entry.issue,
4681
+ root_cause: entry.rootCause,
4682
+ severity: entry.severity,
4683
+ repeatable: entry.repeatable ? 1 : 0,
4684
+ accuracy_score: entry.accuracyScore,
4685
+ efficiency_score: entry.efficiencyScore,
4686
+ tags: JSON.stringify(entry.tags ?? [])
4687
+ });
4688
+ const feedbackId = row["id"];
4689
+ await this.hooks.emit("learning.feedback_captured", {
4690
+ feedbackId,
4691
+ agentId: entry.agentId,
4692
+ issue: entry.issue,
4693
+ severity: entry.severity
4694
+ });
4695
+ if (this.autoPromote) {
4696
+ await this.checkPlaybookPromotion(entry.issue);
4697
+ }
4698
+ return feedbackId;
4699
+ }
4700
+ /**
4701
+ * Get all feedback records, optionally filtered.
4702
+ */
4703
+ async listFeedback(filter) {
4704
+ const where = {};
4705
+ if (filter?.agentId) where["agent_id"] = filter.agentId;
4706
+ if (filter?.severity) where["severity"] = filter.severity;
4707
+ if (filter?.repeatable !== void 0) where["repeatable"] = filter.repeatable ? 1 : 0;
4708
+ return this.db.query("feedback", Object.keys(where).length ? { where } : void 0);
4709
+ }
4710
+ // --- Playbook Layer ---
4711
+ /**
4712
+ * Check if feedback records with similar issues should be promoted to a playbook.
4713
+ * Groups by issue text similarity (exact match for now).
4714
+ */
4715
+ async checkPlaybookPromotion(issue) {
4716
+ const allFeedback = await this.db.query("feedback", {
4717
+ where: { issue }
4718
+ });
4719
+ if (allFeedback.length < this.playbookThreshold) {
4720
+ return void 0;
4721
+ }
4722
+ const existingPlaybooks = await this.db.query("playbooks", {
4723
+ where: { pattern: issue }
4724
+ });
4725
+ if (existingPlaybooks.length > 0) {
4726
+ return existingPlaybooks[0]["id"];
4727
+ }
4728
+ const feedbackIds = allFeedback.map((f) => f["id"]);
4729
+ const rootCauses = allFeedback.map((f) => f["root_cause"]).filter(Boolean);
4730
+ const rule = rootCauses.length > 0 ? `When encountering "${issue}": ${rootCauses[0]}` : `Pattern detected: "${issue}" \u2014 review and add specific guidance.`;
4731
+ const playbookId = await this.promoteToPlaybook({
4732
+ pattern: issue,
4733
+ rule,
4734
+ feedbackIds,
4735
+ projectScoped: true
4736
+ });
4737
+ return playbookId;
4738
+ }
4739
+ /**
4740
+ * Manually create a playbook from a set of feedback records.
4741
+ */
4742
+ async promoteToPlaybook(entry) {
4743
+ const row = await this.db.insert("playbooks", {
4744
+ pattern: entry.pattern,
4745
+ rule: entry.rule,
4746
+ feedback_ids: JSON.stringify(entry.feedbackIds),
4747
+ project_scoped: entry.projectScoped ? 1 : 0
4748
+ });
4749
+ const playbookId = row["id"];
4750
+ if (entry.agentIds) {
4751
+ for (const agentId of entry.agentIds) {
4752
+ await this.db.insert("agent_playbooks", {
4753
+ agent_id: agentId,
4754
+ playbook_id: playbookId
4755
+ });
4756
+ }
4757
+ }
4758
+ await this.hooks.emit("learning.playbook_promoted", {
4759
+ playbookId,
4760
+ pattern: entry.pattern,
4761
+ feedbackCount: entry.feedbackIds.length
4762
+ });
4763
+ return playbookId;
4764
+ }
4765
+ /**
4766
+ * List playbooks, optionally filtered.
4767
+ */
4768
+ async listPlaybooks(filter) {
4769
+ const where = {};
4770
+ if (filter?.projectScoped !== void 0) {
4771
+ where["project_scoped"] = filter.projectScoped ? 1 : 0;
4772
+ }
4773
+ return this.db.query("playbooks", Object.keys(where).length ? { where } : void 0);
4774
+ }
4775
+ // --- Skill Layer ---
4776
+ /**
4777
+ * Check if a playbook should be promoted to a skill.
4778
+ * A playbook becomes a skill when it works across multiple projects
4779
+ * (indicated by being referenced by agents in different contexts).
4780
+ */
4781
+ async checkSkillPromotion(playbookId) {
4782
+ const playbook = await this.db.get("playbooks", { id: playbookId });
4783
+ if (!playbook) return void 0;
4784
+ const links = await this.db.query("agent_playbooks", {
4785
+ where: { playbook_id: playbookId }
4786
+ });
4787
+ if (links.length < this.skillThreshold) {
4788
+ return void 0;
4789
+ }
4790
+ const pattern = playbook["pattern"];
4791
+ const existingSkills = await this.db.query("skills", {
4792
+ where: { name: pattern }
4793
+ });
4794
+ if (existingSkills.length > 0) {
4795
+ return existingSkills[0]["id"];
4796
+ }
4797
+ const slug = pattern.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 64);
4798
+ const skillId = await this.promoteToSkill({
4799
+ name: pattern,
4800
+ slug,
4801
+ description: `Auto-promoted from playbook: ${pattern}`,
4802
+ behavior: playbook["rule"],
4803
+ sourcePlaybookIds: [playbookId]
4804
+ });
4805
+ return skillId;
4806
+ }
4807
+ /**
4808
+ * Manually promote a playbook to a reusable skill.
4809
+ */
4810
+ async promoteToSkill(entry) {
4811
+ const row = await this.db.insert("skills", {
4812
+ name: entry.name,
4813
+ slug: entry.slug,
4814
+ description: entry.description,
4815
+ category: entry.category ?? "learned",
4816
+ definition: JSON.stringify({
4817
+ behavior: entry.behavior,
4818
+ source_playbook_ids: entry.sourcePlaybookIds
4819
+ })
4820
+ });
4821
+ const skillId = row["id"];
4822
+ await this.hooks.emit("learning.skill_promoted", {
4823
+ skillId,
4824
+ name: entry.name,
4825
+ slug: entry.slug,
4826
+ sourcePlaybookCount: entry.sourcePlaybookIds.length
4827
+ });
4828
+ return skillId;
4829
+ }
4830
+ /**
4831
+ * Assign a skill to an agent.
4832
+ */
4833
+ async assignSkill(agentId, skillId) {
4834
+ await this.db.link("agent_skills", {
4835
+ agent_id: agentId,
4836
+ skill_id: skillId
4837
+ });
4838
+ await this.hooks.emit("learning.skill_assigned", { agentId, skillId });
4839
+ }
4840
+ /**
4841
+ * Get learning metrics for an agent.
4842
+ */
4843
+ async getMetrics(agentId) {
4844
+ const feedback = await this.db.query("feedback", { where: { agent_id: agentId } });
4845
+ const accuracyScores = feedback.map((f) => f["accuracy_score"]).filter((s) => s !== null && s !== void 0);
4846
+ const efficiencyScores = feedback.map((f) => f["efficiency_score"]).filter((s) => s !== null && s !== void 0);
4847
+ let playbookCount = 0;
4848
+ try {
4849
+ const links = await this.db.query("agent_playbooks", { where: { agent_id: agentId } });
4850
+ playbookCount = links.length;
4851
+ } catch {
4852
+ }
4853
+ const skillLinks = await this.db.query("agent_skills", { where: { agent_id: agentId } });
4854
+ return {
4855
+ feedbackCount: feedback.length,
4856
+ avgAccuracy: accuracyScores.length > 0 ? accuracyScores.reduce((a, b) => a + b, 0) / accuracyScores.length : null,
4857
+ avgEfficiency: efficiencyScores.length > 0 ? efficiencyScores.reduce((a, b) => a + b, 0) / efficiencyScores.length : null,
4858
+ playbookCount,
4859
+ skillCount: skillLinks.length
4860
+ };
4861
+ }
4862
+ };
4863
+
4864
+ // src/core/orchestrator/permission-relay.ts
4865
+ var DEFAULT_POLL_INTERVAL_MS = 5e3;
4866
+ var DEFAULT_TIMEOUT_MS2 = 5 * 60 * 1e3;
4867
+ var PermissionRelay = class {
4868
+ constructor(hooks, config) {
4869
+ this.hooks = hooks;
4870
+ this.providers = config.providers;
4871
+ this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
4872
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
4873
+ }
4874
+ hooks;
4875
+ providers;
4876
+ pollIntervalMs;
4877
+ timeoutMs;
4878
+ pending = /* @__PURE__ */ new Map();
4879
+ /**
4880
+ * Request approval from all configured providers.
4881
+ * Returns when the first provider responds (approve or deny).
4882
+ */
4883
+ async requestApproval(prompt) {
4884
+ const expiresAt = new Date(Date.now() + this.timeoutMs).toISOString();
4885
+ const promptWithExpiry = { ...prompt, expiresAt };
4886
+ await this.hooks.emit("permission.requested", {
4887
+ promptId: prompt.id,
4888
+ agentId: prompt.agentId,
4889
+ action: prompt.action
4890
+ });
4891
+ const handles = /* @__PURE__ */ new Map();
4892
+ for (const provider of this.providers) {
4893
+ try {
4894
+ const handle = await provider.sendPrompt(promptWithExpiry);
4895
+ handles.set(provider.id, handle);
4896
+ } catch {
4897
+ }
4898
+ }
4899
+ if (handles.size === 0) {
4900
+ throw new Error("No permission providers available");
4901
+ }
4902
+ return new Promise((resolve, reject) => {
4903
+ const entry = {
4904
+ prompt: promptWithExpiry,
4905
+ handles,
4906
+ resolve,
4907
+ reject
4908
+ };
4909
+ this.pending.set(prompt.id, entry);
4910
+ const pollTimer = setInterval(async () => {
4911
+ for (const [providerId, handle] of handles) {
4912
+ const provider = this.providers.find((p) => p.id === providerId);
4913
+ if (!provider) continue;
4914
+ try {
4915
+ const response = await provider.pollResponse(handle);
4916
+ if (response) {
4917
+ clearInterval(pollTimer);
4918
+ clearTimeout(timeoutTimer);
4919
+ this.pending.delete(prompt.id);
4920
+ await this.cancelOtherProviders(handles, providerId);
4921
+ await this.hooks.emit("permission.responded", {
4922
+ promptId: prompt.id,
4923
+ status: response.status,
4924
+ respondedBy: response.respondedBy
4925
+ });
4926
+ resolve(response);
4927
+ return;
4928
+ }
4929
+ } catch {
4930
+ }
4931
+ }
4932
+ }, this.pollIntervalMs);
4933
+ const timeoutTimer = setTimeout(async () => {
4934
+ clearInterval(pollTimer);
4935
+ this.pending.delete(prompt.id);
4936
+ for (const [providerId, handle] of handles) {
4937
+ const provider = this.providers.find((p) => p.id === providerId);
4938
+ if (provider) {
4939
+ try {
4940
+ await provider.cancelPrompt(handle);
4941
+ } catch {
4942
+ }
4943
+ }
4944
+ }
4945
+ await this.hooks.emit("permission.expired", {
4946
+ promptId: prompt.id,
4947
+ agentId: prompt.agentId
4948
+ });
4949
+ reject(new Error(`Permission request expired after ${this.timeoutMs}ms`));
4950
+ }, this.timeoutMs);
4951
+ });
4952
+ }
4953
+ /**
4954
+ * Provide a local approval (from terminal).
4955
+ * Resolves the pending request and cancels remote providers.
4956
+ */
4957
+ async approveLocally(promptId, approved) {
4958
+ const entry = this.pending.get(promptId);
4959
+ if (!entry) return;
4960
+ const response = {
4961
+ promptId,
4962
+ status: approved ? "approved" : "denied",
4963
+ respondedBy: "local",
4964
+ respondedAt: (/* @__PURE__ */ new Date()).toISOString()
4965
+ };
4966
+ this.pending.delete(promptId);
4967
+ for (const [providerId, handle] of entry.handles) {
4968
+ const provider = this.providers.find((p) => p.id === providerId);
4969
+ if (provider) {
4970
+ try {
4971
+ await provider.cancelPrompt(handle);
4972
+ } catch {
4973
+ }
4974
+ }
4975
+ }
4976
+ await this.hooks.emit("permission.responded", {
4977
+ promptId,
4978
+ status: response.status,
4979
+ respondedBy: "local"
4980
+ });
4981
+ entry.resolve(response);
4982
+ }
4983
+ /**
4984
+ * Get all pending approval requests.
4985
+ */
4986
+ getPending() {
4987
+ return Array.from(this.pending.values()).map((e) => e.prompt);
4988
+ }
4989
+ async cancelOtherProviders(handles, excludeProviderId) {
4990
+ for (const [providerId, handle] of handles) {
4991
+ if (providerId === excludeProviderId) continue;
4992
+ const provider = this.providers.find((p) => p.id === providerId);
4993
+ if (provider) {
4994
+ try {
4995
+ await provider.cancelPrompt(handle);
4996
+ } catch {
4997
+ }
4998
+ }
4999
+ }
5000
+ }
5001
+ };
5002
+
5003
+ // src/core/orchestrator/governance-gate.ts
5004
+ var GovernanceGate = class {
5005
+ };
5006
+ var QAGate = class extends GovernanceGate {
5007
+ constructor(validators = []) {
5008
+ super();
5009
+ this.validators = validators;
5010
+ }
5011
+ validators;
5012
+ id = "qa";
5013
+ name = "Quality Assurance";
5014
+ dimension = "data_correctness";
5015
+ async check(input) {
5016
+ const start = Date.now();
5017
+ const findings = [];
5018
+ for (const validator of this.validators) {
5019
+ const results = validator.validate(input.output, input.metadata);
5020
+ findings.push(...results);
5021
+ }
5022
+ const hasErrors = findings.some((f) => f.severity === "error" || f.severity === "critical");
5023
+ const hasWarnings = findings.some((f) => f.severity === "warning");
5024
+ return {
5025
+ gateId: this.id,
5026
+ verdict: hasErrors ? "fail" : hasWarnings ? "warn" : "pass",
5027
+ findings,
5028
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
5029
+ durationMs: Date.now() - start
5030
+ };
5031
+ }
5032
+ };
5033
+ var QualityGate = class extends GovernanceGate {
5034
+ constructor(checks = []) {
5035
+ super();
5036
+ this.checks = checks;
5037
+ }
5038
+ checks;
5039
+ id = "quality";
5040
+ name = "Code Quality";
5041
+ dimension = "code_quality";
5042
+ async check(input) {
5043
+ const start = Date.now();
5044
+ const findings = [];
5045
+ for (const chk of this.checks) {
5046
+ const results = await chk.check(input.output, input.metadata);
5047
+ findings.push(...results);
5048
+ }
5049
+ const hasErrors = findings.some((f) => f.severity === "error" || f.severity === "critical");
5050
+ const hasWarnings = findings.some((f) => f.severity === "warning");
5051
+ return {
5052
+ gateId: this.id,
5053
+ verdict: hasErrors ? "fail" : hasWarnings ? "warn" : "pass",
5054
+ findings,
5055
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
5056
+ durationMs: Date.now() - start
5057
+ };
5058
+ }
5059
+ };
5060
+ var DriftGate = class extends GovernanceGate {
5061
+ constructor(rules = []) {
5062
+ super();
5063
+ this.rules = rules;
5064
+ }
5065
+ rules;
5066
+ id = "drift";
5067
+ name = "Architectural Drift";
5068
+ dimension = "architecture";
5069
+ async check(input) {
5070
+ const start = Date.now();
5071
+ const findings = [];
5072
+ for (const rule of this.rules) {
5073
+ const results = rule.detect(input.output, input.metadata);
5074
+ findings.push(...results);
5075
+ }
5076
+ const hasErrors = findings.some((f) => f.severity === "error" || f.severity === "critical");
5077
+ const hasWarnings = findings.some((f) => f.severity === "warning");
5078
+ return {
5079
+ gateId: this.id,
5080
+ verdict: hasErrors ? "fail" : hasWarnings ? "warn" : "pass",
5081
+ findings,
5082
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
5083
+ durationMs: Date.now() - start
5084
+ };
5085
+ }
5086
+ };
5087
+ var GateRunner = class {
5088
+ constructor(gates, hooks) {
5089
+ this.gates = gates;
5090
+ this.hooks = hooks;
5091
+ }
5092
+ gates;
5093
+ hooks;
5094
+ /**
5095
+ * Run all gates on the given input.
5096
+ * Gates run independently — one failure doesn't block others.
5097
+ */
5098
+ async runAll(input) {
5099
+ const results = [];
5100
+ for (const gate of this.gates) {
5101
+ try {
5102
+ const result = await gate.check(input);
5103
+ results.push(result);
5104
+ await this.hooks.emit("governance.gate_completed", {
5105
+ gateId: gate.id,
5106
+ gateName: gate.name,
5107
+ verdict: result.verdict,
5108
+ findingCount: result.findings.length,
5109
+ agentId: input.agentId,
5110
+ taskId: input.taskId
5111
+ });
5112
+ } catch (err) {
5113
+ results.push({
5114
+ gateId: gate.id,
5115
+ verdict: "fail",
5116
+ findings: [{
5117
+ severity: "error",
5118
+ message: `Gate error: ${err instanceof Error ? err.message : String(err)}`
5119
+ }],
5120
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
5121
+ durationMs: 0
5122
+ });
5123
+ }
5124
+ }
5125
+ const passed = results.every((r) => r.verdict !== "fail");
5126
+ await this.hooks.emit("governance.review_completed", {
5127
+ passed,
5128
+ agentId: input.agentId,
5129
+ taskId: input.taskId,
5130
+ results: results.map((r) => ({
5131
+ gateId: r.gateId,
5132
+ verdict: r.verdict,
5133
+ findingCount: r.findings.length
5134
+ }))
5135
+ });
5136
+ return { passed, results };
5137
+ }
5138
+ };
5139
+
4078
5140
  // src/core/orchestrator/user-registry.ts
4079
5141
  import { v4 as uuidv4 } from "uuid";
4080
5142
  var UserRegistry = class {
@@ -4406,25 +5468,37 @@ export {
4406
5468
  ApiExecutionAdapter,
4407
5469
  AuditEmitter,
4408
5470
  BackupManager,
5471
+ BreakerState,
4409
5472
  BudgetController,
4410
5473
  CORE_MIGRATIONS,
4411
5474
  ChannelRegistry,
4412
5475
  ChannelRegistryError,
4413
5476
  ChatSessionManager,
5477
+ CircuitBreaker,
4414
5478
  CliExecutionAdapter,
4415
5479
  ColumnValidatorImpl,
4416
5480
  DEFAULTS,
4417
5481
  DEFAULT_CONFIG,
4418
5482
  DataStore,
4419
5483
  DataStoreError,
5484
+ DeterministicAdapter,
5485
+ DriftGate,
4420
5486
  EVENTS,
5487
+ GateRunner,
5488
+ GovernanceGate,
4421
5489
  HookBus,
5490
+ LearningPipeline,
5491
+ LoopDetector,
5492
+ LoopType,
4422
5493
  MAX_CHAIN_DEPTH,
4423
5494
  MessagePipeline,
4424
5495
  ModelRouter,
4425
5496
  NdjsonLogger,
4426
5497
  NotificationQueue,
5498
+ PermissionRelay,
4427
5499
  ProviderRegistry,
5500
+ QAGate,
5501
+ QualityGate,
4428
5502
  RUN_STATUSES,
4429
5503
  RunManager,
4430
5504
  Scheduler,
@@ -4433,6 +5507,7 @@ export {
4433
5507
  SessionManager,
4434
5508
  TASK_STATUSES,
4435
5509
  TaskQueue,
5510
+ TriageRouter,
4436
5511
  UpdateChecker,
4437
5512
  UpdateManager,
4438
5513
  UserRegistry,