feat: allow hour durations

This commit is contained in:
Peter Steinberger
2025-12-26 01:34:46 +01:00
parent 9f7b1f0942
commit 19f87f0a89
5 changed files with 16 additions and 7 deletions

View File

@@ -148,7 +148,7 @@ If you omit the provider, CLAWDIS currently assumes `anthropic` as a temporary
deprecation fallback. deprecation fallback.
`agent.heartbeat` configures periodic heartbeat runs: `agent.heartbeat` configures periodic heartbeat runs:
- `every`: duration string (`ms`, `s`, `m`); default unit minutes. Omit or set - `every`: duration string (`ms`, `s`, `m`, `h`); default unit minutes. Omit or set
`0m` to disable. `0m` to disable.
- `model`: optional override model for heartbeat runs (`provider/model`). - `model`: optional override model for heartbeat runs (`provider/model`).

View File

@@ -13,7 +13,7 @@ Goal: add a simple heartbeat poll for the embedded agent that only notifies user
## Config & defaults ## Config & defaults
- New config key: `agent.heartbeat` with: - New config key: `agent.heartbeat` with:
- `every`: duration string (`ms`, `s`, `m`; default unit minutes). `0m` disables. - `every`: duration string (`ms`, `s`, `m`, `h`; default unit minutes). `0m` disables.
- `model`: optional override model (`provider/model`) for heartbeat runs. - `model`: optional override model (`provider/model`) for heartbeat runs.
- Default: disabled unless `agent.heartbeat.every` is set. - Default: disabled unless `agent.heartbeat.every` is set.
- New optional idle override for heartbeats: `session.heartbeatIdleMinutes` (defaults to `idleMinutes`). Heartbeat skips do **not** update the session `updatedAt` so idle expiry still works. - New optional idle override for heartbeats: `session.heartbeatIdleMinutes` (defaults to `idleMinutes`). Heartbeat skips do **not** update the session `updatedAt` so idle expiry still works.

View File

@@ -15,6 +15,10 @@ describe("parseDurationMs", () => {
expect(parseDurationMs("1m")).toBe(60_000); expect(parseDurationMs("1m")).toBe(60_000);
}); });
it("parses hours suffix", () => {
expect(parseDurationMs("2h")).toBe(7_200_000);
});
it("supports decimals", () => { it("supports decimals", () => {
expect(parseDurationMs("0.5s")).toBe(500); expect(parseDurationMs("0.5s")).toBe(500);
}); });

View File

@@ -1,5 +1,5 @@
export type DurationMsParseOptions = { export type DurationMsParseOptions = {
defaultUnit?: "ms" | "s" | "m"; defaultUnit?: "ms" | "s" | "m" | "h";
}; };
export function parseDurationMs( export function parseDurationMs(
@@ -11,7 +11,7 @@ export function parseDurationMs(
.toLowerCase(); .toLowerCase();
if (!trimmed) throw new Error("invalid duration (empty)"); if (!trimmed) throw new Error("invalid duration (empty)");
const m = /^(\d+(?:\.\d+)?)(ms|s|m)?$/.exec(trimmed); const m = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(trimmed);
if (!m) throw new Error(`invalid duration: ${raw}`); if (!m) throw new Error(`invalid duration: ${raw}`);
const value = Number(m[1]); const value = Number(m[1]);
@@ -19,8 +19,13 @@ export function parseDurationMs(
throw new Error(`invalid duration: ${raw}`); throw new Error(`invalid duration: ${raw}`);
} }
const unit = (m[2] ?? opts?.defaultUnit ?? "ms") as "ms" | "s" | "m"; const unit = (m[2] ?? opts?.defaultUnit ?? "ms") as
const multiplier = unit === "ms" ? 1 : unit === "s" ? 1000 : 60_000; | "ms"
| "s"
| "m"
| "h";
const multiplier =
unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60_000 : 3_600_000;
const ms = Math.round(value * multiplier); const ms = Math.round(value * multiplier);
if (!Number.isFinite(ms)) throw new Error(`invalid duration: ${raw}`); if (!Number.isFinite(ms)) throw new Error(`invalid duration: ${raw}`);
return ms; return ms;

View File

@@ -463,7 +463,7 @@ const HeartbeatSchema = z
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
path: ["every"], path: ["every"],
message: "invalid duration (use ms, s, m)", message: "invalid duration (use ms, s, m, h)",
}); });
} }
}) })