twinclaw 1.0.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/README.md +66 -0
- package/bin/npm-twinclaw.js +17 -0
- package/bin/run-twinbot-cli.js +36 -0
- package/bin/twinbot.js +4 -0
- package/bin/twinclaw.js +4 -0
- package/dist/api/handlers/browser.js +160 -0
- package/dist/api/handlers/callback.js +80 -0
- package/dist/api/handlers/config-validate.js +19 -0
- package/dist/api/handlers/health.js +117 -0
- package/dist/api/handlers/local-state-backup.js +118 -0
- package/dist/api/handlers/persona-state.js +59 -0
- package/dist/api/handlers/skill-packages.js +94 -0
- package/dist/api/router.js +278 -0
- package/dist/api/runtime-event-producer.js +99 -0
- package/dist/api/shared.js +82 -0
- package/dist/api/websocket-hub.js +305 -0
- package/dist/config/config-loader.js +2 -0
- package/dist/config/env-schema.js +202 -0
- package/dist/config/env-validator.js +223 -0
- package/dist/config/identity-bootstrap.js +115 -0
- package/dist/config/json-config.js +344 -0
- package/dist/config/workspace.js +186 -0
- package/dist/core/channels-cli.js +77 -0
- package/dist/core/cli.js +119 -0
- package/dist/core/context-assembly.js +33 -0
- package/dist/core/doctor.js +365 -0
- package/dist/core/gateway-cli.js +323 -0
- package/dist/core/gateway.js +416 -0
- package/dist/core/heartbeat.js +54 -0
- package/dist/core/install-cli.js +320 -0
- package/dist/core/lane-executor.js +134 -0
- package/dist/core/logs-cli.js +70 -0
- package/dist/core/onboarding.js +760 -0
- package/dist/core/pairing-cli.js +78 -0
- package/dist/core/secret-vault-cli.js +204 -0
- package/dist/core/types.js +1 -0
- package/dist/index.js +404 -0
- package/dist/interfaces/dispatcher.js +214 -0
- package/dist/interfaces/telegram_handler.js +82 -0
- package/dist/interfaces/tui-dashboard.js +53 -0
- package/dist/interfaces/whatsapp_handler.js +94 -0
- package/dist/release/cli.js +97 -0
- package/dist/release/mvp-gate-cli.js +118 -0
- package/dist/release/twinbot-config-schema.js +162 -0
- package/dist/release/twinclaw-config-schema.js +162 -0
- package/dist/services/block-chunker.js +174 -0
- package/dist/services/browser-service.js +334 -0
- package/dist/services/context-lifecycle.js +314 -0
- package/dist/services/db.js +1055 -0
- package/dist/services/delivery-tracker.js +110 -0
- package/dist/services/dm-pairing.js +245 -0
- package/dist/services/embedding-service.js +125 -0
- package/dist/services/file-watcher.js +125 -0
- package/dist/services/inbound-debounce.js +92 -0
- package/dist/services/incident-manager.js +516 -0
- package/dist/services/job-scheduler.js +176 -0
- package/dist/services/local-state-backup.js +682 -0
- package/dist/services/mcp-client-adapter.js +291 -0
- package/dist/services/mcp-server-manager.js +143 -0
- package/dist/services/model-router.js +927 -0
- package/dist/services/mvp-gate.js +845 -0
- package/dist/services/orchestration-service.js +422 -0
- package/dist/services/persona-state.js +256 -0
- package/dist/services/policy-engine.js +92 -0
- package/dist/services/proactive-notifier.js +94 -0
- package/dist/services/queue-service.js +146 -0
- package/dist/services/release-pipeline.js +652 -0
- package/dist/services/runtime-budget-governor.js +415 -0
- package/dist/services/secret-vault.js +704 -0
- package/dist/services/semantic-memory.js +249 -0
- package/dist/services/skill-package-manager.js +806 -0
- package/dist/services/skill-registry.js +122 -0
- package/dist/services/streaming-output.js +75 -0
- package/dist/services/stt-service.js +39 -0
- package/dist/services/tts-service.js +44 -0
- package/dist/skills/builtin.js +250 -0
- package/dist/skills/shell.js +87 -0
- package/dist/skills/types.js +1 -0
- package/dist/types/api.js +1 -0
- package/dist/types/context-budget.js +1 -0
- package/dist/types/doctor.js +1 -0
- package/dist/types/file-watcher.js +1 -0
- package/dist/types/incident.js +1 -0
- package/dist/types/local-state-backup.js +1 -0
- package/dist/types/mcp.js +1 -0
- package/dist/types/messaging.js +1 -0
- package/dist/types/model-routing.js +1 -0
- package/dist/types/mvp-gate.js +2 -0
- package/dist/types/orchestration.js +1 -0
- package/dist/types/persona-state.js +22 -0
- package/dist/types/policy.js +1 -0
- package/dist/types/reasoning-graph.js +1 -0
- package/dist/types/release.js +1 -0
- package/dist/types/reliability.js +1 -0
- package/dist/types/runtime-budget.js +1 -0
- package/dist/types/scheduler.js +1 -0
- package/dist/types/secret-vault.js +1 -0
- package/dist/types/skill-packages.js +1 -0
- package/dist/types/websocket.js +14 -0
- package/dist/utils/logger.js +57 -0
- package/dist/utils/retry.js +61 -0
- package/dist/utils/secret-scan.js +208 -0
- package/mcp-servers.json +179 -0
- package/package.json +81 -0
- package/skill-packages.json +92 -0
- package/skill-packages.lock.json +5 -0
- package/src/skills/builtin.ts +275 -0
- package/src/skills/shell.ts +118 -0
- package/src/skills/types.ts +30 -0
- package/src/types/api.ts +252 -0
- package/src/types/blessed-contrib.d.ts +4 -0
- package/src/types/context-budget.ts +76 -0
- package/src/types/doctor.ts +29 -0
- package/src/types/file-watcher.ts +26 -0
- package/src/types/incident.ts +57 -0
- package/src/types/local-state-backup.ts +121 -0
- package/src/types/mcp.ts +106 -0
- package/src/types/messaging.ts +35 -0
- package/src/types/model-routing.ts +61 -0
- package/src/types/mvp-gate.ts +99 -0
- package/src/types/orchestration.ts +65 -0
- package/src/types/persona-state.ts +61 -0
- package/src/types/policy.ts +27 -0
- package/src/types/reasoning-graph.ts +58 -0
- package/src/types/release.ts +115 -0
- package/src/types/reliability.ts +43 -0
- package/src/types/runtime-budget.ts +85 -0
- package/src/types/scheduler.ts +47 -0
- package/src/types/secret-vault.ts +62 -0
- package/src/types/skill-packages.ts +81 -0
- package/src/types/sqlite-vec.d.ts +5 -0
- package/src/types/websocket.ts +122 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export class PolicyEngine {
|
|
2
|
+
globalProfile;
|
|
3
|
+
sessionOverrides = new Map();
|
|
4
|
+
// Optional hook for logging all policy decisions
|
|
5
|
+
onDecision;
|
|
6
|
+
constructor(globalProfile) {
|
|
7
|
+
this.globalProfile = globalProfile ?? {
|
|
8
|
+
id: 'global-default',
|
|
9
|
+
defaultAction: 'allow', // By default TwinBot allows if no rule matches
|
|
10
|
+
rules: [],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
setGlobalProfile(profile) {
|
|
14
|
+
this.globalProfile = profile;
|
|
15
|
+
}
|
|
16
|
+
setSessionOverride(sessionId, profile) {
|
|
17
|
+
this.sessionOverrides.set(sessionId, profile);
|
|
18
|
+
}
|
|
19
|
+
removeSessionOverride(sessionId) {
|
|
20
|
+
this.sessionOverrides.delete(sessionId);
|
|
21
|
+
}
|
|
22
|
+
getSessionOverride(sessionId) {
|
|
23
|
+
return this.sessionOverrides.get(sessionId);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Evaluates the policy for a given tool execution.
|
|
27
|
+
* Deterministically orders checks: Session Override rules -> Session Default -> Global Rules -> Global Default.
|
|
28
|
+
*/
|
|
29
|
+
evaluate(sessionId, skillName) {
|
|
30
|
+
const sessionProfile = this.sessionOverrides.get(sessionId);
|
|
31
|
+
// 1. Session Override rules
|
|
32
|
+
if (sessionProfile) {
|
|
33
|
+
const ruleMatch = this.#findRuleMatch(sessionProfile, skillName);
|
|
34
|
+
if (ruleMatch) {
|
|
35
|
+
return this.#finalizeDecision(sessionId, {
|
|
36
|
+
action: ruleMatch.action,
|
|
37
|
+
reason: ruleMatch.reason ?? `Matched specific override rule in profile '${sessionProfile.id}'`,
|
|
38
|
+
skillName,
|
|
39
|
+
profileId: sessionProfile.id,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// 2. Session Profile default (if not fallback)
|
|
43
|
+
if (sessionProfile.defaultAction !== 'fallback') {
|
|
44
|
+
return this.#finalizeDecision(sessionId, {
|
|
45
|
+
action: sessionProfile.defaultAction,
|
|
46
|
+
reason: `Fell back to default action in profile '${sessionProfile.id}'`,
|
|
47
|
+
skillName,
|
|
48
|
+
profileId: sessionProfile.id,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// 3. Global Profile rules
|
|
53
|
+
const globalRuleMatch = this.#findRuleMatch(this.globalProfile, skillName);
|
|
54
|
+
if (globalRuleMatch) {
|
|
55
|
+
return this.#finalizeDecision(sessionId, {
|
|
56
|
+
action: globalRuleMatch.action,
|
|
57
|
+
reason: globalRuleMatch.reason ?? `Matched specific rule in global profile '${this.globalProfile.id}'`,
|
|
58
|
+
skillName,
|
|
59
|
+
profileId: this.globalProfile.id,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// 4. Global Default action
|
|
63
|
+
return this.#finalizeDecision(sessionId, {
|
|
64
|
+
action: this.globalProfile.defaultAction === 'fallback' ? 'allow' : this.globalProfile.defaultAction,
|
|
65
|
+
reason: `Fell back to global default action in profile '${this.globalProfile.id}'`,
|
|
66
|
+
skillName,
|
|
67
|
+
profileId: this.globalProfile.id,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
#findRuleMatch(profile, skillName) {
|
|
71
|
+
// Exact match first
|
|
72
|
+
const exact = profile.rules.find((r) => r.skillName === skillName);
|
|
73
|
+
if (exact)
|
|
74
|
+
return exact;
|
|
75
|
+
// Wildcard match
|
|
76
|
+
const wildcard = profile.rules.find((r) => r.skillName === '*');
|
|
77
|
+
if (wildcard)
|
|
78
|
+
return wildcard;
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
#finalizeDecision(sessionId, decision) {
|
|
82
|
+
if (this.onDecision) {
|
|
83
|
+
try {
|
|
84
|
+
this.onDecision(sessionId, decision);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
console.error(`[PolicyEngine] Error in decision reporting hook:`, e);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return decision;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { logThought } from '../utils/logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Manages proactive (agent-initiated) outbound notifications.
|
|
4
|
+
*
|
|
5
|
+
* Acts as the bridge between background event sources (scheduler, file watcher)
|
|
6
|
+
* and the dispatcher's outbound delivery. The notifier decides *what* to say,
|
|
7
|
+
* and the dispatcher decides *how* to send it.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* ```ts
|
|
11
|
+
* const notifier = new ProactiveNotifier(sendFn, defaultTarget);
|
|
12
|
+
* scheduler.on('job:error', (e) => notifier.onSchedulerEvent(e));
|
|
13
|
+
* fileWatcher.onEvent((e) => notifier.onFileEvent(e));
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export class ProactiveNotifier {
|
|
17
|
+
#send;
|
|
18
|
+
#defaultTarget;
|
|
19
|
+
#enabled;
|
|
20
|
+
constructor(send, defaultTarget, enabled = true) {
|
|
21
|
+
this.#send = send;
|
|
22
|
+
this.#defaultTarget = defaultTarget;
|
|
23
|
+
this.#enabled = enabled;
|
|
24
|
+
}
|
|
25
|
+
/** Enable or disable proactive notifications at runtime. */
|
|
26
|
+
setEnabled(value) {
|
|
27
|
+
this.#enabled = value;
|
|
28
|
+
}
|
|
29
|
+
get enabled() {
|
|
30
|
+
return this.#enabled;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Handle a scheduler event. Only job errors trigger proactive alerts.
|
|
34
|
+
* Job starts/completions are logged but not dispatched to the user.
|
|
35
|
+
*/
|
|
36
|
+
async onSchedulerEvent(event) {
|
|
37
|
+
if (!this.#enabled)
|
|
38
|
+
return;
|
|
39
|
+
switch (event.type) {
|
|
40
|
+
case 'job:error': {
|
|
41
|
+
const message = `⚠️ **Background Job Failed**\n` +
|
|
42
|
+
`Job: \`${event.jobId}\`\n` +
|
|
43
|
+
`Error: ${event.error ?? 'Unknown error'}\n` +
|
|
44
|
+
`Time: ${event.timestamp.toISOString()}`;
|
|
45
|
+
await this.#dispatch(message);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case 'job:done': {
|
|
49
|
+
// Silently log — don't notify user for routine completions.
|
|
50
|
+
await logThought(`[ProactiveNotifier] Job '${event.jobId}' completed successfully.`);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Handle a file-system event. Only file additions and changes are reported
|
|
59
|
+
* to avoid flooding the user with noise from deletions or directory events.
|
|
60
|
+
*/
|
|
61
|
+
async onFileEvent(event) {
|
|
62
|
+
if (!this.#enabled)
|
|
63
|
+
return;
|
|
64
|
+
if (event.type !== 'add' && event.type !== 'change')
|
|
65
|
+
return;
|
|
66
|
+
const label = event.type === 'add' ? '📄 New File Detected' : '✏️ File Modified';
|
|
67
|
+
const message = `${label}\n` +
|
|
68
|
+
`Path: \`${event.path}\`\n` +
|
|
69
|
+
`Time: ${event.timestamp}`;
|
|
70
|
+
await this.#dispatch(message);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Send an arbitrary proactive message. This is the public API for
|
|
74
|
+
* any subsystem that needs to push an agent-initiated notification.
|
|
75
|
+
*/
|
|
76
|
+
async notify(text, target) {
|
|
77
|
+
if (!this.#enabled)
|
|
78
|
+
return;
|
|
79
|
+
await this.#dispatch(text, target);
|
|
80
|
+
}
|
|
81
|
+
// ── Private Helpers ────────────────────────────────────────────────────────
|
|
82
|
+
async #dispatch(text, target) {
|
|
83
|
+
const destination = target ?? this.#defaultTarget;
|
|
84
|
+
try {
|
|
85
|
+
await logThought(`[ProactiveNotifier] Sending proactive message to ${destination.platform}:${destination.chatId}`);
|
|
86
|
+
await this.#send(destination, text);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
console.error('[ProactiveNotifier] Failed to deliver proactive message:', message);
|
|
91
|
+
await logThought(`[ProactiveNotifier] Delivery failed: ${message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { logThought } from '../utils/logger.js';
|
|
3
|
+
import { enqueueDelivery, dequeueDeliveries, updateDeliveryState, updateDeliveryAttempts, recordDeliveryAttemptStart, recordDeliveryAttemptEnd, getDeliveryStateCounts, getDeadLetters, getDeliveryMetrics, } from './db.js';
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
maxAttempts: 3,
|
|
6
|
+
baseDelayMs: 1000,
|
|
7
|
+
backoffFactor: 2,
|
|
8
|
+
maxDelayMs: 15_000,
|
|
9
|
+
batchSize: 10,
|
|
10
|
+
pollIntervalCron: '*/2 * * * * *', // Every 2 seconds
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Durable queue service for outbound message delivery.
|
|
14
|
+
*/
|
|
15
|
+
export class QueueService {
|
|
16
|
+
#dispatchFn;
|
|
17
|
+
#maxAttempts;
|
|
18
|
+
#baseDelayMs;
|
|
19
|
+
#backoffFactor;
|
|
20
|
+
#maxDelayMs;
|
|
21
|
+
#batchSize;
|
|
22
|
+
#scheduler;
|
|
23
|
+
#pollIntervalCron;
|
|
24
|
+
#processingMode = 'normal';
|
|
25
|
+
#retryWindowMultiplier = 1;
|
|
26
|
+
constructor(dispatchFn, scheduler, options = {}) {
|
|
27
|
+
this.#dispatchFn = dispatchFn;
|
|
28
|
+
this.#scheduler = scheduler;
|
|
29
|
+
this.#maxAttempts = options.maxAttempts ?? DEFAULTS.maxAttempts;
|
|
30
|
+
this.#baseDelayMs = options.baseDelayMs ?? DEFAULTS.baseDelayMs;
|
|
31
|
+
this.#backoffFactor = options.backoffFactor ?? DEFAULTS.backoffFactor;
|
|
32
|
+
this.#maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs;
|
|
33
|
+
this.#batchSize = options.batchSize ?? DEFAULTS.batchSize;
|
|
34
|
+
this.#pollIntervalCron = options.pollIntervalCron ?? DEFAULTS.pollIntervalCron;
|
|
35
|
+
}
|
|
36
|
+
start() {
|
|
37
|
+
this.#scheduler.register({
|
|
38
|
+
id: 'queue-processor',
|
|
39
|
+
cronExpression: this.#pollIntervalCron,
|
|
40
|
+
description: 'Process pending delivery queue items',
|
|
41
|
+
handler: async () => {
|
|
42
|
+
await this.processQueue();
|
|
43
|
+
},
|
|
44
|
+
autoStart: true,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
stop() {
|
|
48
|
+
this.#scheduler.unregister('queue-processor');
|
|
49
|
+
}
|
|
50
|
+
enqueue(platform, chatId, textPayload) {
|
|
51
|
+
const id = randomUUID();
|
|
52
|
+
enqueueDelivery(id, platform, String(chatId), textPayload);
|
|
53
|
+
// Soft trigger for low latency
|
|
54
|
+
setTimeout(() => void this.processQueue(), 50);
|
|
55
|
+
return id;
|
|
56
|
+
}
|
|
57
|
+
async processQueue() {
|
|
58
|
+
const batch = dequeueDeliveries(this.#resolveBatchSize());
|
|
59
|
+
if (!batch || batch.length === 0)
|
|
60
|
+
return;
|
|
61
|
+
// Process all picked deliveries concurrently
|
|
62
|
+
await Promise.allSettled(batch.map((job) => this.#processJob(job)));
|
|
63
|
+
}
|
|
64
|
+
async #processJob(job) {
|
|
65
|
+
const attemptId = randomUUID();
|
|
66
|
+
const start = Date.now();
|
|
67
|
+
const startedAt = new Date(start).toISOString();
|
|
68
|
+
recordDeliveryAttemptStart(attemptId, job.id, job.attempts, startedAt);
|
|
69
|
+
let success = false;
|
|
70
|
+
let lastError = '';
|
|
71
|
+
try {
|
|
72
|
+
await this.#dispatchFn(job.platform, job.chat_id, job.text_payload);
|
|
73
|
+
success = true;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
77
|
+
}
|
|
78
|
+
const end = Date.now();
|
|
79
|
+
const completedAt = new Date(end).toISOString();
|
|
80
|
+
const durationMs = end - start;
|
|
81
|
+
recordDeliveryAttemptEnd(attemptId, completedAt, success ? null : lastError, durationMs);
|
|
82
|
+
if (success) {
|
|
83
|
+
updateDeliveryState(job.id, 'sent', completedAt);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
if (job.attempts >= this.#maxAttempts) {
|
|
87
|
+
updateDeliveryState(job.id, 'dead_letter', completedAt);
|
|
88
|
+
void logThought(`[QueueService] Message ${job.id} dead-lettered after ${job.attempts} attempts. Last error: ${lastError}`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const delayMs = Math.min(this.#baseDelayMs * this.#backoffFactor ** (job.attempts - 1) * this.#retryWindowMultiplier, this.#maxDelayMs);
|
|
92
|
+
const nextAttemptAt = new Date(Date.now() + delayMs).toISOString();
|
|
93
|
+
updateDeliveryAttempts(job.id, job.attempts, nextAttemptAt);
|
|
94
|
+
updateDeliveryState(job.id, 'failed', null);
|
|
95
|
+
void logThought(`[QueueService] Message ${job.id} failed attempt ${job.attempts}/${this.#maxAttempts}: ${lastError}. Retrying at ${nextAttemptAt}.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Requeue a dead letter for another set of attempts. */
|
|
100
|
+
requeueDeadLetter(id) {
|
|
101
|
+
const now = new Date().toISOString();
|
|
102
|
+
updateDeliveryAttempts(id, 0, now);
|
|
103
|
+
updateDeliveryState(id, 'queued', null);
|
|
104
|
+
void logThought(`[QueueService] Requeued dead-letter message ${id}.`);
|
|
105
|
+
setTimeout(() => void this.processQueue(), 50);
|
|
106
|
+
}
|
|
107
|
+
setProcessingMode(mode) {
|
|
108
|
+
this.#processingMode = mode;
|
|
109
|
+
void logThought(`[QueueService] Processing mode set to '${mode}'.`);
|
|
110
|
+
}
|
|
111
|
+
setRetryWindowMultiplier(multiplier) {
|
|
112
|
+
const clamped = Math.max(0.5, Math.min(6, Number.isFinite(multiplier) ? multiplier : 1));
|
|
113
|
+
this.#retryWindowMultiplier = clamped;
|
|
114
|
+
void logThought(`[QueueService] Retry window multiplier set to ${clamped}.`);
|
|
115
|
+
}
|
|
116
|
+
getRuntimeControls() {
|
|
117
|
+
return {
|
|
118
|
+
mode: this.#processingMode,
|
|
119
|
+
retryWindowMultiplier: this.#retryWindowMultiplier,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Expose stats for dashboard/metrics. */
|
|
123
|
+
getStats(limit = 50) {
|
|
124
|
+
const recent = getDeliveryMetrics(limit);
|
|
125
|
+
const counts = getDeliveryStateCounts();
|
|
126
|
+
const deadLetters = getDeadLetters();
|
|
127
|
+
return {
|
|
128
|
+
totalQueued: counts.queued ?? 0,
|
|
129
|
+
totalDispatching: counts.dispatching ?? 0,
|
|
130
|
+
totalSent: counts.sent ?? 0,
|
|
131
|
+
totalFailed: counts.failed ?? 0,
|
|
132
|
+
totalDeadLetters: counts.dead_letter ?? deadLetters.length,
|
|
133
|
+
recentRecords: recent,
|
|
134
|
+
deadLetterRecords: deadLetters.slice(0, limit),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
#resolveBatchSize() {
|
|
138
|
+
if (this.#processingMode === 'throttled') {
|
|
139
|
+
return Math.max(1, Math.floor(this.#batchSize / 2));
|
|
140
|
+
}
|
|
141
|
+
if (this.#processingMode === 'drain') {
|
|
142
|
+
return Math.min(this.#batchSize * 2, 100);
|
|
143
|
+
}
|
|
144
|
+
return this.#batchSize;
|
|
145
|
+
}
|
|
146
|
+
}
|