Files
mesh-catalog/modules/model-usage/tools/index.ts
T
jochen 08265a70ca audit-logger, model-usage: retry a failed write and never lose the event (hq issue 276)
Both caught a failed write and took the event, losing it silently; the SDK's rule is to throw when
the work was not done. A failed write now throws so the bus offers the event again, and is spooled
on disk at once; on its last delivery the spooled event is taken, and a background pass replays the
spool once writing works. The runtime does not pass the delivery count, so the spool counts failed
deliveries itself, across restarts. Over its bound (1000 events or 30 minutes) the last delivery is
no longer taken, so the bus gives it up and the controller raises max-deliveries - the one existing
condition that names a consumer which cannot keep up - while the spool still holds it.

Writes are idempotent by event: the trail skips an id it already wrote; the usage upsert keeps the
reading observed latest (migration 2), so a late replay never overwrites a newer one. Each module has
a status tool for the spool, declared as valuable data (ADR 0233). model-usage moves to the bundle
shape (ADR 0198) with its schema in a prepare step and numbered migrations; its old container shape
had no image. Both on mesh-sdk 0.1.13.

The log-only handlers of redis, mssql, mosquitto, mongodb, mesh-vault, showcase and the catalogue no
longer throw a TypeError on an event without a body.
2026-10-06 18:36:16 +02:00

49 lines
2.1 KiB
TypeScript

// model-usage's tools — its own read-only query surface over the usage store (novox/hq ADR 0039:
// the tools and the client they call live in the module), and its standing: what waits in the spool
// (novox/hq issue 276). The store is opened from the environment; when it cannot be (no DATABASE_URL),
// the query tool is not contributed rather than failing the whole tool runtime — the postgres
// precedent — but the status tool is, since the spool is what matters most when the store is away.
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
import { UsageStore } from "../store.js";
import { Spool } from "../spool.js";
import { usageSpoolPath } from "../consume.js";
export function getUsageTools(store: UsageStore | undefined, env: NodeJS.ProcessEnv = process.env): ToolDefinition[] {
const tools: ToolDefinition[] = [
{
name: "model_usage_status",
description:
"The usage store's standing: the usage events that could not be written yet — how many wait in the spool, how many were taken from the bus and are held only there, the oldest, the last error, and whether the spool is over its bound (then the bus gives events up and max-deliveries is raised).",
input: {},
// Launched as its own process beside the handler, so it reads the spool from disk.
run: async () => ({ spool: (await Spool.read(usageSpoolPath(env))).standing() }),
},
];
if (store) {
tools.push({
name: "model_usage_current",
description:
"The latest model-usage reading per (licence, consumer, period, metric), across both grains, optionally restricted to one licence.",
input: {
licence: { type: "string", description: "restrict to one licence (optional)" },
},
run: async (args) => {
const licence = args.licence ? String(args.licence) : undefined;
return { usage: await store.current(licence) };
},
});
}
return tools;
}
registerModuleTools("model-usage", (env) => {
let store: UsageStore | undefined;
try {
store = UsageStore.fromEnv(env);
} catch {
store = undefined;
}
return getUsageTools(store, env);
});