botinabox 2.5.1 → 2.5.2
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/LICENSE +21 -21
- package/README.md +190 -190
- package/bin/botinabox.mjs +1 -1
- package/dist/channels/discord/adapter.d.ts +32 -0
- package/dist/channels/discord/adapter.js +70 -0
- package/dist/channels/discord/inbound.d.ts +25 -0
- package/dist/channels/discord/inbound.js +24 -0
- package/dist/channels/discord/models.d.ts +8 -0
- package/dist/channels/discord/models.js +5 -0
- package/dist/channels/discord/outbound.d.ts +14 -0
- package/dist/channels/discord/outbound.js +38 -0
- package/dist/channels/slack/adapter.d.ts +33 -0
- package/dist/channels/slack/adapter.js +74 -0
- package/dist/channels/slack/inbound.d.ts +59 -0
- package/dist/channels/slack/inbound.js +96 -0
- package/dist/channels/slack/index.d.ts +1 -1
- package/dist/channels/slack/models.d.ts +9 -0
- package/dist/channels/slack/models.js +5 -0
- package/dist/channels/slack/outbound.d.ts +12 -0
- package/dist/channels/slack/outbound.js +18 -0
- package/dist/channels/slack/transcribe.d.ts +41 -0
- package/dist/channels/slack/transcribe.js +106 -0
- package/dist/channels/webhook/adapter.d.ts +23 -0
- package/dist/channels/webhook/adapter.js +86 -0
- package/dist/channels/webhook/hmac.d.ts +13 -0
- package/dist/channels/webhook/hmac.js +26 -0
- package/dist/channels/webhook/models.d.ts +9 -0
- package/dist/channels/webhook/models.js +5 -0
- package/dist/channels/webhook/server.d.ts +20 -0
- package/dist/channels/webhook/server.js +91 -0
- package/dist/chat-pipeline-C-XlLGNl.d.ts +648 -0
- package/dist/chat-pipeline-CR1KF6eX.d.ts +652 -0
- package/dist/chat-pipeline-DisuC8SB.d.ts +643 -0
- package/dist/chat-pipeline-DuNX5WoL.d.ts +655 -0
- package/dist/chunk-2LGXQPEA.js +41 -0
- package/dist/chunk-3X3YKI4T.js +357 -0
- package/dist/chunk-D47AIFOD.js +351 -0
- package/dist/chunk-DSNJKNEW.js +328 -0
- package/dist/chunk-GS2JFL6I.js +144 -0
- package/dist/chunk-J6S6QMUY.js +144 -0
- package/dist/chunk-QLA6YOFN.js +22 -0
- package/dist/chunk-UACT2WXX.js +381 -0
- package/dist/cli/templates/config.yml.d.ts +7 -0
- package/dist/cli/templates/config.yml.js +61 -0
- package/dist/cli/templates/env.d.ts +1 -0
- package/dist/cli/templates/env.js +30 -0
- package/dist/cli/templates/index.ts.d.ts +2 -0
- package/dist/cli/templates/index.ts.js +30 -0
- package/dist/cli/templates/package.json.d.ts +5 -0
- package/dist/cli/templates/package.json.js +28 -0
- package/dist/cli.js +0 -0
- package/dist/connector-DDahQw-2.d.ts +63 -0
- package/dist/connectors/google/calendar-connector.d.ts +40 -0
- package/dist/connectors/google/calendar-connector.js +243 -0
- package/dist/connectors/google/gmail-connector.d.ts +42 -0
- package/dist/connectors/google/gmail-connector.js +345 -0
- package/dist/connectors/google/oauth.d.ts +48 -0
- package/dist/connectors/google/oauth.js +112 -0
- package/dist/connectors/google/types.d.ts +78 -0
- package/dist/connectors/google/types.js +2 -0
- package/dist/core/chat/auto-discovery.d.ts +16 -0
- package/dist/core/chat/auto-discovery.js +54 -0
- package/dist/core/chat/channel-registry.d.ts +45 -0
- package/dist/core/chat/channel-registry.js +96 -0
- package/dist/core/chat/chat-pipeline.d.ts +113 -0
- package/dist/core/chat/chat-pipeline.js +395 -0
- package/dist/core/chat/chat-responder.d.ts +90 -0
- package/dist/core/chat/chat-responder.js +185 -0
- package/dist/core/chat/formatter.d.ts +11 -0
- package/dist/core/chat/formatter.js +60 -0
- package/dist/core/chat/index.d.ts +24 -0
- package/dist/core/chat/index.js +18 -0
- package/dist/core/chat/message-interpreter.d.ts +91 -0
- package/dist/core/chat/message-interpreter.js +166 -0
- package/dist/core/chat/message-store.d.ts +66 -0
- package/dist/core/chat/message-store.js +131 -0
- package/dist/core/chat/notification-queue.d.ts +34 -0
- package/dist/core/chat/notification-queue.js +111 -0
- package/dist/core/chat/pipeline.d.ts +38 -0
- package/dist/core/chat/pipeline.js +89 -0
- package/dist/core/chat/policies.d.ts +16 -0
- package/dist/core/chat/policies.js +25 -0
- package/dist/core/chat/routing.d.ts +17 -0
- package/dist/core/chat/routing.js +36 -0
- package/dist/core/chat/session-key.d.ts +30 -0
- package/dist/core/chat/session-key.js +65 -0
- package/dist/core/chat/session-manager.d.ts +17 -0
- package/dist/core/chat/session-manager.js +23 -0
- package/dist/core/chat/text-chunker.d.ts +9 -0
- package/dist/core/chat/text-chunker.js +48 -0
- package/dist/core/chat/triage-router.d.ts +75 -0
- package/dist/core/chat/triage-router.js +142 -0
- package/dist/core/chat/types.d.ts +5 -0
- package/dist/core/chat/types.js +5 -0
- package/dist/core/config/defaults.d.ts +2 -0
- package/dist/core/config/defaults.js +38 -0
- package/dist/core/config/index.d.ts +6 -0
- package/dist/core/config/index.js +4 -0
- package/dist/core/config/interpolate.d.ts +5 -0
- package/dist/core/config/interpolate.js +27 -0
- package/dist/core/config/loader.d.ts +24 -0
- package/dist/core/config/loader.js +59 -0
- package/dist/core/config/schema.d.ts +5 -0
- package/dist/core/config/schema.js +119 -0
- package/dist/core/data/core-entity-contexts.d.ts +14 -0
- package/dist/core/data/core-entity-contexts.js +197 -0
- package/dist/core/data/core-migrations.d.ts +5 -0
- package/dist/core/data/core-migrations.js +45 -0
- package/dist/core/data/core-schema.d.ts +6 -0
- package/dist/core/data/core-schema.js +454 -0
- package/dist/core/data/data-store.d.ts +67 -0
- package/dist/core/data/data-store.js +218 -0
- package/dist/core/data/domain-entity-contexts.d.ts +29 -0
- package/dist/core/data/domain-entity-contexts.js +321 -0
- package/dist/core/data/domain-schema.d.ts +36 -0
- package/dist/core/data/domain-schema.js +323 -0
- package/dist/core/data/index.d.ts +7 -0
- package/dist/core/data/index.js +7 -0
- package/dist/core/data/types.d.ts +111 -0
- package/dist/core/data/types.js +1 -0
- package/dist/core/hooks/hook-bus.d.ts +18 -0
- package/dist/core/hooks/hook-bus.js +120 -0
- package/dist/core/hooks/index.d.ts +2 -0
- package/dist/core/hooks/index.js +1 -0
- package/dist/core/hooks/types.d.ts +19 -0
- package/dist/core/hooks/types.js +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +4 -0
- package/dist/core/llm/auto-discovery.d.ts +11 -0
- package/dist/core/llm/auto-discovery.js +49 -0
- package/dist/core/llm/cost-tracker.d.ts +6 -0
- package/dist/core/llm/cost-tracker.js +38 -0
- package/dist/core/llm/index.d.ts +4 -0
- package/dist/core/llm/index.js +3 -0
- package/dist/core/llm/model-router.d.ts +25 -0
- package/dist/core/llm/model-router.js +49 -0
- package/dist/core/llm/provider-registry.d.ts +9 -0
- package/dist/core/llm/provider-registry.js +25 -0
- package/dist/core/llm/types.d.ts +2 -0
- package/dist/core/llm/types.js +2 -0
- package/dist/core/orchestrator/adapters/api-adapter.d.ts +34 -0
- package/dist/core/orchestrator/adapters/api-adapter.js +88 -0
- package/dist/core/orchestrator/adapters/cli-adapter.d.ts +22 -0
- package/dist/core/orchestrator/adapters/cli-adapter.js +69 -0
- package/dist/core/orchestrator/adapters/deterministic-adapter.d.ts +35 -0
- package/dist/core/orchestrator/adapters/deterministic-adapter.js +75 -0
- package/dist/core/orchestrator/adapters/env-whitelist.d.ts +4 -0
- package/dist/core/orchestrator/adapters/env-whitelist.js +27 -0
- package/dist/core/orchestrator/adapters/output-extractor.d.ts +11 -0
- package/dist/core/orchestrator/adapters/output-extractor.js +59 -0
- package/dist/core/orchestrator/adapters/process-manager.d.ts +15 -0
- package/dist/core/orchestrator/adapters/process-manager.js +26 -0
- package/dist/core/orchestrator/adapters/tool-loop.d.ts +22 -0
- package/dist/core/orchestrator/adapters/tool-loop.js +66 -0
- package/dist/core/orchestrator/agent-registry.d.ts +31 -0
- package/dist/core/orchestrator/agent-registry.js +135 -0
- package/dist/core/orchestrator/budget-controller.d.ts +19 -0
- package/dist/core/orchestrator/budget-controller.js +73 -0
- package/dist/core/orchestrator/chain-guard.d.ts +14 -0
- package/dist/core/orchestrator/chain-guard.js +23 -0
- package/dist/core/orchestrator/circuit-breaker.d.ts +65 -0
- package/dist/core/orchestrator/circuit-breaker.js +159 -0
- package/dist/core/orchestrator/claude-stream-parser.d.ts +31 -0
- package/dist/core/orchestrator/claude-stream-parser.js +99 -0
- package/dist/core/orchestrator/config-revisions.d.ts +6 -0
- package/dist/core/orchestrator/config-revisions.js +17 -0
- package/dist/core/orchestrator/dependency-resolver.d.ts +20 -0
- package/dist/core/orchestrator/dependency-resolver.js +78 -0
- package/dist/core/orchestrator/governance-gate.d.ts +110 -0
- package/dist/core/orchestrator/governance-gate.js +170 -0
- package/dist/core/orchestrator/learning-pipeline.d.ts +109 -0
- package/dist/core/orchestrator/learning-pipeline.js +249 -0
- package/dist/core/orchestrator/loop-detector.d.ts +51 -0
- package/dist/core/orchestrator/loop-detector.js +133 -0
- package/dist/core/orchestrator/ndjson-logger.d.ts +6 -0
- package/dist/core/orchestrator/ndjson-logger.js +18 -0
- package/dist/core/orchestrator/permission-relay.d.ts +72 -0
- package/dist/core/orchestrator/permission-relay.js +164 -0
- package/dist/core/orchestrator/run-manager.d.ts +31 -0
- package/dist/core/orchestrator/run-manager.js +178 -0
- package/dist/core/orchestrator/scheduler.d.ts +70 -0
- package/dist/core/orchestrator/scheduler.js +198 -0
- package/dist/core/orchestrator/secret-store.d.ts +57 -0
- package/dist/core/orchestrator/secret-store.js +171 -0
- package/dist/core/orchestrator/session-manager.d.ts +13 -0
- package/dist/core/orchestrator/session-manager.js +66 -0
- package/dist/core/orchestrator/task-queue.d.ts +34 -0
- package/dist/core/orchestrator/task-queue.js +83 -0
- package/dist/core/orchestrator/template-interpolate.d.ts +5 -0
- package/dist/core/orchestrator/template-interpolate.js +18 -0
- package/dist/core/orchestrator/user-registry.d.ts +47 -0
- package/dist/core/orchestrator/user-registry.js +76 -0
- package/dist/core/orchestrator/wakeup-queue.d.ts +9 -0
- package/dist/core/orchestrator/wakeup-queue.js +45 -0
- package/dist/core/orchestrator/workflow-engine.d.ts +47 -0
- package/dist/core/orchestrator/workflow-engine.js +204 -0
- package/dist/core/security/audit.d.ts +20 -0
- package/dist/core/security/audit.js +33 -0
- package/dist/core/security/column-validator.d.ts +20 -0
- package/dist/core/security/column-validator.js +37 -0
- package/dist/core/security/index.d.ts +5 -0
- package/dist/core/security/index.js +5 -0
- package/dist/core/security/process-env.d.ts +13 -0
- package/dist/core/security/process-env.js +49 -0
- package/dist/core/security/sanitizer.d.ts +11 -0
- package/dist/core/security/sanitizer.js +39 -0
- package/dist/core/security/types.d.ts +11 -0
- package/dist/core/security/types.js +1 -0
- package/dist/core/update/auto-update.d.ts +21 -0
- package/dist/core/update/auto-update.js +102 -0
- package/dist/core/update/backup-manager.d.ts +7 -0
- package/dist/core/update/backup-manager.js +24 -0
- package/dist/core/update/index.d.ts +8 -0
- package/dist/core/update/index.js +6 -0
- package/dist/core/update/migration-hooks.d.ts +11 -0
- package/dist/core/update/migration-hooks.js +10 -0
- package/dist/core/update/types.d.ts +11 -0
- package/dist/core/update/types.js +1 -0
- package/dist/core/update/update-checker.d.ts +11 -0
- package/dist/core/update/update-checker.js +63 -0
- package/dist/core/update/update-manager.d.ts +25 -0
- package/dist/core/update/update-manager.js +101 -0
- package/dist/core/update/version-utils.d.ts +6 -0
- package/dist/core/update/version-utils.js +34 -0
- package/dist/gmail-connector-2FVYTQJH.js +6 -0
- package/dist/gmail-connector-MNUBRNFM.js +6 -0
- package/dist/gmail-connector-PS2VLGNE.js +6 -0
- package/dist/gmail-connector-ULSMN6X2.js +6 -0
- package/dist/gmail-connector-URRFX6A3.js +6 -0
- package/dist/inbound-AFBUPSPG.js +10 -0
- package/dist/inbound-AFOHYNUY.js +6 -0
- package/dist/inbound-CGIXRXGC.js +8 -0
- package/dist/inbound-MCOLRH6U.js +10 -0
- package/dist/inbound-SNEMBLGA.js +6 -0
- package/dist/inbound-ZJHAYVMF.js +10 -0
- package/dist/index.d.ts +16 -3
- package/dist/index.js +67 -28
- package/dist/provider-qqJYv9nv.d.ts +75 -0
- package/dist/providers/anthropic/models.d.ts +2 -0
- package/dist/providers/anthropic/models.js +29 -0
- package/dist/providers/anthropic/provider.d.ts +13 -0
- package/dist/providers/anthropic/provider.js +119 -0
- package/dist/providers/anthropic/tool-converter.d.ts +10 -0
- package/dist/providers/anthropic/tool-converter.js +7 -0
- package/dist/providers/ollama/provider.d.ts +17 -0
- package/dist/providers/ollama/provider.js +185 -0
- package/dist/providers/openai/models.d.ts +2 -0
- package/dist/providers/openai/models.js +29 -0
- package/dist/providers/openai/provider.d.ts +13 -0
- package/dist/providers/openai/provider.js +163 -0
- package/dist/providers/openai/tool-converter.d.ts +10 -0
- package/dist/providers/openai/tool-converter.js +10 -0
- package/dist/shared/constants.d.ts +50 -0
- package/dist/shared/constants.js +64 -0
- package/dist/shared/index.d.ts +14 -0
- package/dist/shared/index.js +14 -0
- package/dist/shared/types/agent.d.ts +36 -0
- package/dist/shared/types/agent.js +2 -0
- package/dist/shared/types/channel.d.ts +70 -0
- package/dist/shared/types/channel.js +2 -0
- package/dist/shared/types/config.d.ts +111 -0
- package/dist/shared/types/config.js +2 -0
- package/dist/shared/types/connector.d.ts +77 -0
- package/dist/shared/types/connector.js +2 -0
- package/dist/shared/types/execution.d.ts +29 -0
- package/dist/shared/types/execution.js +2 -0
- package/dist/shared/types/provider.d.ts +73 -0
- package/dist/shared/types/provider.js +2 -0
- package/dist/shared/types/task.d.ts +47 -0
- package/dist/shared/types/task.js +2 -0
- package/dist/shared/types/workflow.d.ts +39 -0
- package/dist/shared/types/workflow.js +2 -0
- package/dist/shared/utils.d.ts +6 -0
- package/dist/shared/utils.js +13 -0
- package/dist/update-check.d.ts +5 -0
- package/dist/update-check.js +56 -0
- package/package.json +100 -100
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DataStore } from '../data/data-store.js';
|
|
2
|
+
import type { HookBus } from '../hooks/hook-bus.js';
|
|
3
|
+
import type { CircuitBreaker } from './circuit-breaker.js';
|
|
4
|
+
export declare class RunManager {
|
|
5
|
+
private db;
|
|
6
|
+
private hooks;
|
|
7
|
+
private config?;
|
|
8
|
+
private locks;
|
|
9
|
+
private orphanTimer;
|
|
10
|
+
private readonly staleThresholdMs;
|
|
11
|
+
private circuitBreaker?;
|
|
12
|
+
constructor(db: DataStore, hooks: HookBus, config?: {
|
|
13
|
+
staleThresholdMs?: number;
|
|
14
|
+
maxBackoffMs?: number;
|
|
15
|
+
} | undefined);
|
|
16
|
+
/**
|
|
17
|
+
* Attach a CircuitBreaker to prevent retries on broken agents.
|
|
18
|
+
*/
|
|
19
|
+
setCircuitBreaker(cb: CircuitBreaker): void;
|
|
20
|
+
isLocked(agentId: string): boolean;
|
|
21
|
+
startRun(agentId: string, taskId: string, adapter?: string): Promise<string>;
|
|
22
|
+
finishRun(runId: string, result: {
|
|
23
|
+
exitCode: number;
|
|
24
|
+
output?: string;
|
|
25
|
+
costCents?: number;
|
|
26
|
+
usage?: unknown;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
reapOrphans(): Promise<void>;
|
|
29
|
+
startOrphanReaper(intervalMs?: number): void;
|
|
30
|
+
stopOrphanReaper(): void;
|
|
31
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { checkChainDepth, MAX_CHAIN_DEPTH } from './chain-guard.js';
|
|
2
|
+
import { interpolate } from './template-interpolate.js';
|
|
3
|
+
const DEFAULT_STALE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
|
|
4
|
+
const BASE_BACKOFF_MS = 5_000;
|
|
5
|
+
const DEFAULT_MAX_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
|
+
export class RunManager {
|
|
7
|
+
db;
|
|
8
|
+
hooks;
|
|
9
|
+
config;
|
|
10
|
+
locks = new Map(); // agentId → runId
|
|
11
|
+
orphanTimer = null;
|
|
12
|
+
staleThresholdMs;
|
|
13
|
+
circuitBreaker;
|
|
14
|
+
constructor(db, hooks, config) {
|
|
15
|
+
this.db = db;
|
|
16
|
+
this.hooks = hooks;
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.staleThresholdMs = config?.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Attach a CircuitBreaker to prevent retries on broken agents.
|
|
22
|
+
*/
|
|
23
|
+
setCircuitBreaker(cb) {
|
|
24
|
+
this.circuitBreaker = cb;
|
|
25
|
+
}
|
|
26
|
+
isLocked(agentId) {
|
|
27
|
+
return this.locks.has(agentId);
|
|
28
|
+
}
|
|
29
|
+
async startRun(agentId, taskId, adapter) {
|
|
30
|
+
if (this.locks.has(agentId)) {
|
|
31
|
+
throw new Error(`Agent already has an active run`);
|
|
32
|
+
}
|
|
33
|
+
const row = await this.db.insert('runs', {
|
|
34
|
+
agent_id: agentId,
|
|
35
|
+
task_id: taskId,
|
|
36
|
+
adapter: adapter,
|
|
37
|
+
status: 'running',
|
|
38
|
+
started_at: new Date().toISOString(),
|
|
39
|
+
});
|
|
40
|
+
const runId = row['id'];
|
|
41
|
+
this.locks.set(agentId, runId);
|
|
42
|
+
return runId;
|
|
43
|
+
}
|
|
44
|
+
async finishRun(runId, result) {
|
|
45
|
+
const run = await this.db.get('runs', { id: runId });
|
|
46
|
+
if (!run)
|
|
47
|
+
throw new Error(`Run not found: ${runId}`);
|
|
48
|
+
const succeeded = result.exitCode === 0;
|
|
49
|
+
const status = succeeded ? 'succeeded' : 'failed';
|
|
50
|
+
const usage = result.usage;
|
|
51
|
+
await this.db.update('runs', { id: runId }, {
|
|
52
|
+
status,
|
|
53
|
+
completed_at: new Date().toISOString(),
|
|
54
|
+
exit_code: result.exitCode,
|
|
55
|
+
cost_cents: result.costCents ?? 0,
|
|
56
|
+
input_tokens: usage?.['inputTokens'] ?? 0,
|
|
57
|
+
output_tokens: usage?.['outputTokens'] ?? 0,
|
|
58
|
+
error_message: result.exitCode !== 0 ? result.output : undefined,
|
|
59
|
+
});
|
|
60
|
+
// Release lock
|
|
61
|
+
const agentId = run['agent_id'];
|
|
62
|
+
this.locks.delete(agentId);
|
|
63
|
+
const taskId = run['task_id'];
|
|
64
|
+
if (!succeeded) {
|
|
65
|
+
// Record failure in circuit breaker (if attached)
|
|
66
|
+
if (this.circuitBreaker) {
|
|
67
|
+
await this.circuitBreaker.recordFailure(agentId, result.output);
|
|
68
|
+
}
|
|
69
|
+
// Retry policy — skip retry if circuit breaker is open
|
|
70
|
+
const task = await this.db.get('tasks', { id: taskId });
|
|
71
|
+
if (task) {
|
|
72
|
+
const retryCount = task['retry_count'] ?? 0;
|
|
73
|
+
const maxRetries = task['max_retries'] ?? 0;
|
|
74
|
+
const circuitOpen = this.circuitBreaker
|
|
75
|
+
? !this.circuitBreaker.canExecute(agentId)
|
|
76
|
+
: false;
|
|
77
|
+
if (retryCount < maxRetries && !circuitOpen) {
|
|
78
|
+
// Exponential backoff: BASE_BACKOFF_MS * 2^retryCount
|
|
79
|
+
const maxBackoff = this.config?.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
|
|
80
|
+
const backoffMs = Math.min(BASE_BACKOFF_MS * Math.pow(2, retryCount), maxBackoff);
|
|
81
|
+
const nextRetryAt = new Date(Date.now() + backoffMs).toISOString();
|
|
82
|
+
await this.db.update('tasks', { id: taskId }, {
|
|
83
|
+
retry_count: retryCount + 1,
|
|
84
|
+
next_retry_at: nextRetryAt,
|
|
85
|
+
status: 'todo',
|
|
86
|
+
execution_run_id: null,
|
|
87
|
+
updated_at: new Date().toISOString(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Retries exhausted or circuit open — mark task as failed
|
|
92
|
+
await this.db.update('tasks', { id: taskId }, {
|
|
93
|
+
status: 'failed',
|
|
94
|
+
updated_at: new Date().toISOString(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Record success in circuit breaker (if attached)
|
|
101
|
+
if (this.circuitBreaker) {
|
|
102
|
+
await this.circuitBreaker.recordSuccess(agentId);
|
|
103
|
+
}
|
|
104
|
+
// Mark task done with result before emitting run.completed,
|
|
105
|
+
// so hook handlers can read task.result immediately.
|
|
106
|
+
await this.db.update('tasks', { id: taskId }, {
|
|
107
|
+
status: 'done',
|
|
108
|
+
result: result.output,
|
|
109
|
+
updated_at: new Date().toISOString(),
|
|
110
|
+
});
|
|
111
|
+
// Followup chain
|
|
112
|
+
const task = await this.db.get('tasks', { id: taskId });
|
|
113
|
+
if (task && task['followup_agent_id']) {
|
|
114
|
+
const chainDepth = (task['chain_depth'] ?? 0) + 1;
|
|
115
|
+
checkChainDepth(chainDepth, MAX_CHAIN_DEPTH);
|
|
116
|
+
const followupAgentId = task['followup_agent_id'];
|
|
117
|
+
const followupTemplate = task['followup_template'] ?? 'Followup: {{output}}';
|
|
118
|
+
const context = { output: result.output ?? '' };
|
|
119
|
+
const title = interpolate(followupTemplate, context);
|
|
120
|
+
const chainOriginId = task['chain_origin_id'] ?? taskId;
|
|
121
|
+
await this.db.insert('tasks', {
|
|
122
|
+
title,
|
|
123
|
+
description: title,
|
|
124
|
+
assignee_id: followupAgentId,
|
|
125
|
+
status: 'todo',
|
|
126
|
+
priority: task['priority'] ?? 5,
|
|
127
|
+
chain_depth: chainDepth,
|
|
128
|
+
chain_origin_id: chainOriginId,
|
|
129
|
+
});
|
|
130
|
+
await this.hooks.emit('task.followup.created', {
|
|
131
|
+
originTaskId: taskId,
|
|
132
|
+
followupAgentId,
|
|
133
|
+
chainDepth,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
await this.hooks.emit('run.completed', {
|
|
138
|
+
runId,
|
|
139
|
+
agentId,
|
|
140
|
+
taskId,
|
|
141
|
+
status,
|
|
142
|
+
exitCode: result.exitCode,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async reapOrphans() {
|
|
146
|
+
const cutoff = new Date(Date.now() - this.staleThresholdMs).toISOString();
|
|
147
|
+
const staleRuns = (await this.db.query('runs', { where: { status: 'running' } }))
|
|
148
|
+
.filter((r) => {
|
|
149
|
+
const startedAt = r['started_at'];
|
|
150
|
+
return startedAt != null && startedAt < cutoff;
|
|
151
|
+
});
|
|
152
|
+
for (const run of staleRuns) {
|
|
153
|
+
await this.db.update('runs', { id: run['id'] }, {
|
|
154
|
+
status: 'failed',
|
|
155
|
+
completed_at: new Date().toISOString(),
|
|
156
|
+
error_message: 'Orphaned run reaped by RunManager',
|
|
157
|
+
});
|
|
158
|
+
// Release in-memory lock if we hold it
|
|
159
|
+
const agentId = run['agent_id'];
|
|
160
|
+
if (this.locks.get(agentId) === run['id']) {
|
|
161
|
+
this.locks.delete(agentId);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
startOrphanReaper(intervalMs = 60_000) {
|
|
166
|
+
if (this.orphanTimer)
|
|
167
|
+
return;
|
|
168
|
+
this.orphanTimer = setInterval(() => {
|
|
169
|
+
void this.reapOrphans();
|
|
170
|
+
}, intervalMs);
|
|
171
|
+
}
|
|
172
|
+
stopOrphanReaper() {
|
|
173
|
+
if (this.orphanTimer) {
|
|
174
|
+
clearInterval(this.orphanTimer);
|
|
175
|
+
this.orphanTimer = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler — database-backed job scheduling with cron expressions.
|
|
3
|
+
*
|
|
4
|
+
* Supports one-time and recurring schedules. When a schedule fires,
|
|
5
|
+
* it emits the schedule's `action` as a hook event with the
|
|
6
|
+
* `action_config` payload. Consumers subscribe to handle the action.
|
|
7
|
+
*
|
|
8
|
+
* Supports one-time and recurring use cases.
|
|
9
|
+
*/
|
|
10
|
+
import type { DataStore } from "../data/data-store.js";
|
|
11
|
+
import type { HookBus } from "../hooks/hook-bus.js";
|
|
12
|
+
export interface ScheduleDef {
|
|
13
|
+
name: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Cron expression for recurring schedules */
|
|
16
|
+
cron?: string;
|
|
17
|
+
/** ISO 8601 datetime for one-time schedules */
|
|
18
|
+
runAt?: string;
|
|
19
|
+
/** Hook event name to emit when fired */
|
|
20
|
+
action: string;
|
|
21
|
+
/** JSON-serializable payload passed to the hook */
|
|
22
|
+
actionConfig?: Record<string, unknown>;
|
|
23
|
+
timezone?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface Schedule {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
description: string | null;
|
|
29
|
+
type: "one_time" | "recurring";
|
|
30
|
+
cron: string | null;
|
|
31
|
+
run_at: string | null;
|
|
32
|
+
timezone: string;
|
|
33
|
+
enabled: number;
|
|
34
|
+
action: string;
|
|
35
|
+
action_config: string;
|
|
36
|
+
last_fired_at: string | null;
|
|
37
|
+
next_fire_at: string | null;
|
|
38
|
+
created_at: string;
|
|
39
|
+
updated_at: string;
|
|
40
|
+
deleted_at: string | null;
|
|
41
|
+
}
|
|
42
|
+
export declare class Scheduler {
|
|
43
|
+
private db;
|
|
44
|
+
private hooks;
|
|
45
|
+
private timer;
|
|
46
|
+
constructor(db: DataStore, hooks: HookBus);
|
|
47
|
+
/**
|
|
48
|
+
* Start the scheduler. Computes initial next_fire_at for schedules
|
|
49
|
+
* that don't have one, then polls for due schedules.
|
|
50
|
+
*/
|
|
51
|
+
start(pollIntervalMs?: number): Promise<void>;
|
|
52
|
+
stop(): void;
|
|
53
|
+
/** Check for and fire due schedules. */
|
|
54
|
+
tick(): Promise<void>;
|
|
55
|
+
/** Register a new schedule. */
|
|
56
|
+
register(def: ScheduleDef): Promise<string>;
|
|
57
|
+
/** Update an existing schedule. */
|
|
58
|
+
update(id: string, changes: Partial<Pick<ScheduleDef, "name" | "cron" | "runAt" | "action" | "actionConfig" | "timezone" | "description">> & {
|
|
59
|
+
enabled?: boolean;
|
|
60
|
+
}): Promise<void>;
|
|
61
|
+
/** Soft-delete a schedule. */
|
|
62
|
+
unregister(id: string): Promise<void>;
|
|
63
|
+
/** List schedules, optionally filtered. */
|
|
64
|
+
list(filter?: {
|
|
65
|
+
enabled?: boolean;
|
|
66
|
+
action?: string;
|
|
67
|
+
}): Promise<Schedule[]>;
|
|
68
|
+
/** Compute next_fire_at for any enabled schedule missing it. */
|
|
69
|
+
private initializeNextFireTimes;
|
|
70
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler — database-backed job scheduling with cron expressions.
|
|
3
|
+
*
|
|
4
|
+
* Supports one-time and recurring schedules. When a schedule fires,
|
|
5
|
+
* it emits the schedule's `action` as a hook event with the
|
|
6
|
+
* `action_config` payload. Consumers subscribe to handle the action.
|
|
7
|
+
*
|
|
8
|
+
* Supports one-time and recurring use cases.
|
|
9
|
+
*/
|
|
10
|
+
import cronParser from "cron-parser";
|
|
11
|
+
import { v4 as uuid } from "uuid";
|
|
12
|
+
function computeNextFire(cron, timezone, after) {
|
|
13
|
+
const interval = cronParser.parseExpression(cron, {
|
|
14
|
+
currentDate: after ?? new Date(),
|
|
15
|
+
tz: timezone,
|
|
16
|
+
});
|
|
17
|
+
return interval.next().toISOString();
|
|
18
|
+
}
|
|
19
|
+
export class Scheduler {
|
|
20
|
+
db;
|
|
21
|
+
hooks;
|
|
22
|
+
timer = null;
|
|
23
|
+
constructor(db, hooks) {
|
|
24
|
+
this.db = db;
|
|
25
|
+
this.hooks = hooks;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Start the scheduler. Computes initial next_fire_at for schedules
|
|
29
|
+
* that don't have one, then polls for due schedules.
|
|
30
|
+
*/
|
|
31
|
+
async start(pollIntervalMs = 30_000) {
|
|
32
|
+
await this.initializeNextFireTimes();
|
|
33
|
+
this.timer = setInterval(() => {
|
|
34
|
+
void this.tick();
|
|
35
|
+
}, pollIntervalMs);
|
|
36
|
+
// Fire immediately on start
|
|
37
|
+
void this.tick();
|
|
38
|
+
}
|
|
39
|
+
stop() {
|
|
40
|
+
if (this.timer) {
|
|
41
|
+
clearInterval(this.timer);
|
|
42
|
+
this.timer = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Check for and fire due schedules. */
|
|
46
|
+
async tick() {
|
|
47
|
+
const now = new Date().toISOString();
|
|
48
|
+
const schedules = (await this.db.query("schedules", {
|
|
49
|
+
where: { enabled: 1 },
|
|
50
|
+
})).filter((s) => s["deleted_at"] == null &&
|
|
51
|
+
s["next_fire_at"] != null &&
|
|
52
|
+
s["next_fire_at"] <= now);
|
|
53
|
+
for (const schedule of schedules) {
|
|
54
|
+
try {
|
|
55
|
+
const config = JSON.parse(schedule.action_config || "{}");
|
|
56
|
+
// Filter prototype pollution vectors
|
|
57
|
+
const safeConfig = {};
|
|
58
|
+
for (const [k, v] of Object.entries(config)) {
|
|
59
|
+
if (!k.startsWith('__'))
|
|
60
|
+
safeConfig[k] = v;
|
|
61
|
+
}
|
|
62
|
+
// Emit the action hook (e.g. 'connector.sync', 'channel.send')
|
|
63
|
+
await this.hooks.emit(schedule.action, {
|
|
64
|
+
schedule_id: schedule.id,
|
|
65
|
+
schedule_name: schedule.name,
|
|
66
|
+
...safeConfig,
|
|
67
|
+
});
|
|
68
|
+
// Emit observability hook
|
|
69
|
+
await this.hooks.emit("schedule.fired", {
|
|
70
|
+
schedule_id: schedule.id,
|
|
71
|
+
schedule_name: schedule.name,
|
|
72
|
+
action: schedule.action,
|
|
73
|
+
fired_at: now,
|
|
74
|
+
});
|
|
75
|
+
// Update last_fired_at and compute next
|
|
76
|
+
if (schedule.type === "recurring" && schedule.cron) {
|
|
77
|
+
const nextFire = computeNextFire(schedule.cron, schedule.timezone, new Date());
|
|
78
|
+
await this.db.update("schedules", { id: schedule.id }, {
|
|
79
|
+
last_fired_at: now,
|
|
80
|
+
next_fire_at: nextFire,
|
|
81
|
+
updated_at: now,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// One-time: disable after firing
|
|
86
|
+
await this.db.update("schedules", { id: schedule.id }, {
|
|
87
|
+
last_fired_at: now,
|
|
88
|
+
next_fire_at: null,
|
|
89
|
+
enabled: 0,
|
|
90
|
+
updated_at: now,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.error(`[Scheduler] Error firing schedule "${schedule.name}":`, err);
|
|
96
|
+
await this.hooks.emit("schedule.error", {
|
|
97
|
+
schedule_id: schedule.id,
|
|
98
|
+
schedule_name: schedule.name,
|
|
99
|
+
error: String(err),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Register a new schedule. */
|
|
105
|
+
async register(def) {
|
|
106
|
+
const id = uuid();
|
|
107
|
+
const type = def.cron ? "recurring" : "one_time";
|
|
108
|
+
const timezone = def.timezone ?? "UTC";
|
|
109
|
+
let nextFire = null;
|
|
110
|
+
if (def.cron) {
|
|
111
|
+
nextFire = computeNextFire(def.cron, timezone);
|
|
112
|
+
}
|
|
113
|
+
else if (def.runAt) {
|
|
114
|
+
nextFire = new Date(def.runAt).toISOString();
|
|
115
|
+
}
|
|
116
|
+
await this.db.insert("schedules", {
|
|
117
|
+
id,
|
|
118
|
+
name: def.name,
|
|
119
|
+
description: def.description ?? null,
|
|
120
|
+
type,
|
|
121
|
+
cron: def.cron ?? null,
|
|
122
|
+
run_at: def.runAt ?? null,
|
|
123
|
+
timezone,
|
|
124
|
+
enabled: 1,
|
|
125
|
+
action: def.action,
|
|
126
|
+
action_config: JSON.stringify(def.actionConfig ?? {}),
|
|
127
|
+
next_fire_at: nextFire,
|
|
128
|
+
});
|
|
129
|
+
return id;
|
|
130
|
+
}
|
|
131
|
+
/** Update an existing schedule. */
|
|
132
|
+
async update(id, changes) {
|
|
133
|
+
const row = {
|
|
134
|
+
updated_at: new Date().toISOString(),
|
|
135
|
+
};
|
|
136
|
+
if (changes.name !== undefined)
|
|
137
|
+
row["name"] = changes.name;
|
|
138
|
+
if (changes.description !== undefined)
|
|
139
|
+
row["description"] = changes.description;
|
|
140
|
+
if (changes.action !== undefined)
|
|
141
|
+
row["action"] = changes.action;
|
|
142
|
+
if (changes.actionConfig !== undefined)
|
|
143
|
+
row["action_config"] = JSON.stringify(changes.actionConfig);
|
|
144
|
+
if (changes.enabled !== undefined)
|
|
145
|
+
row["enabled"] = changes.enabled ? 1 : 0;
|
|
146
|
+
if (changes.cron !== undefined) {
|
|
147
|
+
row["cron"] = changes.cron;
|
|
148
|
+
row["type"] = "recurring";
|
|
149
|
+
row["run_at"] = null;
|
|
150
|
+
row["next_fire_at"] = computeNextFire(changes.cron, changes.timezone ?? "UTC");
|
|
151
|
+
}
|
|
152
|
+
else if (changes.runAt !== undefined) {
|
|
153
|
+
row["run_at"] = changes.runAt;
|
|
154
|
+
row["type"] = "one_time";
|
|
155
|
+
row["cron"] = null;
|
|
156
|
+
row["next_fire_at"] = new Date(changes.runAt).toISOString();
|
|
157
|
+
}
|
|
158
|
+
if (changes.timezone !== undefined)
|
|
159
|
+
row["timezone"] = changes.timezone;
|
|
160
|
+
await this.db.update("schedules", { id }, row);
|
|
161
|
+
}
|
|
162
|
+
/** Soft-delete a schedule. */
|
|
163
|
+
async unregister(id) {
|
|
164
|
+
await this.db.update("schedules", { id }, {
|
|
165
|
+
enabled: 0,
|
|
166
|
+
deleted_at: new Date().toISOString(),
|
|
167
|
+
updated_at: new Date().toISOString(),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/** List schedules, optionally filtered. */
|
|
171
|
+
async list(filter) {
|
|
172
|
+
const where = {};
|
|
173
|
+
if (filter?.enabled !== undefined)
|
|
174
|
+
where["enabled"] = filter.enabled ? 1 : 0;
|
|
175
|
+
if (filter?.action !== undefined)
|
|
176
|
+
where["action"] = filter.action;
|
|
177
|
+
return (await this.db.query("schedules", { where })).filter((s) => s["deleted_at"] == null);
|
|
178
|
+
}
|
|
179
|
+
/** Compute next_fire_at for any enabled schedule missing it. */
|
|
180
|
+
async initializeNextFireTimes() {
|
|
181
|
+
const schedules = (await this.db.query("schedules", { where: { enabled: 1 } })).filter((s) => s["deleted_at"] == null && s["next_fire_at"] == null);
|
|
182
|
+
for (const s of schedules) {
|
|
183
|
+
let nextFire = null;
|
|
184
|
+
if (s.type === "recurring" && s.cron) {
|
|
185
|
+
nextFire = computeNextFire(s.cron, s.timezone);
|
|
186
|
+
}
|
|
187
|
+
else if (s.type === "one_time" && s.run_at) {
|
|
188
|
+
nextFire = new Date(s.run_at).toISOString();
|
|
189
|
+
}
|
|
190
|
+
if (nextFire) {
|
|
191
|
+
await this.db.update("schedules", { id: s.id }, {
|
|
192
|
+
next_fire_at: nextFire,
|
|
193
|
+
updated_at: new Date().toISOString(),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { DataStore } from "../data/data-store.js";
|
|
2
|
+
import type { HookBus } from "../hooks/hook-bus.js";
|
|
3
|
+
export interface SecretInput {
|
|
4
|
+
name: string;
|
|
5
|
+
type?: string;
|
|
6
|
+
environment?: string;
|
|
7
|
+
value?: string;
|
|
8
|
+
location?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
rotation_schedule?: string;
|
|
11
|
+
expires_at?: string;
|
|
12
|
+
notes?: string;
|
|
13
|
+
org_id?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface SecretMeta {
|
|
16
|
+
id: string;
|
|
17
|
+
org_id: string | null;
|
|
18
|
+
name: string;
|
|
19
|
+
type: string;
|
|
20
|
+
environment: string;
|
|
21
|
+
location: string | null;
|
|
22
|
+
description: string | null;
|
|
23
|
+
rotation_schedule: string | null;
|
|
24
|
+
expires_at: string | null;
|
|
25
|
+
notes: string | null;
|
|
26
|
+
created_at: string;
|
|
27
|
+
updated_at: string;
|
|
28
|
+
}
|
|
29
|
+
export declare class SecretStore {
|
|
30
|
+
private readonly db;
|
|
31
|
+
private readonly hooks;
|
|
32
|
+
private readonly encKey;
|
|
33
|
+
/**
|
|
34
|
+
* @param db - DataStore instance
|
|
35
|
+
* @param hooks - HookBus instance
|
|
36
|
+
* @param encryptionKey - Optional master key for encrypting secrets at rest.
|
|
37
|
+
* When provided, all new secrets are encrypted with AES-256-GCM.
|
|
38
|
+
* Existing plaintext secrets are read transparently (passthrough on decrypt).
|
|
39
|
+
*/
|
|
40
|
+
constructor(db: DataStore, hooks: HookBus, encryptionKey?: string);
|
|
41
|
+
set(input: SecretInput): Promise<SecretMeta>;
|
|
42
|
+
get(name: string, environment?: string): Promise<string | null>;
|
|
43
|
+
getMeta(name: string, environment?: string): Promise<SecretMeta | null>;
|
|
44
|
+
list(): Promise<SecretMeta[]>;
|
|
45
|
+
rotate(name: string, newValue: string, environment?: string): Promise<void>;
|
|
46
|
+
delete(name: string, environment?: string): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Load a sync cursor by key. Returns undefined if not found.
|
|
49
|
+
* Cursors are stored as secrets with type='sync_cursor'.
|
|
50
|
+
*/
|
|
51
|
+
loadCursor(key: string): Promise<string | undefined>;
|
|
52
|
+
/**
|
|
53
|
+
* Persist a sync cursor by key. Creates or updates the secret.
|
|
54
|
+
*/
|
|
55
|
+
saveCursor(key: string, value: string): Promise<void>;
|
|
56
|
+
private _toMeta;
|
|
57
|
+
}
|