mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-07 08:26:06 +00:00
fix: address TypeScript integration review feedback
This commit is contained in:
parent
7e299624f8
commit
4c3bdec430
17 changed files with 208 additions and 82 deletions
|
|
@ -12,8 +12,8 @@ selected adapter:
|
|||
reme start workspace_dir=/absolute/path/to/workspace
|
||||
```
|
||||
|
||||
The default endpoint is `http://127.0.0.1:2333`. All entries support `REME_URL`, or `REME_HOST` plus `REME_PORT`, and an
|
||||
optional `REME_API_KEY` bearer token.
|
||||
The default endpoint is `http://127.0.0.1:2333`. All entries support `REME_URL`, or `REME_HOST` plus `REME_PORT`.
|
||||
ReMe's HTTP service does not use API-key authentication.
|
||||
|
||||
## DeepSeek Harness
|
||||
|
||||
|
|
@ -46,21 +46,21 @@ Configure the bundle by replacing its row in the profile's `cordis.patch.yml`:
|
|||
On the DSH Web profile, the same fields are available under **Settings → Plugins → Plugin configuration → ReMe
|
||||
Memory**. Changes are stored in DSH's user settings document and apply to subsequent requests and captures. Changing
|
||||
the daily dream controls reschedules the next run; changing the guidance language affects newly started sessions.
|
||||
Deployment-only `apiKey` and `dreamIntervalMs` values remain outside the user-settings section.
|
||||
The test-only `dreamIntervalMs` value remains outside the user-settings section.
|
||||
|
||||
| Option | Default | Meaning |
|
||||
| --------------------- | ----------------------- | ------------------------------------------- |
|
||||
| `endpoint` | `http://127.0.0.1:2333` | ReMe HTTP service URL |
|
||||
| `language` | `en` | Memory guidance language: `en` or `zh` |
|
||||
| `autoMemoryEnabled` | `true` | Capture completed main-agent turns |
|
||||
| `autoMemoryInterval` | `5` | Submit after this many completed turns |
|
||||
| `autoDreamEnabled` | `true` | Enable daily dream maintenance |
|
||||
| `dreamCron` | `0 23 * * *` | Daily schedule in the DSH process timezone |
|
||||
| `rootAgentsOnly` | `true` | Exclude subagents from guidance and capture |
|
||||
| `requestTimeoutMs` | `10000` | Search request timeout |
|
||||
| `backgroundTimeoutMs` | `3600000` | Automatic-memory and dream timeout |
|
||||
| `shutdownTimeoutMs` | `5000` | Best-effort shutdown drain budget |
|
||||
| `timezone` | `Asia/Shanghai` | IANA timezone used for daily batches |
|
||||
| Option | Default | Meaning |
|
||||
| --------------------- | ----------------------- | --------------------------------------------- |
|
||||
| `endpoint` | `http://127.0.0.1:2333` | ReMe HTTP service URL |
|
||||
| `language` | `en` | Memory guidance language: `en` or `zh` |
|
||||
| `autoMemoryEnabled` | `true` | Capture completed main-agent turns |
|
||||
| `autoMemoryInterval` | `5` | Submit after this many completed turns |
|
||||
| `autoDreamEnabled` | `true` | Enable daily dream maintenance |
|
||||
| `dreamCron` | `0 23 * * *` | Daily schedule in the workspace timezone |
|
||||
| `rootAgentsOnly` | `true` | Exclude subagents from guidance and capture |
|
||||
| `requestTimeoutMs` | `10000` | Search request timeout |
|
||||
| `backgroundTimeoutMs` | `3600000` | Automatic-memory and dream timeout |
|
||||
| `shutdownTimeoutMs` | `5000` | Best-effort shutdown drain budget |
|
||||
| `timezone` | `Asia/Shanghai` | IANA timezone used for batches and scheduling |
|
||||
|
||||
The ReMe card reads the service's `health_check` and `status` jobs on demand. It shows the ReMe version, component
|
||||
health, chunk/index counts, process RSS, and estimated component memory; it can also display the redacted `app_config`
|
||||
|
|
@ -68,10 +68,6 @@ response and trigger one `auto_dream` run. Diagnostics are refreshed when the ca
|
|||
not polled continuously. The page calls the configured ReMe HTTP endpoint from the local browser, so that service must
|
||||
remain browser-reachable and allow the DSH origin.
|
||||
|
||||
The current ReMe HTTP service does not authenticate its job routes. `apiKey` only adds an `Authorization: Bearer ...`
|
||||
header for a deployment whose reverse proxy requires it; it is intentionally not exposed or returned by the DSH
|
||||
settings page.
|
||||
|
||||
## OpenClaw
|
||||
|
||||
OpenClaw `2026.3.12` or later can install the same package:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
reme start workspace_dir=/absolute/path/to/workspace
|
||||
```
|
||||
|
||||
默认服务地址为 `http://127.0.0.1:2333`。所有入口均支持 `REME_URL`,也支持组合使用 `REME_HOST` 和 `REME_PORT`;还可以通过 `REME_API_KEY` 配置可选的 Bearer Token。
|
||||
默认服务地址为 `http://127.0.0.1:2333`。所有入口均支持 `REME_URL`,也支持组合使用 `REME_HOST` 和 `REME_PORT`。ReMe HTTP 服务不使用 API Key 认证。
|
||||
|
||||
## DeepSeek Harness
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ reme start workspace_dir=/absolute/path/to/workspace
|
|||
dsh plugin --profile web add @agentscope-ai/reme
|
||||
```
|
||||
|
||||
安装后可在 **设置 → 插件 → 插件配置 → ReMe Memory** 中配置服务地址、记忆指引语言、自动记忆、每日记忆整理和超时时间,并查看服务健康状态。`apiKey` 属于部署级密钥,不会显示在设置页面中。
|
||||
安装后可在 **设置 → 插件 → 插件配置 → ReMe Memory** 中配置服务地址、记忆指引语言、自动记忆、每日记忆整理和超时时间,并查看服务健康状态。每日记忆整理和每日批次统一使用配置的 workspace 时区。
|
||||
|
||||
完整配置项和 `cordis.patch.yml` 示例请参阅[英文文档](./README.md#deepseek-harness)。
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,6 @@
|
|||
"label": "ReMe endpoint",
|
||||
"placeholder": "http://127.0.0.1:2333"
|
||||
},
|
||||
"apiKey": {
|
||||
"label": "ReMe API key",
|
||||
"sensitive": true,
|
||||
"advanced": true
|
||||
},
|
||||
"autoRecall": {
|
||||
"label": "Auto recall"
|
||||
},
|
||||
|
|
@ -29,7 +24,6 @@
|
|||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"endpoint": { "type": "string" },
|
||||
"apiKey": { "type": "string" },
|
||||
"requestTimeoutMs": {
|
||||
"type": "integer",
|
||||
"minimum": 1000,
|
||||
|
|
|
|||
|
|
@ -193,9 +193,6 @@ export class ReMeClient {
|
|||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(config.apiKey
|
||||
? { Authorization: `Bearer ${config.apiKey}` }
|
||||
: {}),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
/** Connection settings shared by every TypeScript host adapter. */
|
||||
export interface ReMeClientConfig {
|
||||
endpoint: string;
|
||||
apiKey: string;
|
||||
requestTimeoutMs: number;
|
||||
backgroundTimeoutMs: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1171,7 +1171,6 @@ function isRuntimeSnapshot(value: unknown): value is ReMeRuntimeSnapshot {
|
|||
function diagnosticClient(settings: ReMeSettings): ReMeClient {
|
||||
return new ReMeClient({
|
||||
endpoint: settings.endpoint,
|
||||
apiKey: "",
|
||||
requestTimeoutMs: settings.requestTimeoutMs,
|
||||
backgroundTimeoutMs: settings.backgroundTimeoutMs,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,14 +9,13 @@ export const REME_SETTINGS_NAMESPACE = settingsNamespace("reme-memory");
|
|||
|
||||
export const Config = z.object({
|
||||
endpoint: z.string().description("ReMe HTTP service URL"),
|
||||
apiKey: z.string().role("secret").description("Optional ReMe bearer token"),
|
||||
requestTimeoutMs: z.natural().min(1000).max(120000).default(10000),
|
||||
backgroundTimeoutMs: z.natural().min(1000).max(3600000).default(3600000),
|
||||
shutdownTimeoutMs: z.natural().min(100).max(60000).default(5000),
|
||||
autoMemoryEnabled: z.boolean().default(true),
|
||||
autoMemoryInterval: z.natural().min(1).max(1000).default(5),
|
||||
autoDreamEnabled: z.boolean().default(true),
|
||||
dreamCron: z.string().description("Daily cron in the DSH process timezone"),
|
||||
dreamCron: z.string().description("Daily cron in the workspace timezone"),
|
||||
dreamHint: z.string().default(""),
|
||||
dreamIntervalMs: z.natural().max(2147483647).default(0),
|
||||
rootAgentsOnly: z.boolean().default(true),
|
||||
|
|
@ -40,7 +39,7 @@ export const SettingsConfig: z<ReMeSettings> = z.object({
|
|||
dreamCron: z
|
||||
.string()
|
||||
.required()
|
||||
.description("Daily cron in the DSH process timezone"),
|
||||
.description("Daily cron in the workspace timezone"),
|
||||
dreamHint: z.string().default(""),
|
||||
rootAgentsOnly: z.boolean().default(true),
|
||||
language: z.union(["en", "zh"]).default("en"),
|
||||
|
|
@ -53,7 +52,6 @@ export const SettingsConfig: z<ReMeSettings> = z.object({
|
|||
|
||||
const DEFAULT_CONFIG: Readonly<ReMeConfig> = Object.freeze({
|
||||
endpoint: "http://127.0.0.1:2333",
|
||||
apiKey: "",
|
||||
requestTimeoutMs: 10000,
|
||||
backgroundTimeoutMs: 3600000,
|
||||
shutdownTimeoutMs: 5000,
|
||||
|
|
@ -86,7 +84,6 @@ export function resolveConfig(
|
|||
...DEFAULT_CONFIG,
|
||||
...input,
|
||||
endpoint: input.endpoint || env.REME_URL || `http://${host}:${port}`,
|
||||
apiKey: input.apiKey || env.REME_API_KEY || "",
|
||||
dreamCron:
|
||||
input.dreamCron || env.REME_DSH_DREAM_CRON || DEFAULT_CONFIG.dreamCron,
|
||||
};
|
||||
|
|
@ -130,17 +127,13 @@ export function resolveConfig(
|
|||
config.language = config.language === "zh" ? "zh" : "en";
|
||||
if (!validTimezone(config.timezone))
|
||||
throw new TypeError(`Invalid ReMe timezone: ${String(config.timezone)}`);
|
||||
nextDailyRun(config.dreamCron);
|
||||
nextDailyRun(config.dreamCron, config.timezone);
|
||||
return config;
|
||||
}
|
||||
|
||||
/** Project the full plugin configuration into its user-editable settings section. */
|
||||
export function settingsFrom(config: ReMeConfig): ReMeSettings {
|
||||
const {
|
||||
apiKey: _apiKey,
|
||||
dreamIntervalMs: _dreamIntervalMs,
|
||||
...settings
|
||||
} = config;
|
||||
const { dreamIntervalMs: _dreamIntervalMs, ...settings } = config;
|
||||
return settings;
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +151,7 @@ export function validateSettings(settings: ReMeSettings): void {
|
|||
if (!validTimezone(settings.timezone)) {
|
||||
throw new TypeError(`Invalid ReMe timezone: ${String(settings.timezone)}`);
|
||||
}
|
||||
nextDailyRun(settings.dreamCron);
|
||||
nextDailyRun(settings.dreamCron, settings.timezone);
|
||||
}
|
||||
|
||||
function assertEndpoint(value: string): void {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export class ReMeRuntime {
|
|||
readonly states = new Map<string, SessionState>();
|
||||
private readonly configSource: () => ReMeConfig;
|
||||
private dreamTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private dreamScheduleGeneration = 0;
|
||||
private dreamTask: Promise<void> | null = null;
|
||||
private dreamController: AbortController | null = null;
|
||||
private started = false;
|
||||
|
|
@ -168,7 +169,7 @@ export class ReMeRuntime {
|
|||
start(): void {
|
||||
this.started = true;
|
||||
if (!this.configSource().autoDreamEnabled || this.stopping) return;
|
||||
this.scheduleDream();
|
||||
this.scheduleDream(this.dreamScheduleGeneration);
|
||||
}
|
||||
|
||||
/** Apply a changed settings snapshot to pending batching and dream scheduling. */
|
||||
|
|
@ -179,21 +180,29 @@ export class ReMeRuntime {
|
|||
for (const state of this.states.values()) this.scheduleAutoMemory(state);
|
||||
}
|
||||
if (!this.started) return;
|
||||
this.dreamScheduleGeneration += 1;
|
||||
if (this.dreamTimer) clearTimeout(this.dreamTimer);
|
||||
this.dreamTimer = null;
|
||||
this.nextDreamAt = undefined;
|
||||
if (config.autoDreamEnabled) this.scheduleDream();
|
||||
if (config.autoDreamEnabled)
|
||||
this.scheduleDream(this.dreamScheduleGeneration);
|
||||
}
|
||||
|
||||
private scheduleDream(): void {
|
||||
private scheduleDream(generation: number): void {
|
||||
const config = this.configSource();
|
||||
if (this.stopping || !config.autoDreamEnabled) return;
|
||||
if (
|
||||
generation !== this.dreamScheduleGeneration ||
|
||||
this.stopping ||
|
||||
!config.autoDreamEnabled
|
||||
)
|
||||
return;
|
||||
let delay: number;
|
||||
try {
|
||||
delay =
|
||||
config.dreamIntervalMs > 0
|
||||
? config.dreamIntervalMs
|
||||
: nextDailyRun(config.dreamCron).getTime() - Date.now();
|
||||
: nextDailyRun(config.dreamCron, config.timezone).getTime() -
|
||||
Date.now();
|
||||
} catch (error) {
|
||||
this.log("warn", "auto_dream_schedule_invalid", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
|
|
@ -204,7 +213,7 @@ export class ReMeRuntime {
|
|||
this.dreamTimer = setTimeout(() => {
|
||||
this.dreamTimer = null;
|
||||
this.nextDreamAt = undefined;
|
||||
void this.runDream().finally(() => this.scheduleDream());
|
||||
void this.runDream().finally(() => this.scheduleDream(generation));
|
||||
}, delay);
|
||||
this.dreamTimer.unref?.();
|
||||
}
|
||||
|
|
@ -289,6 +298,7 @@ export class ReMeRuntime {
|
|||
async disposeAll(): Promise<void> {
|
||||
this.stopping = true;
|
||||
this.started = false;
|
||||
this.dreamScheduleGeneration += 1;
|
||||
if (this.dreamTimer) clearTimeout(this.dreamTimer);
|
||||
this.dreamTimer = null;
|
||||
this.nextDreamAt = undefined;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
const DAILY_CRON = /^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+\*$/;
|
||||
|
||||
export function nextDailyRun(cron: string, now = new Date()): Date {
|
||||
export function nextDailyRun(
|
||||
cron: string,
|
||||
timezone: string,
|
||||
now = new Date(),
|
||||
): Date {
|
||||
const match = DAILY_CRON.exec(String(cron || "").trim());
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
|
|
@ -11,8 +15,32 @@ export function nextDailyRun(cron: string, now = new Date()): Date {
|
|||
const hour = Number(match[2]);
|
||||
if (minute > 59 || hour > 23)
|
||||
throw new Error("dreamCron contains an invalid hour or minute");
|
||||
const next = new Date(now.getTime());
|
||||
next.setHours(hour, minute, 0, 0);
|
||||
if (next.getTime() <= now.getTime()) next.setDate(next.getDate() + 1);
|
||||
return next;
|
||||
const formatter = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone: timezone,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hourCycle: "h23",
|
||||
});
|
||||
const next = new Date(now.getTime() - 26 * 60 * 60 * 1000);
|
||||
next.setUTCSeconds(0, 0);
|
||||
const scheduledDays = new Set<string>();
|
||||
for (let checked = 0; checked < 5 * 24 * 60; checked += 1) {
|
||||
const parts = formatter.formatToParts(next);
|
||||
const part = (type: Intl.DateTimeFormatPartTypes): string | undefined =>
|
||||
parts.find((candidate) => candidate.type === type)?.value;
|
||||
const candidateHour = Number(part("hour"));
|
||||
const candidateMinute = Number(part("minute"));
|
||||
if (candidateHour === hour && candidateMinute === minute) {
|
||||
const day = `${part("year")}-${part("month")}-${part("day")}`;
|
||||
if (!scheduledDays.has(day)) {
|
||||
scheduledDays.add(day);
|
||||
if (next.getTime() > now.getTime()) return next;
|
||||
}
|
||||
}
|
||||
next.setUTCMinutes(next.getUTCMinutes() + 1);
|
||||
}
|
||||
throw new Error("dreamCron has no occurrence in the scheduling window");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import type { ReMeClientConfig } from "../core/types.js";
|
|||
|
||||
export interface ReMeConfigInput {
|
||||
endpoint?: string;
|
||||
apiKey?: string;
|
||||
requestTimeoutMs?: number;
|
||||
backgroundTimeoutMs?: number;
|
||||
shutdownTimeoutMs?: number;
|
||||
|
|
@ -33,7 +32,7 @@ export interface ReMeConfig extends ReMeClientConfig {
|
|||
}
|
||||
|
||||
/** ReMe integration fields owned by the DSH user-settings document. */
|
||||
export type ReMeSettings = Omit<ReMeConfig, "apiKey" | "dreamIntervalMs">;
|
||||
export type ReMeSettings = Omit<ReMeConfig, "dreamIntervalMs">;
|
||||
|
||||
export interface SessionEvent {
|
||||
type: string;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ export interface OpenClawReMeConfig extends ReMeClientConfig {
|
|||
|
||||
const DEFAULT_CONFIG: Readonly<OpenClawReMeConfig> = Object.freeze({
|
||||
endpoint: "http://127.0.0.1:2333",
|
||||
apiKey: "",
|
||||
requestTimeoutMs: 5000,
|
||||
backgroundTimeoutMs: 3600000,
|
||||
shutdownTimeoutMs: 5000,
|
||||
|
|
@ -26,7 +25,6 @@ export const OPENCLAW_CONFIG_SCHEMA = {
|
|||
additionalProperties: false,
|
||||
properties: {
|
||||
endpoint: { type: "string" },
|
||||
apiKey: { type: "string" },
|
||||
requestTimeoutMs: { type: "integer", minimum: 1000, maximum: 120000 },
|
||||
backgroundTimeoutMs: { type: "integer", minimum: 1000, maximum: 3600000 },
|
||||
shutdownTimeoutMs: { type: "integer", minimum: 100, maximum: 60000 },
|
||||
|
|
@ -60,7 +58,6 @@ export function resolveOpenClawConfig(
|
|||
}
|
||||
return {
|
||||
endpoint: normalizedEndpoint,
|
||||
apiKey: stringValue(input.apiKey) || env.REME_API_KEY || "",
|
||||
requestTimeoutMs: integer(
|
||||
input.requestTimeoutMs,
|
||||
1000,
|
||||
|
|
|
|||
|
|
@ -42,9 +42,8 @@ function normalizeMessage(
|
|||
sessionId: string,
|
||||
index: number,
|
||||
): ReMeMessage | null {
|
||||
const text = messageText(value.content);
|
||||
if (!text || text.includes('<reme-context source="auto-recall">'))
|
||||
return null;
|
||||
const text = stripAutoRecallContext(messageText(value.content));
|
||||
if (!text) return null;
|
||||
const nativeId =
|
||||
typeof value.id === "string" && value.id ? value.id : `${index}\n${text}`;
|
||||
const createdAt = timestamp(value.created_at ?? value.timestamp);
|
||||
|
|
@ -57,6 +56,15 @@ function normalizeMessage(
|
|||
};
|
||||
}
|
||||
|
||||
function stripAutoRecallContext(value: string): string {
|
||||
const opening = '<reme-context source="auto-recall">';
|
||||
if (!value.startsWith(opening)) return value;
|
||||
const closing = "</reme-context>";
|
||||
const end = value.indexOf(closing, opening.length);
|
||||
if (end === -1) return value;
|
||||
return value.slice(end + closing.length).trim();
|
||||
}
|
||||
|
||||
function messageText(content: unknown): string {
|
||||
if (typeof content === "string") return content.trim();
|
||||
if (!Array.isArray(content)) return "";
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ test("calls ReMe jobs with their native request and response envelopes", async (
|
|||
endpoint: "http://127.0.0.1:2333",
|
||||
requestTimeoutMs: 1000,
|
||||
backgroundTimeoutMs: 1000,
|
||||
apiKey: "",
|
||||
});
|
||||
const result = await client.search("deployment", { limit: 5, minScore: 0 });
|
||||
assert.equal(result.ok, true);
|
||||
|
|
@ -49,7 +48,6 @@ test("combines caller cancellation with the request timeout", async () => {
|
|||
endpoint: "http://127.0.0.1:2333",
|
||||
requestTimeoutMs: 1000,
|
||||
backgroundTimeoutMs: 1000,
|
||||
apiKey: "",
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const request = client.search("deployment", { signal: controller.signal });
|
||||
|
|
@ -102,7 +100,6 @@ test("returns typed health, memory status, and redacted server configuration", a
|
|||
endpoint,
|
||||
requestTimeoutMs: 1000,
|
||||
backgroundTimeoutMs: 1000,
|
||||
apiKey: "",
|
||||
}));
|
||||
const health = await client.healthCheck();
|
||||
endpoint = "http://second.test";
|
||||
|
|
@ -149,7 +146,6 @@ test("lists and loads read-only ReMe workspace files", async () => {
|
|||
endpoint: "http://127.0.0.1:2333",
|
||||
requestTimeoutMs: 1000,
|
||||
backgroundTimeoutMs: 1000,
|
||||
apiKey: "",
|
||||
});
|
||||
const listing = await client.listFiles("daily", { limit: 1 });
|
||||
const file = await client.loadFile("daily/2026-08-20/session.md");
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ test("rejects unknown options and invalid IANA timezones", () => {
|
|||
() => resolveConfig({ timezone: "Mars/Olympus" }, {}),
|
||||
/Invalid ReMe timezone/,
|
||||
);
|
||||
assert.throws(
|
||||
() => resolveConfig({ apiKey: "unsupported" }, {}),
|
||||
/Unknown ReMe config option/,
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes bounded plugin configuration", () => {
|
||||
|
|
@ -60,10 +64,9 @@ test("normalizes bounded plugin configuration", () => {
|
|||
assert.equal(config.rootAgentsOnly, false);
|
||||
});
|
||||
|
||||
test("projects the editable DSH settings without deployment-only secrets", async () => {
|
||||
const base = resolveConfig({ apiKey: "secret", dreamIntervalMs: 5000 }, {});
|
||||
test("projects editable DSH settings without test-only intervals", async () => {
|
||||
const base = resolveConfig({ dreamIntervalMs: 5000 }, {});
|
||||
const settings = settingsFrom(base);
|
||||
assert.equal("apiKey" in settings, false);
|
||||
assert.equal("dreamIntervalMs" in settings, false);
|
||||
const validated = await SettingsConfig["~standard"].validate({
|
||||
...settings,
|
||||
|
|
@ -71,7 +74,6 @@ test("projects the editable DSH settings without deployment-only secrets", async
|
|||
});
|
||||
assert.equal(validated.issues, undefined);
|
||||
const merged = mergeSettings(base, validated.value);
|
||||
assert.equal(merged.apiKey, "secret");
|
||||
assert.equal(merged.searchLimit, 8);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ test("normalizes OpenClaw configuration and stable session ids", () => {
|
|||
() => resolveOpenClawConfig({ endpoint: "file:///tmp/reme" }, {}),
|
||||
/http\(s\)/,
|
||||
);
|
||||
assert.throws(
|
||||
() => resolveOpenClawConfig({ apiKey: "unsupported" }, {}),
|
||||
/Unknown ReMe config option/,
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps the runtime schema aligned with the OpenClaw manifest", async () => {
|
||||
|
|
@ -54,6 +58,27 @@ test("captures only the last OpenClaw user and assistant pair", () => {
|
|||
);
|
||||
});
|
||||
|
||||
test("removes recalled context while preserving the current OpenClaw prompt", () => {
|
||||
const messages = captureLastTurn(
|
||||
[
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
'<reme-context source="auto-recall">\nremembered deployment\n</reme-context>\n\nremember blue',
|
||||
},
|
||||
{ role: "assistant", content: "noted" },
|
||||
],
|
||||
"session",
|
||||
);
|
||||
assert.deepEqual(
|
||||
messages.map((message) => [message.role, message.content[0].text]),
|
||||
[
|
||||
["user", "remember blue"],
|
||||
["assistant", "noted"],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("registers OpenClaw recall, capture, tool, and shutdown lifecycle", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls = [];
|
||||
|
|
|
|||
|
|
@ -302,6 +302,41 @@ test("applies changed batching and dream settings without replacing the runtime"
|
|||
await runtime.disposeAll();
|
||||
});
|
||||
|
||||
test("keeps one auto-dream schedule when reconfigured during a run", async () => {
|
||||
let calls = 0;
|
||||
let releaseFirst;
|
||||
const firstRequest = new Promise((resolve) => {
|
||||
releaseFirst = resolve;
|
||||
});
|
||||
let config = {
|
||||
...CONFIG,
|
||||
autoDreamEnabled: true,
|
||||
dreamIntervalMs: 20,
|
||||
shutdownTimeoutMs: 100,
|
||||
};
|
||||
const runtime = new ReMeRuntime(
|
||||
{
|
||||
async autoDream() {
|
||||
calls += 1;
|
||||
if (calls === 1) await firstRequest;
|
||||
return { ok: true };
|
||||
},
|
||||
},
|
||||
() => config,
|
||||
silentLogger(),
|
||||
);
|
||||
|
||||
runtime.start();
|
||||
await delay(25);
|
||||
config = { ...config };
|
||||
runtime.reconfigure();
|
||||
releaseFirst();
|
||||
await delay(55);
|
||||
|
||||
assert.ok(calls >= 2 && calls <= 3, `expected one schedule, got ${calls}`);
|
||||
await runtime.disposeAll();
|
||||
});
|
||||
|
||||
function completeTurn(runtime, session, turn, seq, time) {
|
||||
runtime.capture(session, { type: "turn/start", data: { turn } });
|
||||
runtime.capture(session, {
|
||||
|
|
@ -335,3 +370,7 @@ function completeTurn(runtime, session, turn, seq, time) {
|
|||
function silentLogger() {
|
||||
return { debug() {}, warn() {}, log() {} };
|
||||
}
|
||||
|
||||
function delay(milliseconds) {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,62 @@ import test from "node:test";
|
|||
import { nextDailyRun } from "../dist/dsh/scheduler.js";
|
||||
|
||||
test("computes today's or tomorrow's daily dream run", () => {
|
||||
const before = new Date(2026, 7, 19, 22, 30, 0);
|
||||
const today = nextDailyRun("0 23 * * *", before);
|
||||
assert.equal(today.getDate(), 19);
|
||||
assert.equal(today.getHours(), 23);
|
||||
const before = new Date("2026-08-19T14:30:00Z");
|
||||
const today = nextDailyRun("0 23 * * *", "Asia/Shanghai", before);
|
||||
assert.equal(today.toISOString(), "2026-08-19T15:00:00.000Z");
|
||||
|
||||
const after = new Date(2026, 7, 19, 23, 30, 0);
|
||||
const tomorrow = nextDailyRun("0 23 * * *", after);
|
||||
assert.equal(tomorrow.getDate(), 20);
|
||||
assert.equal(tomorrow.getHours(), 23);
|
||||
const after = new Date("2026-08-19T15:30:00Z");
|
||||
const tomorrow = nextDailyRun("0 23 * * *", "Asia/Shanghai", after);
|
||||
assert.equal(tomorrow.toISOString(), "2026-08-20T15:00:00.000Z");
|
||||
});
|
||||
|
||||
test("uses the configured IANA timezone instead of the process timezone", () => {
|
||||
const now = new Date("2026-08-19T12:00:00Z");
|
||||
assert.equal(
|
||||
nextDailyRun("0 23 * * *", "UTC", now).toISOString(),
|
||||
"2026-08-19T23:00:00.000Z",
|
||||
);
|
||||
assert.equal(
|
||||
nextDailyRun("0 23 * * *", "America/Los_Angeles", now).toISOString(),
|
||||
"2026-08-20T06:00:00.000Z",
|
||||
);
|
||||
});
|
||||
|
||||
test("skips a nonexistent local time across daylight-saving changes", () => {
|
||||
const beforeSpringForward = new Date("2026-03-08T09:00:00Z");
|
||||
assert.equal(
|
||||
nextDailyRun(
|
||||
"30 2 * * *",
|
||||
"America/Los_Angeles",
|
||||
beforeSpringForward,
|
||||
).toISOString(),
|
||||
"2026-03-09T09:30:00.000Z",
|
||||
);
|
||||
});
|
||||
|
||||
test("runs only once on a local date when daylight saving time repeats", () => {
|
||||
const beforeFirstOccurrence = new Date("2026-11-01T07:00:00Z");
|
||||
assert.equal(
|
||||
nextDailyRun(
|
||||
"30 1 * * *",
|
||||
"America/Los_Angeles",
|
||||
beforeFirstOccurrence,
|
||||
).toISOString(),
|
||||
"2026-11-01T08:30:00.000Z",
|
||||
);
|
||||
|
||||
const afterFirstOccurrence = new Date("2026-11-01T08:45:00Z");
|
||||
assert.equal(
|
||||
nextDailyRun(
|
||||
"30 1 * * *",
|
||||
"America/Los_Angeles",
|
||||
afterFirstOccurrence,
|
||||
).toISOString(),
|
||||
"2026-11-02T09:30:00.000Z",
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects unsupported or invalid cron expressions", () => {
|
||||
assert.throws(() => nextDailyRun("*/5 * * * *"), /daily form/);
|
||||
assert.throws(() => nextDailyRun("99 23 * * *"), /invalid/);
|
||||
assert.throws(() => nextDailyRun("*/5 * * * *", "UTC"), /daily form/);
|
||||
assert.throws(() => nextDailyRun("99 23 * * *", "UTC"), /invalid/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue