mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 10:55:31 -06:00
refactor: consolidate core micro-helpers (#117825)
* refactor: centralize stable stringification * refactor: reuse canonical record coercion * refactor: reuse safe JSON parsing in cron storage * refactor: centralize environment truthiness * fix: enforce model scan and block reply timeouts * refactor: consolidate signal-aware sleep helper * fix: preserve plugin SDK sleep contract * test: satisfy model scan timeout lint
This commit is contained in:
committed by
GitHub
parent
df8cc5a458
commit
33ea3e16e9
@@ -1,9 +1,14 @@
|
||||
// Memory Host SDK module implements embeddings debug behavior.
|
||||
import { parseBoolean } from "@openclaw/normalization-core/boolean-coercion";
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
// Lightweight debug logging for memory embedding internals.
|
||||
|
||||
const debugEmbeddings = isTruthyEnvValue(process.env.OPENCLAW_DEBUG_MEMORY_EMBEDDINGS);
|
||||
const normalizedDebugEmbeddings = normalizeLowercaseStringOrEmpty(
|
||||
process.env.OPENCLAW_DEBUG_MEMORY_EMBEDDINGS,
|
||||
);
|
||||
const debugEmbeddings =
|
||||
parseBoolean(normalizedDebugEmbeddings) ?? ["1", "on", "yes"].includes(normalizedDebugEmbeddings);
|
||||
|
||||
/** Write embedding debug metadata when OPENCLAW_DEBUG_MEMORY_EMBEDDINGS is enabled. */
|
||||
export function debugEmbeddingsLog(message: string, meta?: Record<string, unknown>): void {
|
||||
@@ -13,16 +18,3 @@ export function debugEmbeddingsLog(message: string, meta?: Record<string, unknow
|
||||
const suffix = meta ? ` ${JSON.stringify(meta)}` : "";
|
||||
console.warn(`${message}${suffix}`);
|
||||
}
|
||||
|
||||
/** Parse common truthy env values for debug toggles. */
|
||||
function isTruthyEnvValue(value?: string): boolean {
|
||||
switch (normalizeLowercaseStringOrEmpty(value)) {
|
||||
case "1":
|
||||
case "on":
|
||||
case "true":
|
||||
case "yes":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,11 @@
|
||||
"import": "./dist/stable-node-path.mjs",
|
||||
"default": "./dist/stable-node-path.mjs"
|
||||
},
|
||||
"./stable-stringify": {
|
||||
"types": "./dist/stable-stringify.d.mts",
|
||||
"import": "./dist/stable-stringify.mjs",
|
||||
"default": "./dist/stable-stringify.mjs"
|
||||
},
|
||||
"./utf16-slice": {
|
||||
"types": "./dist/utf16-slice.d.mts",
|
||||
"import": "./dist/utf16-slice.mjs",
|
||||
@@ -86,7 +91,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown src/index.ts src/agent-id.ts src/boolean-coercion.ts src/cjk-chars.ts src/error-coercion.ts src/expect.ts src/json-schema.ts src/number-coercion.ts src/phone-presentation.ts src/record-coerce.ts src/result.ts src/stable-node-path.ts src/string-coerce.ts src/string-normalization.ts src/utf16-slice.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
||||
"build": "tsdown src/index.ts src/agent-id.ts src/boolean-coercion.ts src/cjk-chars.ts src/error-coercion.ts src/expect.ts src/json-schema.ts src/number-coercion.ts src/phone-presentation.ts src/record-coerce.ts src/result.ts src/stable-node-path.ts src/stable-stringify.ts src/string-coerce.ts src/string-normalization.ts src/utf16-slice.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"libphonenumber-js": "1.13.9",
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from "./format.js";
|
||||
export * from "./json-coercion.js";
|
||||
export * from "./number-coercion.js";
|
||||
export * from "./record-coerce.js";
|
||||
export * from "./stable-stringify.js";
|
||||
export * from "./string-coerce.js";
|
||||
export * from "./string-normalization.js";
|
||||
export * from "./text-decoding.js";
|
||||
|
||||
+3
-1
@@ -2,10 +2,12 @@
|
||||
* Regression coverage for deterministic unknown-value stringification.
|
||||
* Verifies sorted keys, repeated references, cycles, binary data, and errors.
|
||||
*/
|
||||
import { sanitizeSurrogates } from "@openclaw/ai/internal/shared";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { stableStringify } from "./stable-stringify.js";
|
||||
|
||||
const sanitizeSurrogates = (text: string) =>
|
||||
text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
|
||||
|
||||
describe("stableStringify", () => {
|
||||
it("sorts object keys recursively", () => {
|
||||
expect(stableStringify({ b: { d: 4, c: 3 }, a: 1 })).toBe('{"a":1,"b":{"c":3,"d":4}}');
|
||||
+9
-2
@@ -3,7 +3,6 @@
|
||||
* Serializes arbitrary values with deterministic key ordering and explicit
|
||||
* handling for errors, binary data, bigint, non-finite numbers, and cycles.
|
||||
*/
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
type StableStringNormalizer = (value: string) => string;
|
||||
|
||||
@@ -69,7 +68,7 @@ function stringifyObjectValue(
|
||||
return stringifyStableValue(
|
||||
{
|
||||
type: "Uint8Array",
|
||||
data: Buffer.from(value).toString("base64"),
|
||||
data: encodeBase64(value),
|
||||
},
|
||||
stack,
|
||||
normalizeString,
|
||||
@@ -99,6 +98,14 @@ function stringifyObjectValue(
|
||||
return `{${serializedFields.join(",")}}`;
|
||||
}
|
||||
|
||||
function encodeBase64(value: Uint8Array): string {
|
||||
let binary = "";
|
||||
for (const byte of value) {
|
||||
binary += String.fromCharCode(byte);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function compareStableStrings(left: string, right: string): number {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asOptionalObjectRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import {
|
||||
isOffsetInProtectedRanges,
|
||||
type PlainTextToolCallNameMatcher,
|
||||
@@ -103,10 +104,6 @@ type SuppressingPendingState = {
|
||||
|
||||
type PendingState = CandidatePendingState | SuppressingPendingState;
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
||||
}
|
||||
|
||||
function eventContentIndex(event: Record<string, unknown>): number {
|
||||
const index = event.contentIndex;
|
||||
return typeof index === "number" && Number.isInteger(index) && index >= 0 ? index : 0;
|
||||
@@ -120,7 +117,7 @@ function extractStandaloneCandidate(
|
||||
message: unknown,
|
||||
requireAssistantRole = false,
|
||||
): StandalonePlainTextToolCallCandidate | undefined {
|
||||
const record = asRecord(message);
|
||||
const record = asOptionalObjectRecord(message);
|
||||
if (!record || (requireAssistantRole && record.role !== "assistant")) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -132,7 +129,7 @@ function extractStandaloneCandidate(
|
||||
}
|
||||
const candidate: StandalonePlainTextToolCallCandidate = { text: "", parts: [] };
|
||||
for (const [contentIndex, block] of record.content.entries()) {
|
||||
const value = asRecord(block);
|
||||
const value = asOptionalObjectRecord(block);
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -397,7 +394,7 @@ function projectRangesOntoMessage(
|
||||
const sourceToProjectedContentIndex = new Map<number, number>();
|
||||
for (const [index, block] of record.content.entries()) {
|
||||
const part = parts.get(index);
|
||||
const blockRecord = asRecord(block);
|
||||
const blockRecord = asOptionalObjectRecord(block);
|
||||
if (!part || blockRecord?.type !== "text" || typeof blockRecord.text !== "string") {
|
||||
sourceToProjectedContentIndex.set(index, content.length);
|
||||
content.push(block);
|
||||
@@ -422,7 +419,7 @@ export function projectScrubbedPlainTextToolCallMessage(params: {
|
||||
resolveProtectedRanges?: PlainTextToolCallProtectedRangeResolver;
|
||||
requireAssistantRole?: boolean;
|
||||
}): PlainTextToolCallMessageProjection | undefined {
|
||||
const record = asRecord(params.message);
|
||||
const record = asOptionalObjectRecord(params.message);
|
||||
const candidate = extractStandaloneCandidate(
|
||||
params.message,
|
||||
params.requireAssistantRole === true,
|
||||
@@ -486,7 +483,7 @@ function resolvePartialProtectionCheck(params: {
|
||||
resolveProtectedRanges: PlainTextToolCallProtectedRangeResolver;
|
||||
}): ((offset: number) => boolean) | undefined {
|
||||
const candidate = extractStandaloneCandidate(params.partial);
|
||||
const record = asRecord(params.partial);
|
||||
const record = asOptionalObjectRecord(params.partial);
|
||||
if (!candidate || !record) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -500,7 +497,7 @@ function resolvePartialProtectionCheck(params: {
|
||||
} else {
|
||||
const part = candidate.parts.find((entry) => entry.contentIndex === params.contentIndex);
|
||||
const block = Array.isArray(record.content)
|
||||
? asRecord(record.content[params.contentIndex])
|
||||
? asOptionalObjectRecord(record.content[params.contentIndex])
|
||||
: undefined;
|
||||
if (!part || block?.type !== "text" || typeof block.text !== "string") {
|
||||
return undefined;
|
||||
@@ -711,14 +708,14 @@ function projectedTextForEvent(
|
||||
event: Record<string, unknown>,
|
||||
projection: PlainTextToolCallMessageProjection,
|
||||
): string | undefined {
|
||||
const content = asRecord(projection.message)?.content;
|
||||
const content = asOptionalObjectRecord(projection.message)?.content;
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
}
|
||||
const projectedIndex = projection.sourceToProjectedContentIndex.get(eventContentIndex(event));
|
||||
const block =
|
||||
Array.isArray(content) && projectedIndex !== undefined
|
||||
? asRecord(content[projectedIndex])
|
||||
? asOptionalObjectRecord(content[projectedIndex])
|
||||
: undefined;
|
||||
return block?.type === "text" && typeof block.text === "string" ? block.text : undefined;
|
||||
}
|
||||
@@ -1088,7 +1085,7 @@ function orderByContentIndex(
|
||||
): unknown[] {
|
||||
const contentLength = Array.isArray(message.content) ? message.content.length : 0;
|
||||
const order = (event: unknown) => {
|
||||
const index = asRecord(event)?.contentIndex;
|
||||
const index = asOptionalObjectRecord(event)?.contentIndex;
|
||||
return typeof index === "number" &&
|
||||
Number.isInteger(index) &&
|
||||
index >= 0 &&
|
||||
@@ -1223,7 +1220,7 @@ export async function* normalizePlainTextToolCallStreamEvents(
|
||||
|
||||
async function* normalizeEvents() {
|
||||
for await (const sourceEvent of source) {
|
||||
let record = asRecord(sourceEvent);
|
||||
let record = asOptionalObjectRecord(sourceEvent);
|
||||
if (!record) {
|
||||
yield sourceEvent;
|
||||
continue;
|
||||
@@ -1411,7 +1408,7 @@ export async function* normalizePlainTextToolCallStreamEvents(
|
||||
yield createSyntheticTextDelta(
|
||||
visibleTemplate,
|
||||
novelVisiblePrefix,
|
||||
asRecord(visibleProjection?.message),
|
||||
asOptionalObjectRecord(visibleProjection?.message),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1456,7 +1453,7 @@ export async function* normalizePlainTextToolCallStreamEvents(
|
||||
createSyntheticTextDelta(
|
||||
pending.template,
|
||||
candidateText,
|
||||
asRecord(record.partial),
|
||||
asOptionalObjectRecord(record.partial),
|
||||
),
|
||||
{ ...incomingRecord, content: incoming },
|
||||
];
|
||||
@@ -1823,7 +1820,7 @@ export async function* normalizePlainTextToolCallStreamEvents(
|
||||
}
|
||||
}
|
||||
for await (const event of normalizeEvents()) {
|
||||
const record = asRecord(event);
|
||||
const record = asOptionalObjectRecord(event);
|
||||
if (record?.type === "text_delta" && typeof record.delta === "string") {
|
||||
const key = eventKey(record);
|
||||
const previous = emittedTextUnits.get(key) ?? 0;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { sanitizeSurrogates } from "@openclaw/ai/internal/shared";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
@@ -12,7 +13,6 @@ import { safeJsonStringify } from "../utils/safe-json.js";
|
||||
import { redactAgentDiagnosticPayload } from "./diagnostic-redaction.js";
|
||||
import { getQueuedFileWriter, type QueuedFileWriter } from "./queued-file-writer.js";
|
||||
import type { AgentMessage, StreamFn } from "./runtime/index.js";
|
||||
import { stableStringify } from "./stable-stringify.js";
|
||||
import { buildAgentTraceBase } from "./trace-base.js";
|
||||
|
||||
// Payloads are redacted before JSONL output while stable digests preserve
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { NODE_FS_LIST_DIR_COMMAND } from "../infra/node-commands.js";
|
||||
import { emitSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js";
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
} from "./code-mode-runtime.js";
|
||||
import { readCodeModeSkill } from "./code-mode-skills.js";
|
||||
import type { AgentToolUpdateCallback } from "./runtime/index.js";
|
||||
import { stableStringify } from "./stable-stringify.js";
|
||||
import { getSwarmRunByLaunchReplayKey, initSubagentRegistry } from "./subagent-registry.js";
|
||||
import type { SubagentRunRecord } from "./subagent-registry.types.js";
|
||||
import {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createCodeModeNamespaceRuntime } from "./code-mode-namespaces.js";
|
||||
import { resolveCodeModeConfig } from "./code-mode.js";
|
||||
import { testing } from "./code-mode.test-support.js";
|
||||
import { stableStringify } from "./stable-stringify.js";
|
||||
import {
|
||||
SWARM_CODE_MODE_IDEMPOTENCY_KEY,
|
||||
SWARM_CODE_MODE_REQUEST_FINGERPRINT,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
/**
|
||||
* Builds structured observations for embedded-agent API/text failures.
|
||||
*/
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
parseApiErrorInfo,
|
||||
type ProviderRuntimeFailureKind,
|
||||
} from "./embedded-agent-helpers.js";
|
||||
import { stableStringify } from "./stable-stringify.js";
|
||||
|
||||
const MAX_OBSERVATION_INPUT_CHARS = 64_000;
|
||||
const MAX_FINGERPRINT_MESSAGE_CHARS = 8_000;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
/**
|
||||
* Converts raw provider/transport errors into concise user-facing copy.
|
||||
*/
|
||||
@@ -27,7 +28,6 @@ import { findCodeRegions } from "../../shared/text/code-regions.js";
|
||||
import { stripFinalTags } from "../../shared/text/final-tags.js";
|
||||
import { formatExecDeniedUserMessage } from "../exec-approval-result.js";
|
||||
import { stripInternalRuntimeContext } from "../internal-runtime-context.js";
|
||||
import { stableStringify } from "../stable-stringify.js";
|
||||
import {
|
||||
isBillingErrorMessage,
|
||||
isOverloadedErrorMessage,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
stripSystemPromptCacheBoundary,
|
||||
} from "@openclaw/ai/internal/shared";
|
||||
import { mergeTransportHeaders, sanitizeTransportPayloadText } from "@openclaw/ai/transports";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import {
|
||||
asDateTimestampMs,
|
||||
isFutureDateTimestampMs,
|
||||
@@ -28,7 +29,6 @@ import { resolveProviderRequestHeaders } from "../provider-request-config.js";
|
||||
import { buildGuardedModelFetch } from "../provider-transport-fetch.js";
|
||||
import type { StreamFn } from "../runtime/index.js";
|
||||
import { isSessionWriteLockAcquireError } from "../session-write-lock-error.js";
|
||||
import { stableStringify } from "../stable-stringify.js";
|
||||
import { log } from "./logger.js";
|
||||
import { isGooglePromptCacheEligible, resolveCacheRetention } from "./prompt-cache-retention.js";
|
||||
import { EmbeddedAttemptSessionTakeoverError } from "./run/attempt.session-lock.js";
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
sortPromptCacheToolsByName,
|
||||
splitSystemPromptCacheBoundary,
|
||||
} from "@openclaw/ai/internal/shared";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { stableStringify } from "../stable-stringify.js";
|
||||
import type { NormalizedUsage } from "../usage.js";
|
||||
|
||||
type PromptCacheChangeCode =
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import crypto from "node:crypto";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
|
||||
import type { Model } from "openclaw/plugin-sdk/llm";
|
||||
import { resolveGlobalSingleton } from "../../shared/global-singleton.js";
|
||||
import { stableStringify } from "../stable-stringify.js";
|
||||
|
||||
type ProviderPromptSnapshot = {
|
||||
scopeDigest: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { formatContextJsonBlock } from "../../../auto-reply/reply/channel-prompt-context.js";
|
||||
import { markInboundContextLabel } from "../../../auto-reply/reply/inbound-context-marker.js";
|
||||
import {
|
||||
@@ -5,7 +6,6 @@ import {
|
||||
INTER_SESSION_PROMPT_PREFIX_BASE,
|
||||
} from "../../../sessions/input-provenance.js";
|
||||
import type { AgentMessage } from "../../runtime/index.js";
|
||||
import { stableStringify } from "../../stable-stringify.js";
|
||||
import { isRunnerToolCallBlockType } from "./attempt.tool-call-block-type.js";
|
||||
|
||||
export type UserTranscriptContext = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { stableStringify } from "../stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { normalizeToolName } from "../tool-policy.js";
|
||||
import { codexNativeHookRelayResponseCodec } from "./native-hook-relay-response-codec.js";
|
||||
import type {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import {
|
||||
getAgentToolResultMiddlewareMatcherScope,
|
||||
listAgentToolResultMiddlewares,
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
hasBeforeToolCallPolicy,
|
||||
runBeforeToolCallHook,
|
||||
} from "../agent-tools.before-tool-call.js";
|
||||
import { stableStringify } from "../stable-stringify.js";
|
||||
import { resolveToolLoopDetectionConfig } from "../tool-loop-detection-config.js";
|
||||
import { payloadTextResult } from "../tools/common.js";
|
||||
import { runAgentHarnessAfterToolCallHook } from "./hook-helpers.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
/**
|
||||
* Shared media generation task status and duplicate-guard helpers.
|
||||
*
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
import { listFreshTasksForOwnerKey } from "../tasks/runtime-internal.js";
|
||||
import type { TaskRecord } from "../tasks/task-registry.types.js";
|
||||
import { buildSessionAsyncTaskStatusDetails } from "./session-async-task-status.js";
|
||||
import { stableStringify } from "./stable-stringify.js";
|
||||
|
||||
/** Marks media as ready while requester delivery is still being confirmed. */
|
||||
export const MEDIA_GENERATION_DELIVERING_COMPLETION_PROGRESS =
|
||||
|
||||
@@ -179,16 +179,9 @@ describe("scanOpenRouterModels", () => {
|
||||
|
||||
it("applies the scan timeout before the OpenRouter catalog responds", async () => {
|
||||
vi.useFakeTimers();
|
||||
const fetchImpl: typeof fetch = async (_input, init) =>
|
||||
await new Promise<Response>((_resolve, reject) => {
|
||||
const signal = typeof init === "object" && init ? init.signal : undefined;
|
||||
if (signal?.aborted) {
|
||||
reject(new Error("catalog aborted"));
|
||||
return;
|
||||
}
|
||||
signal?.addEventListener("abort", () => reject(new Error("catalog aborted")), {
|
||||
once: true,
|
||||
});
|
||||
const fetchImpl: typeof fetch = async () =>
|
||||
await new Promise<Response>(() => {
|
||||
// Deliberately ignore cancellation to prove the timeout race settles independently.
|
||||
});
|
||||
|
||||
const scan = expect(
|
||||
@@ -197,7 +190,7 @@ describe("scanOpenRouterModels", () => {
|
||||
probe: false,
|
||||
timeoutMs: 1,
|
||||
}),
|
||||
).rejects.toThrow(/catalog aborted/);
|
||||
).rejects.toThrow(/OpenRouter model scan timed out/);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await scan;
|
||||
@@ -253,7 +246,7 @@ describe("scanOpenRouterModels", () => {
|
||||
|
||||
const scan = expect(
|
||||
scanOpenRouterModels({ fetchImpl, probe: false, timeoutMs }),
|
||||
).rejects.toThrow(/aborted/i);
|
||||
).rejects.toThrow(/timed out/i);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(timeoutMs - 1);
|
||||
expect(chunkCount).toBe(3);
|
||||
|
||||
+87
-88
@@ -24,6 +24,7 @@ import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { readResponseWithLimit } from "../infra/http-body.js";
|
||||
import "../llm/ai-transport-host.js";
|
||||
import type { Context, Model, Tool } from "../llm/types.js";
|
||||
import { withTimeout } from "../node-host/with-timeout.js";
|
||||
import { inferParamBFromIdOrName } from "../shared/model-param-b.js";
|
||||
|
||||
const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
||||
@@ -181,19 +182,6 @@ function isFreeOpenRouterModel(entry: OpenRouterModelMeta): boolean {
|
||||
return entry.pricing.prompt === 0 && entry.pricing.completion === 0;
|
||||
}
|
||||
|
||||
async function withTimeout<T>(
|
||||
timeoutMs: number,
|
||||
fn: (signal: AbortSignal) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(controller.abort.bind(controller), timeoutMs);
|
||||
try {
|
||||
return await fn(controller.signal);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
// Reads the OpenRouter /models success body under a byte cap before JSON.parse.
|
||||
// The success path was previously buffered with an unbounded res.json(); a faulty
|
||||
// or hostile provider could stream an effectively endless document and exhaust
|
||||
@@ -222,74 +210,79 @@ async function fetchOpenRouterModels(
|
||||
try {
|
||||
// fetch resolves after headers, so keep the shared timeout active until
|
||||
// the provider-controlled catalog body has been consumed.
|
||||
return await withTimeout(timeoutMs, async (signal) => {
|
||||
res = await fetchImpl(OPENROUTER_MODELS_URL, {
|
||||
headers: { Accept: "application/json" },
|
||||
signal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`OpenRouter /models failed: HTTP ${res.status}`);
|
||||
}
|
||||
const payload = (await readOpenRouterModelsJson(res, timeoutMs)) as { data?: unknown };
|
||||
const entries = Array.isArray(payload.data) ? payload.data : [];
|
||||
return await withTimeout(
|
||||
async (signal) => {
|
||||
res = await fetchImpl(OPENROUTER_MODELS_URL, {
|
||||
headers: { Accept: "application/json" },
|
||||
signal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`OpenRouter /models failed: HTTP ${res.status}`);
|
||||
}
|
||||
const payload = (await readOpenRouterModelsJson(res, timeoutMs)) as { data?: unknown };
|
||||
const entries = Array.isArray(payload.data) ? payload.data : [];
|
||||
|
||||
return entries
|
||||
.map((entry) => {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return null;
|
||||
}
|
||||
const obj = entry as Record<string, unknown>;
|
||||
const id = normalizeOptionalString(obj.id) ?? "";
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
const name = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id;
|
||||
return entries
|
||||
.map((entry) => {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return null;
|
||||
}
|
||||
const obj = entry as Record<string, unknown>;
|
||||
const id = normalizeOptionalString(obj.id) ?? "";
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
const name = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id;
|
||||
|
||||
const contextLength =
|
||||
typeof obj.context_length === "number" && Number.isFinite(obj.context_length)
|
||||
? obj.context_length
|
||||
: null;
|
||||
|
||||
const maxCompletionTokens =
|
||||
typeof obj.max_completion_tokens === "number" &&
|
||||
Number.isFinite(obj.max_completion_tokens)
|
||||
? obj.max_completion_tokens
|
||||
: typeof obj.max_output_tokens === "number" && Number.isFinite(obj.max_output_tokens)
|
||||
? obj.max_output_tokens
|
||||
const contextLength =
|
||||
typeof obj.context_length === "number" && Number.isFinite(obj.context_length)
|
||||
? obj.context_length
|
||||
: null;
|
||||
|
||||
const supportedParameters = Array.isArray(obj.supported_parameters)
|
||||
? normalizeStringEntries(
|
||||
obj.supported_parameters.filter((value) => typeof value === "string"),
|
||||
)
|
||||
: [];
|
||||
const maxCompletionTokens =
|
||||
typeof obj.max_completion_tokens === "number" &&
|
||||
Number.isFinite(obj.max_completion_tokens)
|
||||
? obj.max_completion_tokens
|
||||
: typeof obj.max_output_tokens === "number" &&
|
||||
Number.isFinite(obj.max_output_tokens)
|
||||
? obj.max_output_tokens
|
||||
: null;
|
||||
|
||||
const supportedParametersCount = supportedParameters.length;
|
||||
const supportsToolsMeta = supportedParameters.includes("tools");
|
||||
const supportedParameters = Array.isArray(obj.supported_parameters)
|
||||
? normalizeStringEntries(
|
||||
obj.supported_parameters.filter((value) => typeof value === "string"),
|
||||
)
|
||||
: [];
|
||||
|
||||
const modality =
|
||||
typeof obj.modality === "string" && obj.modality.trim() ? obj.modality.trim() : null;
|
||||
const supportedParametersCount = supportedParameters.length;
|
||||
const supportsToolsMeta = supportedParameters.includes("tools");
|
||||
|
||||
const inferredParamB = inferParamBFromIdOrName(`${id} ${name}`);
|
||||
const createdAtMs = normalizeCreatedAtMs(obj.created_at);
|
||||
const pricing = parseOpenRouterPricing(obj.pricing);
|
||||
const modality =
|
||||
typeof obj.modality === "string" && obj.modality.trim() ? obj.modality.trim() : null;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
contextLength,
|
||||
maxCompletionTokens,
|
||||
supportedParameters,
|
||||
supportedParametersCount,
|
||||
supportsToolsMeta,
|
||||
modality,
|
||||
inferredParamB,
|
||||
createdAtMs,
|
||||
pricing,
|
||||
} satisfies OpenRouterModelMeta;
|
||||
})
|
||||
.filter((entry): entry is OpenRouterModelMeta => Boolean(entry));
|
||||
});
|
||||
const inferredParamB = inferParamBFromIdOrName(`${id} ${name}`);
|
||||
const createdAtMs = normalizeCreatedAtMs(obj.created_at);
|
||||
const pricing = parseOpenRouterPricing(obj.pricing);
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
contextLength,
|
||||
maxCompletionTokens,
|
||||
supportedParameters,
|
||||
supportedParametersCount,
|
||||
supportsToolsMeta,
|
||||
modality,
|
||||
inferredParamB,
|
||||
createdAtMs,
|
||||
pricing,
|
||||
} satisfies OpenRouterModelMeta;
|
||||
})
|
||||
.filter((entry): entry is OpenRouterModelMeta => Boolean(entry));
|
||||
},
|
||||
timeoutMs,
|
||||
"OpenRouter model scan",
|
||||
);
|
||||
} finally {
|
||||
if (res && !res.bodyUsed) {
|
||||
await res.body?.cancel().catch(() => undefined);
|
||||
@@ -315,14 +308,17 @@ async function probeTool(
|
||||
};
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const message = await withTimeout(timeoutMs, (signal) =>
|
||||
complete(model, context, {
|
||||
apiKey,
|
||||
maxTokens: 256,
|
||||
temperature: 0,
|
||||
toolChoice: "required",
|
||||
signal,
|
||||
} satisfies OpenAICompletionsOptions),
|
||||
const message = await withTimeout(
|
||||
(signal) =>
|
||||
complete(model, context, {
|
||||
apiKey,
|
||||
maxTokens: 256,
|
||||
temperature: 0,
|
||||
toolChoice: "required",
|
||||
signal,
|
||||
} satisfies OpenAICompletionsOptions),
|
||||
timeoutMs,
|
||||
"model tool probe",
|
||||
);
|
||||
|
||||
const hasToolCall = message.content.some((block) => block.type === "toolCall");
|
||||
@@ -364,13 +360,16 @@ async function probeImage(
|
||||
};
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
await withTimeout(timeoutMs, (signal) =>
|
||||
complete(model, context, {
|
||||
apiKey,
|
||||
maxTokens: 16,
|
||||
temperature: 0,
|
||||
signal,
|
||||
} satisfies OpenAICompletionsOptions),
|
||||
await withTimeout(
|
||||
(signal) =>
|
||||
complete(model, context, {
|
||||
apiKey,
|
||||
maxTokens: 16,
|
||||
temperature: 0,
|
||||
signal,
|
||||
} satisfies OpenAICompletionsOptions),
|
||||
timeoutMs,
|
||||
"model image probe",
|
||||
);
|
||||
return { ok: true, latencyMs: Date.now() - startedAt };
|
||||
} catch (err) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import {
|
||||
getRuntimeConfig,
|
||||
@@ -44,7 +45,6 @@ import {
|
||||
resolvePluginModelCatalogOwnerPluginId,
|
||||
type PersistedPluginModelCatalog,
|
||||
} from "./plugin-model-catalog.js";
|
||||
import { stableStringify } from "./stable-stringify.js";
|
||||
|
||||
type PreparedOpenClawModelsJsonSource = ModelsJsonReadyResult & {
|
||||
fingerprint: string;
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from "node:path";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import type { ConfiguredModelRef } from "@openclaw/model-catalog-core/configured-model-refs";
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import type { PreparedMessageToolCatalog } from "../channels/plugins/message-action-discovery.js";
|
||||
import { hashRuntimeConfigValue } from "../config/runtime-snapshot.js";
|
||||
import { sha256Base64Url } from "../infra/crypto-digest.js";
|
||||
@@ -60,7 +61,6 @@ import type {
|
||||
import { loadAgentRuntimePluginRegistryHandle } from "./runtime-plugins.js";
|
||||
import type { AuthStorage, AuthStorageData } from "./sessions/auth-storage.js";
|
||||
import type { ModelRegistry } from "./sessions/model-registry.js";
|
||||
import { stableStringify } from "./stable-stringify.js";
|
||||
|
||||
const MODEL_RUNTIME_PROVIDER_DISCOVERY_TIMEOUT_MS = 5_000;
|
||||
const fullModelCatalogSnapshots = new WeakSet<ModelCatalogSnapshot>();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Watches recent tool history for repeated no-progress patterns and circuit-breaker thresholds.
|
||||
*/
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import {
|
||||
normalizeNullableString as nonEmptyStringField,
|
||||
normalizeOptionalString as normalizeRunId,
|
||||
@@ -13,7 +14,6 @@ import type { SessionState, ToolCallRecord } from "../logging/diagnostic-session
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { isPlainObject } from "../utils.js";
|
||||
import { isMessagingToolSendAction } from "./embedded-agent-messaging.js";
|
||||
import { stableStringify } from "./stable-stringify.js";
|
||||
import {
|
||||
buildArgumentChurnWarning,
|
||||
getArgumentChurnNoProgressStreak,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* union with approval assertions and the audit log.
|
||||
*/
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { Type } from "typebox";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import {
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
} from "../../system-agent/operations.js";
|
||||
import { validateSystemAgentPluginInstallSpec } from "../../system-agent/plugin-install.js";
|
||||
import { stringEnum } from "../schema/typebox.js";
|
||||
import { stableStringify } from "../stable-stringify.js";
|
||||
import { textResult, ToolInputError, readStringParam, type AnyAgentTool } from "./common.js";
|
||||
|
||||
export type SystemAgentToolOptions = {
|
||||
|
||||
@@ -55,6 +55,22 @@ describe("createBlockReplyContentKey", () => {
|
||||
});
|
||||
|
||||
describe("createBlockReplyPipeline dedup with threading", () => {
|
||||
it("keeps an un-aborted delivery signal when timeouts are disabled", async () => {
|
||||
let deliverySignal: AbortSignal | undefined;
|
||||
const pipeline = createBlockReplyPipeline({
|
||||
onBlockReply: async (_payload, options) => {
|
||||
deliverySignal = options?.abortSignal;
|
||||
},
|
||||
timeoutMs: 0,
|
||||
});
|
||||
|
||||
pipeline.enqueue({ text: "response text" });
|
||||
await pipeline.flush({ force: true });
|
||||
|
||||
expect(deliverySignal).toBeDefined();
|
||||
expect(deliverySignal?.aborted).toBe(false);
|
||||
});
|
||||
|
||||
it("does not count reasoning or commentary as a terminal reply", async () => {
|
||||
const pipeline = createBlockReplyPipeline({
|
||||
onBlockReply: async () => {},
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
resolveSendableOutboundReplyParts,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { withTimeout } from "../../node-host/with-timeout.js";
|
||||
import { getReplyPayloadMetadata, isReplyPayloadStatusNotice } from "../reply-payload.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import { createBlockReplyCoalescer } from "./block-reply-coalescer.js";
|
||||
@@ -79,27 +80,6 @@ export function createBlockReplyContentKey(payload: ReplyPayload): string {
|
||||
});
|
||||
}
|
||||
|
||||
const withTimeout = async <T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number,
|
||||
timeoutError: Error,
|
||||
): Promise<T> => {
|
||||
if (!timeoutMs || timeoutMs <= 0) {
|
||||
return promise;
|
||||
}
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => reject(timeoutError), timeoutMs);
|
||||
});
|
||||
try {
|
||||
return await Promise.race([promise, timeoutPromise]);
|
||||
} finally {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function resolveBlockReplyTimeoutMs(timeoutMs: number): number {
|
||||
return clampPositiveTimerTimeoutMs(timeoutMs) ?? 0;
|
||||
}
|
||||
@@ -158,22 +138,23 @@ export function createBlockReplyPipeline(params: {
|
||||
pendingKeys.add(payloadKey);
|
||||
|
||||
// Preserve outbound order by chaining sends; abort after timeout to avoid stale blocks.
|
||||
const timeoutError = new Error(`block reply delivery timed out after ${timeoutMs}ms`);
|
||||
const abortController = new AbortController();
|
||||
const fallbackAbortController = new AbortController();
|
||||
let timeoutSignal: AbortSignal | undefined;
|
||||
sendChain = sendChain
|
||||
.then(async () => {
|
||||
if (aborted) {
|
||||
return false;
|
||||
}
|
||||
await withTimeout(
|
||||
Promise.resolve(
|
||||
onBlockReply(payload, {
|
||||
abortSignal: abortController.signal,
|
||||
async (signal) => {
|
||||
timeoutSignal = signal;
|
||||
await onBlockReply(payload, {
|
||||
abortSignal: signal ?? fallbackAbortController.signal,
|
||||
timeoutMs,
|
||||
}),
|
||||
),
|
||||
timeoutMs,
|
||||
timeoutError,
|
||||
});
|
||||
},
|
||||
timeoutMs || undefined,
|
||||
"block reply delivery",
|
||||
);
|
||||
return true;
|
||||
})
|
||||
@@ -208,8 +189,7 @@ export function createBlockReplyPipeline(params: {
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (err === timeoutError) {
|
||||
abortController.abort();
|
||||
if (timeoutSignal?.aborted) {
|
||||
aborted = true;
|
||||
if (!didLogTimeout) {
|
||||
didLogTimeout = true;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { runAgentHarnessBeforeMessageWriteHook } from "../../../agents/harness/hook-helpers.js";
|
||||
import { stableStringify } from "../../../agents/stable-stringify.js";
|
||||
import { normalizeChatType } from "../../../channels/chat-type.js";
|
||||
import { resolveStorePath } from "../../../config/sessions.js";
|
||||
import { loadSessionEntryReadOnly } from "../../../config/sessions/session-accessor.js";
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
// Applies the package, agent, workspace, and managed-file slices of a consented Claw add plan.
|
||||
import { lstat, mkdir, rmdir } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { findOverlappingWorkspaceAgentIds } from "../agents/agent-delete-safety.js";
|
||||
import { listAgentEntries } from "../agents/agent-scope.js";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { transformConfigFileWithRetry } from "../config/config.js";
|
||||
import type { AgentConfig } from "../config/types.agents.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import type { OpenClawStateDatabaseOptions } from "../state/openclaw-state-db.js";
|
||||
import {
|
||||
CLAW_CRON_REF_SCHEMA_VERSION,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
// Claw doctor diagnostics project the lifecycle ownership ledger into health findings.
|
||||
import { createHash } from "node:crypto";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { listConfiguredMcpServers } from "../config/mcp-config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveDefaultCronStaggerMs } from "../cron/stagger.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { listAgentEntries } from "../agents/agent-scope.js";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { getRuntimeConfig } from "../config/config.js";
|
||||
import type { AgentConfig } from "../config/types.agents.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { getRuntimeConfig } from "../config/config.js";
|
||||
import { listConfiguredMcpServers, unsetConfiguredMcpServer } from "../config/mcp-config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
||||
import { lstat, realpath } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { resolvePathViaExistingAncestorSync } from "../infra/boundary-path.js";
|
||||
import { assertNoSymlinkParents } from "../infra/fs-safe-advanced.js";
|
||||
import { FsSafeError, root as fsSafeRoot, type Root } from "../infra/fs-safe.js";
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { canonicalizeConfiguredMcpServer } from "../config/mcp-config-normalize.js";
|
||||
import { listConfiguredMcpServers, setConfiguredMcpServer } from "../config/mcp-config.js";
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import {
|
||||
runOpenClawStateWriteTransaction,
|
||||
type OpenClawStateDatabaseOptions,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { preflightPluginInstall } from "../plugins/plugin-install-preflight.js";
|
||||
import type { OpenClawStateDatabaseOptions } from "../state/openclaw-state-db.js";
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Persists the root ownership record for one Claw-created agent and workspace.
|
||||
import { createHash } from "node:crypto";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import {
|
||||
openOpenClawStateDatabase,
|
||||
runOpenClawStateWriteTransaction,
|
||||
|
||||
@@ -654,4 +654,4 @@ describe("applyClawUpdatePlan", () => {
|
||||
});
|
||||
});
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { listAgentEntries } from "../agents/agent-scope.js";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { transformConfigFileWithRetry } from "../config/config.js";
|
||||
import type { AgentConfig } from "../config/types.agents.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Builds field-level capability change summaries for Claw update previews.
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { listAgentEntries, toAgentEntriesRecord } from "../agents/agent-scope.js";
|
||||
import { resolveSandboxConfigForAgent } from "../agents/sandbox/config.js";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { expandToolGroups, resolveToolProfilePolicy } from "../agents/tool-policy-shared.js";
|
||||
import { parseDurationMs } from "../cli/parse-duration.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import type { McpServerConfig } from "../config/types.mcp.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Builds read-only, agent-centric Claw update plans from grouped manifests and ownership state.
|
||||
import { createHash } from "node:crypto";
|
||||
import { lstat } from "node:fs/promises";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { normalizeConfiguredMcpServers } from "../config/mcp-config-normalize.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { root as fsSafeRoot } from "../infra/fs-safe.js";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { redactSensitiveUrlLikeString } from "@openclaw/net-policy/redact-sensitive-url";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import {
|
||||
listAgentEntries,
|
||||
listAgentIds,
|
||||
resolveAgentWorkspaceDir,
|
||||
} from "../agents/agent-scope-config.js";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import {
|
||||
applyClawAddPlan,
|
||||
CLAW_ADD_RESULT_SCHEMA_VERSION,
|
||||
|
||||
@@ -242,10 +242,6 @@ export async function collectMacGatewayPlatformWarnings(
|
||||
return warnings;
|
||||
}
|
||||
|
||||
function isTruthyEnvValue(value: string | undefined): boolean {
|
||||
return Boolean(normalizeOptionalString(value));
|
||||
}
|
||||
|
||||
function isTmpCompileCachePath(cachePath: string): boolean {
|
||||
const normalized = cachePath.trim().replace(/\/+$/, "");
|
||||
return (
|
||||
@@ -296,7 +292,7 @@ export function noteStartupOptimizationHints(
|
||||
);
|
||||
}
|
||||
|
||||
if (isTruthyEnvValue(disableCompileCache)) {
|
||||
if (disableCompileCache) {
|
||||
lines.push("- NODE_DISABLE_COMPILE_CACHE is set; startup compile cache is disabled.");
|
||||
}
|
||||
|
||||
@@ -315,7 +311,7 @@ export function noteStartupOptimizationHints(
|
||||
" export NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache",
|
||||
" mkdir -p /var/tmp/openclaw-compile-cache",
|
||||
" export OPENCLAW_NO_RESPAWN=1",
|
||||
isTruthyEnvValue(disableCompileCache) ? " unset NODE_DISABLE_COMPILE_CACHE" : undefined,
|
||||
disableCompileCache ? " unset NODE_DISABLE_COMPILE_CACHE" : undefined,
|
||||
].filter((line): line is string => Boolean(line));
|
||||
|
||||
noteFn([...lines, ...suggestions].join("\n"), "Startup optimization");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
// Normalizes MCP server config for runtime launch and validation.
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { markClawMcpServerIndependentlyOwned } from "../state/claw-mcp-adoption.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { readSourceConfigSnapshot } from "./io.js";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Opaque revision token for cron configuration, excluding scheduler-maintained state. */
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { sha256Base64Url } from "../infra/crypto-digest.js";
|
||||
import { projectCronJobThroughStorageCodec } from "./store/row-codec.js";
|
||||
import type { CronJob } from "./types.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { sha256Base64Url } from "../infra/crypto-digest.js";
|
||||
import type { CronJob } from "./types.js";
|
||||
|
||||
|
||||
@@ -68,10 +68,6 @@ export function formatUnknownError(error: unknown): string {
|
||||
return String(error);
|
||||
}
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object";
|
||||
}
|
||||
|
||||
export function normalizeToolName(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
|
||||
+14
-12
@@ -1,4 +1,5 @@
|
||||
/** Builds bounded, redacted diagnostics for cron run logs and UI surfaces. */
|
||||
import { asOptionalObjectRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { isToolAllowedByPolicyName } from "../agents/tool-policy-match.js";
|
||||
import { normalizeToolName as normalizePolicyToolName } from "../agents/tool-policy.js";
|
||||
@@ -6,7 +7,6 @@ import { getReplyPayloadMetadata } from "../auto-reply/reply-payload.js";
|
||||
import { redactSensitiveText } from "../logging/redact.js";
|
||||
import {
|
||||
formatUnknownError,
|
||||
isRecord,
|
||||
normalizeCronRunDiagnosticSummary,
|
||||
normalizeCronRunDiagnostics as normalizeCronRunDiagnosticsValue,
|
||||
normalizeExitCode,
|
||||
@@ -165,16 +165,17 @@ function createCronRunDiagnosticsFromExecDetails(
|
||||
finalStatus?: "ok" | "error" | "skipped";
|
||||
},
|
||||
): CronRunDiagnostics | undefined {
|
||||
if (!isRecord(details)) {
|
||||
const record = asOptionalObjectRecord(details);
|
||||
if (!record) {
|
||||
return undefined;
|
||||
}
|
||||
const status = typeof details.status === "string" ? details.status : undefined;
|
||||
const exitCode = normalizeExitCode(details.exitCode);
|
||||
const status = typeof record.status === "string" ? record.status : undefined;
|
||||
const exitCode = normalizeExitCode(record.exitCode);
|
||||
const relevant = status === "failed" || (typeof exitCode === "number" && exitCode !== 0);
|
||||
if (!relevant) {
|
||||
return undefined;
|
||||
}
|
||||
const aggregated = normalizeOptionalString(details.aggregated);
|
||||
const aggregated = normalizeOptionalString(record.aggregated);
|
||||
const message = aggregated
|
||||
? tailText(aggregated, EXEC_DIAGNOSTIC_TAIL_CHARS)
|
||||
: typeof exitCode === "number"
|
||||
@@ -203,20 +204,21 @@ function createCronRunDiagnosticsFromToolPayload(
|
||||
payload: unknown,
|
||||
opts?: { nowMs?: () => number; finalStatus?: "ok" | "error" | "skipped" },
|
||||
): CronRunDiagnostics | undefined {
|
||||
if (!isRecord(payload)) {
|
||||
const record = asOptionalObjectRecord(payload);
|
||||
if (!record) {
|
||||
return undefined;
|
||||
}
|
||||
const toolName = normalizeToolName(payload.toolName) ?? normalizeToolName(payload.name);
|
||||
const detailsDiagnostics = createCronRunDiagnosticsFromExecDetails(payload.details, {
|
||||
const toolName = normalizeToolName(record.toolName) ?? normalizeToolName(record.name);
|
||||
const detailsDiagnostics = createCronRunDiagnosticsFromExecDetails(record.details, {
|
||||
nowMs: opts?.nowMs,
|
||||
toolName,
|
||||
finalStatus: opts?.finalStatus,
|
||||
});
|
||||
const isError = payload.isError === true;
|
||||
const text = typeof payload.text === "string" ? payload.text : undefined;
|
||||
const isError = record.isError === true;
|
||||
const text = typeof record.text === "string" ? record.text : undefined;
|
||||
const isNonTerminalToolWarning =
|
||||
opts?.finalStatus === "ok" &&
|
||||
getReplyPayloadMetadata(payload)?.nonTerminalToolErrorWarning === true;
|
||||
getReplyPayloadMetadata(record)?.nonTerminalToolErrorWarning === true;
|
||||
const textDiagnostics =
|
||||
isError && text
|
||||
? createCronRunDiagnosticsFromError("tool", text, {
|
||||
@@ -233,7 +235,7 @@ export function createCronRunDiagnosticsFromAgentResult(
|
||||
result: unknown,
|
||||
opts?: { nowMs?: () => number; finalStatus?: "ok" | "error" | "skipped" },
|
||||
): CronRunDiagnostics | undefined {
|
||||
const record = isRecord(result) ? result : {};
|
||||
const record = asOptionalObjectRecord(result) ?? {};
|
||||
const meta =
|
||||
record.meta && typeof record.meta === "object" ? (record.meta as Record<string, unknown>) : {};
|
||||
const diagnostics: Array<CronRunDiagnostics | undefined> = [];
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/** SQLite column codec for cron payload variants. */
|
||||
import { safeParseJson } from "@openclaw/normalization-core";
|
||||
import type { CronPayload } from "../types.js";
|
||||
import {
|
||||
booleanToInteger,
|
||||
integerToBoolean,
|
||||
normalizeNumber,
|
||||
parseJsonArray,
|
||||
parseJsonValue,
|
||||
serializeJson,
|
||||
} from "./scalar-codec.js";
|
||||
import type { CronJobInsert, CronJobRow } from "./schema.js";
|
||||
@@ -40,14 +40,14 @@ function payloadToolAllowFromRow(
|
||||
}
|
||||
|
||||
function parseExternalContentSource(raw: string | null): "gmail" | "webhook" | undefined {
|
||||
const parsed = raw ? parseJsonValue<unknown>(raw, undefined) : undefined;
|
||||
const parsed = raw ? safeParseJson(raw) : undefined;
|
||||
return parsed === "gmail" || parsed === "webhook" ? parsed : undefined;
|
||||
}
|
||||
|
||||
function parseCommandPayloadMessage(
|
||||
raw: string | null,
|
||||
): Omit<Extract<CronPayload, { kind: "command" }>, "kind" | "timeoutSeconds"> | null {
|
||||
const parsed = raw ? parseJsonValue<unknown>(raw, undefined) : undefined;
|
||||
const parsed = raw ? safeParseJson(raw) : undefined;
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return null;
|
||||
}
|
||||
@@ -92,7 +92,7 @@ function parseCommandPayloadMessage(
|
||||
function parseScriptPayloadMessage(
|
||||
raw: string | null,
|
||||
): Omit<Extract<CronPayload, { kind: "script" }>, "kind" | "timeoutSeconds"> | null {
|
||||
const parsed = raw ? parseJsonValue<unknown>(raw, undefined) : undefined;
|
||||
const parsed = raw ? safeParseJson(raw) : undefined;
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Converts cron jobs between public store shape and normalized SQLite rows. */
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { safeParseJson } from "@openclaw/normalization-core";
|
||||
import { asOptionalObjectRecord, isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { executeSqliteQuerySync } from "../../infra/kysely-sync.js";
|
||||
import { normalizeOptionalAccountId } from "../../routing/account-id.js";
|
||||
import { normalizeCronJobIdentityFields } from "../normalize-job-identity.js";
|
||||
@@ -12,12 +13,7 @@ import type { CronJob, CronJobState, CronPacing, CronSchedule, CronStoreFile } f
|
||||
import { bindDeliveryColumns, deliveryFromRow } from "./delivery-codec.js";
|
||||
import { bindFailureAlertColumns, failureAlertFromRow } from "./failure-alert-codec.js";
|
||||
import { bindPayloadColumns, payloadFromRow } from "./payload-codec.js";
|
||||
import {
|
||||
booleanToInteger,
|
||||
integerToBoolean,
|
||||
normalizeNumber,
|
||||
parseJsonObject,
|
||||
} from "./scalar-codec.js";
|
||||
import { booleanToInteger, integerToBoolean, normalizeNumber } from "./scalar-codec.js";
|
||||
import type { CronJobInsert, CronJobRow } from "./schema.js";
|
||||
import { getCronStoreKysely } from "./schema.js";
|
||||
import { bindStateColumns, stateFromRow } from "./state-codec.js";
|
||||
@@ -252,7 +248,7 @@ function scheduleFromRow(row: CronJobRow): CronSchedule | null {
|
||||
};
|
||||
}
|
||||
if (row.schedule_kind === "stream") {
|
||||
const schedule = parseJsonObject<Record<string, unknown>>(row.job_json, {}).schedule;
|
||||
const schedule = asOptionalObjectRecord(safeParseJson(row.job_json))?.schedule;
|
||||
if (!isRecord(schedule) || schedule.kind !== "stream" || !Array.isArray(schedule.command)) {
|
||||
return null;
|
||||
}
|
||||
@@ -262,7 +258,7 @@ function scheduleFromRow(row: CronJobRow): CronSchedule | null {
|
||||
}
|
||||
|
||||
function pacingFromRow(row: CronJobRow): CronPacing | undefined {
|
||||
const pacing = parseJsonObject<Record<string, unknown>>(row.job_json, {}).pacing;
|
||||
const pacing = asOptionalObjectRecord(safeParseJson(row.job_json))?.pacing;
|
||||
if (!isRecord(pacing) || Array.isArray(pacing)) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -273,7 +269,7 @@ function pacingFromRow(row: CronJobRow): CronPacing | undefined {
|
||||
}
|
||||
|
||||
function rowToCronJob(row: CronJobRow): CronJob | null {
|
||||
const jobJson = parseJsonObject<Record<string, unknown>>(row.job_json, {});
|
||||
const jobJson = asOptionalObjectRecord(safeParseJson(row.job_json)) ?? {};
|
||||
const jsonOwner = isRecord(jobJson.owner) ? jobJson.owner : undefined;
|
||||
const ownerAccountId = normalizeOptionalAccountId(
|
||||
typeof jsonOwner?.accountId === "string" ? jsonOwner.accountId : undefined,
|
||||
@@ -480,7 +476,8 @@ export function loadedCronStoreFromRows(rows: CronJobRow[]): LoadedCronStore {
|
||||
for (const [index, row] of rows.entries()) {
|
||||
const job = rowToCronJob(row);
|
||||
const configJob = mergeFailureDestinationProjection(
|
||||
parseJsonObject<Record<string, unknown>>(row.job_json, job ? stripJobRuntimeFields(job) : {}),
|
||||
asOptionalObjectRecord(safeParseJson(row.job_json)) ??
|
||||
(job ? stripJobRuntimeFields(job) : {}),
|
||||
job,
|
||||
);
|
||||
const runtimeEntry = {
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
import { safeParseJson } from "@openclaw/normalization-core";
|
||||
import { normalizeSqliteNumber } from "../../infra/sqlite-number.js";
|
||||
|
||||
/** Parses a JSON object column, returning the fallback for malformed or non-object values. */
|
||||
export function parseJsonObject<T>(raw: string, fallback: T): T {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
return parsed && typeof parsed === "object" ? (parsed as T) : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses a JSON column without shape validation, returning the fallback only on parse failure. */
|
||||
export function parseJsonValue<T>(raw: string, fallback: T): T {
|
||||
try {
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalizes SQLite number/bigint columns into JavaScript numbers. */
|
||||
export { normalizeSqliteNumber as normalizeNumber };
|
||||
|
||||
@@ -43,7 +25,7 @@ export function parseJsonArray(raw: string | null): string[] | undefined {
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = parseJsonObject<unknown>(raw, undefined);
|
||||
const parsed = safeParseJson(raw);
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((item): item is string => typeof item === "string")
|
||||
: undefined;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
/** SQLite column codec for mutable cron runtime state. */
|
||||
import { safeParseJson } from "@openclaw/normalization-core";
|
||||
import { asRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import type { CronJobState } from "../types.js";
|
||||
import {
|
||||
booleanToInteger,
|
||||
integerToBoolean,
|
||||
normalizeNumber,
|
||||
parseJsonObject,
|
||||
} from "./scalar-codec.js";
|
||||
import { booleanToInteger, integerToBoolean, normalizeNumber } from "./scalar-codec.js";
|
||||
import type { CronJobInsert, CronJobRow } from "./schema.js";
|
||||
|
||||
/** Maps mutable cron runtime state into normalized SQLite columns. */
|
||||
@@ -49,7 +46,7 @@ export function stateFromRow(row: CronJobRow): CronJobState {
|
||||
return {
|
||||
// Keep unknown runtime fields from state_json while letting indexed columns
|
||||
// win for fields that SQLite updates independently during hot-path writes.
|
||||
...parseJsonObject<CronJobState>(row.state_json, {}),
|
||||
...(asRecord(safeParseJson(row.state_json)) as CronJobState),
|
||||
...(row.next_run_at_ms != null ? { nextRunAtMs: normalizeNumber(row.next_run_at_ms) } : {}),
|
||||
...(row.running_at_ms != null ? { runningAtMs: normalizeNumber(row.running_at_ms) } : {}),
|
||||
...(row.last_run_at_ms != null ? { lastRunAtMs: normalizeNumber(row.last_run_at_ms) } : {}),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Gateway handlers expose reviewed, memory-only migration plans to trusted operators.
|
||||
import crypto from "node:crypto";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
validateMigrationsMemoryPlanParams,
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import { listAgentIds, resolveAgentWorkspaceDir } from "../../agents/agent-scope.js";
|
||||
import { stableStringify } from "../../agents/stable-stringify.js";
|
||||
import {
|
||||
applyProviderMemoryImport,
|
||||
listMemoryMigrationProviders,
|
||||
|
||||
@@ -2,12 +2,12 @@ import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type {
|
||||
WorkerInferenceStartParams,
|
||||
WorkerInferenceTerminalOutcome,
|
||||
} from "../../../packages/gateway-protocol/src/schema/worker-inference.js";
|
||||
import { stableStringify } from "../../agents/stable-stringify.js";
|
||||
import {
|
||||
closeOpenClawStateDatabaseForTest,
|
||||
openOpenClawStateDatabase,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import {
|
||||
WORKER_PROTOCOL_MAX_INFERENCE_PAYLOAD_BYTES,
|
||||
type WorkerInferenceCancelParams,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
validateWorkerInferenceTerminalFrame,
|
||||
validateWorkerInferenceTerminalOutcome,
|
||||
} from "../../../packages/gateway-protocol/src/schema/worker-inference.js";
|
||||
import { stableStringify } from "../../agents/stable-stringify.js";
|
||||
import type { OpenClawConfig } from "../../config/types.js";
|
||||
import { withTimeout } from "../../infra/fs-safe.js";
|
||||
import { boundedJsonUtf8Bytes } from "../../infra/json-utf8-bytes.js";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import type {
|
||||
WorkerTranscriptCommitParams,
|
||||
WorkerTranscriptMessage,
|
||||
} from "../../../packages/gateway-protocol/src/schema/worker-admission.js";
|
||||
import type { AgentMessage } from "../../agents/runtime/index.js";
|
||||
import { SessionManager } from "../../agents/sessions/session-manager.js";
|
||||
import { stableStringify } from "../../agents/stable-stringify.js";
|
||||
import { redactTranscriptMessage } from "../../agents/transcript-redact.js";
|
||||
import {
|
||||
loadSessionEntry,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Respawns the gateway process when no supervisor handles restart.
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { scheduleDetachedLaunchdRestartHandoff } from "../daemon/launchd-restart-handoff.js";
|
||||
import { isContainerEnvironment } from "./container-environment.js";
|
||||
import { isTruthyEnvValue } from "./env.js";
|
||||
import { formatErrorMessage } from "./errors.js";
|
||||
import { triggerOpenClawRestart } from "./restart.js";
|
||||
import { detectGatewayRespawnSupervisor } from "./supervisor-markers.js";
|
||||
@@ -23,11 +23,6 @@ type GatewayRespawnOptions = {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
function isTruthy(value: string | undefined): boolean {
|
||||
const normalized = normalizeOptionalLowercaseString(value);
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
||||
}
|
||||
|
||||
const PNPM_VERSIONED_OPENCLAW_ENTRY_PATTERN =
|
||||
/^(.*?)([\\/])node_modules\2\.pnpm\2openclaw@[^\\/]+\2node_modules\2openclaw\2.+$/;
|
||||
|
||||
@@ -83,7 +78,7 @@ function scheduleLaunchdRestartAfterExit(): GatewayRespawnResult {
|
||||
export function restartGatewayProcessWithFreshPid(
|
||||
_opts: GatewayRespawnOptions = {},
|
||||
): GatewayRespawnResult {
|
||||
if (isTruthy(process.env.OPENCLAW_NO_RESPAWN)) {
|
||||
if (isTruthyEnvValue(process.env.OPENCLAW_NO_RESPAWN)) {
|
||||
return { mode: "disabled" };
|
||||
}
|
||||
const supervisor = detectGatewayRespawnSupervisor(process.env);
|
||||
@@ -154,7 +149,7 @@ export function respawnGatewayProcessForUpdate(
|
||||
}
|
||||
return { mode: "supervised" };
|
||||
}
|
||||
if (isTruthy(process.env.OPENCLAW_NO_RESPAWN)) {
|
||||
if (isTruthyEnvValue(process.env.OPENCLAW_NO_RESPAWN)) {
|
||||
return { mode: "disabled", detail: "OPENCLAW_NO_RESPAWN" };
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Verifies staged legacy transcript rows against the committed canonical store.
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import type { TranscriptUtterance } from "../transcripts/provider-types.js";
|
||||
import { transcriptSessionSelector, TranscriptsStore } from "../transcripts/store.js";
|
||||
import {
|
||||
@@ -20,23 +21,6 @@ type StoredUtteranceRow = {
|
||||
utterance_id: string | null;
|
||||
};
|
||||
|
||||
function canonicalJson(value: unknown): string {
|
||||
if (value === undefined) {
|
||||
return "undefined";
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map(canonicalJson).join(",")}]`;
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
const record = value as Record<string, unknown>;
|
||||
return `{${Object.keys(record)
|
||||
.toSorted()
|
||||
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value) ?? "undefined";
|
||||
}
|
||||
|
||||
function storedUtteranceFromRow(row: StoredUtteranceRow): TranscriptUtterance {
|
||||
const utterance: TranscriptUtterance = { sessionId: row.session_id, text: row.text };
|
||||
if (row.utterance_id !== null) {
|
||||
@@ -88,7 +72,7 @@ export async function verifyImportedMeetingTranscriptSnapshots(params: {
|
||||
`);
|
||||
for (const snapshot of params.snapshots) {
|
||||
const session = await params.store.readSession(transcriptSessionSelector(snapshot.session));
|
||||
if (!session || canonicalJson(session) !== canonicalJson(snapshot.session)) {
|
||||
if (!session || stableStringify(session) !== stableStringify(snapshot.session)) {
|
||||
throw new Error(`meeting transcript import verification failed: ${snapshot.relativeDir}`);
|
||||
}
|
||||
for (
|
||||
@@ -110,14 +94,14 @@ export async function verifyImportedMeetingTranscriptSnapshots(params: {
|
||||
start,
|
||||
)
|
||||
.map((row) => storedUtteranceFromRow(row as StoredUtteranceRow));
|
||||
if (canonicalJson(actual) !== canonicalJson(expected)) {
|
||||
if (stableStringify(actual) !== stableStringify(expected)) {
|
||||
throw new Error(`meeting transcript import verification failed: ${snapshot.relativeDir}`);
|
||||
}
|
||||
}
|
||||
const summary = await params.store.readSummary(session);
|
||||
if (
|
||||
canonicalJson(summary.summary) !== canonicalJson(snapshot.summary) ||
|
||||
canonicalJson(summary.markdown?.trimEnd()) !== canonicalJson(snapshot.markdown?.trimEnd())
|
||||
stableStringify(summary.summary) !== stableStringify(snapshot.summary) ||
|
||||
stableStringify(summary.markdown?.trimEnd()) !== stableStringify(snapshot.markdown?.trimEnd())
|
||||
) {
|
||||
throw new Error(`meeting transcript summary verification failed: ${snapshot.relativeDir}`);
|
||||
}
|
||||
|
||||
@@ -192,12 +192,6 @@ function resolveDiagnosticLivenessRecordLevel(
|
||||
return hasBlockingWork || (event.active > 0 && hasSustainedEventLoopDelay) ? "warning" : "info";
|
||||
}
|
||||
|
||||
function isRecord(
|
||||
record: DiagnosticStabilityEventRecord | undefined,
|
||||
): record is DiagnosticStabilityEventRecord {
|
||||
return record !== undefined;
|
||||
}
|
||||
|
||||
function sanitizeDiagnosticEvent(event: DiagnosticEventPayload): DiagnosticStabilityEventRecord {
|
||||
const record: DiagnosticStabilityEventRecord = {
|
||||
seq: event.seq,
|
||||
@@ -585,12 +579,14 @@ function listRecords(): DiagnosticStabilityEventRecord[] {
|
||||
return [];
|
||||
}
|
||||
if (state.count < state.capacity) {
|
||||
return state.records.slice(0, state.count).filter(isRecord);
|
||||
return state.records
|
||||
.slice(0, state.count)
|
||||
.filter((record): record is DiagnosticStabilityEventRecord => record !== undefined);
|
||||
}
|
||||
return [
|
||||
...state.records.slice(state.nextIndex),
|
||||
...state.records.slice(0, state.nextIndex),
|
||||
].filter(isRecord);
|
||||
].filter((record): record is DiagnosticStabilityEventRecord => record !== undefined);
|
||||
}
|
||||
|
||||
function summarizeRecords(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { decodeMeetingAudioBase64 } from "./audio-base64.js";
|
||||
import { terminateMeetingBridgeProcess } from "./bridge-process.js";
|
||||
@@ -74,12 +75,6 @@ function readStringArray(value: unknown): string[] | undefined {
|
||||
return result.length > 0 ? result : undefined;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||
}
|
||||
@@ -689,7 +684,7 @@ export function createMeetingNodeHost(options: MeetingNodeHostOptions): {
|
||||
throw new Error(`${options.displayName} node host received malformed params JSON.`);
|
||||
}
|
||||
}
|
||||
const params = asRecord(raw);
|
||||
const params = asOptionalRecord(raw) ?? {};
|
||||
const action = readString(params.action);
|
||||
let result: unknown;
|
||||
switch (action) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import type {
|
||||
OpenClawPluginNodeInvokePolicy,
|
||||
OpenClawPluginNodeInvokePolicyResult,
|
||||
@@ -29,12 +30,6 @@ type PolicyDecision =
|
||||
| { approved: true; params: Record<string, unknown> }
|
||||
| { approved: false; result: OpenClawPluginNodeInvokePolicyResult };
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||
}
|
||||
@@ -258,7 +253,7 @@ export function createMeetingBrowserNodeInvokePolicy(
|
||||
if (ctx.command !== options.commandName) {
|
||||
return denied(options, `unsupported ${options.displayName} node command: ${ctx.command}`);
|
||||
}
|
||||
const params = asRecord(ctx.params);
|
||||
const params = asOptionalRecord(ctx.params) ?? {};
|
||||
const action = readString(params.action);
|
||||
if (action === "setup" && options.useConfiguredSetupCommands) {
|
||||
const setupParams: Record<string, unknown> = { action };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { TObject } from "typebox";
|
||||
import { ErrorCodes, errorShape } from "../../packages/gateway-protocol/src/schema/error-codes.js";
|
||||
@@ -79,12 +80,6 @@ export type MeetingPluginEntryOptions<
|
||||
unknownActionMessage: string;
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function readErrorDetails(error: unknown): unknown {
|
||||
return error && typeof error === "object" && "details" in error
|
||||
? (error as { details?: unknown }).details
|
||||
@@ -251,7 +246,7 @@ export function createMeetingPluginEntryOptions<
|
||||
`${options.gatewayMethodPrefix}.join`,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = keepTrustedToolContext(asRecord(params), client);
|
||||
const raw = keepTrustedToolContext(asOptionalRecord(params) ?? {}, client);
|
||||
respond(true, await (await ensureRuntime()).join(joinRequest(raw)));
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
@@ -262,7 +257,7 @@ export function createMeetingPluginEntryOptions<
|
||||
`${options.gatewayMethodPrefix}.leave`,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const raw = asOptionalRecord(params) ?? {};
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const sessionId = requireString(raw.sessionId, "sessionId");
|
||||
const rt = await ensureRuntime();
|
||||
@@ -281,7 +276,7 @@ export function createMeetingPluginEntryOptions<
|
||||
`${options.gatewayMethodPrefix}.status`,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const raw = asOptionalRecord(params) ?? {};
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const rt = await ensureRuntime();
|
||||
respond(
|
||||
@@ -299,7 +294,7 @@ export function createMeetingPluginEntryOptions<
|
||||
`${options.gatewayMethodPrefix}.transcript`,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const raw = asOptionalRecord(params) ?? {};
|
||||
const sessionId = requireString(raw.sessionId, "sessionId");
|
||||
const sinceIndex = readSinceIndex(raw);
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
@@ -319,7 +314,7 @@ export function createMeetingPluginEntryOptions<
|
||||
`${options.gatewayMethodPrefix}.speak`,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asRecord(params);
|
||||
const raw = asOptionalRecord(params) ?? {};
|
||||
const sessionId = requireString(raw.sessionId, "sessionId");
|
||||
const agentId = trustedToolAgentId(raw, client);
|
||||
const rt = await ensureRuntime();
|
||||
@@ -368,7 +363,7 @@ export function createMeetingPluginEntryOptions<
|
||||
method,
|
||||
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = keepTrustedToolContext(asRecord(params), client);
|
||||
const raw = keepTrustedToolContext(asOptionalRecord(params) ?? {}, client);
|
||||
respond(true, await run(await ensureRuntime(), raw));
|
||||
} catch (error) {
|
||||
sendRequestError(respond, error);
|
||||
@@ -383,7 +378,7 @@ export function createMeetingPluginEntryOptions<
|
||||
description: options.toolDescription,
|
||||
parameters: options.toolParameters,
|
||||
async execute(_toolCallId, params) {
|
||||
const raw = asRecord(params);
|
||||
const raw = asOptionalRecord(params) ?? {};
|
||||
const action = raw.action as MeetingToolAction;
|
||||
const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
|
||||
const contextAgentId =
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import type { PluginRuntime, RuntimeLogger } from "../plugins/runtime/types.js";
|
||||
import { decodeMeetingAudioBase64 } from "./audio-base64.js";
|
||||
@@ -9,12 +10,6 @@ const NODE_OUTPUT_GENERATION_CAPABILITY = Symbol.for(
|
||||
"openclaw.internal.meeting-node-output-generation.v1",
|
||||
);
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value : undefined;
|
||||
}
|
||||
@@ -85,7 +80,8 @@ export function createNodeMeetingRealtimeAudioTransport(params: {
|
||||
params: { action: "pullAudio", bridgeId: params.bridgeId, timeoutMs: 250 },
|
||||
timeoutMs: 2_000,
|
||||
});
|
||||
const result = asRecord(asRecord(raw).payload ?? raw);
|
||||
const rawRecord = asOptionalRecord(raw);
|
||||
const result = asOptionalRecord(rawRecord?.payload ?? raw) ?? {};
|
||||
const base64 = readString(result.base64);
|
||||
if (base64) {
|
||||
const audio = decodeMeetingAudioBase64(base64, "pullAudio");
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
|
||||
import { isVitestRuntimeEnv } from "../infra/env.js";
|
||||
import { isTruthyEnvValue, isVitestRuntimeEnv } from "../infra/env.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import { isPathInside } from "../infra/path-guards.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
@@ -39,11 +39,6 @@ function isSourceCheckoutRoot(packageRoot: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isTruthyEnvValue(value: string | undefined): boolean {
|
||||
const normalized = value?.trim().toLowerCase();
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
||||
}
|
||||
|
||||
function shouldTrustTestBundledPluginsDirOverride(env: NodeJS.ProcessEnv): boolean {
|
||||
const isVitestProcess = isVitestRuntimeEnv(env) || isVitestRuntimeEnv(process.env);
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Checks web-search credential presence from config and plugin metadata.
|
||||
import { asOptionalObjectRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js";
|
||||
import type { PluginManifestRecord } from "./manifest-registry.js";
|
||||
@@ -10,27 +11,24 @@ function hasConfiguredCredentialValue(value: unknown): boolean {
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function hasConfiguredSearchCredentialCandidate(searchConfig: unknown): boolean {
|
||||
if (!isRecord(searchConfig)) {
|
||||
const record = asOptionalObjectRecord(searchConfig);
|
||||
if (!record) {
|
||||
return false;
|
||||
}
|
||||
return Object.entries(searchConfig).some(
|
||||
return Object.entries(record).some(
|
||||
([key, value]) => key !== "enabled" && hasConfiguredCredentialValue(value),
|
||||
);
|
||||
}
|
||||
|
||||
function hasConfiguredPluginWebSearchCandidate(config: OpenClawConfig): boolean {
|
||||
const entries = isRecord(config.plugins?.entries) ? config.plugins.entries : undefined;
|
||||
const entries = asOptionalObjectRecord(config.plugins?.entries);
|
||||
if (!entries) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(entries).some((entry) => {
|
||||
const pluginConfig = isRecord(entry) ? entry.config : undefined;
|
||||
return isRecord(pluginConfig) && hasConfiguredSearchCredentialCandidate(pluginConfig.webSearch);
|
||||
const pluginConfig = asOptionalObjectRecord(entry)?.config;
|
||||
return hasConfiguredSearchCredentialCandidate(asOptionalObjectRecord(pluginConfig)?.webSearch);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ import { request as httpsRequest } from "node:https";
|
||||
import net from "node:net";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
import { URL } from "node:url";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { ensureDebugProxyCa } from "./ca.js";
|
||||
import type { DebugProxySettings } from "./env.js";
|
||||
import { redactedCaptureHeaders } from "./header-redaction.js";
|
||||
import { getDebugProxyCaptureStore } from "./store.sqlite.js";
|
||||
import type { CaptureEventRecord } from "./types.js";
|
||||
|
||||
const TRUTHY_ENV = new Set(["1", "true", "yes", "on"]);
|
||||
const DEBUG_PROXY_DIRECT_CONNECT_OVERRIDE =
|
||||
"OPENCLAW_DEBUG_PROXY_ALLOW_DIRECT_CONNECT_WITH_MANAGED_PROXY";
|
||||
const CAPTURE_BODY_PREVIEW_BYTES = 8192;
|
||||
@@ -25,10 +25,6 @@ type BodyPreviewCapture = {
|
||||
truncated: boolean;
|
||||
};
|
||||
|
||||
function isTruthyEnvValue(value: string | undefined): boolean {
|
||||
return TRUTHY_ENV.has((value ?? "").trim().toLowerCase());
|
||||
}
|
||||
|
||||
function isManagedProxyActive(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
return isTruthyEnvValue(env["OPENCLAW_PROXY_ACTIVE"]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Process-local identity for the non-secret config that an owner may use with a credential. */
|
||||
import { createHash } from "node:crypto";
|
||||
import { stableStringify } from "../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { coerceSecretRef } from "../config/types.secrets.js";
|
||||
import { secretRefKey } from "./ref-contract.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Session snapshot helpers capture and restore runtime skill state for sessions.
|
||||
import crypto from "node:crypto";
|
||||
import { stableStringify } from "../../agents/stable-stringify.js";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import { redactConfigObject } from "../../config/redact-snapshot.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { matchesSkillFilter } from "../discovery/filter.js";
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type OverlayHandle,
|
||||
type SelectItem,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import { asOptionalObjectRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { isApprovalStaleError } from "../infra/approval-errors.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { selectListTheme, theme } from "./theme/theme.js";
|
||||
@@ -126,10 +127,6 @@ const DECISION_ITEMS: Record<TuiApprovalDecision, SelectItem> = {
|
||||
},
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function parseDecision(value: unknown): TuiApprovalDecision | null {
|
||||
return value === "allow-once" || value === "allow-always" || value === "deny" ? value : null;
|
||||
}
|
||||
@@ -154,13 +151,15 @@ function parseSeverity(value: unknown): TuiPluginApproval["request"]["severity"]
|
||||
|
||||
/** Parses the gateway event/list shape used for pending plugin approvals. */
|
||||
function parseTuiPluginApproval(payload: unknown): TuiPluginApproval | null {
|
||||
if (!isRecord(payload) || !isRecord(payload.request)) {
|
||||
const record = asOptionalObjectRecord(payload);
|
||||
const request = asOptionalObjectRecord(record?.request);
|
||||
if (!record || !request) {
|
||||
return null;
|
||||
}
|
||||
const id = typeof payload.id === "string" ? payload.id.trim() : "";
|
||||
const title = typeof payload.request.title === "string" ? payload.request.title.trim() : "";
|
||||
const createdAtMs = typeof payload.createdAtMs === "number" ? payload.createdAtMs : 0;
|
||||
const expiresAtMs = typeof payload.expiresAtMs === "number" ? payload.expiresAtMs : 0;
|
||||
const id = typeof record.id === "string" ? record.id.trim() : "";
|
||||
const title = typeof request.title === "string" ? request.title.trim() : "";
|
||||
const createdAtMs = typeof record.createdAtMs === "number" ? record.createdAtMs : 0;
|
||||
const expiresAtMs = typeof record.expiresAtMs === "number" ? record.expiresAtMs : 0;
|
||||
if (!id || !title || !createdAtMs || !expiresAtMs) {
|
||||
return null;
|
||||
}
|
||||
@@ -168,15 +167,13 @@ function parseTuiPluginApproval(payload: unknown): TuiPluginApproval | null {
|
||||
id,
|
||||
request: {
|
||||
title,
|
||||
description:
|
||||
typeof payload.request.description === "string" ? payload.request.description : null,
|
||||
pluginId: typeof payload.request.pluginId === "string" ? payload.request.pluginId : null,
|
||||
severity: parseSeverity(payload.request.severity),
|
||||
toolName: typeof payload.request.toolName === "string" ? payload.request.toolName : null,
|
||||
allowedDecisions: parseAllowedDecisions(payload.request.allowedDecisions),
|
||||
agentId: typeof payload.request.agentId === "string" ? payload.request.agentId : null,
|
||||
sessionKey:
|
||||
typeof payload.request.sessionKey === "string" ? payload.request.sessionKey : null,
|
||||
description: typeof request.description === "string" ? request.description : null,
|
||||
pluginId: typeof request.pluginId === "string" ? request.pluginId : null,
|
||||
severity: parseSeverity(request.severity),
|
||||
toolName: typeof request.toolName === "string" ? request.toolName : null,
|
||||
allowedDecisions: parseAllowedDecisions(request.allowedDecisions),
|
||||
agentId: typeof request.agentId === "string" ? request.agentId : null,
|
||||
sessionKey: typeof request.sessionKey === "string" ? request.sessionKey : null,
|
||||
},
|
||||
createdAtMs,
|
||||
expiresAtMs,
|
||||
@@ -184,10 +181,11 @@ function parseTuiPluginApproval(payload: unknown): TuiPluginApproval | null {
|
||||
}
|
||||
|
||||
function parseResolvedApprovalId(payload: unknown): string | null {
|
||||
if (!isRecord(payload) || typeof payload.id !== "string") {
|
||||
const id = asOptionalObjectRecord(payload)?.id;
|
||||
if (typeof id !== "string") {
|
||||
return null;
|
||||
}
|
||||
return payload.id.trim() || null;
|
||||
return id.trim() || null;
|
||||
}
|
||||
|
||||
function decisionLabel(decision: TuiApprovalDecision): string {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type OverlayHandle,
|
||||
type SelectItem,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import { asOptionalObjectRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import type { TaskSuggestion } from "../../packages/gateway-protocol/src/index.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { selectListTheme, theme } from "./theme/theme.js";
|
||||
@@ -64,33 +65,30 @@ function sanitizeTaskText(text: string): string {
|
||||
return sanitizeRenderableText(text.replace(TASK_BIDI_CONTROL_RE, ""));
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
/** Parses the task suggestion shape carried by Gateway list and event payloads. */
|
||||
function parseTuiTaskSuggestion(value: unknown): TaskSuggestion | null {
|
||||
if (!isRecord(value)) {
|
||||
const record = asOptionalObjectRecord(value);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
const required = ["id", "title", "prompt", "tldr", "cwd", "sessionKey"] as const;
|
||||
if (required.some((field) => typeof value[field] !== "string" || !value[field].trim())) {
|
||||
if (required.some((field) => typeof record[field] !== "string" || !record[field].trim())) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value.createdAt !== "number" || value.createdAt < 0) {
|
||||
if (typeof record.createdAt !== "number" || record.createdAt < 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: (value.id as string).trim(),
|
||||
title: (value.title as string).trim(),
|
||||
prompt: (value.prompt as string).trim(),
|
||||
tldr: (value.tldr as string).trim(),
|
||||
cwd: (value.cwd as string).trim(),
|
||||
sessionKey: (value.sessionKey as string).trim(),
|
||||
...(typeof value.agentId === "string" && value.agentId.trim()
|
||||
? { agentId: value.agentId.trim() }
|
||||
id: (record.id as string).trim(),
|
||||
title: (record.title as string).trim(),
|
||||
prompt: (record.prompt as string).trim(),
|
||||
tldr: (record.tldr as string).trim(),
|
||||
cwd: (record.cwd as string).trim(),
|
||||
sessionKey: (record.sessionKey as string).trim(),
|
||||
...(typeof record.agentId === "string" && record.agentId.trim()
|
||||
? { agentId: record.agentId.trim() }
|
||||
: {}),
|
||||
createdAt: value.createdAt,
|
||||
createdAt: record.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -410,11 +408,12 @@ export function createTuiTaskSuggestionController(deps: TaskSuggestionController
|
||||
|
||||
return {
|
||||
handleEvent(event: string, payload: unknown) {
|
||||
if (disposed || event !== "task.suggestion" || !isRecord(payload)) {
|
||||
const record = asOptionalObjectRecord(payload);
|
||||
if (disposed || event !== "task.suggestion" || !record) {
|
||||
return;
|
||||
}
|
||||
if (payload.action === "created") {
|
||||
const suggestion = parseTuiTaskSuggestion(payload.suggestion);
|
||||
if (record.action === "created") {
|
||||
const suggestion = parseTuiTaskSuggestion(record.suggestion);
|
||||
if (suggestion) {
|
||||
revision += 1;
|
||||
hiddenIds.delete(suggestion.id);
|
||||
@@ -423,8 +422,8 @@ export function createTuiTaskSuggestionController(deps: TaskSuggestionController
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (payload.action === "resolved" && typeof payload.taskId === "string") {
|
||||
remove(payload.taskId);
|
||||
if (record.action === "resolved" && typeof record.taskId === "string") {
|
||||
remove(record.taskId);
|
||||
presentNext();
|
||||
deps.requestRender();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user