diff --git a/extensions/google/embedding-batch.ts b/extensions/google/embedding-batch.ts index 10ac7217d42..0f74174579f 100644 --- a/extensions/google/embedding-batch.ts +++ b/extensions/google/embedding-batch.ts @@ -2,7 +2,6 @@ import crypto from "node:crypto"; import { buildEmbeddingBatchGroupOptions, runEmbeddingBatchGroups, - type EmbeddingBatchExecutionParams, buildBatchHeaders, debugEmbeddingsLog, normalizeBatchBaseUrl, @@ -12,6 +11,14 @@ import { import { createProviderHttpError } from "openclaw/plugin-sdk/provider-http"; import type { GeminiEmbeddingClient, GeminiTextEmbeddingRequest } from "./embedding-provider.js"; +type EmbeddingBatchExecutionParams = { + wait: boolean; + pollIntervalMs: number; + timeoutMs: number; + concurrency: number; + debug?: (message: string, data?: Record) => void; +}; + export type GeminiBatchRequest = { custom_id: string; request: GeminiTextEmbeddingRequest; diff --git a/extensions/memory-core/src/memory/manager-embedding-ops.ts b/extensions/memory-core/src/memory/manager-embedding-ops.ts index ef783f95024..06dab0e915c 100644 --- a/extensions/memory-core/src/memory/manager-embedding-ops.ts +++ b/extensions/memory-core/src/memory/manager-embedding-ops.ts @@ -7,14 +7,12 @@ import { type MemoryEmbeddingProviderRuntime, } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; import { createSubsystemLogger } from "openclaw/plugin-sdk/memory-core-host-engine-foundation"; -import { type SessionFileEntry } from "openclaw/plugin-sdk/memory-core-host-engine-qmd"; import { buildMultimodalChunkForIndexing, chunkMarkdown, hashText, remapChunkLines, type MemoryChunk, - type MemoryFileEntry, type MemorySource, } from "openclaw/plugin-sdk/memory-core-host-engine-storage"; import { @@ -55,6 +53,17 @@ const EMBEDDING_BATCH_TIMEOUT_LOCAL_MS = 10 * 60_000; const log = createSubsystemLogger("memory"); +type MemoryIndexEntry = { + path: string; + absPath: string; + mtimeMs: number; + size: number; + hash: string; + kind?: "markdown" | "multimodal"; + contentText?: string; + lineMap?: number[]; +}; + export function resolveEmbeddingTimeoutMs(params: { kind: "query" | "batch"; providerId?: string; @@ -207,7 +216,7 @@ export abstract class MemoryManagerEmbeddingOps extends MemoryManagerSyncOps { private async embedChunksWithBatch( chunks: MemoryChunk[], - _entry: MemoryFileEntry | SessionFileEntry, + _entry: MemoryIndexEntry, source: MemorySource, ): Promise { const provider = this.provider; @@ -544,7 +553,7 @@ export abstract class MemoryManagerEmbeddingOps extends MemoryManagerSyncOps { this.db.prepare(`DELETE FROM chunks WHERE path = ? AND source = ?`).run(pathname, source); } - private upsertFileRecord(entry: MemoryFileEntry | SessionFileEntry, source: MemorySource): void { + private upsertFileRecord(entry: MemoryIndexEntry, source: MemorySource): void { this.db .prepare( `INSERT INTO files (path, source, hash, mtime, size) VALUES (?, ?, ?, ?, ?) @@ -567,7 +576,7 @@ export abstract class MemoryManagerEmbeddingOps extends MemoryManagerSyncOps { * Pass an empty embeddings array to skip vector writes (FTS-only mode). */ private writeChunks( - entry: MemoryFileEntry | SessionFileEntry, + entry: MemoryIndexEntry, source: MemorySource, model: string, chunks: MemoryChunk[], @@ -634,7 +643,7 @@ export abstract class MemoryManagerEmbeddingOps extends MemoryManagerSyncOps { } protected async indexFile( - entry: MemoryFileEntry | SessionFileEntry, + entry: MemoryIndexEntry, options: { source: MemorySource; content?: string }, ) { // FTS-only mode: no embedding provider, but we can still build a FTS index diff --git a/extensions/memory-core/src/memory/manager-sync-ops.ts b/extensions/memory-core/src/memory/manager-sync-ops.ts index 2d34a630082..f32d52aba3d 100644 --- a/extensions/memory-core/src/memory/manager-sync-ops.ts +++ b/extensions/memory-core/src/memory/manager-sync-ops.ts @@ -19,7 +19,6 @@ import { buildSessionEntry, listSessionFilesForAgent, sessionPathForFile, - type SessionFileEntry, } from "openclaw/plugin-sdk/memory-core-host-engine-qmd"; import { buildFileEntry, @@ -29,7 +28,6 @@ import { loadSqliteVecExtension, normalizeExtraMemoryPaths, runWithConcurrency, - type MemoryFileEntry, type MemorySource, type MemorySyncProgressUpdate, } from "openclaw/plugin-sdk/memory-core-host-engine-storage"; @@ -67,6 +65,15 @@ type MemorySyncProgressState = { report: (update: MemorySyncProgressUpdate) => void; }; +type MemoryIndexEntry = { + path: string; + absPath: string; + mtimeMs: number; + size: number; + hash: string; + content?: string; +}; + const META_KEY = "memory_index_meta_v1"; const VECTOR_TABLE = "chunks_vec"; const FTS_TABLE = "chunks_fts"; @@ -197,7 +204,7 @@ export abstract class MemoryManagerSyncOps { protected abstract getIndexConcurrency(): number; protected abstract pruneEmbeddingCacheIfNeeded(): void; protected abstract indexFile( - entry: MemoryFileEntry | SessionFileEntry, + entry: MemoryIndexEntry, options: { source: MemorySource; content?: string }, ): Promise; @@ -712,7 +719,7 @@ export abstract class MemoryManagerSyncOps { ), this.getIndexConcurrency(), ) - ).filter((entry): entry is MemoryFileEntry => entry !== null); + ).filter((entry): entry is MemoryIndexEntry => entry !== null); log.debug("memory sync: indexing memory files", { files: fileEntries.length, needsFullReindex: params.needsFullReindex, diff --git a/extensions/openai/embedding-batch.ts b/extensions/openai/embedding-batch.ts index ccbc0e3e462..f26707cbb4d 100644 --- a/extensions/openai/embedding-batch.ts +++ b/extensions/openai/embedding-batch.ts @@ -11,7 +11,6 @@ import { resolveCompletedBatchResult, runEmbeddingBatchGroups, throwIfBatchTerminalFailure, - type EmbeddingBatchExecutionParams, type EmbeddingBatchStatus, type BatchCompletionResult, type ProviderBatchOutputLine, @@ -20,6 +19,14 @@ import { } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; import type { OpenAiEmbeddingClient } from "./embedding-provider.js"; +type EmbeddingBatchExecutionParams = { + wait: boolean; + pollIntervalMs: number; + timeoutMs: number; + concurrency: number; + debug?: (message: string, data?: Record) => void; +}; + export type OpenAiBatchRequest = { custom_id: string; method: "POST"; diff --git a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts index 1a51b1cc26c..bcf62f1d904 100644 --- a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts +++ b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts @@ -466,6 +466,14 @@ describe("telegram live qa runtime", () => { expect(signal?.aborted).toBe(true); }); + it("treats transient Telegram getUpdates network errors as recoverable", () => { + expect(__testing.isRecoverableTelegramQaPollError(new TypeError("fetch failed"))).toBe(true); + expect(__testing.isRecoverableTelegramQaPollError(new Error("socket hang up"))).toBe(true); + expect( + __testing.isRecoverableTelegramQaPollError(new Error("Bad Request: chat not found")), + ).toBe(false); + }); + it("redacts observed message content by default in artifacts", () => { expect( __testing.buildObservedMessagesArtifact({ diff --git a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts index fa4357322c5..d0c80a44983 100644 --- a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts +++ b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts @@ -550,6 +550,17 @@ async function callTelegramApi( } } +function isRecoverableTelegramQaPollError(error: unknown): boolean { + const message = formatErrorMessage(error).toLowerCase(); + return ( + message.includes("fetch failed") || + message.includes("econnreset") || + message.includes("etimedout") || + message.includes("socket hang up") || + message.includes("terminated") + ); +} + async function getBotIdentity(token: string) { return await callTelegramApi(token, "getMe"); } @@ -601,16 +612,24 @@ async function waitForObservedMessage(params: { Math.min(10_000, params.timeoutMs - (Date.now() - startedAt)), ); const timeoutSeconds = Math.max(1, Math.min(10, Math.floor(remainingMs / 1000))); - const updates = await callTelegramApi( - params.token, - "getUpdates", - { - offset, - timeout: timeoutSeconds, - allowed_updates: ["message"], - }, - timeoutSeconds * 1000 + 5_000, - ); + let updates: TelegramUpdate[]; + try { + updates = await callTelegramApi( + params.token, + "getUpdates", + { + offset, + timeout: timeoutSeconds, + allowed_updates: ["message"], + }, + timeoutSeconds * 1000 + 5_000, + ); + } catch (error) { + if (!isRecoverableTelegramQaPollError(error)) { + throw error; + } + continue; + } const batchObservedAtMs = Date.now(); if (updates.length === 0) { continue; @@ -1440,6 +1459,7 @@ export const __testing = { buildObservedMessagesArtifact, canaryFailureMessage, callTelegramApi, + isRecoverableTelegramQaPollError, assertTelegramScenarioReply, classifyCanaryReply, findScenario, diff --git a/scripts/docker/install-sh-smoke/run.sh b/scripts/docker/install-sh-smoke/run.sh index 8265a2f763e..c4e3a7a7e0b 100755 --- a/scripts/docker/install-sh-smoke/run.sh +++ b/scripts/docker/install-sh-smoke/run.sh @@ -109,6 +109,13 @@ run_with_heartbeat() { return "$status" } +is_self_swapped_package_process_exit() { + local stderr="$1" + [[ "$stderr" == *"[openclaw] Failed to start CLI:"* ]] && + [[ "$stderr" == *"ERR_MODULE_NOT_FOUND"* ]] && + [[ "$stderr" == *"/node_modules/openclaw/dist/"* ]] +} + npm_install_global() { local label="$1" shift @@ -262,8 +269,12 @@ run_update_smoke() { printf "%s\n" "$update_stderr" >&2 fi if [[ "$update_status" -ne 0 ]]; then - echo "ERROR: openclaw update failed with exit code $update_status" >&2 - return "$update_status" + if is_self_swapped_package_process_exit "$update_stderr"; then + echo "WARN: legacy updater process exited after self-swap; validating update JSON and installed CLI" >&2 + else + echo "ERROR: openclaw update failed with exit code $update_status" >&2 + return "$update_status" + fi fi UPDATE_JSON="$UPDATE_JSON" \ diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index be7533d76df..1664d9f4648 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -122,6 +122,8 @@ describe("install-sh smoke runner", () => { expect(script).toContain("print_install_audit"); expect(script).toContain('install -g "$@"'); expect(script).toContain("openclaw update --tag"); + expect(script).toContain("is_self_swapped_package_process_exit"); + expect(script).toContain("legacy updater process exited after self-swap"); expect(script).toContain("parseFirstJsonObject"); expect(script).toContain("unterminated update JSON object"); });