Skip to content

Instantly share code, notes, and snippets.

@Kadajett
Created March 5, 2026 15:32
Show Gist options
  • Select an option

  • Save Kadajett/bf7d11464e719957df2bc98a7194ddc5 to your computer and use it in GitHub Desktop.

Select an option

Save Kadajett/bf7d11464e719957df2bc98a7194ddc5 to your computer and use it in GitHub Desktop.
OpenClaw custom extensions (orchestrator + related extension code, sanitized)
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
import { DatabaseSync } from "node:sqlite";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
type GateConfig = {
enabled?: boolean;
channel?: string;
protectedTo?: string[];
allowedFrom?: string[];
triggerRegex?: string;
armTtlMs?: number;
maxUsesPerArm?: number;
requireBodyAfterTrigger?: boolean;
debug?: boolean;
dbPath?: string;
wrapperTargets?: string[];
wrapperPrefix?: string;
wrapperSuffix?: string;
missedResponseTtlMs?: number;
};
type ArmRow = {
arm_key: string;
request_id: string;
source_message_id: string;
sender: string;
expires_at: number;
uses_left: number;
updated_at: number;
};
function normalizePhone(v: string | undefined): string {
if (!v) return "";
const digits = v.replace(/\D+/g, "");
return digits ? `+${digits}` : v.trim();
}
function normalizeKey(channelId: string, to: string): string {
return `${channelId.toLowerCase()}:${normalizePhone(to)}`;
}
function nowMs(): number {
return Date.now();
}
function classifyTopic(body: string): string {
const t = body.toLowerCase();
if (/(movie|show|episode|series|plex|radarr|sonarr)/.test(t)) return "media";
if (/(calendar|remind|reminder|schedule|meeting)/.test(t)) return "scheduling";
return "general";
}
function withWrapper(content: string, prefix: string, suffix: string): string {
const trimmed = content.trim();
if (!trimmed) return content;
if (trimmed.startsWith(prefix) && trimmed.endsWith(suffix)) return content;
return `${prefix}\n${trimmed}\n${suffix}`;
}
export default function register(api: OpenClawPluginApi) {
const cfg = (api.pluginConfig ?? {}) as GateConfig;
const enabled = cfg.enabled ?? true;
const channel = (cfg.channel ?? "whatsapp").toLowerCase();
const protectedTo = new Set((cfg.protectedTo ?? ["+1XXXXXXXXXX"]).map(normalizePhone));
const allowedFrom = new Set(
(cfg.allowedFrom ?? ["+1XXXXXXXXXX", "+1XXXXXXXXXX"]).map(normalizePhone),
);
const triggerRegex = new RegExp(cfg.triggerRegex ?? "^\\s*friday:\\s*", "i");
const armTtlMs = Math.max(15_000, cfg.armTtlMs ?? 120_000);
const maxUsesPerArm = Math.max(1, Math.floor(cfg.maxUsesPerArm ?? 3));
const requireBodyAfterTrigger = cfg.requireBodyAfterTrigger ?? true;
const debug = cfg.debug ?? false;
const missedResponseTtlMs = Math.max(30_000, cfg.missedResponseTtlMs ?? 10 * 60_000);
const wrapperTargets = new Set(
(cfg.wrapperTargets ?? ["+1XXXXXXXXXX", "+1XXXXXXXXXX"]).map(normalizePhone),
);
const wrapperPrefix = cfg.wrapperPrefix ?? "↓↓↓ F.R.I.D.A.Y. ↓↓↓";
const wrapperSuffix = cfg.wrapperSuffix ?? "↑↑↑ F.R.I.D.A.Y. ↑↑↑";
const dbPath = cfg.dbPath ?? "/home/node/.openclaw/workspace/.openclaw/state/lorene-gate.db";
mkdirSync(dirname(dbPath), { recursive: true });
const db = new DatabaseSync(dbPath);
db.exec("PRAGMA journal_mode = WAL;");
db.exec("PRAGMA busy_timeout = 5000;");
db.exec(`
CREATE TABLE IF NOT EXISTS seen_messages (
channel TEXT NOT NULL,
message_id TEXT NOT NULL,
sender TEXT,
peer TEXT,
triggered INTEGER NOT NULL DEFAULT 0,
request_id TEXT,
thread_id TEXT,
created_at INTEGER NOT NULL,
PRIMARY KEY (channel, message_id)
);
CREATE TABLE IF NOT EXISTS threads (
thread_id TEXT PRIMARY KEY,
channel TEXT NOT NULL,
peer TEXT NOT NULL,
topic TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS requests (
request_id TEXT PRIMARY KEY,
channel TEXT NOT NULL,
sender TEXT NOT NULL,
peer TEXT NOT NULL,
inbound_message_id TEXT NOT NULL,
thread_id TEXT NOT NULL,
trigger_text TEXT,
body TEXT,
status TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_response_at INTEGER
);
CREATE TABLE IF NOT EXISTS responses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id TEXT,
outbound_message_id TEXT,
channel TEXT NOT NULL,
peer TEXT NOT NULL,
kind TEXT NOT NULL,
content TEXT,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS arms (
arm_key TEXT PRIMARY KEY,
channel TEXT NOT NULL,
peer TEXT NOT NULL,
request_id TEXT NOT NULL,
source_message_id TEXT,
sender TEXT,
expires_at INTEGER NOT NULL,
uses_left INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
`);
const upsertSeen = db.prepare(`
INSERT INTO seen_messages(channel, message_id, sender, peer, triggered, request_id, thread_id, created_at)
VALUES(?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(channel, message_id) DO UPDATE SET
sender=excluded.sender,
peer=excluded.peer,
triggered=excluded.triggered,
request_id=excluded.request_id,
thread_id=excluded.thread_id
`);
const findThread = db.prepare(`
SELECT thread_id FROM threads
WHERE channel = ? AND peer = ? AND topic = ?
ORDER BY updated_at DESC LIMIT 1
`);
const upsertThread = db.prepare(`
INSERT INTO threads(thread_id, channel, peer, topic, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(thread_id) DO UPDATE SET updated_at=excluded.updated_at
`);
const insertRequest = db.prepare(`
INSERT OR REPLACE INTO requests(
request_id, channel, sender, peer, inbound_message_id, thread_id,
trigger_text, body, status, created_at, updated_at, last_response_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const upsertArm = db.prepare(`
INSERT OR REPLACE INTO arms(
arm_key, channel, peer, request_id, source_message_id, sender,
expires_at, uses_left, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const selectArm = db.prepare(`SELECT * FROM arms WHERE arm_key = ?`);
const deleteArm = db.prepare(`DELETE FROM arms WHERE arm_key = ?`);
const cleanupArms = db.prepare(`DELETE FROM arms WHERE expires_at <= ? OR uses_left <= 0`);
const updateArm = db.prepare(`
UPDATE arms SET uses_left = ?, updated_at = ?, expires_at = ? WHERE arm_key = ?
`);
const updateRequestAfterSend = db.prepare(
`UPDATE requests SET status = ?, updated_at = ?, last_response_at = ? WHERE request_id = ?`,
);
const insertResponse = db.prepare(`
INSERT INTO responses(request_id, outbound_message_id, channel, peer, kind, content, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
const selectLatestPendingRequest = db.prepare(`
SELECT request_id, created_at FROM requests
WHERE channel = ? AND peer = ? AND status = 'received'
ORDER BY created_at DESC
LIMIT 1
`);
const markRequestMissed = db.prepare(`
UPDATE requests SET status = ?, updated_at = ? WHERE request_id = ?
`);
function log(msg: string): void {
if (debug) api.logger.info?.(`[lorene-gate] ${msg}`);
}
function getOrCreateThreadId(channelId: string, peer: string, body: string): string {
const topic = classifyTopic(body);
const existing = findThread.get(channelId, peer, topic) as { thread_id?: string } | undefined;
const t = nowMs();
const threadId = existing?.thread_id ?? `${channelId}:${peer}:${topic}:${t}`;
upsertThread.run(threadId, channelId, peer, topic, t, t);
return threadId;
}
function recoverMissedArm(channelId: string, peer: string): ArmRow | undefined {
const t = nowMs();
const pending = selectLatestPendingRequest.get(channelId, peer) as
| { request_id: string; created_at: number }
| undefined;
if (!pending) return undefined;
const ageMs = t - pending.created_at;
if (ageMs > missedResponseTtlMs) {
markRequestMissed.run("missed", t, pending.request_id);
log(`request ${pending.request_id} exceeded watchdog TTL (${ageMs}ms), marked missed`);
return undefined;
}
const armKey = normalizeKey(channelId, peer);
upsertArm.run(armKey, channelId, peer, pending.request_id, null, null, t + armTtlMs, 1, t);
const recovered = selectArm.get(armKey) as ArmRow | undefined;
log(`watchdog recovered arm for ${peer}; request=${pending.request_id}; ageMs=${ageMs}`);
return recovered;
}
api.on("message_received", (event, ctx) => {
if (!enabled) return;
if (ctx.channelId?.toLowerCase() !== channel) return;
const sender = normalizePhone(event.from);
const content = (event.content ?? "").trim();
const messageId = String((event.metadata?.messageId as string | undefined) ?? "").trim();
const peer = normalizePhone((ctx.conversationId as string | undefined) ?? "");
if (messageId) {
upsertSeen.run(channel, messageId, sender, peer, 0, null, null, nowMs());
}
// Only enforce special trigger behavior for protected chat peers.
if (!protectedTo.has(peer)) {
return;
}
if (!allowedFrom.has(sender)) {
log(`ignore inbound from disallowed sender ${sender}`);
return;
}
if (!triggerRegex.test(content)) {
log(`inbound from ${sender} without trigger`);
return;
}
const body = content.replace(triggerRegex, "").trim();
if (requireBodyAfterTrigger && !/[\p{L}\p{N}]/u.test(body)) {
log(`trigger rejected (empty body) from ${sender}`);
return;
}
cleanupArms.run(nowMs());
const requestId = `${channel}:${messageId || nowMs()}`;
const threadId = getOrCreateThreadId(channel, peer, body);
const t = nowMs();
insertRequest.run(
requestId,
channel,
sender,
peer,
messageId || requestId,
threadId,
content,
body,
"received",
t,
t,
null,
);
if (messageId) {
upsertSeen.run(channel, messageId, sender, peer, 1, requestId, threadId, t);
}
const armKey = normalizeKey(channel, peer);
upsertArm.run(armKey, channel, peer, requestId, messageId, sender, t + armTtlMs, maxUsesPerArm, t);
log(`armed ${armKey} by ${sender}, request=${requestId}, uses=${maxUsesPerArm}`);
});
api.on("message_sending", (event, ctx) => {
if (!enabled) return;
if (ctx.channelId?.toLowerCase() !== channel) return;
const to = normalizePhone(event.to);
const text = (event.content ?? "").trim();
// Deterministic formatting for target DMs (text only).
let contentOut = event.content;
if (text && wrapperTargets.has(to)) {
contentOut = withWrapper(text, wrapperPrefix, wrapperSuffix);
}
// Guard only protected peer(s); other chats pass through with optional formatting.
if (!protectedTo.has(to)) {
if (contentOut !== event.content) return { content: contentOut };
return;
}
cleanupArms.run(nowMs());
const armKey = normalizeKey(channel, to);
let arm = selectArm.get(armKey) as ArmRow | undefined;
if (!arm || arm.expires_at <= nowMs() || arm.uses_left <= 0) {
arm = recoverMissedArm(channel, to);
}
if (!arm || arm.expires_at <= nowMs() || arm.uses_left <= 0) {
log(`blocked outbound to ${to}: not armed`);
return { cancel: true };
}
const usesLeft = arm.uses_left - 1;
if (usesLeft <= 0) {
deleteArm.run(armKey);
} else {
updateArm.run(usesLeft, nowMs(), nowMs() + armTtlMs, armKey);
}
const t = nowMs();
updateRequestAfterSend.run("responded", t, t, arm.request_id);
insertResponse.run(arm.request_id, null, channel, to, "text", text || null, t);
log(`allowed outbound to ${to}; request=${arm.request_id}; usesLeft=${usesLeft}`);
if (contentOut !== event.content) return { content: contentOut };
return;
});
}
{
"id": "lorene-gate",
"name": "Lorene WhatsApp Gate",
"description": "Deterministic inbound trigger + outbound guard with persistent SQLite ledger and text wrapper enforcement.",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"channel": { "type": "string" },
"protectedTo": { "type": "array", "items": { "type": "string" } },
"allowedFrom": { "type": "array", "items": { "type": "string" } },
"triggerRegex": { "type": "string" },
"armTtlMs": { "type": "number" },
"maxUsesPerArm": { "type": "number" },
"requireBodyAfterTrigger": { "type": "boolean" },
"dbPath": { "type": "string" },
"wrapperTargets": { "type": "array", "items": { "type": "string" } },
"wrapperPrefix": { "type": "string" },
"wrapperSuffix": { "type": "string" },
"missedResponseTtlMs": { "type": "number" },
"debug": { "type": "boolean" }
}
}
}

OpenClaw Custom Extensions (Sanitized Export)

Exported from local OpenClaw extension sources.

Included files:

  • orchestrator.index.ts (source: ~/.openclaw/extensions/orchestrator/index.ts)
  • orchestrator.openclaw.plugin.json (source: ~/.openclaw/extensions/orchestrator/openclaw.plugin.json)
  • lorene-gate.index.ts (source: ~/.openclaw/workspace/.openclaw/extensions/lorene-gate/index.ts)
  • lorene-gate.openclaw.plugin.json (source: ~/.openclaw/workspace/.openclaw/extensions/lorene-gate/openclaw.plugin.json)
  • token-audit.index.ts (source: ~/.openclaw/workspace/.openclaw/extensions/token-audit/index.ts)
  • token-audit.openclaw.plugin.json (source: ~/.openclaw/workspace/.openclaw/extensions/token-audit/openclaw.plugin.json)
  • usage-ledger.index.ts (source: ~/.openclaw/workspace/.openclaw/extensions/usage-ledger/index.ts)
  • usage-ledger.openclaw.plugin.json (source: ~/.openclaw/workspace/.openclaw/extensions/usage-ledger/openclaw.plugin.json)
  • usage-ledger.README.md (source: ~/.openclaw/workspace/.openclaw/extensions/usage-ledger/README.md)

Sanitization:

  • Replaced hardcoded US phone-like literals in lorene-gate.index.ts with +1XXXXXXXXXX.
import { createHash } from "node:crypto";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
type RetryOutcome = "error" | "timeout" | "killed" | "reset" | "deleted";
type SpawnSpec = {
task: string;
label?: string;
agentId?: string;
model?: string;
thinking?: string;
mode?: string;
thread?: boolean;
};
type PluginConfig = {
injectOrchestratorPrompt?: boolean;
enforceMainDelegation?: boolean;
autoRetryFailedSubagents?: boolean;
maxRetriesPerTask?: number;
retryOutcomes?: RetryOutcome[];
allowedMainTools?: string[];
allowedOrchestratorTools?: string[];
orchestratorLabelPrefix?: string;
};
type PendingSpawn = {
requesterSessionKey: string;
spec: SpawnSpec;
kind: "orchestrator" | "worker";
};
type WorkerStatus = "running" | "ok" | "error" | "timeout" | "killed" | "reset" | "deleted" | "unknown";
type WorkerState = {
childSessionKey: string;
orchestratorSessionKey: string;
label?: string;
taskPreview: string;
status: WorkerStatus;
startedAtMs: number;
updatedAtMs: number;
outcome?: string;
};
const DEFAULT_ALLOWED_MAIN_TOOLS = [
"sessions_spawn",
"sessions_send",
"sessions_list",
"sessions_history",
"session_status",
"memory_search",
"memory_get",
];
const DEFAULT_ALLOWED_ORCHESTRATOR_TOOLS = [
"sessions_spawn",
"sessions_send",
"sessions_list",
"sessions_history",
"session_status",
"memory_search",
"memory_get",
];
const DEFAULT_RETRY_OUTCOMES: RetryOutcome[] = ["error", "timeout"];
const STRICT_ROUTER_TOOLS = new Set([
"sessions_spawn",
"sessions_send",
"sessions_list",
"sessions_history",
"session_status",
"memory_search",
"memory_get",
]);
function asRecord(value: unknown): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
return value as Record<string, unknown>;
}
function asString(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function asBoolean(value: unknown, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
}
function asPositiveInt(value: unknown, fallback: number): number {
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
const floored = Math.floor(value);
return floored > 0 ? floored : fallback;
}
function asStringArray(value: unknown): string[] | undefined {
if (!Array.isArray(value)) return undefined;
const out = value
.map((item) => (typeof item === "string" ? item.trim() : ""))
.filter((item) => item.length > 0);
return out.length > 0 ? out : undefined;
}
function resolveRetryOutcomes(value: unknown): Set<RetryOutcome> {
const fromCfg = asStringArray(value);
const raw = fromCfg ?? DEFAULT_RETRY_OUTCOMES;
const allowed = new Set<RetryOutcome>(["error", "timeout", "killed", "reset", "deleted"]);
const selected = raw.filter((entry): entry is RetryOutcome => allowed.has(entry as RetryOutcome));
return new Set(selected.length > 0 ? selected : DEFAULT_RETRY_OUTCOMES);
}
function isFrontControllerSessionKey(sessionKey: string | undefined): boolean {
if (!sessionKey) return false;
if (sessionKey.includes(":subagent:")) return false;
return /^agent:/i.test(sessionKey);
}
function shortHash(input: string): string {
return createHash("sha256").update(input).digest("hex").slice(0, 10);
}
function sanitizeLabel(input: string | undefined): string | undefined {
const raw = (input ?? "").trim();
if (!raw) return undefined;
const compact = raw.replace(/\s+/g, "-").toLowerCase();
const safe = compact.replace(/[^a-z0-9._-]/g, "");
return safe.slice(0, 64) || undefined;
}
function taskPreview(input: string | undefined, max = 180): string {
const raw = (input ?? "").trim();
if (!raw) return "(no task)";
const compact = raw.replace(/\s+/g, " ");
return compact.length <= max ? compact : `${compact.slice(0, max - 1)}…`;
}
function asSessionMessage(value: unknown): string | undefined {
if (typeof value === "string") return asString(value);
if (!Array.isArray(value)) return undefined;
const chunks: string[] = [];
for (const part of value) {
const obj = asRecord(part);
if (obj.type !== "text") continue;
const text = asString(obj.text);
if (text) chunks.push(text);
}
const joined = chunks.join("\n").trim();
return joined.length > 0 ? joined : undefined;
}
function extractLastUserMessage(messages: unknown[]): string | undefined {
for (let i = messages.length - 1; i >= 0; i -= 1) {
const obj = asRecord(messages[i]);
if (asString(obj.role) !== "user") continue;
const content = asSessionMessage(obj.content);
if (content) return content;
}
return undefined;
}
function buildWorkerStatusSnapshot(orchestratorSessionKey: string, workers: Iterable<WorkerState>): string {
const items = [...workers].sort((a, b) => b.updatedAtMs - a.updatedAtMs).slice(0, 12);
if (items.length === 0) {
return `- Hidden orchestrator ${orchestratorSessionKey} has no tracked worker sessions.`;
}
return [
`- Hidden orchestrator ${orchestratorSessionKey}`,
`- Workers tracked: ${items.length}`,
...items.map(
(worker) =>
`- [${worker.status}] ${worker.childSessionKey} (${worker.label ?? "no-label"}) :: ${worker.taskPreview}`,
),
].join("\n");
}
function makePendingKey(
runId: string | undefined,
toolCallId: string | undefined,
sessionKey: string | undefined,
): string {
return `${runId ?? ""}|${toolCallId ?? ""}|${sessionKey ?? ""}`;
}
function stableTaskKey(requesterSessionKey: string, spec: SpawnSpec): string {
const normalized = [
requesterSessionKey,
spec.agentId ?? "",
spec.label ?? "",
spec.task.trim(),
].join("\n");
return createHash("sha256").update(normalized).digest("hex");
}
function extractChildSessionKey(value: unknown, depth = 0): string | undefined {
if (depth > 4) return undefined;
const obj = asRecord(value);
const direct = asString(obj.childSessionKey);
if (direct) return direct;
for (const nested of Object.values(obj)) {
if (!nested || typeof nested !== "object") continue;
const found = extractChildSessionKey(nested, depth + 1);
if (found) return found;
}
return undefined;
}
function isSpawnAccepted(value: unknown): boolean {
const obj = asRecord(value);
const details = asRecord(obj.details);
const status =
asString(obj.status)?.toLowerCase() ??
asString(details.status)?.toLowerCase();
if (status) {
return status === "accepted" || status === "ok" || status === "success";
}
if (obj.isError === true || details.isError === true) return false;
if (asString(obj.error) || asString(details.error)) return false;
return true;
}
function isOrchestratorEchoMessage(text: string): boolean {
const normalized = text.toLowerCase();
return (
normalized.includes("[orchestrator mode is active]") &&
normalized.includes("spawn exactly one hidden orchestrator session")
);
}
function buildOrchestratorPrompt(maxRetries: number, orchestratorSessionKey?: string): string {
const lines = [
"[Orchestrator mode is active]",
"- You are the fast front-controller. Keep the first user-facing response short.",
"- Front-controller should not call read/exec/process directly.",
"- Spawn exactly one hidden orchestrator session for this thread via sessions_spawn.",
"- Delegate all real work to that orchestrator via sessions_send.",
"- For live progress, query the hidden orchestrator via session_status/sessions_history.",
"- If user asks for progress/status, query hidden orchestrator first, then answer.",
`- If a system event contains <orchestrator-retry>, immediately respawn the failed task. Retry limit: ${maxRetries}.`,
];
if (orchestratorSessionKey) {
lines.push(`- Active hidden orchestrator session: ${orchestratorSessionKey}`);
lines.push(
`- Use sessions_send with sessionKey=${orchestratorSessionKey} for new delegated work in this thread.`,
);
}
return lines.join("\n");
}
function buildHiddenOrchestratorTask(requesterSessionKey: string, userTask: string): string {
return [
"[Hidden Thread Orchestrator]",
`requesterSessionKey: ${requesterSessionKey}`,
"You are a hidden orchestrator for one front-controller thread.",
"Execution contract:",
"1) Break the task into concrete worker subtasks.",
"2) Spawn worker subagents (sessions_spawn) for tool/file/shell/web work.",
"3) Track worker status (pending/running/succeeded/failed) and retry failed workers only when useful.",
"4) Keep progress updates concise and structured so requester can poll status.",
"5) Send progress updates back to requester via sessions_send as useful.",
"6) When all workers finish, send a final summary to requester via sessions_send with completed changes, failures, and next actions.",
"7) Do not run read/write/edit/exec/process yourself unless delegation repeatedly fails.",
"",
"Primary task from front-controller:",
userTask,
].join("\n");
}
function buildDelegationHint(toolName: string, params: unknown): string {
const preview = taskPreview(JSON.stringify(params), 260);
return [
`Hidden orchestrator policy blocked '${toolName}'.`,
"Delegate this to a worker using sessions_spawn with a concrete task.",
"Example:",
`sessions_spawn({\"label\":\"worker-${toolName}\",\"task\":\"Use tool ${toolName} with params ${preview}. Return concise findings and artifacts.\"})`,
].join(" ");
}
function buildRetrySystemEvent(
spec: SpawnSpec,
failedSessionKey: string,
outcome: string,
attempt: number,
maxRetries: number,
): string {
return [
"<orchestrator-retry>",
`failedChildSession: ${failedSessionKey}`,
`outcome: ${outcome}`,
`attempt: ${attempt}/${maxRetries}`,
"action: Respawn this subagent task immediately via sessions_spawn.",
`task: ${spec.task}`,
`label: ${spec.label ?? ""}`,
`agentId: ${spec.agentId ?? ""}`,
`model: ${spec.model ?? ""}`,
`thinking: ${spec.thinking ?? ""}`,
`mode: ${spec.mode ?? ""}`,
`thread: ${String(spec.thread ?? false)}`,
"</orchestrator-retry>",
].join("\n");
}
export default function register(api: OpenClawPluginApi) {
const cfg = asRecord(api.pluginConfig) as PluginConfig;
const injectOrchestratorPrompt = asBoolean(cfg.injectOrchestratorPrompt, true);
const enforceMainDelegation = asBoolean(cfg.enforceMainDelegation, false);
const autoRetryFailedSubagents = asBoolean(cfg.autoRetryFailedSubagents, true);
const maxRetriesPerTask = asPositiveInt(cfg.maxRetriesPerTask, 2);
const orchestratorLabelPrefix = sanitizeLabel(asString(cfg.orchestratorLabelPrefix)) ?? "thread-orchestrator";
const retryOutcomes = resolveRetryOutcomes(cfg.retryOutcomes);
const allowedMainTools = new Set([
...DEFAULT_ALLOWED_MAIN_TOOLS,
...(asStringArray(cfg.allowedMainTools) ?? []),
]);
const allowedOrchestratorTools = new Set([
...DEFAULT_ALLOWED_ORCHESTRATOR_TOOLS,
...(asStringArray(cfg.allowedOrchestratorTools) ?? []),
]);
// Keep front/orchestrator strict even if config accidentally adds direct tools.
for (const toolId of [...allowedMainTools]) {
if (!STRICT_ROUTER_TOOLS.has(toolId)) allowedMainTools.delete(toolId);
}
for (const toolId of [...allowedOrchestratorTools]) {
if (!STRICT_ROUTER_TOOLS.has(toolId)) allowedOrchestratorTools.delete(toolId);
}
const pendingSpawns = new Map<string, PendingSpawn>();
const spawnByChildSession = new Map<string, PendingSpawn>();
const retryCountByTask = new Map<string, number>();
const orchestratorByRequester = new Map<string, string>();
const requesterByOrchestrator = new Map<string, string>();
const orchestratorSessions = new Set<string>();
const lastUserMessageBySession = new Map<string, string>();
const lastDelegatedTaskByOrchestrator = new Map<string, string>();
const blockedAttemptsByRun = new Map<string, number>();
const workerStateBySession = new Map<string, WorkerState>();
const workersByOrchestrator = new Map<string, Set<string>>();
api.on("before_prompt_build", async (event, ctx) => {
if (ctx.sessionKey) {
const latestUser = extractLastUserMessage(event.messages);
if (latestUser && !isOrchestratorEchoMessage(latestUser)) {
lastUserMessageBySession.set(ctx.sessionKey, latestUser);
}
}
if (!injectOrchestratorPrompt) return;
if (!isFrontControllerSessionKey(ctx.sessionKey)) return;
const knownOrchestrator = ctx.sessionKey ? orchestratorByRequester.get(ctx.sessionKey) : undefined;
let statusBlock = "";
if (knownOrchestrator) {
const workerKeys = workersByOrchestrator.get(knownOrchestrator);
const workers = workerKeys
? [...workerKeys].map((key) => workerStateBySession.get(key)).filter((item): item is WorkerState => Boolean(item))
: [];
if (workers.length > 0) {
statusBlock = `\n[Orchestrator status snapshot]\n${buildWorkerStatusSnapshot(knownOrchestrator, workers)}\n`;
}
}
return {
prependContext: `${buildOrchestratorPrompt(maxRetriesPerTask, knownOrchestrator)}${statusBlock}`,
};
});
api.on("before_tool_call", async (event, ctx) => {
const sessionKey = ctx.sessionKey;
const isOrchestratorSession = sessionKey ? orchestratorSessions.has(sessionKey) : false;
const isFrontSession = isFrontControllerSessionKey(sessionKey) && !isOrchestratorSession;
if (enforceMainDelegation && isFrontSession) {
if (!allowedMainTools.has(event.toolName)) {
const orchestratorSession = sessionKey ? orchestratorByRequester.get(sessionKey) : undefined;
const suffix = orchestratorSession
? ` Use sessions_send with sessionKey='${orchestratorSession}' to delegate work.`
: " Use sessions_spawn to start a hidden orchestrator first.";
return {
block: true,
blockReason: `Front-controller session tool '${event.toolName}' blocked by orchestrator policy.${suffix}`,
};
}
}
if (isOrchestratorSession && !allowedOrchestratorTools.has(event.toolName)) {
const blockKey = `${event.runId ?? "run"}|${sessionKey ?? "session"}|${event.toolName}`;
const blockedAttempts = (blockedAttemptsByRun.get(blockKey) ?? 0) + 1;
blockedAttemptsByRun.set(blockKey, blockedAttempts);
if (blockedAttempts >= 3) {
api.logger.warn(
`orchestrator: allowing direct tool fallback after repeated policy blocks (${event.toolName}, attempts=${blockedAttempts})`,
);
} else {
const hint = buildDelegationHint(event.toolName, event.params);
if (sessionKey) lastDelegatedTaskByOrchestrator.set(sessionKey, hint);
return {
block: true,
blockReason: hint,
};
}
}
if (!sessionKey) return;
if (isOrchestratorSession && event.toolName === "sessions_send") {
const requesterSession = requesterByOrchestrator.get(sessionKey);
if (requesterSession) {
const params = asRecord(event.params);
const hasSessionKey = Boolean(asString(params.sessionKey));
const hasLabel = Boolean(asString(params.label));
if (!hasSessionKey && !hasLabel) {
const msg = asString(params.message);
if (msg) lastDelegatedTaskByOrchestrator.set(sessionKey, msg);
return {
params: {
...params,
sessionKey: requesterSession,
},
};
}
}
}
if (isFrontSession && event.toolName === "sessions_send") {
const params = asRecord(event.params);
const knownOrchestrator = orchestratorByRequester.get(sessionKey);
const sessionTarget = asString(params.sessionKey);
if (knownOrchestrator && (!sessionTarget || sessionTarget === knownOrchestrator)) {
const msg = asString(params.message);
if (msg) lastDelegatedTaskByOrchestrator.set(knownOrchestrator, msg);
}
}
if (isFrontSession && (event.toolName === "session_status" || event.toolName === "sessions_history")) {
const knownOrchestrator = orchestratorByRequester.get(sessionKey);
if (knownOrchestrator) {
const params = asRecord(event.params);
const hasSessionKey = Boolean(asString(params.sessionKey));
const hasLabel = Boolean(asString(params.label));
if (!hasSessionKey && !hasLabel) {
return {
params: {
...params,
sessionKey: knownOrchestrator,
},
};
}
}
}
if (event.toolName !== "sessions_spawn") return;
const params = asRecord(event.params);
const rawTask = asString(params.task);
const knownOrchestrator = orchestratorByRequester.get(sessionKey);
const delegatedTask = isOrchestratorSession ? lastDelegatedTaskByOrchestrator.get(sessionKey) : undefined;
const userFallbackTask = lastUserMessageBySession.get(sessionKey);
const task =
rawTask ??
delegatedTask ??
userFallbackTask ??
(isOrchestratorSession
? "Delegate the current thread objective to a worker and report results."
: "Initialize hidden thread orchestrator and continue the current user objective.");
let nextParams: Record<string, unknown> = { ...params, task };
let pendingKind: "orchestrator" | "worker" = "worker";
if (isFrontSession) {
if (knownOrchestrator) {
return {
block: true,
blockReason: `Hidden orchestrator already active (${knownOrchestrator}). Use sessions_send to delegate additional work and session_status/sessions_history to poll progress.`,
};
}
const defaultLabel = `${orchestratorLabelPrefix}-${shortHash(sessionKey)}`;
nextParams = {
...nextParams,
label: sanitizeLabel(asString(params.label)) ?? defaultLabel,
task: buildHiddenOrchestratorTask(sessionKey, task),
thread: true,
};
pendingKind = "orchestrator";
}
const effectiveTask = asString(nextParams.task) ?? task;
const spec: SpawnSpec = {
task: effectiveTask,
label: asString(nextParams.label),
agentId: asString(nextParams.agentId),
model: asString(nextParams.model),
thinking: asString(nextParams.thinking),
mode: asString(nextParams.mode),
thread: typeof nextParams.thread === "boolean" ? nextParams.thread : undefined,
};
const pendingKey = makePendingKey(event.runId, event.toolCallId, sessionKey);
pendingSpawns.set(pendingKey, {
requesterSessionKey: sessionKey,
spec,
kind: pendingKind,
});
if (pendingKind === "orchestrator" || !rawTask) {
return { params: nextParams };
}
});
api.on("after_tool_call", async (event, ctx) => {
if (event.toolName !== "sessions_spawn") return;
const pendingKey = makePendingKey(event.runId, event.toolCallId, ctx.sessionKey);
const pending = pendingSpawns.get(pendingKey);
if (!pending) return;
pendingSpawns.delete(pendingKey);
if (!isSpawnAccepted(event.result)) {
api.logger.warn(
`orchestrator: sessions_spawn was not accepted for ${pending.requesterSessionKey}; skipping bind/worker tracking`,
);
return;
}
const childSessionKey = extractChildSessionKey(event.result);
if (!childSessionKey) {
api.logger.warn(
`orchestrator: sessions_spawn accepted without childSessionKey for ${pending.requesterSessionKey}`,
);
return;
}
spawnByChildSession.set(childSessionKey, pending);
if (pending.kind === "orchestrator") {
orchestratorSessions.add(childSessionKey);
orchestratorByRequester.set(pending.requesterSessionKey, childSessionKey);
requesterByOrchestrator.set(childSessionKey, pending.requesterSessionKey);
api.logger.info(
`orchestrator: bound hidden orchestrator ${childSessionKey} -> requester ${pending.requesterSessionKey}`,
);
return;
}
const orchestratorSessionKey = pending.requesterSessionKey;
const startedAtMs = Date.now();
const workerState: WorkerState = {
childSessionKey,
orchestratorSessionKey,
label: pending.spec.label,
taskPreview: taskPreview(pending.spec.task),
status: "running",
startedAtMs,
updatedAtMs: startedAtMs,
};
workerStateBySession.set(childSessionKey, workerState);
const workerSet = workersByOrchestrator.get(orchestratorSessionKey) ?? new Set<string>();
workerSet.add(childSessionKey);
workersByOrchestrator.set(orchestratorSessionKey, workerSet);
api.logger.info(
`orchestrator: worker spawned ${childSessionKey} under ${orchestratorSessionKey} (${workerState.label ?? "no-label"})`,
);
});
api.on("subagent_spawned", async (event, ctx) => {
const childSessionKey = event.childSessionKey;
const requesterSessionKey = ctx.requesterSessionKey;
if (!childSessionKey || !requesterSessionKey) return;
if (!orchestratorSessions.has(requesterSessionKey)) return;
const state = workerStateBySession.get(childSessionKey);
if (!state) return;
state.updatedAtMs = Date.now();
workerStateBySession.set(childSessionKey, state);
});
api.on("subagent_delivery_target", async (event) => {
const requesterSessionKey = event.requesterSessionKey;
if (!requesterSessionKey) return;
if (!orchestratorSessions.has(requesterSessionKey)) return;
const state = workerStateBySession.get(event.childSessionKey);
if (!state) return;
state.updatedAtMs = Date.now();
workerStateBySession.set(event.childSessionKey, state);
});
api.on("subagent_ended", async (event) => {
if (event.targetKind !== "subagent") return;
const childSessionKey = event.targetSessionKey;
const workerState = workerStateBySession.get(childSessionKey);
if (workerState) {
const status = (asString(event.outcome)?.toLowerCase() ?? "unknown") as WorkerStatus;
workerState.status = status;
workerState.outcome = asString(event.outcome);
workerState.updatedAtMs = Date.now();
workerStateBySession.set(childSessionKey, workerState);
}
if (orchestratorSessions.has(childSessionKey)) {
orchestratorSessions.delete(childSessionKey);
const requester = requesterByOrchestrator.get(childSessionKey);
requesterByOrchestrator.delete(childSessionKey);
if (requester && orchestratorByRequester.get(requester) === childSessionKey) {
orchestratorByRequester.delete(requester);
}
const workerKeys = workersByOrchestrator.get(childSessionKey);
if (workerKeys) {
for (const workerKey of workerKeys) workerStateBySession.delete(workerKey);
workersByOrchestrator.delete(childSessionKey);
}
api.logger.info(`orchestrator: hidden orchestrator ended ${childSessionKey}`);
}
if (!autoRetryFailedSubagents) return;
const pending = spawnByChildSession.get(childSessionKey);
if (!pending) return;
if (pending.kind === "orchestrator") {
spawnByChildSession.delete(childSessionKey);
return;
}
const outcome = asString(event.outcome)?.toLowerCase() ?? "unknown";
if (!retryOutcomes.has(outcome as RetryOutcome)) {
spawnByChildSession.delete(childSessionKey);
return;
}
const taskKey = stableTaskKey(pending.requesterSessionKey, pending.spec);
const retries = retryCountByTask.get(taskKey) ?? 0;
if (retries >= maxRetriesPerTask) {
api.logger.warn(
`orchestrator: retry budget exhausted (${retries}/${maxRetriesPerTask}) for ${childSessionKey}`,
);
spawnByChildSession.delete(childSessionKey);
return;
}
const nextAttempt = retries + 1;
retryCountByTask.set(taskKey, nextAttempt);
const eventText = buildRetrySystemEvent(
pending.spec,
childSessionKey,
outcome,
nextAttempt,
maxRetriesPerTask,
);
const queued = api.runtime.system.enqueueSystemEvent(eventText, {
sessionKey: pending.requesterSessionKey,
});
if (!queued) {
api.logger.warn(
`orchestrator: failed to queue retry event for requester session ${pending.requesterSessionKey}`,
);
return;
}
api.runtime.system.requestHeartbeatNow({
reason: "orchestrator-subagent-retry",
sessionKey: pending.requesterSessionKey,
});
api.logger.info(
`orchestrator: queued retry ${nextAttempt}/${maxRetriesPerTask} for subagent ${childSessionKey}`,
);
});
api.on("session_end", async (event) => {
const sessionKey = event.sessionKey;
if (!sessionKey) return;
const knownOrchestrator = orchestratorByRequester.get(sessionKey);
if (knownOrchestrator) {
orchestratorByRequester.delete(sessionKey);
requesterByOrchestrator.delete(knownOrchestrator);
orchestratorSessions.delete(knownOrchestrator);
const workerKeys = workersByOrchestrator.get(knownOrchestrator);
if (workerKeys) {
for (const workerKey of workerKeys) workerStateBySession.delete(workerKey);
workersByOrchestrator.delete(knownOrchestrator);
}
}
if (orchestratorSessions.has(sessionKey)) {
orchestratorSessions.delete(sessionKey);
const requester = requesterByOrchestrator.get(sessionKey);
requesterByOrchestrator.delete(sessionKey);
if (requester && orchestratorByRequester.get(requester) === sessionKey) {
orchestratorByRequester.delete(requester);
}
const workerKeys = workersByOrchestrator.get(sessionKey);
if (workerKeys) {
for (const workerKey of workerKeys) workerStateBySession.delete(workerKey);
workersByOrchestrator.delete(sessionKey);
}
}
workerStateBySession.delete(sessionKey);
lastUserMessageBySession.delete(sessionKey);
lastDelegatedTaskByOrchestrator.delete(sessionKey);
});
}
{
"id": "orchestrator",
"name": "Orchestrator Delegation",
"description": "Fast front-controller orchestration with subagent delegation guardrails and automatic retry events.",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"injectOrchestratorPrompt": {
"type": "boolean",
"description": "Inject orchestration instructions into main-session prompt builds."
},
"enforceMainDelegation": {
"type": "boolean",
"description": "Block non-session tools in main sessions so heavy work is delegated."
},
"autoRetryFailedSubagents": {
"type": "boolean",
"description": "Queue a retry system event when a subagent ends with retryable outcomes."
},
"maxRetriesPerTask": {
"type": "integer",
"minimum": 1,
"maximum": 5,
"description": "Retry budget per delegated task key."
},
"retryOutcomes": {
"type": "array",
"items": {
"type": "string",
"enum": ["error", "timeout", "killed", "reset", "deleted"]
},
"description": "Subagent outcomes that trigger automatic retry."
},
"allowedMainTools": {
"type": "array",
"items": { "type": "string" },
"description": "Additional tool names allowed in main sessions when delegation enforcement is active."
}
}
}
}
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
const LOG_FILE = "/home/node/.openclaw/logs/token-audit.jsonl";
const DUMP_FILE = "/home/node/.openclaw/logs/token-audit-dump.json";
const LARGE_PROMPT_THRESHOLD = 100_000; // characters
export default function register(api: OpenClawPluginApi) {
// Ensure log directory exists
try {
mkdirSync(dirname(LOG_FILE), { recursive: true });
} catch {
// directory likely already exists
}
// llm_output hook: append a JSONL record for every LLM response
api.on("llm_output", (event) => {
try {
const record = {
timestamp: new Date().toISOString(),
sessionId: event.sessionId,
runId: event.runId,
provider: event.provider,
model: event.model,
usage: {
input: event.usage?.input ?? 0,
output: event.usage?.output ?? 0,
cacheRead: event.usage?.cacheRead ?? 0,
cacheWrite: event.usage?.cacheWrite ?? 0,
total: event.usage?.total ?? (
(event.usage?.input ?? 0) +
(event.usage?.output ?? 0)
),
},
};
appendFileSync(LOG_FILE, JSON.stringify(record) + "\n", "utf8");
} catch (err) {
api.logger.error(
`[token-audit] Failed to write llm_output record: ${err instanceof Error ? err.message : String(err)}`
);
}
});
// llm_input hook: warn if the prompt is very large
api.on("llm_input", (event) => {
try {
const promptLen = typeof event.prompt === "string" ? event.prompt.length : 0;
const historyLen = Array.isArray(event.historyMessages)
? JSON.stringify(event.historyMessages).length
: 0;
const systemLen = typeof event.systemPrompt === "string" ? event.systemPrompt.length : 0;
const totalChars = promptLen + historyLen + systemLen;
const historyCount = Array.isArray(event.historyMessages) ? event.historyMessages.length : 0;
if (totalChars > LARGE_PROMPT_THRESHOLD) {
api.logger.warn(
`[token-audit] LARGE PROMPT: session=${event.sessionId} provider=${event.provider} model=${event.model} ` +
`systemLen=${systemLen} promptLen=${promptLen} historyLen=${historyLen} historyMsgs=${historyCount} totalChars=${totalChars}`
);
// Dump breakdown for debugging (overwrites each time)
try {
const dump = {
timestamp: new Date().toISOString(),
sessionId: event.sessionId,
runId: event.runId,
provider: event.provider,
model: event.model,
systemLen,
promptLen,
historyLen,
historyCount,
totalChars,
systemPromptPreview: typeof event.systemPrompt === "string" ? event.systemPrompt.slice(0, 2000) : null,
promptPreview: typeof event.prompt === "string" ? event.prompt.slice(0, 2000) : null,
};
writeFileSync(DUMP_FILE, JSON.stringify(dump, null, 2), "utf8");
} catch {
// ignore dump errors
}
}
} catch (err) {
api.logger.error(
`[token-audit] Failed in llm_input hook: ${err instanceof Error ? err.message : String(err)}`
);
}
});
api.logger.info("[token-audit] Plugin loaded. Logging to " + LOG_FILE);
}
{
"id": "token-audit",
"name": "Token Audit",
"version": "1.0.0",
"description": "Logs per-turn token usage to a JSONL file and alerts on large prompts",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
import { mkdirSync } from 'node:fs'
import { dirname } from 'node:path'
import { DatabaseSync } from 'node:sqlite'
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
type UsageLedgerConfig = {
enabled?: boolean
dbPath?: string
debug?: boolean
}
type UsageEvent = {
ts: number
requestId: string | null
model: string | null
provider: string | null
inputTokens: number
outputTokens: number
totalTokens: number
cacheCreationInputTokens: number
cacheReadInputTokens: number
latencyMs: number | null
success: number
sourceEvent: string
}
function toFiniteNumber(value: unknown, fallback = 0): number {
if (typeof value === 'number' && Number.isFinite(value)) return value
if (typeof value === 'string') {
const n = Number(value)
if (Number.isFinite(n)) return n
}
return fallback
}
function firstString(...values: unknown[]): string | null {
for (const value of values) {
if (typeof value === 'string' && value.trim()) return value.trim()
}
return null
}
function extractUsage(event: unknown): Partial<UsageEvent> | null {
if (!event || typeof event !== 'object') return null
const root = event as Record<string, unknown>
const usage = (root.usage ?? root.tokenUsage ?? root.metrics ?? root.usageMetadata) as Record<string, unknown> | undefined
const response = root.response as Record<string, unknown> | undefined
// Common fields: handle OpenAI, Anthropic, Google, AWS Bedrock formats
const inputTokens = toFiniteNumber(
usage?.input ?? usage?.input_tokens ?? usage?.inputTokens ?? usage?.prompt_tokens ?? usage?.promptTokenCount ?? usage?.inputTokenCount ?? root.inputTokens,
)
const outputTokens = toFiniteNumber(
usage?.output ?? usage?.output_tokens ?? usage?.outputTokens ?? usage?.completion_tokens ?? usage?.candidatesTokenCount ?? usage?.outputTokenCount ?? root.outputTokens,
)
const totalTokens = toFiniteNumber(
usage?.total ?? usage?.total_tokens ?? usage?.totalTokens ?? usage?.totalTokenCount,
inputTokens + outputTokens,
)
// cacheWrite ~= Anthropic/OpenAI cache creation input tokens
const cacheCreationInputTokens = toFiniteNumber(
usage?.cacheWrite ?? usage?.cache_creation_input_tokens ?? usage?.cacheCreationInputTokens,
)
const cacheReadInputTokens = toFiniteNumber(
usage?.cacheRead ?? usage?.cache_read_input_tokens ?? usage?.cacheReadInputTokens,
)
const hasUsage =
inputTokens > 0 ||
outputTokens > 0 ||
totalTokens > 0 ||
cacheCreationInputTokens > 0 ||
cacheReadInputTokens > 0
if (!hasUsage) return null
return {
requestId: firstString(root.requestId, root.traceId, root.runId, root.sessionId, response?.id),
model: firstString(root.model, response?.model, usage?.model, root.modelName),
provider: firstString(root.provider, root.vendor, usage?.provider),
inputTokens,
outputTokens,
totalTokens,
cacheCreationInputTokens,
cacheReadInputTokens,
latencyMs: toFiniteNumber(root.latencyMs ?? root.durationMs, NaN) || null,
success: root.error ? 0 : 1,
}
}
export default function register(api: OpenClawPluginApi) {
const cfg = (api.pluginConfig ?? {}) as UsageLedgerConfig
const enabled = cfg.enabled ?? true
const debug = cfg.debug ?? false
if (!enabled) return
const dbPath = cfg.dbPath ?? '/home/node/.openclaw/workspace/.openclaw/state/usage-ledger.db'
mkdirSync(dirname(dbPath), { recursive: true })
const db = new DatabaseSync(dbPath)
db.exec('PRAGMA journal_mode = WAL;')
db.exec('PRAGMA busy_timeout = 3000;')
db.exec(`
CREATE TABLE IF NOT EXISTS llm_usage_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
request_id TEXT,
model TEXT,
provider TEXT,
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
total_tokens INTEGER NOT NULL DEFAULT 0,
cache_creation_input_tokens INTEGER NOT NULL DEFAULT 0,
cache_read_input_tokens INTEGER NOT NULL DEFAULT 0,
latency_ms INTEGER,
success INTEGER NOT NULL DEFAULT 1,
source_event TEXT NOT NULL,
raw_json TEXT
);
CREATE INDEX IF NOT EXISTS idx_llm_usage_events_ts ON llm_usage_events(ts);
CREATE INDEX IF NOT EXISTS idx_llm_usage_events_model_ts ON llm_usage_events(model, ts);
`)
const insertUsage = db.prepare(`
INSERT INTO llm_usage_events (
ts, request_id, model, provider,
input_tokens, output_tokens, total_tokens,
cache_creation_input_tokens, cache_read_input_tokens,
latency_ms, success, source_event, raw_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
function log(message: string) {
if (!debug) return
api.logger.info?.(`[usage-ledger] ${message}`)
}
function record(sourceEvent: string, payload: unknown) {
try {
log(`Event fired: ${sourceEvent} keys=${Object.keys(payload as object || {}).join(',')}`)
const usage = extractUsage(payload)
if (!usage) return
const event: UsageEvent = {
ts: Date.now(),
requestId: usage.requestId ?? null,
model: usage.model ?? null,
provider: usage.provider ?? null,
inputTokens: usage.inputTokens ?? 0,
outputTokens: usage.outputTokens ?? 0,
totalTokens: usage.totalTokens ?? 0,
cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
cacheReadInputTokens: usage.cacheReadInputTokens ?? 0,
latencyMs: usage.latencyMs ?? null,
success: usage.success ?? 1,
sourceEvent,
}
insertUsage.run(
event.ts,
event.requestId,
event.model,
event.provider,
event.inputTokens,
event.outputTokens,
event.totalTokens,
event.cacheCreationInputTokens,
event.cacheReadInputTokens,
event.latencyMs,
event.success,
event.sourceEvent,
JSON.stringify(payload),
)
} catch (err) {
log(`failed to record event ${sourceEvent}: ${err instanceof Error ? err.message : String(err)}`)
}
}
// Potential event names based on typical LLM plugin hooks
const events = [
'llm_output',
'before_message_write',
'llm_completion',
'model_response',
'completion',
'llm.response',
'llm_generate',
'generate',
'message_complete',
'on_llm_start',
'on_llm_end'
]
for (const eventName of events) {
try {
// @ts-ignore - dynamic event binding
api.on(eventName, (event) => {
record(eventName, event)
})
log(`hooked ${eventName}`)
} catch (e) {
log(`failed to hook ${eventName}: ${e}`)
}
}
}
{
"id": "usage-ledger",
"name": "Usage Ledger",
"description": "Best-effort non-blocking ledger of LLM usage written to SQLite for dashboard aggregation.",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"dbPath": { "type": "string" },
"debug": { "type": "boolean" }
}
}
}

Usage Ledger Plugin (Operator Notes)

This plugin records LLM usage metadata into SQLite for low-risk observability.

What it records

  • timestamp
  • request id (if available)
  • model/provider (if available)
  • input/output/total token counts
  • cache token metrics (if available)
  • source event name and raw payload JSON

DB location

Default: ~/.openclaw/workspace/.openclaw/state/usage-ledger.db

Override with plugin config:

{
  "enabled": true,
  "dbPath": "/custom/path/usage-ledger.db",
  "debug": false
}

Hooking behavior

  • Best-effort and non-blocking.
  • Tries multiple gateway/LLM response-like hook names.
  • If a hook does not exist in a runtime, it is skipped.
  • If event payload has no usage fields, no row is written.

Dashboard

friday-dashboard reads this DB and exposes data.usageSummary. Use component type usage-summary in config/dashboard-config.json.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment