mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-19 09:01:39 -06:00
chore: enforce type evidence lint rules
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["unicorn", "typescript", "oxc"],
|
||||
"jsPlugins": ["./scripts/oxlint-type-evidence.mjs"],
|
||||
"categories": {
|
||||
"correctness": "error",
|
||||
"perf": "error",
|
||||
@@ -66,6 +67,8 @@
|
||||
"oxc/no-accumulating-spread": "error",
|
||||
"oxc/no-async-endpoint-handlers": "error",
|
||||
"oxc/no-map-spread": "error",
|
||||
"openclaw-type-evidence/no-unknown-type-aliases": "error",
|
||||
"openclaw-type-evidence/no-widen-then-assert": "error",
|
||||
"promise/no-callback-in-promise": "error",
|
||||
"promise/no-multiple-resolved": "error",
|
||||
"promise/no-promise-in-callback": "error",
|
||||
@@ -213,6 +216,7 @@
|
||||
// Intentional negative-test corpora contain parser and lint violations by contract.
|
||||
".agents/skills/autoreview/tests/fixtures/**",
|
||||
"test/fixtures/oxlint-boundary-guards/**",
|
||||
"test/fixtures/oxlint-type-evidence/**",
|
||||
"**/a2ui.bundle.js",
|
||||
"extensions/diffs/assets/viewer-runtime.js",
|
||||
"extensions/diffs-language-pack/assets/viewer-runtime.js",
|
||||
@@ -274,6 +278,12 @@
|
||||
"typescript/no-explicit-any": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/*.d.ts"],
|
||||
"rules": {
|
||||
"openclaw-type-evidence/no-unknown-type-aliases": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"src/**/*.{js,ts,mts,cts}",
|
||||
|
||||
@@ -35,3 +35,34 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## anti-slop
|
||||
|
||||
OpenClaw's type-evidence Oxlint rules were adapted from anti-slop.
|
||||
|
||||
- Upstream: https://github.com/dmmulroy/anti-slop
|
||||
- Source revision: abaeb63b29e63062f778771d5447bd2e9c3c680f
|
||||
- License: MIT
|
||||
- Copyright: Copyright (c) 2026 Dillon Mulroy
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Dillon Mulroy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -69,6 +69,7 @@ const ROOT_TEST_ENTRY_GLOBS = [
|
||||
"test/fixtures/ts-topology/basic/**/*.{js,mjs,cjs,ts,mts,cts}!",
|
||||
// The focused Oxlint test invokes these deliberate violations by path.
|
||||
"test/fixtures/oxlint-boundary-guards/*.ts!",
|
||||
"test/fixtures/oxlint-type-evidence/*.ts!",
|
||||
] as const;
|
||||
|
||||
const workspaces = Object.fromEntries(
|
||||
|
||||
@@ -81,6 +81,8 @@ const repositoryScriptEntries = [
|
||||
"scripts/openclaw-release-clawhub-runtime-state.ts!",
|
||||
// Oxlint loads this JS plugin by path from config/oxlint/boundary-guards.json.
|
||||
"scripts/oxlint-boundary-guards.mjs!",
|
||||
// Oxlint loads this JS plugin by path from the root and focused lint configs.
|
||||
"scripts/oxlint-type-evidence.mjs!",
|
||||
"scripts/plugin-prerelease-liveish-matrix.mts!",
|
||||
// Generates the checked-in native protocol models from core descriptor metadata.
|
||||
"scripts/protocol-gen.ts!",
|
||||
|
||||
@@ -58,6 +58,7 @@ const config = {
|
||||
],
|
||||
// Oxlint consumes this required default export through a JSON config path.
|
||||
"scripts/oxlint-boundary-guards.mjs": ["exports"],
|
||||
"scripts/oxlint-type-evidence.mjs": ["exports"],
|
||||
// Wrangler consumes the Worker default export and instantiates the Durable
|
||||
// Object class by name from wrangler.jsonc; Knip cannot resolve either.
|
||||
"scripts/cloudflare/src/index.ts": ["exports"],
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "../../node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": [],
|
||||
"jsPlugins": ["../../scripts/oxlint-type-evidence.mjs"],
|
||||
"categories": { "correctness": "off" },
|
||||
"rules": {
|
||||
"openclaw-type-evidence/no-unknown-type-aliases": "error",
|
||||
"openclaw-type-evidence/no-widen-then-assert": "error"
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"e2a5ce75757e7a275312c91ce95bdb4f13091a3c613dd4c66fdda0b2e4d3bf83","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
|
||||
{"contentHash":"f95093ed8782337b995c648a13f460ecddba40b11431d1eba6a9781804a0e99d","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"0e4494129d83b508c82788383f8dfead578976ed474be2cfbec557aa6caf88e7","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
|
||||
{"contentHash":"afcb925bae1788d479e61faf6da9b1f4255d133cd42f387fc31555890fc3cfc1","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"8643a3553553855a519e825fcaa483024ecd3538675c5a2d474af841fb5070d7","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
|
||||
{"contentHash":"9cac147d4e5170745d298b867ca8c6ffb0513b940cf1d26a62b46a6cceb10852","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"13ec7276e7c37ae4e7589e67302c2bf531674f2c439b70d4a8a44a4ee136f685","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
|
||||
{"contentHash":"4cff7cdf2e9a2818661157b37c6d14dd020f527e00b90fe4452c3aa8281ef16e","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"ed0a5bf19bbe6e9641c8de12ea94105865850343c319a58953a2dce21e6d3f49","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"}
|
||||
{"contentHash":"b4d9b4733b97ce9fa39e03b83b5c4c7b169e2992a2f1f75dcf052cc4c3c294c5","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"feb6187469b8f0e88c0445ba025cc557c94871db55f39075eb63044d8ba48cd7","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
|
||||
{"contentHash":"d832c4bc18c7995c6971bbabf9cd742a01656278e8b51ab4d63cd7322f545a88","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"97d39817d24eaf5b2808960770447188daa0b2959514efbd44db007797e40e3b","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
|
||||
{"contentHash":"8211226bd9a2b20b486cf7581e65f116c30063b3971a359cade2921a6a54d6f8","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"0d9fc3f9ef06aaa9995c5a670d62b99cfce25f0b2c42152aa33214f9dec78b19","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing"}
|
||||
{"contentHash":"0186f09bddb33bf9dbe6c2874d3b0e4704d8ce8d055abc694a1fe93f3e5cb980","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"e824840c22ccade58891ad2952a6aea4aafa8c163adb4383e9a5aa8b47cce44b","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
|
||||
{"contentHash":"c89bfe63fcec8e912673caa0995ed2b0de7a624c385f22c24c061239cbbe12bb","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"08bba8059fb44659737936c0ef4c163cfff29872db3088b72d2fff82f4c4ac85","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
|
||||
{"contentHash":"ece426ee0e06d143d79b9a761743e26564cff5562d6bff95c3467da7387f0518","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"db6ac3119377920f375bdb1cd01082815022f2ad49de3dc5d7ac258a6a864ca6","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
|
||||
{"contentHash":"75e88b7733d802d1119709f0a88ab2e2a28d9ec95c42e3872bd3a065b8f9a663","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"7bb8bb13f101a7018aea253debd9c3611635541b8ad2d847e827300b9f94991f","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
|
||||
{"contentHash":"d75dd7f78180cebeebd788eec6d738178e0e9a89fef7904ff3807ec35d690cc5","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"e5c950c85caf1fe7edf4d672f95afc074e926e300981d96707ed904304e001b4","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
|
||||
{"contentHash":"d12d25f6cc1177632afe16e110dab1a12f65db41382d3507b99c5eba88fa55b4","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"5bb2b4560b715a99bb635510b031b4350b9af16ca4c9247c6d28b54753ef1b7a","entrypoint":"plugin-command-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime"}
|
||||
{"contentHash":"d25a630bd5eec03aadda39b73e4b6b8ef1ba636b2c98afb5e3a52d07f82448bb","entrypoint":"plugin-command-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"f6867c932f2632409f933d8e158d01f303679950b4e6e3a01ba28b0ea8b4d7da","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
|
||||
{"contentHash":"ba3e6b379ababd8130f7e600b6860ce07f7121dbc104712a97805c2f3da3e510","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"53a44cf7b1b8866054e905dd7a34623e75dbf4be6da5b58e7d47fd638e2a92f5","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
|
||||
{"contentHash":"e72b069106dbe98a76e548f1af7e3af244f381e2d6618dd069320a44e966608d","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"03aeade34faf6a1c6bfee3e88f684ccb4abe657ea8f3475c96162698baeeb314","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
|
||||
{"contentHash":"aa2a15aa436ec9cb463a2521ded8cfd92225b441649445a0b1f2deb5f9f7a034","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"b965caa74f02ea0fa0b4b5a57bfacd0699df991452b5aa95f21237858930508f","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store"}
|
||||
{"contentHash":"10169d7b8325ccc0de4d24d3d17aeefc689b9508347b2a8698e74d90e54857ae","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"1c76ac2e6a40a56ae0dbf5f6e2b5d911883f8e11cc7c0f435f6cce0c185a60c2","entrypoint":"session-catalog","importSpecifier":"openclaw/plugin-sdk/session-catalog"}
|
||||
{"contentHash":"016903fae0d5b0858eca5310dd81a38005d2de05343f9e058971a8ca4eaf25e4","entrypoint":"session-catalog","importSpecifier":"openclaw/plugin-sdk/session-catalog"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"96150841a6e550228fcf0310e0aaf56ebd75b53ad9c5eb2d0557bbb8a5b67290","entrypoint":"session-store-runtime","importSpecifier":"openclaw/plugin-sdk/session-store-runtime"}
|
||||
{"contentHash":"42b029dd356f0bb03525f66bb7eb62dd3a2d02aaf72abafd47967d431c625c9b","entrypoint":"session-store-runtime","importSpecifier":"openclaw/plugin-sdk/session-store-runtime"}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"contentHash":"83795fac5e17a90840255de8446aaf23b988a14d19bc76a327b0b74ef5f9ee8a","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
|
||||
{"contentHash":"be52565eb108d7328f563882b5bcc26e03496bb3b5ef88879976f03f41b1a3bf","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"contentHash":"9511576a6c8d6631cbe81dc7aca200f670feaf7fc03d6b3308066b5caa3ab50c","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
|
||||
{"contentHash":"4a7e8f3791e029e4ecb8aaa9750190dde04678c5164e151ec2643d677f2627c9","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
|
||||
|
||||
@@ -8,14 +8,12 @@ import {
|
||||
installPwToolsCoreTestHooks();
|
||||
const mod = await import("./pw-tools-core.interactions.js");
|
||||
|
||||
type EvaluateArg = unknown;
|
||||
|
||||
function evaluateMockReturning(view: { x: number; y: number; width?: number; height?: number }) {
|
||||
// Caller reads { x, y, width, height } in one evaluate; default to a normal
|
||||
// desktop viewport so refs near the top stay in-viewport unless a test puts
|
||||
// them out of range explicitly.
|
||||
const result = { width: 1280, height: 720, ...view };
|
||||
return vi.fn(async (arg: EvaluateArg) => {
|
||||
return vi.fn(async (arg: unknown) => {
|
||||
if (typeof arg === "function") {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
resolveMemoryLightDreamingConfig,
|
||||
resolveMemoryRemDreamingConfig,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-status";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { appendFailedDreamingEvent } from "./dreaming-events.js";
|
||||
import {
|
||||
@@ -67,7 +67,6 @@ import {
|
||||
} from "./short-term-promotion.js";
|
||||
|
||||
type Logger = Pick<OpenClawPluginApi["logger"], "info" | "warn" | "error">;
|
||||
type DreamingHostConfig = unknown;
|
||||
type DreamingPhaseStorageConfig = {
|
||||
timezone?: string;
|
||||
storage: { mode: "inline" | "separate" | "both"; separateReports: boolean };
|
||||
@@ -557,7 +556,7 @@ function isCheckpointSessionTranscriptPath(absolutePath: string): boolean {
|
||||
}
|
||||
|
||||
function resolveSessionAgentsForWorkspace(params: {
|
||||
cfg: DreamingHostConfig;
|
||||
cfg: OpenClawConfig;
|
||||
workspaceDir: string;
|
||||
primaryWorkspaceDir?: string;
|
||||
}): string[] {
|
||||
@@ -566,13 +565,10 @@ function resolveSessionAgentsForWorkspace(params: {
|
||||
return [];
|
||||
}
|
||||
const target = normalizeMemoryCoreWorkspaceKey(workspaceDir);
|
||||
const workspaces = resolveMemoryDreamingWorkspaces(
|
||||
cfg as Parameters<typeof resolveMemoryDreamingWorkspaces>[0],
|
||||
{
|
||||
primaryWorkspaceDir,
|
||||
primaryAgentId: "main",
|
||||
},
|
||||
);
|
||||
const workspaces = resolveMemoryDreamingWorkspaces(cfg, {
|
||||
primaryWorkspaceDir,
|
||||
primaryAgentId: "main",
|
||||
});
|
||||
const match = workspaces.find(
|
||||
(entry) => normalizeMemoryCoreWorkspaceKey(entry.workspaceDir) === target,
|
||||
);
|
||||
@@ -584,7 +580,7 @@ function resolveSessionAgentsForWorkspace(params: {
|
||||
|
||||
async function collectSessionIngestionBatches(params: {
|
||||
workspaceDir: string;
|
||||
cfg?: DreamingHostConfig;
|
||||
cfg?: OpenClawConfig;
|
||||
primaryWorkspaceDir?: string;
|
||||
lookbackDays: number;
|
||||
nowMs: number;
|
||||
@@ -719,7 +715,7 @@ async function collectSessionIngestionBatches(params: {
|
||||
|
||||
async function ingestSessionTranscriptSignals(params: {
|
||||
workspaceDir: string;
|
||||
cfg?: DreamingHostConfig;
|
||||
cfg?: OpenClawConfig;
|
||||
primaryWorkspaceDir?: string;
|
||||
lookbackDays: number;
|
||||
nowMs: number;
|
||||
@@ -1286,7 +1282,7 @@ export function previewRemDreaming(params: {
|
||||
async function runLightDreaming(params: {
|
||||
agentId?: string;
|
||||
workspaceDir: string;
|
||||
cfg?: DreamingHostConfig;
|
||||
cfg?: OpenClawConfig;
|
||||
primaryWorkspaceDir?: string;
|
||||
config: LightDreamingConfig;
|
||||
logger: Logger;
|
||||
@@ -1386,7 +1382,7 @@ async function runLightDreaming(params: {
|
||||
async function runRemDreaming(params: {
|
||||
agentId?: string;
|
||||
workspaceDir: string;
|
||||
cfg?: DreamingHostConfig;
|
||||
cfg?: OpenClawConfig;
|
||||
primaryWorkspaceDir?: string;
|
||||
config: RemDreamingConfig;
|
||||
logger: Logger;
|
||||
@@ -1504,7 +1500,7 @@ export async function runDreamingSweepPhases(params: {
|
||||
agentId?: string;
|
||||
workspaceDir: string;
|
||||
pluginConfig?: Record<string, unknown>;
|
||||
cfg?: DreamingHostConfig;
|
||||
cfg?: OpenClawConfig;
|
||||
logger: Logger;
|
||||
subagent?: DreamNarrativeRequest["subagent"];
|
||||
detachNarratives?: boolean;
|
||||
@@ -1524,7 +1520,7 @@ export async function runDreamingSweepPhases(params: {
|
||||
|
||||
const light = resolveMemoryLightDreamingConfig({
|
||||
pluginConfig: params.pluginConfig,
|
||||
cfg: params.cfg as Parameters<typeof resolveMemoryLightDreamingConfig>[0]["cfg"],
|
||||
cfg: params.cfg,
|
||||
});
|
||||
if (light.enabled && light.limit > 0) {
|
||||
try {
|
||||
@@ -1555,7 +1551,7 @@ export async function runDreamingSweepPhases(params: {
|
||||
|
||||
const rem = resolveMemoryRemDreamingConfig({
|
||||
pluginConfig: params.pluginConfig,
|
||||
cfg: params.cfg as Parameters<typeof resolveMemoryRemDreamingConfig>[0]["cfg"],
|
||||
cfg: params.cfg,
|
||||
});
|
||||
if (rem.enabled && rem.limit > 0) {
|
||||
try {
|
||||
|
||||
@@ -195,11 +195,11 @@ describe("searxng web search provider", () => {
|
||||
|
||||
it("persists base URL to plugin config via setConfiguredCredentialValue", () => {
|
||||
const provider = createSearxngWebSearchProvider();
|
||||
const config = {} as Record<string, unknown>;
|
||||
const setConfiguredCredentialValue = provider.setConfiguredCredentialValue;
|
||||
if (!setConfiguredCredentialValue) {
|
||||
throw new Error("Expected SearXNG provider setConfiguredCredentialValue");
|
||||
}
|
||||
const config: Parameters<typeof setConfiguredCredentialValue>[0] = {};
|
||||
|
||||
setConfiguredCredentialValue(config, "http://search.local:9000");
|
||||
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
// Adapted from dmmulroy/anti-slop at abaeb63b29e63062f778771d5447bd2e9c3c680f.
|
||||
// See THIRD_PARTY_NOTICES.md for the upstream MIT license.
|
||||
|
||||
const functionBoundaryTypes = new Set([
|
||||
"ArrowFunctionExpression",
|
||||
"FunctionDeclaration",
|
||||
"FunctionExpression",
|
||||
"TSDeclareFunction",
|
||||
"TSEmptyBodyFunctionExpression",
|
||||
]);
|
||||
|
||||
function unwrapExpressionParentheses(expression) {
|
||||
let current = expression;
|
||||
while (current.type === "ParenthesizedExpression") {
|
||||
current = current.expression;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function unwrapTypeParentheses(type) {
|
||||
let current = type;
|
||||
while (current.type === "TSParenthesizedType") {
|
||||
current = current.typeAnnotation;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function typeReferenceName(type) {
|
||||
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
||||
}
|
||||
|
||||
function isUnknownOrAnyType(type) {
|
||||
const unwrapped = unwrapTypeParentheses(type);
|
||||
return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
|
||||
}
|
||||
|
||||
function isBroadRecordKeyType(type) {
|
||||
const unwrapped = unwrapTypeParentheses(type);
|
||||
if (
|
||||
unwrapped.type === "TSStringKeyword" ||
|
||||
unwrapped.type === "TSNumberKeyword" ||
|
||||
unwrapped.type === "TSSymbolKeyword"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (unwrapped.type === "TSUnionType") {
|
||||
return unwrapped.types.every(isBroadRecordKeyType);
|
||||
}
|
||||
return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
|
||||
}
|
||||
|
||||
function isBroadRecordType(type) {
|
||||
const unwrapped = unwrapTypeParentheses(type);
|
||||
|
||||
if (unwrapped.type === "TSTypeReference") {
|
||||
if (typeReferenceName(unwrapped) === "Readonly") {
|
||||
const [inner] = unwrapped.typeArguments?.params ?? [];
|
||||
return inner !== undefined && isBroadRecordType(inner);
|
||||
}
|
||||
|
||||
if (typeReferenceName(unwrapped) !== "Record") {
|
||||
return false;
|
||||
}
|
||||
const parameters = unwrapped.typeArguments?.params ?? [];
|
||||
return (
|
||||
parameters.length === 2 &&
|
||||
parameters[0] !== undefined &&
|
||||
parameters[1] !== undefined &&
|
||||
isBroadRecordKeyType(parameters[0]) &&
|
||||
isUnknownOrAnyType(parameters[1])
|
||||
);
|
||||
}
|
||||
|
||||
if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
const [member] = unwrapped.members;
|
||||
const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
|
||||
return (
|
||||
member?.type === "TSIndexSignature" &&
|
||||
member.parameters.length === 1 &&
|
||||
parameter !== undefined &&
|
||||
isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) &&
|
||||
isUnknownOrAnyType(member.typeAnnotation.typeAnnotation)
|
||||
);
|
||||
}
|
||||
|
||||
function broadTypeKind(type) {
|
||||
const unwrapped = unwrapTypeParentheses(type);
|
||||
if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") {
|
||||
return "top";
|
||||
}
|
||||
if (unwrapped.type === "TSObjectKeyword") {
|
||||
return "object";
|
||||
}
|
||||
return isBroadRecordType(unwrapped) ? "record" : null;
|
||||
}
|
||||
|
||||
function assertedExpression(node) {
|
||||
return unwrapExpressionParentheses(node.expression);
|
||||
}
|
||||
|
||||
function assertionFromExpression(expression) {
|
||||
const unwrapped = unwrapExpressionParentheses(expression);
|
||||
return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion"
|
||||
? unwrapped
|
||||
: null;
|
||||
}
|
||||
|
||||
function normalizedTypeText(sourceText, type) {
|
||||
return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
|
||||
}
|
||||
|
||||
function typesHaveSameSyntax(sourceText, left, right) {
|
||||
return (
|
||||
left !== null &&
|
||||
normalizedTypeText(sourceText, unwrapTypeParentheses(left)) ===
|
||||
normalizedTypeText(sourceText, unwrapTypeParentheses(right))
|
||||
);
|
||||
}
|
||||
|
||||
function isDefinitelyObjectType(type) {
|
||||
const unwrapped = unwrapTypeParentheses(type);
|
||||
switch (unwrapped.type) {
|
||||
case "TSArrayType":
|
||||
case "TSConstructorType":
|
||||
case "TSFunctionType":
|
||||
case "TSMappedType":
|
||||
case "TSObjectKeyword":
|
||||
case "TSTupleType":
|
||||
return true;
|
||||
case "TSTypeLiteral":
|
||||
return unwrapped.members.length > 0;
|
||||
case "TSIntersectionType":
|
||||
return unwrapped.types.every(isDefinitelyObjectType);
|
||||
case "TSTypeOperator":
|
||||
return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isDefinitelyNarrowerRecordType(type) {
|
||||
const unwrapped = unwrapTypeParentheses(type);
|
||||
if (unwrapped.type === "TSTypeLiteral") {
|
||||
return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
|
||||
}
|
||||
|
||||
if (unwrapped.type !== "TSTypeReference") {
|
||||
return false;
|
||||
}
|
||||
if (typeReferenceName(unwrapped) === "Readonly") {
|
||||
const [inner] = unwrapped.typeArguments?.params ?? [];
|
||||
return inner !== undefined && isDefinitelyNarrowerRecordType(inner);
|
||||
}
|
||||
if (typeReferenceName(unwrapped) !== "Record") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parameters = unwrapped.typeArguments?.params ?? [];
|
||||
return (
|
||||
parameters.length === 2 && parameters[1] !== undefined && !isUnknownOrAnyType(parameters[1])
|
||||
);
|
||||
}
|
||||
|
||||
function functionBoundary(node) {
|
||||
let current = node.parent;
|
||||
while (current !== null && current.type !== "Program") {
|
||||
if (functionBoundaryTypes.has(current.type)) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvedVariableForIdentifier(scopes, identifier) {
|
||||
for (const scope of scopes) {
|
||||
const reference = scope.references.find(
|
||||
(candidate) =>
|
||||
candidate.identifier.start === identifier.start &&
|
||||
candidate.identifier.end === identifier.end,
|
||||
);
|
||||
if (reference !== undefined) {
|
||||
return reference.resolved;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function variableDeclarator(variable) {
|
||||
for (const definition of variable.defs) {
|
||||
if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") {
|
||||
return definition.node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function knownValueEvidence(expression, scopes, boundary, visitedVariables) {
|
||||
const unwrapped = unwrapExpressionParentheses(expression);
|
||||
|
||||
if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
|
||||
if (broadTypeKind(unwrapped.typeAnnotation) !== null) {
|
||||
return null;
|
||||
}
|
||||
return { type: unwrapped.typeAnnotation };
|
||||
}
|
||||
|
||||
if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") {
|
||||
return { type: null };
|
||||
}
|
||||
|
||||
if (
|
||||
unwrapped.type === "ArrayExpression" ||
|
||||
unwrapped.type === "ArrowFunctionExpression" ||
|
||||
unwrapped.type === "ClassExpression" ||
|
||||
unwrapped.type === "FunctionExpression" ||
|
||||
unwrapped.type === "NewExpression" ||
|
||||
unwrapped.type === "ObjectExpression"
|
||||
) {
|
||||
return { type: null };
|
||||
}
|
||||
|
||||
if (unwrapped.type !== "Identifier") {
|
||||
return null;
|
||||
}
|
||||
const variable = resolvedVariableForIdentifier(scopes, unwrapped);
|
||||
if (variable === null || visitedVariables.has(variable)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const annotatedIdentifier = variable.identifiers.find(
|
||||
(identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== undefined,
|
||||
);
|
||||
const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
|
||||
if (annotation !== undefined && annotatedIdentifier !== undefined) {
|
||||
if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) {
|
||||
return null;
|
||||
}
|
||||
return { type: annotation };
|
||||
}
|
||||
|
||||
const declarator = variableDeclarator(variable);
|
||||
if (
|
||||
declarator === null ||
|
||||
declarator.parent.type !== "VariableDeclaration" ||
|
||||
declarator.parent.kind !== "const" ||
|
||||
declarator.init === null ||
|
||||
variable.references.some((reference) => reference.isWrite() && !reference.init) ||
|
||||
functionBoundary(declarator) !== boundary
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return knownValueEvidence(
|
||||
declarator.init,
|
||||
scopes,
|
||||
boundary,
|
||||
new Set([...visitedVariables, variable]),
|
||||
);
|
||||
}
|
||||
|
||||
function widenedBinding(variable, scopes) {
|
||||
const declarator = variableDeclarator(variable);
|
||||
if (
|
||||
declarator === null ||
|
||||
declarator.parent.type !== "VariableDeclaration" ||
|
||||
declarator.parent.kind !== "const" ||
|
||||
declarator.id.type !== "Identifier" ||
|
||||
declarator.init === null ||
|
||||
variable.references.some((reference) => reference.isWrite() && !reference.init)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const boundary = functionBoundary(declarator);
|
||||
const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
|
||||
const initializerAssertion = assertionFromExpression(declarator.init);
|
||||
const initializerBroadKind =
|
||||
initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
|
||||
const declaredBroadKind = declaredType === undefined ? null : broadTypeKind(declaredType);
|
||||
const broadKind = declaredBroadKind ?? initializerBroadKind;
|
||||
if (broadKind === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const originalExpression =
|
||||
initializerAssertion !== null && initializerBroadKind !== null
|
||||
? assertedExpression(initializerAssertion)
|
||||
: declarator.init;
|
||||
const evidence = knownValueEvidence(originalExpression, scopes, boundary, new Set([variable]));
|
||||
return evidence === null ? null : { broadKind, evidence, declaredAt: declarator.end, boundary };
|
||||
}
|
||||
|
||||
function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
|
||||
if (broadTypeKind(assertedType) !== null) {
|
||||
return false;
|
||||
}
|
||||
if (broadKind === "top") {
|
||||
return true;
|
||||
}
|
||||
if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) {
|
||||
return true;
|
||||
}
|
||||
if (broadKind === "object") {
|
||||
return isDefinitelyObjectType(assertedType);
|
||||
}
|
||||
return isDefinitelyNarrowerRecordType(assertedType);
|
||||
}
|
||||
|
||||
const noUnknownTypeAliasesRule = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary.",
|
||||
},
|
||||
messages: {
|
||||
unknownAlias:
|
||||
"Type alias `{{alias}}` only renames `unknown`. Keep `unknown` explicit at the boundary or replace it with the parsed owner type.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const aliases = new Map();
|
||||
|
||||
const resolvesToUnknown = (type, visited = new Set()) => {
|
||||
if (type.type === "TSUnknownKeyword") {
|
||||
return true;
|
||||
}
|
||||
if (type.type === "TSParenthesizedType") {
|
||||
return resolvesToUnknown(type.typeAnnotation, visited);
|
||||
}
|
||||
const name =
|
||||
type.type === "TSTypeReference" &&
|
||||
type.typeName.type === "Identifier" &&
|
||||
(type.typeArguments === null ||
|
||||
type.typeArguments === undefined ||
|
||||
type.typeArguments.params.length === 0)
|
||||
? type.typeName.name
|
||||
: null;
|
||||
if (name === null || visited.has(name)) {
|
||||
return false;
|
||||
}
|
||||
const alias = aliases.get(name);
|
||||
if (
|
||||
alias === undefined ||
|
||||
(alias.typeParameters !== null && alias.typeParameters !== undefined)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const nextVisited = new Set(visited);
|
||||
nextVisited.add(name);
|
||||
return resolvesToUnknown(alias.typeAnnotation, nextVisited);
|
||||
};
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
for (const statement of node.body) {
|
||||
const declaration =
|
||||
statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
||||
if (declaration?.type === "TSTypeAliasDeclaration") {
|
||||
aliases.set(declaration.id.name, declaration);
|
||||
}
|
||||
}
|
||||
for (const alias of aliases.values()) {
|
||||
if (!resolvesToUnknown(alias.typeAnnotation, new Set([alias.id.name]))) {
|
||||
continue;
|
||||
}
|
||||
context.report({
|
||||
node: alias.id,
|
||||
messageId: "unknownAlias",
|
||||
data: { alias: alias.id.name },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const noWidenThenAssertRule = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.",
|
||||
},
|
||||
messages: {
|
||||
widenThenAssert:
|
||||
'Binding "{{name}}" erases established type evidence by widening the value, then reconstructs that evidence with a type assertion. Preserve the precise type end-to-end; if the input is genuinely unknown, parse it once at the boundary instead.',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const scopes = context.sourceCode.scopeManager.scopes;
|
||||
|
||||
const checkAssertion = (node) => {
|
||||
const expression = assertedExpression(node);
|
||||
if (expression.type !== "Identifier") {
|
||||
return;
|
||||
}
|
||||
|
||||
const variable = resolvedVariableForIdentifier(scopes, expression);
|
||||
if (variable === null) {
|
||||
return;
|
||||
}
|
||||
const widened = widenedBinding(variable, scopes);
|
||||
if (
|
||||
widened === null ||
|
||||
node.start <= widened.declaredAt ||
|
||||
functionBoundary(node) !== widened.boundary ||
|
||||
!assertionIsNarrower(
|
||||
context.sourceCode.text,
|
||||
widened.broadKind,
|
||||
widened.evidence,
|
||||
node.typeAnnotation,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "widenThenAssert",
|
||||
data: { name: expression.name },
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
TSAsExpression: checkAssertion,
|
||||
TSTypeAssertion: checkAssertion,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
meta: { name: "openclaw-type-evidence" },
|
||||
rules: {
|
||||
"no-unknown-type-aliases": noUnknownTypeAliasesRule,
|
||||
"no-widen-then-assert": noWidenThenAssertRule,
|
||||
},
|
||||
};
|
||||
@@ -512,9 +512,9 @@ export function wrapStreamFnSanitizeMalformedToolCalls(
|
||||
}
|
||||
}
|
||||
const nextContext = {
|
||||
...(context as unknown as Record<string, unknown>),
|
||||
messages: nextMessages,
|
||||
} as unknown;
|
||||
return baseFn(model, nextContext as typeof context, options);
|
||||
...context,
|
||||
messages: nextMessages as typeof context.messages,
|
||||
};
|
||||
return baseFn(model, nextContext, options);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
/**
|
||||
* Builds prepared runtime plans consumed by embedded agent runs. A plan
|
||||
* centralizes provider hooks, auth, tool schema policy, transcript policy,
|
||||
@@ -46,10 +45,6 @@ function formatResolvedRef(params: { provider: string; modelId: string }): strin
|
||||
return `${params.provider}/${params.modelId}`;
|
||||
}
|
||||
|
||||
function asOpenClawConfig(value: unknown): OpenClawConfig | undefined {
|
||||
return asOptionalRecord(value) as OpenClawConfig | undefined;
|
||||
}
|
||||
|
||||
function asProviderRuntimeModel(
|
||||
value: BuildAgentRuntimePlanParams["model"],
|
||||
): ProviderRuntimeModel | undefined {
|
||||
@@ -62,7 +57,7 @@ type RuntimePlanMetadataParams = BuildAgentRuntimeDeliveryPlanParams & {
|
||||
|
||||
function resolveCompatibleMetadataSnapshot(
|
||||
params: RuntimePlanMetadataParams,
|
||||
config: OpenClawConfig | undefined = asOpenClawConfig(params.config),
|
||||
config: OpenClawConfig | undefined = params.config,
|
||||
): PluginMetadataSnapshot | undefined {
|
||||
const metadataSnapshot = params.metadataSnapshot as PluginMetadataSnapshot | undefined;
|
||||
return metadataSnapshot &&
|
||||
@@ -96,7 +91,7 @@ function resolvePreparedProviderRuntimeHandle(
|
||||
...resolveProviderRuntimePluginHandle({
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
config: asOpenClawConfig(params.config),
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: process.env,
|
||||
...(compatibleMetadataSnapshot ? { pluginMetadataSnapshot: compatibleMetadataSnapshot } : {}),
|
||||
@@ -110,7 +105,7 @@ function resolvePreparedProviderRuntimeHandle(
|
||||
export function buildAgentRuntimeDeliveryPlan(
|
||||
params: BuildAgentRuntimeDeliveryPlanParams,
|
||||
): AgentRuntimeDeliveryPlan {
|
||||
const config = asOpenClawConfig(params.config);
|
||||
const config = params.config;
|
||||
const providerRuntimeHandle = resolvePreparedProviderRuntimeHandle(params);
|
||||
return {
|
||||
isSilentPayload(payload): boolean {
|
||||
@@ -151,7 +146,7 @@ function buildAgentRuntimeOutcomePlan(): AgentRuntimeOutcomePlan {
|
||||
|
||||
/** Build the complete runtime plan for an embedded agent attempt. */
|
||||
export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): AgentRuntimePlan {
|
||||
const config = asOpenClawConfig(params.config);
|
||||
const config = params.config;
|
||||
const model = asProviderRuntimeModel(params.model);
|
||||
const modelApi = params.modelApi ?? params.model?.api ?? undefined;
|
||||
const transport = params.resolvedTransport;
|
||||
@@ -282,7 +277,7 @@ export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): Agen
|
||||
runtimeHandle: providerRuntimeHandleForPlugins,
|
||||
context: {
|
||||
...context,
|
||||
config: asOpenClawConfig(context.config),
|
||||
config: context.config,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -294,7 +289,7 @@ export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): Agen
|
||||
runtimeHandle: providerRuntimeHandleForPlugins,
|
||||
context: {
|
||||
...context,
|
||||
config: asOpenClawConfig(context.config),
|
||||
config: context.config,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* observability decisions shared across embedded-agent hot paths.
|
||||
*/
|
||||
import type { TSchema } from "typebox";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { ModelPickerAction } from "../../interactive/payload.js";
|
||||
import type {
|
||||
ModelApi,
|
||||
@@ -51,9 +52,6 @@ type AgentRuntimeFailoverReason =
|
||||
| "unclassified"
|
||||
| "unknown";
|
||||
|
||||
/** Provider/runtime config object passed through plugin boundaries. */
|
||||
type AgentRuntimeConfig = unknown;
|
||||
|
||||
/** Provider model descriptor consumed by runtime-plan hooks. */
|
||||
type AgentRuntimeModel = {
|
||||
id?: string;
|
||||
@@ -91,7 +89,7 @@ type AgentRuntimeTextTransforms = {
|
||||
type AgentRuntimeProviderHandle = {
|
||||
provider: string;
|
||||
modelId?: string | null;
|
||||
config?: AgentRuntimeConfig;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
applyAutoEnable?: boolean;
|
||||
@@ -341,7 +339,7 @@ type AgentRuntimeSystemPromptContribution = {
|
||||
|
||||
/** Context passed when resolving provider system prompt contributions. */
|
||||
type AgentRuntimeSystemPromptContributionContext = {
|
||||
config?: AgentRuntimeConfig;
|
||||
config?: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
provider: string;
|
||||
@@ -566,7 +564,7 @@ export type AgentRuntimePlan = {
|
||||
|
||||
/** Inputs needed to build delivery-only runtime decisions. */
|
||||
export type BuildAgentRuntimeDeliveryPlanParams = {
|
||||
config?: AgentRuntimeConfig;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
agentDir?: string;
|
||||
provider: string;
|
||||
@@ -576,7 +574,7 @@ export type BuildAgentRuntimeDeliveryPlanParams = {
|
||||
|
||||
/** Inputs needed to build the full prepared runtime plan. */
|
||||
export type BuildAgentRuntimePlanParams = {
|
||||
config?: AgentRuntimeConfig;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
agentDir?: string;
|
||||
provider: string;
|
||||
|
||||
@@ -1086,8 +1086,6 @@ export interface ContextEventResult {
|
||||
messages?: AgentMessage[];
|
||||
}
|
||||
|
||||
type BeforeProviderRequestEventResult = unknown;
|
||||
|
||||
export interface ToolCallEventResult {
|
||||
/** Block tool execution. To modify arguments, mutate `event.input` in place instead. */
|
||||
block?: boolean;
|
||||
@@ -1225,7 +1223,7 @@ export interface ExtensionAPI {
|
||||
on(event: "context", handler: ExtensionHandler<ContextEvent, ContextEventResult>): void;
|
||||
on(
|
||||
event: "before_provider_request",
|
||||
handler: ExtensionHandler<BeforeProviderRequestEvent, BeforeProviderRequestEventResult>,
|
||||
handler: ExtensionHandler<BeforeProviderRequestEvent, unknown>,
|
||||
): void;
|
||||
on(event: "after_provider_response", handler: ExtensionHandler<AfterProviderResponseEvent>): void;
|
||||
on(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import type { TranscriptEvent } from "../../config/sessions/session-accessor.js";
|
||||
import { selectSessionTranscriptLeafControlledPath } from "../../config/sessions/transcript-tree.js";
|
||||
import { CURRENT_SESSION_VERSION } from "../../config/sessions/version.js";
|
||||
import { logWarn } from "../../logger.js";
|
||||
@@ -388,13 +389,13 @@ export function parseOpaqueLeafEntry(record: unknown):
|
||||
};
|
||||
}
|
||||
|
||||
export function partitionSessionFileEntries(entries: readonly FileEntry[]): {
|
||||
export function partitionSessionFileEntries(entries: readonly TranscriptEvent[]): {
|
||||
fileEntries: FileEntry[];
|
||||
opaqueEntries: Array<{ index: number; record: unknown }>;
|
||||
opaqueEntries: Array<{ index: number; record: TranscriptEvent }>;
|
||||
fileEntriesByOriginalIndex: Array<FileEntry | undefined>;
|
||||
} {
|
||||
const fileEntries: FileEntry[] = [];
|
||||
const opaqueEntries: Array<{ index: number; record: unknown }> = [];
|
||||
const opaqueEntries: Array<{ index: number; record: TranscriptEvent }> = [];
|
||||
const fileEntriesByOriginalIndex: Array<FileEntry | undefined> = [];
|
||||
const header = entries.find(
|
||||
(entry) => isRecord(entry) && entry.type === "session" && typeof entry.id === "string",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import {
|
||||
loadTranscriptEventsSync,
|
||||
replaceTranscriptEventsSync,
|
||||
type SessionTranscriptRuntimeTarget,
|
||||
type TranscriptEvent,
|
||||
} from "../../config/sessions/session-accessor.js";
|
||||
import { isSessionTranscriptSideAppendEntry } from "../../config/sessions/transcript-tree.js";
|
||||
import { CURRENT_SESSION_VERSION } from "../../config/sessions/version.js";
|
||||
@@ -58,19 +60,19 @@ export class SessionManagerCore {
|
||||
}
|
||||
|
||||
setSessionTarget(target: SessionManagerPersistenceTarget): void {
|
||||
const entries = loadTranscriptEventsSync(target) as FileEntry[];
|
||||
const entries = loadTranscriptEventsSync(target);
|
||||
const header = entries.find(
|
||||
(entry) => typeof entry === "object" && entry !== null && entry.type === "session",
|
||||
(entry): entry is Record<string, unknown> => isRecord(entry) && entry.type === "session",
|
||||
);
|
||||
this.setLoadedSessionTarget(target, entries);
|
||||
if (header?.cwd) {
|
||||
if (typeof header?.cwd === "string") {
|
||||
this.cwd = header.cwd;
|
||||
}
|
||||
}
|
||||
|
||||
protected setLoadedSessionTarget(
|
||||
target: SessionManagerPersistenceTarget | undefined,
|
||||
entries: FileEntry[],
|
||||
entries: TranscriptEvent[],
|
||||
): void {
|
||||
const partitioned = partitionSessionFileEntries(entries);
|
||||
// Only a physically empty transcript may initialize lazily. Opaque persisted rows still need
|
||||
@@ -441,13 +443,17 @@ export class SessionManagerCore {
|
||||
protected getPersistedFileEntries(
|
||||
leafAppendParentId: string | null = this.appendParentId,
|
||||
leafAppendMode?: "side",
|
||||
): unknown[] {
|
||||
): TranscriptEvent[] {
|
||||
this.clampOpaqueFileEntryIndexes();
|
||||
const entries: unknown[] = [];
|
||||
const entries: TranscriptEvent[] = [];
|
||||
let opaqueIndex = 0;
|
||||
for (let index = 0; index <= this.fileEntries.length; index += 1) {
|
||||
while (this.opaqueFileEntries[opaqueIndex]?.index === index) {
|
||||
entries.push(this.opaqueFileEntries[opaqueIndex]?.record);
|
||||
const opaqueEntry = this.opaqueFileEntries[opaqueIndex];
|
||||
if (!opaqueEntry) {
|
||||
break;
|
||||
}
|
||||
entries.push(opaqueEntry.record);
|
||||
opaqueIndex += 1;
|
||||
}
|
||||
const entry = this.fileEntries[index];
|
||||
@@ -456,7 +462,11 @@ export class SessionManagerCore {
|
||||
}
|
||||
}
|
||||
while (opaqueIndex < this.opaqueFileEntries.length) {
|
||||
entries.push(this.opaqueFileEntries[opaqueIndex]?.record);
|
||||
const opaqueEntry = this.opaqueFileEntries[opaqueIndex];
|
||||
if (!opaqueEntry) {
|
||||
break;
|
||||
}
|
||||
entries.push(opaqueEntry.record);
|
||||
opaqueIndex += 1;
|
||||
}
|
||||
|
||||
@@ -498,7 +508,7 @@ export class SessionManagerCore {
|
||||
return entries;
|
||||
}
|
||||
|
||||
getPersistedEntries(): unknown[] {
|
||||
getPersistedEntries(): TranscriptEvent[] {
|
||||
return this.getPersistedFileEntries();
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ export class SessionManagerPersistence extends SessionManagerCore {
|
||||
this.persistenceHeaderPending = false;
|
||||
}
|
||||
const leafEntry = parseOpaqueLeafEntry(entry);
|
||||
if (leafEntry) {
|
||||
if (leafEntry && isRecord(entry)) {
|
||||
requireTranscriptEventAppend(
|
||||
appendTranscriptEventSync(scope, entry),
|
||||
`Session transcript leaf control was not persisted: ${leafEntry.id}`,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { TranscriptEvent } from "../../config/sessions/session-accessor.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { ImageContent, TextContent } from "../../llm/types.js";
|
||||
import type { AgentMessage } from "../runtime/index.js";
|
||||
@@ -133,7 +134,7 @@ export interface SessionContext {
|
||||
|
||||
export type PreservedOpaqueFileEntry = {
|
||||
index: number;
|
||||
record: unknown;
|
||||
record: TranscriptEvent;
|
||||
};
|
||||
|
||||
export type SessionLeafControl = {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SessionManager } from "../agents/sessions/session-manager.js";
|
||||
import {
|
||||
loadTranscriptEventsSync,
|
||||
replaceTranscriptEventsSync,
|
||||
type TranscriptEvent,
|
||||
upsertSessionEntryCore,
|
||||
} from "../config/sessions/session-accessor.js";
|
||||
import { readTranscriptStorageRows } from "../config/sessions/session-accessor.sqlite-read.js";
|
||||
@@ -67,7 +68,7 @@ describe("doctor SQLite session transcript header repair", () => {
|
||||
});
|
||||
|
||||
async function seedHeaderlessTranscript(
|
||||
events: readonly unknown[],
|
||||
events: readonly TranscriptEvent[],
|
||||
options: { spawnedCwd?: string } = { spawnedCwd: SPAWNED_CWD },
|
||||
): Promise<void> {
|
||||
await upsertSessionEntryCore(scope, {
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
SessionLifecycleArtifactCleanupResult,
|
||||
SessionLifecycleStoreTarget,
|
||||
} from "./session-accessor.lifecycle-types.js";
|
||||
import type { TranscriptEvent } from "./session-accessor.types.js";
|
||||
import type { ResolvedSessionMaintenanceConfig } from "./store-maintenance.js";
|
||||
import type { TranscriptEntryAnchor } from "./transcript-entry-anchor.js";
|
||||
import type { SessionEntry } from "./types.js";
|
||||
@@ -77,8 +78,6 @@ export type SessionTranscriptInstance = SessionEntrySummary & {
|
||||
updatedAtMs: number;
|
||||
};
|
||||
|
||||
export type TranscriptEvent = unknown;
|
||||
|
||||
export type TranscriptEventAppendOptions = {
|
||||
appendIntent?: "active-branch";
|
||||
};
|
||||
@@ -119,6 +118,7 @@ export type {
|
||||
SessionTranscriptRawDeltaResult,
|
||||
SessionTranscriptVisibleMessageDeltaLimits,
|
||||
SessionTranscriptVisibleMessageDeltaResult,
|
||||
TranscriptEvent,
|
||||
} from "./session-accessor.types.js";
|
||||
|
||||
export type TranscriptMessageAppendOptions<TMessage> = {
|
||||
|
||||
@@ -193,8 +193,8 @@ export type ExactSessionEntry = {
|
||||
entry: SessionEntry;
|
||||
};
|
||||
|
||||
/** Raw transcript record for non-message events; message records use appendTranscriptMessage. */
|
||||
export type TranscriptEvent = unknown;
|
||||
/** JSON event persisted in a transcript; message records use appendTranscriptMessage. */
|
||||
export type TranscriptEvent = object | string | number | boolean | null;
|
||||
|
||||
export type SessionTranscriptStats = {
|
||||
eventCount: number;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import path from "node:path";
|
||||
import type { TranscriptEvent } from "./session-accessor.types.js";
|
||||
import {
|
||||
DEFAULT_REPLAY_MAX_MESSAGES,
|
||||
replayableTranscriptRole,
|
||||
@@ -21,7 +22,7 @@ type SessionResetBoundaryEvent = {
|
||||
|
||||
export type SessionResetBoundaryPlan = {
|
||||
event: SessionResetBoundaryEvent;
|
||||
seedEvents: unknown[];
|
||||
seedEvents: object[];
|
||||
};
|
||||
|
||||
function recordId(record: unknown): string | undefined {
|
||||
@@ -154,7 +155,7 @@ async function readLegacyTranscriptEvents(sessionFile: string | undefined): Prom
|
||||
}
|
||||
|
||||
export async function buildSessionResetBoundaryPlan(params: {
|
||||
events: readonly unknown[];
|
||||
events: readonly TranscriptEvent[];
|
||||
legacySessionFile?: string;
|
||||
reason: SessionResetBoundaryReason;
|
||||
}): Promise<SessionResetBoundaryPlan> {
|
||||
@@ -169,7 +170,7 @@ export async function buildSessionResetBoundaryPlan(params: {
|
||||
? []
|
||||
: await readLegacyTranscriptEvents(params.legacySessionFile);
|
||||
const seedEvents = legacyEvents.filter(
|
||||
(event) =>
|
||||
(event): event is object =>
|
||||
event !== null &&
|
||||
typeof event === "object" &&
|
||||
!Array.isArray(event) &&
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
import { replaceTranscriptEvents } from "../config/sessions/session-accessor.js";
|
||||
import {
|
||||
replaceTranscriptEvents,
|
||||
type TranscriptEvent,
|
||||
} from "../config/sessions/session-accessor.js";
|
||||
import { closeOpenClawAgentDatabasesForTest } from "../state/openclaw-agent-db.js";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import { captureEnv, setTestEnvValue } from "../test-utils/env.js";
|
||||
@@ -37,7 +40,7 @@ describe("session transcript reader marker projection", () => {
|
||||
|
||||
async function writeTranscript(
|
||||
sessionId: string,
|
||||
events: unknown[],
|
||||
events: TranscriptEvent[],
|
||||
): Promise<SessionTranscriptReadScope> {
|
||||
const scope = {
|
||||
agentId: "main",
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as sessionAccessor from "../config/sessions/session-accessor.js";
|
||||
import {
|
||||
persistSessionTranscriptTurn,
|
||||
replaceTranscriptEvents,
|
||||
type TranscriptEvent,
|
||||
upsertSessionEntryCore,
|
||||
} from "../config/sessions/session-accessor.js";
|
||||
import { waitForSessionTranscriptIndexReconcile } from "../config/sessions/session-transcript-reconcile.js";
|
||||
@@ -65,7 +66,7 @@ describe("session transcript reader facade", () => {
|
||||
|
||||
async function writeTranscript(
|
||||
sessionId: string,
|
||||
events: unknown[],
|
||||
events: TranscriptEvent[],
|
||||
): Promise<SessionTranscriptReadScope> {
|
||||
const scope = {
|
||||
agentId: "main",
|
||||
|
||||
@@ -127,8 +127,9 @@ function assertEventIdentitiesUnchanged(
|
||||
if (before.length !== after.length) {
|
||||
throw new Error(`${owner} event count changed during media migration`);
|
||||
}
|
||||
for (let index = 0; index < before.length; index += 1) {
|
||||
if (eventIdentity(before[index]) !== eventIdentity(after[index])) {
|
||||
for (const [index, beforeEvent] of before.entries()) {
|
||||
const afterEvent = after[index];
|
||||
if (afterEvent === undefined || eventIdentity(beforeEvent) !== eventIdentity(afterEvent)) {
|
||||
throw new Error(`${owner} event identity changed at index ${index}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export type {
|
||||
BundledEntryModuleLoadOptions,
|
||||
} from "./channel-entry-contract.types.js";
|
||||
|
||||
type BundledChannelRuntime = unknown;
|
||||
type BundledChannelRuntime = OpenClawPluginApi["runtime"];
|
||||
|
||||
type ChannelEntryConfigSchema<TPlugin> =
|
||||
TPlugin extends ChannelPlugin<unknown>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
resolveSessionTranscriptRuntimeTarget,
|
||||
withTranscriptWriteLock,
|
||||
type SessionTranscriptWriteLockAccessorContext,
|
||||
type TranscriptEvent,
|
||||
type TranscriptMessageAppendOptions,
|
||||
type TranscriptMessageAppendResult,
|
||||
type TranscriptUpdatePayload,
|
||||
@@ -31,7 +32,7 @@ export type InternalSessionTranscriptWriteLockContext = {
|
||||
options: Omit<TranscriptMessageAppendOptions<TMessage>, "config">,
|
||||
) => Promise<TranscriptMessageAppendResult<TMessage> | undefined>;
|
||||
publishUpdate: (update?: TranscriptUpdatePayload) => Promise<void>;
|
||||
readEvents: () => Promise<unknown[]>;
|
||||
readEvents: () => Promise<TranscriptEvent[]>;
|
||||
target: InternalSessionTranscriptTarget;
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type SessionTranscriptRawDeltaLimits,
|
||||
type SessionTranscriptRawDeltaResult,
|
||||
type SessionTranscriptVisibleMessageDeltaLimits,
|
||||
type TranscriptEvent as SessionTranscriptEvent,
|
||||
} from "../config/sessions/session-accessor.js";
|
||||
import { resolveMirroredTranscriptText } from "../config/sessions/transcript-mirror.js";
|
||||
import {
|
||||
@@ -67,10 +68,9 @@ export type {
|
||||
SessionTranscriptMemoryHitKey,
|
||||
SessionTranscriptMemoryHitKeyParams,
|
||||
SessionTranscriptReadParams,
|
||||
SessionTranscriptEvent,
|
||||
};
|
||||
|
||||
export type SessionTranscriptEvent = unknown;
|
||||
|
||||
export type SessionTranscriptTargetParams = SessionTranscriptReadParams;
|
||||
|
||||
/** Scoped target and bounds for one raw generation-aware transcript page. */
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
type DirectUnknown = unknown;
|
||||
type ParenthesizedUnknown = unknown;
|
||||
type IndirectUnknown = DirectUnknown;
|
||||
|
||||
type GenericAlias<T> = T;
|
||||
type ConcreteAlias = GenericAlias<unknown>;
|
||||
type UsefulBoundary = { value: unknown };
|
||||
@@ -0,0 +1,13 @@
|
||||
type User = { id: string };
|
||||
|
||||
const user: unknown = { id: "user-1" };
|
||||
const assertedUser = user as User;
|
||||
|
||||
const original = { id: "user-2" };
|
||||
const erased = original as unknown;
|
||||
const restoredUser = erased as User;
|
||||
|
||||
const preserved = { id: "user-3" } satisfies User;
|
||||
const preservedUser: User = preserved;
|
||||
|
||||
export { assertedUser, preservedUser, restoredUser };
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
type User = { id: string };
|
||||
type UsefulBoundary = { value: unknown };
|
||||
|
||||
const preserved = { id: "user-1" } satisfies User;
|
||||
const preservedUser: User = preserved;
|
||||
|
||||
function parseBoundary(value: unknown): User | undefined {
|
||||
if (typeof value !== "object" || value === null || !("id" in value)) {
|
||||
return undefined;
|
||||
}
|
||||
return typeof value.id === "string" ? { id: value.id } : undefined;
|
||||
}
|
||||
|
||||
export { parseBoundary, preservedUser, type UsefulBoundary };
|
||||
@@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
type OxlintConfig = {
|
||||
ignorePatterns?: string[];
|
||||
jsPlugins?: string[];
|
||||
overrides?: Array<{
|
||||
excludeFiles?: string[];
|
||||
files?: string[];
|
||||
@@ -147,6 +148,7 @@ describe("oxlint config", () => {
|
||||
"docs/_layouts/",
|
||||
".agents/skills/autoreview/tests/fixtures/**",
|
||||
"test/fixtures/oxlint-boundary-guards/**",
|
||||
"test/fixtures/oxlint-type-evidence/**",
|
||||
"**/a2ui.bundle.js",
|
||||
"extensions/diffs/assets/viewer-runtime.js",
|
||||
"extensions/diffs-language-pack/assets/viewer-runtime.js",
|
||||
@@ -291,6 +293,18 @@ describe("oxlint config", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("enables the repository type-evidence plugin", () => {
|
||||
const config = readJson(".oxlintrc.json") as OxlintConfig;
|
||||
|
||||
expect(config.jsPlugins).toContain("./scripts/oxlint-type-evidence.mjs");
|
||||
expect(config.rules?.["openclaw-type-evidence/no-unknown-type-aliases"]).toBe("error");
|
||||
expect(config.rules?.["openclaw-type-evidence/no-widen-then-assert"]).toBe("error");
|
||||
expect(config.overrides).toContainEqual({
|
||||
files: ["**/*.d.ts"],
|
||||
rules: { "openclaw-type-evidence/no-unknown-type-aliases": "off" },
|
||||
});
|
||||
});
|
||||
|
||||
it("enables clean zero-baseline lint rules", () => {
|
||||
const config = readJson(".oxlintrc.json") as OxlintConfig;
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const FIXTURES = "test/fixtures/oxlint-type-evidence";
|
||||
const cases = [
|
||||
{
|
||||
rule: "openclaw-type-evidence/no-unknown-type-aliases",
|
||||
violation: `${FIXTURES}/no-unknown-type-aliases-violation.ts`,
|
||||
violations: 3,
|
||||
},
|
||||
{
|
||||
rule: "openclaw-type-evidence/no-widen-then-assert",
|
||||
violation: `${FIXTURES}/no-widen-then-assert-violation.ts`,
|
||||
violations: 2,
|
||||
},
|
||||
];
|
||||
|
||||
function runRule(target: string) {
|
||||
return spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/run-oxlint.mjs",
|
||||
"--openclaw-focused-config",
|
||||
"--config",
|
||||
"config/oxlint/type-evidence.json",
|
||||
target,
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
}
|
||||
|
||||
describe("oxlint type-evidence rules", () => {
|
||||
it.each(cases)("reports the intended $rule flows", (testCase) => {
|
||||
const violation = runRule(testCase.violation);
|
||||
const output = `${violation.stdout}${violation.stderr}`;
|
||||
expect(violation.status).toBe(1);
|
||||
expect(output.split(`${testCase.rule.replace("/", "(")})`)).toHaveLength(
|
||||
testCase.violations + 1,
|
||||
);
|
||||
});
|
||||
|
||||
it("allows explicit unknown boundaries and preserved type evidence", () => {
|
||||
const result = runRule(`${FIXTURES}/valid.ts`);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(`${result.stdout}${result.stderr}`).not.toContain("openclaw-type-evidence");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user