Skip to content

Commit b089614

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex-supervisor-extension
# Conflicts: # src/cron/schedule-identity.test.ts
2 parents 76557bc + 2f8b1a8 commit b089614

47 files changed

Lines changed: 1703 additions & 121 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/concepts/models.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ When live probes run in a TTY, you can select fallbacks interactively. In non-in
340340

341341
## Models registry (`models.json`)
342342

343-
Custom providers in `models.providers` are written into `models.json` under the agent directory (default `~/.openclaw/agents/<agentId>/agent/models.json`). This file is merged by default unless `models.mode` is set to `replace`.
343+
Custom providers in `models.providers` are written into `models.json` under the agent directory (default `~/.openclaw/agents/<agentId>/agent/models.json`). Provider-plugin catalogs are stored as generated plugin-owned catalog shards under the agent's plugin state and loaded automatically. This file is merged by default unless `models.mode` is set to `replace`.
344344

345345
<AccordionGroup>
346346
<Accordion title="Merge mode precedence">

docs/gateway/config-tools.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,8 @@ Configuring a custom/local provider `baseUrl` is also the narrow network trust d
488488
- Empty or missing agent `apiKey`/`baseUrl` fall back to `models.providers` in config.
489489
- Matching model `contextWindow`/`maxTokens` use the higher value between explicit config and implicit catalog values.
490490
- Matching model `contextTokens` preserves an explicit runtime cap when present; use it to limit effective context without changing native model metadata.
491-
- Use `models.mode: "replace"` when you want config to fully rewrite `models.json`.
491+
- Provider-plugin catalogs are stored as generated plugin-owned catalog shards under the agent's plugin state.
492+
- Use `models.mode: "replace"` when you want config to fully rewrite `models.json` and active plugin catalog shards.
492493
- Marker persistence is source-authoritative: markers are written from the active source config snapshot (pre-resolution), not from resolved runtime secret values.
493494

494495
</Accordion>

packages/gateway-client/src/client.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { resolveGatewayStartupRetryAfterMs } from "@openclaw/gateway-protocol/st
2828
import ipaddr from "ipaddr.js";
2929
import { WebSocket, type ClientOptions, type CertMeta } from "ws";
3030
import { buildDeviceAuthPayloadV3 } from "./device-auth.js";
31-
import { resolveConnectChallengeTimeoutMs } from "./timeouts.js";
31+
import { resolveConnectChallengeTimeoutMs, resolveSafeTimeoutDelayMs } from "./timeouts.js";
3232

3333
export type DeviceIdentity = {
3434
deviceId: string;
@@ -84,10 +84,6 @@ function normalizeLowercaseStringOrEmpty(value: unknown): string {
8484
return typeof value === "string" ? value.trim().toLowerCase() : "";
8585
}
8686

87-
function resolveSafeTimeoutDelayMs(value: number): number {
88-
return Math.max(0, Math.min(value, 2_147_483_647));
89-
}
90-
9187
function rawDataToString(data: unknown): string {
9288
if (typeof data === "string") {
9389
return data;
@@ -516,7 +512,7 @@ export class GatewayClient {
516512
};
517513
this.requestTimeoutMs =
518514
typeof opts.requestTimeoutMs === "number" && Number.isFinite(opts.requestTimeoutMs)
519-
? resolveSafeTimeoutDelayMs(opts.requestTimeoutMs)
515+
? resolveSafeTimeoutDelayMs(opts.requestTimeoutMs, { minMs: 0 })
520516
: 30_000;
521517
}
522518

@@ -1468,7 +1464,7 @@ export class GatewayClient {
14681464
opts?.timeoutMs === null
14691465
? null
14701466
: typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
1471-
? resolveSafeTimeoutDelayMs(opts.timeoutMs)
1467+
? resolveSafeTimeoutDelayMs(opts.timeoutMs, { minMs: 0 })
14721468
: expectFinal
14731469
? null
14741470
: this.requestTimeoutMs;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
import { waitForEventLoopReady } from "./event-loop-ready.js";
3+
4+
describe("waitForEventLoopReady", () => {
5+
afterEach(() => {
6+
vi.useRealTimers();
7+
vi.restoreAllMocks();
8+
});
9+
10+
it("falls back when maxWaitMs is non-finite instead of arming a NaN timer", async () => {
11+
vi.useFakeTimers();
12+
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
13+
14+
const readiness = waitForEventLoopReady({
15+
maxWaitMs: Number.NaN,
16+
intervalMs: 25,
17+
consecutiveReadyChecks: 2,
18+
});
19+
20+
expect(setTimeoutSpy).toHaveBeenLastCalledWith(expect.any(Function), 25);
21+
22+
await vi.advanceTimersByTimeAsync(50);
23+
await expect(readiness).resolves.toMatchObject({
24+
ready: true,
25+
checks: 2,
26+
});
27+
});
28+
});

packages/gateway-client/src/event-loop-ready.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
function resolveSafeTimeoutDelayMs(value: number): number {
2-
return Math.max(0, Math.min(value, 2_147_483_647));
3-
}
1+
import { resolveFiniteTimeoutDelayMs } from "./timeouts.js";
42

53
export type EventLoopReadyResult = {
64
ready: boolean;
@@ -30,7 +28,9 @@ function resolvePositiveInteger(value: number | undefined, fallback: number): nu
3028
export async function waitForEventLoopReady(
3129
options: EventLoopReadyOptions = {},
3230
): Promise<EventLoopReadyResult> {
33-
const maxWaitMs = resolveSafeTimeoutDelayMs(options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS);
31+
const maxWaitMs = resolveFiniteTimeoutDelayMs(options.maxWaitMs, DEFAULT_MAX_WAIT_MS, {
32+
minMs: 0,
33+
});
3434
const intervalMs = resolvePositiveInteger(options.intervalMs, DEFAULT_INTERVAL_MS);
3535
const driftThresholdMs = resolvePositiveInteger(
3636
options.driftThresholdMs,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
MAX_SAFE_TIMEOUT_DELAY_MS,
4+
resolveFiniteTimeoutDelayMs,
5+
resolveSafeTimeoutDelayMs,
6+
} from "./timeouts.js";
7+
8+
describe("resolveSafeTimeoutDelayMs", () => {
9+
it("clamps to Node's signed-32-bit timer ceiling", () => {
10+
expect(resolveSafeTimeoutDelayMs(3_000_000_000)).toBe(MAX_SAFE_TIMEOUT_DELAY_MS);
11+
});
12+
13+
it("falls back to the minimum for non-finite delays", () => {
14+
expect(resolveSafeTimeoutDelayMs(Number.NaN)).toBe(1);
15+
expect(resolveSafeTimeoutDelayMs(Number.POSITIVE_INFINITY, { minMs: 250 })).toBe(250);
16+
});
17+
18+
it("preserves callers that intentionally allow zero-delay timers", () => {
19+
expect(resolveSafeTimeoutDelayMs(Number.NaN, { minMs: 0 })).toBe(0);
20+
expect(resolveSafeTimeoutDelayMs(-5, { minMs: 0 })).toBe(0);
21+
});
22+
});
23+
24+
describe("resolveFiniteTimeoutDelayMs", () => {
25+
it("uses the fallback for missing or non-finite overrides", () => {
26+
expect(resolveFiniteTimeoutDelayMs(undefined, 10_000, { minMs: 0 })).toBe(10_000);
27+
expect(resolveFiniteTimeoutDelayMs(Number.NaN, 10_000, { minMs: 0 })).toBe(10_000);
28+
expect(resolveFiniteTimeoutDelayMs(Number.POSITIVE_INFINITY, 10_000, { minMs: 0 })).toBe(
29+
10_000,
30+
);
31+
});
32+
33+
it("still clamps finite overrides through safe timer bounds", () => {
34+
expect(resolveFiniteTimeoutDelayMs(3_000_000_000, 10_000)).toBe(MAX_SAFE_TIMEOUT_DELAY_MS);
35+
expect(resolveFiniteTimeoutDelayMs(-5, 10_000, { minMs: 0 })).toBe(0);
36+
});
37+
});

packages/gateway-client/src/timeouts.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,31 @@ function parseStrictPositiveInteger(value: string): number | undefined {
66
return Number.isSafeInteger(parsed) ? parsed : undefined;
77
}
88

9+
export const MAX_SAFE_TIMEOUT_DELAY_MS = 2_147_483_647;
910
export const DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 15_000;
1011
export const MIN_CONNECT_CHALLENGE_TIMEOUT_MS = 250;
1112
export const MAX_CONNECT_CHALLENGE_TIMEOUT_MS = DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS;
1213

14+
export function resolveSafeTimeoutDelayMs(delayMs: number, opts?: { minMs?: number }): number {
15+
const rawMinMs = opts?.minMs ?? 1;
16+
const minMs = Math.min(
17+
MAX_SAFE_TIMEOUT_DELAY_MS,
18+
Math.max(0, Number.isFinite(rawMinMs) ? Math.floor(rawMinMs) : 1),
19+
);
20+
const candidateMs = Number.isFinite(delayMs) ? Math.floor(delayMs) : minMs;
21+
return Math.min(MAX_SAFE_TIMEOUT_DELAY_MS, Math.max(minMs, candidateMs));
22+
}
23+
24+
export function resolveFiniteTimeoutDelayMs(
25+
delayMs: number | null | undefined,
26+
fallbackMs: number,
27+
opts?: { minMs?: number },
28+
): number {
29+
const candidateMs =
30+
typeof delayMs === "number" && Number.isFinite(delayMs) ? delayMs : fallbackMs;
31+
return resolveSafeTimeoutDelayMs(candidateMs, opts);
32+
}
33+
1334
export function clampConnectChallengeTimeoutMs(
1435
timeoutMs: number,
1536
maxTimeoutMs = MAX_CONNECT_CHALLENGE_TIMEOUT_MS,

packages/memory-host-sdk/src/host/backend-config.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,86 @@ describe("resolveMemoryBackendConfig", () => {
393393
expect(update.embedTimeoutMs).toBe(360_000);
394394
});
395395

396+
it("keeps sub-unit positive qmd numeric overrides usable", () => {
397+
const cfg = {
398+
agents: { defaults: { workspace: "/tmp/memory-test" } },
399+
memory: {
400+
backend: "qmd",
401+
qmd: {
402+
sessions: {
403+
enabled: true,
404+
retentionDays: 0.5,
405+
},
406+
update: {
407+
commandTimeoutMs: 0.5,
408+
updateTimeoutMs: 0.5,
409+
embedTimeoutMs: 0.5,
410+
},
411+
limits: {
412+
maxResults: 0.5,
413+
maxSnippetChars: 0.5,
414+
maxInjectedChars: 0.5,
415+
timeoutMs: 0.5,
416+
},
417+
},
418+
},
419+
} as OpenClawConfig;
420+
421+
const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" });
422+
const qmd = requireQmdConfig(resolved);
423+
424+
expect(qmd.sessions.retentionDays).toBe(1);
425+
expect(qmd.update.commandTimeoutMs).toBe(1);
426+
expect(qmd.update.updateTimeoutMs).toBe(1);
427+
expect(qmd.update.embedTimeoutMs).toBe(1);
428+
expect(qmd.limits).toMatchObject({
429+
maxResults: 1,
430+
maxSnippetChars: 1,
431+
maxInjectedChars: 1,
432+
timeoutMs: 1,
433+
});
434+
});
435+
436+
it("falls back for non-finite qmd numeric overrides", () => {
437+
const cfg = {
438+
agents: { defaults: { workspace: "/tmp/memory-test" } },
439+
memory: {
440+
backend: "qmd",
441+
qmd: {
442+
sessions: {
443+
enabled: true,
444+
retentionDays: Number.NaN,
445+
},
446+
update: {
447+
commandTimeoutMs: Number.POSITIVE_INFINITY,
448+
updateTimeoutMs: Number.NaN,
449+
embedTimeoutMs: Number.NEGATIVE_INFINITY,
450+
},
451+
limits: {
452+
maxResults: Number.NaN,
453+
maxSnippetChars: Number.POSITIVE_INFINITY,
454+
maxInjectedChars: Number.NEGATIVE_INFINITY,
455+
timeoutMs: Number.NaN,
456+
},
457+
},
458+
},
459+
} as OpenClawConfig;
460+
461+
const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" });
462+
const qmd = requireQmdConfig(resolved);
463+
464+
expect(qmd.sessions.retentionDays).toBeUndefined();
465+
expect(qmd.update.commandTimeoutMs).toBe(30_000);
466+
expect(qmd.update.updateTimeoutMs).toBe(120_000);
467+
expect(qmd.update.embedTimeoutMs).toBe(120_000);
468+
expect(qmd.limits).toMatchObject({
469+
maxResults: 4,
470+
maxSnippetChars: 450,
471+
maxInjectedChars: 2_200,
472+
timeoutMs: 4_000,
473+
});
474+
});
475+
396476
it("resolves qmd startup refresh overrides", () => {
397477
const cfg = {
398478
agents: { defaults: { workspace: "/tmp/memory-test" } },

packages/memory-host-sdk/src/host/backend-config.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,19 @@ function resolveDebounceMs(raw: number | undefined): number {
216216
}
217217

218218
function resolveTimeoutMs(raw: number | undefined, fallback: number): number {
219-
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
220-
return Math.floor(raw);
219+
return resolvePositiveIntegerConfig(raw, fallback);
220+
}
221+
222+
function resolvePositiveIntegerConfig(raw: number | undefined, fallback: number): number;
223+
function resolvePositiveIntegerConfig(raw: number | undefined): number | undefined;
224+
function resolvePositiveIntegerConfig(
225+
raw: number | undefined,
226+
fallback?: number,
227+
): number | undefined {
228+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) {
229+
return fallback;
221230
}
222-
return fallback;
231+
return Math.max(1, Math.floor(raw));
223232
}
224233

225234
function resolveStartupMode(raw: MemoryQmdConfig["update"]): MemoryQmdStartupMode {
@@ -238,20 +247,18 @@ function resolveStartupDelayMs(raw: number | undefined): number {
238247
}
239248

240249
function resolveLimits(raw?: MemoryQmdConfig["limits"]): ResolvedQmdLimitsConfig {
241-
const parsed: ResolvedQmdLimitsConfig = { ...DEFAULT_QMD_LIMITS };
242-
if (raw?.maxResults && raw.maxResults > 0) {
243-
parsed.maxResults = Math.floor(raw.maxResults);
244-
}
245-
if (raw?.maxSnippetChars && raw.maxSnippetChars > 0) {
246-
parsed.maxSnippetChars = Math.floor(raw.maxSnippetChars);
247-
}
248-
if (raw?.maxInjectedChars && raw.maxInjectedChars > 0) {
249-
parsed.maxInjectedChars = Math.floor(raw.maxInjectedChars);
250-
}
251-
if (raw?.timeoutMs && raw.timeoutMs > 0) {
252-
parsed.timeoutMs = Math.floor(raw.timeoutMs);
253-
}
254-
return parsed;
250+
return {
251+
maxResults: resolvePositiveIntegerConfig(raw?.maxResults, DEFAULT_QMD_LIMITS.maxResults),
252+
maxSnippetChars: resolvePositiveIntegerConfig(
253+
raw?.maxSnippetChars,
254+
DEFAULT_QMD_LIMITS.maxSnippetChars,
255+
),
256+
maxInjectedChars: resolvePositiveIntegerConfig(
257+
raw?.maxInjectedChars,
258+
DEFAULT_QMD_LIMITS.maxInjectedChars,
259+
),
260+
timeoutMs: resolvePositiveIntegerConfig(raw?.timeoutMs, DEFAULT_QMD_LIMITS.timeoutMs),
261+
};
255262
}
256263

257264
function resolveSearchMode(raw?: MemoryQmdConfig["searchMode"]): MemoryQmdSearchMode {
@@ -273,8 +280,7 @@ function resolveSessionConfig(
273280
const enabled = Boolean(cfg?.enabled);
274281
const exportDirRaw = cfg?.exportDir?.trim();
275282
const exportDir = exportDirRaw ? resolvePath(exportDirRaw, workspaceDir) : undefined;
276-
const retentionDays =
277-
cfg?.retentionDays && cfg.retentionDays > 0 ? Math.floor(cfg.retentionDays) : undefined;
283+
const retentionDays = resolvePositiveIntegerConfig(cfg?.retentionDays);
278284
return {
279285
enabled,
280286
exportDir,

scripts/qa-otel-smoke.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ const DISALLOWED_ATTRIBUTE_KEYS = new Set([
112112
]);
113113
const DISALLOWED_BODY_NEEDLES = ["OTEL-QA-SECRET", "OTEL-QA-OK"];
114114
const COLLECTOR_OUTPUT_TAIL_BYTES = 16_000;
115+
const POSITIVE_INTEGER_PATTERN = /^[1-9]\d*$/u;
115116
const MAX_OTLP_COMPRESSED_BODY_BYTES = readPositiveIntegerEnv(
116117
"OPENCLAW_QA_OTEL_MAX_COMPRESSED_BODY_BYTES",
117118
2 * 1024 * 1024,
@@ -125,9 +126,24 @@ const MAX_CAPTURED_BODY_TEXT_BYTES = readPositiveIntegerEnv(
125126
512 * 1024,
126127
);
127128

128-
function readPositiveIntegerEnv(name: string, fallback: number): number {
129-
const parsed = Number.parseInt(process.env[name] ?? "", 10);
130-
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
129+
function readPositiveIntegerEnv(
130+
name: string,
131+
fallback: number,
132+
env: NodeJS.ProcessEnv = process.env,
133+
): number {
134+
const raw = env[name];
135+
if (raw == null || raw.trim() === "") {
136+
return fallback;
137+
}
138+
const value = raw.trim();
139+
if (!POSITIVE_INTEGER_PATTERN.test(value)) {
140+
throw new Error(`${name} must be a positive integer`);
141+
}
142+
const parsed = Number(value);
143+
if (!Number.isSafeInteger(parsed)) {
144+
throw new Error(`${name} must be a safe integer`);
145+
}
146+
return parsed;
131147
}
132148

133149
function oversizedBodyError(
@@ -1318,6 +1334,7 @@ async function main() {
13181334
export const testing = {
13191335
appendCapturedBodyText,
13201336
decodeRequestBody,
1337+
readPositiveIntegerEnv,
13211338
readRequestBody,
13221339
};
13231340

0 commit comments

Comments
 (0)