refactor: move session/transcript runtime state to SQLite

Migrate OpenClaw session, transcript, and runtime state off per-file
.jsonl/JSON storage onto the SQLite state and agent databases, brought up to
date with current main. Channel plugins persist via the plugin-state SDK
keyed-store seam rather than the raw DB; storage-heavy plugins keep their own
SQLite stores via SDK path helpers.
This commit is contained in:
Josh Lehman
2026-05-31 09:23:11 -07:00
parent 4b1e5b7943
commit 43ea501f38
3309 changed files with 175707 additions and 136819 deletions
+13 -14
View File
@@ -15,6 +15,8 @@ import process from "node:process";
import { pathToFileURL } from "node:url";
import { resolveDefaultAgentDir } from "../src/agents/agent-scope.js";
import { ensureAuthProfileStore, type AuthProfileCredential } from "../src/agents/auth-profiles.js";
import { savePersistedAuthProfileSecretsStore } from "../src/agents/auth-profiles/persisted.js";
import type { AuthProfileSecretsStore } from "../src/agents/auth-profiles/types.js";
import { normalizeProviderId } from "../src/agents/model-selection.js";
import { validateAnthropicSetupToken } from "../src/commands/auth-token.js";
import { callGateway } from "../src/gateway/call.js";
@@ -584,22 +586,19 @@ async function runGatewayPrompt(prompt: string): Promise<PromptResult> {
2,
)}\n`,
);
await fs.writeFile(
path.join(agentDir, "auth-profiles.json"),
`${JSON.stringify(
{
version: 1,
profiles: {
[tokenSource.profileId]: {
type: "token",
provider: "anthropic",
token: tokenSource.token,
},
savePersistedAuthProfileSecretsStore(
{
version: 1,
profiles: {
[tokenSource.profileId]: {
type: "token",
provider: "anthropic",
token: tokenSource.token,
},
},
null,
2,
)}\n`,
} as AuthProfileSecretsStore,
agentDir,
{ env: { ...process.env, OPENCLAW_STATE_DIR: stateDir } },
);
const gateway = await startGatewayProcess({
+65
View File
@@ -0,0 +1,65 @@
import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { appendUnreleasedChangelogEntry } from "../src/infra/changelog-unreleased.js";
type SectionArg = "breaking" | "changes" | "fixes";
function parseArgs(argv: string[]): {
changelogPath: string;
section: "Breaking" | "Changes" | "Fixes";
entry: string;
} {
let changelogPath = resolve("CHANGELOG.md");
let section: SectionArg | undefined;
const entryParts: string[] = [];
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--file") {
const next = argv[index + 1];
if (!next) {
throw new Error("Missing value for --file.");
}
changelogPath = resolve(next);
index += 1;
continue;
}
if (arg === "--section") {
const next = argv[index + 1] as SectionArg | undefined;
if (!next || !["breaking", "changes", "fixes"].includes(next)) {
throw new Error("Missing or invalid value for --section.");
}
section = next;
index += 1;
continue;
}
entryParts.push(arg);
}
if (!section) {
throw new Error("Missing required --section <breaking|changes|fixes>.");
}
const entry = entryParts.join(" ").trim();
if (!entry) {
throw new Error("Missing changelog entry text.");
}
return {
changelogPath,
section: section === "breaking" ? "Breaking" : section === "changes" ? "Changes" : "Fixes",
entry,
};
}
if (import.meta.main) {
const { changelogPath, section, entry } = parseArgs(process.argv.slice(2));
const content = readFileSync(changelogPath, "utf8");
const next = appendUnreleasedChangelogEntry(content, {
section,
entry,
});
if (next !== content) {
writeFileSync(changelogPath, next);
}
console.log(`Updated ${changelogPath} (${section}).`);
}
+13
View File
@@ -37,6 +37,16 @@ const CORE_LINT_OPTIMIZATION_NEUTRAL_PATH_RE =
/^(?:scripts|test\/scripts)\/|^\.github\/workflows\/ci\.yml$/u;
let corepackPnpmShimDir;
const KYSELY_CODEGEN_PATHS = new Set([
"scripts/generate-kysely-types.mjs",
"src/state/openclaw-agent-db.generated.d.ts",
"src/state/openclaw-agent-schema.sql",
"src/state/openclaw-agent-schema.generated.ts",
"src/state/openclaw-state-db.generated.d.ts",
"src/state/openclaw-state-schema.sql",
"src/state/openclaw-state-schema.generated.ts",
]);
export function createChangedCheckChildEnv(baseEnv = process.env) {
const resolvedBaseEnv = resolveLocalHeavyCheckEnv(baseEnv);
return {
@@ -194,6 +204,9 @@ export function createChangedCheckPlan(result, options = {}) {
);
}
add("package patch guard", ["deps:patches:check"]);
if (result.paths.some((changedPath) => KYSELY_CODEGEN_PATHS.has(changedPath))) {
add("Kysely generated database types", ["db:kysely:check"]);
}
if (result.docsOnly) {
return {
@@ -0,0 +1,803 @@
#!/usr/bin/env node
import { promises as fs } from "node:fs";
import path from "node:path";
import { resolveRepoRoot, runAsScript } from "./lib/ts-guard-utils.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const sourceRoots = ["src", "extensions", "packages", "ui", "apps"];
const bridgeContractRoots = [...sourceRoots, "test"];
const sourceExtensions = new Set([".ts", ".tsx", ".mts", ".js", ".mjs", ".swift", ".kt"]);
const displayPathRoots = ["docs", "scripts"];
const displayPathExtensions = new Set([".md", ".mdx", ".ts", ".tsx", ".mts", ".js", ".mjs", ".sh"]);
const legacyStoreMarkers = [
{ label: "sessions.json", pattern: /\bsessions\.json\b/u },
{ label: "legacy transcript lock file", pattern: /\.jsonl\.lock\b/u },
{ label: "cron jobs JSON", pattern: /\bjobs\.json\b/u },
{ label: "cron jobs state JSON", pattern: /\bjobs-state\.json\b/u },
{ label: "cron run JSONL log", pattern: /\bcron[/\\]runs[/\\][A-Za-z0-9._-]+\.jsonl\b/u },
{ label: "trajectory JSONL sidecar", pattern: /\.trajectory\.jsonl\b/u },
{ label: "ACP stream JSONL sidecar", pattern: /\.acp-stream\.jsonl\b/u },
{ label: "ACP event ledger JSON", pattern: /\bacp[/\\]event-ledger\.json\b/u },
{ label: "runtime cache JSON", pattern: /\bcache[/\\][A-Za-z0-9._-]+\.json\b/u },
{ label: "voice-call JSONL call log", pattern: /\bcalls\.jsonl\b/u },
{ label: "device-pair notify JSON", pattern: /\bdevice-pair-notify\.json\b/u },
{ label: "Active Memory session toggles JSON", pattern: /\bsession-toggles\.json\b/u },
{ label: "Nostr bus state JSON", pattern: /\bbus-state-[A-Za-z0-9._-]+\.json\b/u },
{ label: "Nostr profile state JSON", pattern: /\bprofile-state-[A-Za-z0-9._-]+\.json\b/u },
{ label: "Skill Workshop proposal JSON", pattern: /\bskill-workshop[/\\][a-f0-9]{16}\.json\b/iu },
{
label: "Skill Workshop reviewer session JSON",
pattern: /\bskill-workshop[/\\]skill-workshop-review-[A-Za-z0-9._-]+\.json\b/u,
},
{
label: "outbound delivery queue JSON",
pattern: /\bdelivery-queue[/\\][A-Za-z0-9._-]+\.json\b/u,
},
{
label: "session delivery queue JSON",
pattern: /\bsession-delivery-queue[/\\][A-Za-z0-9._-]+\.json\b/u,
},
{ label: "subagent registry JSON", pattern: /\bsubagents[/\\]runs\.json\b/u },
{ label: "OpenRouter model cache JSON", pattern: /\bopenrouter-models\.json\b/u },
{ label: "auth profile JSON", pattern: /\bauth-profiles\.json\b/u },
{ label: "auth profile state JSON", pattern: /\bauth-state\.json\b/u },
{
label: "retired per-agent auth JSON",
pattern: /\bagents[/\\][A-Za-z0-9._-]+[/\\]agent[/\\]auth\.json\b/u,
},
{
label: "retired per-agent model catalog JSON",
pattern: /\bagents[/\\][A-Za-z0-9._-]+[/\\]agent[/\\]models\.json\b/u,
},
{ label: "retired shared OAuth JSON", pattern: /\bcredentials[/\\]oauth\.json\b/u },
{ label: "exec approvals JSON", pattern: /\bexec-approvals\.json\b/u },
{ label: "workspace setup JSON", pattern: /\bworkspace-state\.json\b/u },
{
label: "pairing pending/paired JSON",
pattern: /\b(?:devices|nodes)[/\\](?:pending|paired)\.json\b/u,
},
{
label: "device bootstrap JSON",
pattern: /\bdevices[/\\]bootstrap\.json\b/u,
},
{ label: "device identity JSON", pattern: /\bidentity[/\\]device\.json\b/u },
{ label: "device auth JSON", pattern: /\bidentity[/\\]device-auth\.json\b/u },
{
label: "web push subscription JSON",
pattern: /\bpush[/\\]web-push-subscriptions\.json\b/u,
},
{ label: "web push VAPID JSON", pattern: /\bpush[/\\]vapid-keys\.json\b/u },
{ label: "APNs registration JSON", pattern: /\bpush[/\\]apns-registrations\.json\b/u },
{ label: "exec approvals JSON", pattern: /\bexec-approvals\.json\b/u },
{ label: "ACPX process leases JSON", pattern: /\bprocess-leases\.json\b/u },
{ label: "ACPX gateway instance id file", pattern: /\bgateway-instance-id\b/u },
{
label: "memory-core dreaming event JSONL",
pattern: /\bmemory[/\\]\.dreams[/\\]events\.jsonl\b/u,
},
{
label: "memory-core dreaming session corpus",
pattern: /\bmemory[/\\]\.dreams[/\\]session-corpus\b/u,
},
{
label: "memory-core dreaming checkpoint JSON",
pattern:
/\bmemory[/\\]\.dreams[/\\](?:daily-ingestion|session-ingestion|short-term-recall|phase-signals)\.json\b/u,
},
{ label: "file-shaped memory index table", pattern: /\bmemory_index_files\b/u },
{
label: "memory-core dreaming promotion lock",
pattern: /\bmemory[/\\]\.dreams[/\\]short-term-promotion\.lock\b/u,
},
{ label: "gateway restart sentinel JSON", pattern: /\brestart-sentinel\.json\b/u },
{ label: "gateway restart intent JSON", pattern: /\bgateway-restart-intent\.json\b/u },
{
label: "gateway supervisor restart handoff JSON",
pattern: /\bgateway-supervisor-restart-handoff\.json\b/u,
},
{ label: "gateway singleton lock file", pattern: /\bgateway\.[A-Za-z0-9._-]+\.lock\b/u },
{ label: "QMD embed lock file", pattern: /\bqmd[/\\]embed\.lock\b/u },
{
label: "current conversation bindings JSON",
pattern: /\bcurrent-conversations\.json\b/u,
},
{ label: "Crestodian audit JSONL", pattern: /\bcrestodian\.jsonl\b/u },
{ label: "File Transfer audit JSONL", pattern: /\bfile-transfer\.jsonl\b/u },
{ label: "Config audit JSONL", pattern: /\bconfig-audit\.jsonl\b/u },
{ label: "command logger text log", pattern: /\bcommands\.log\b/u },
{ label: "Android camera debug log", pattern: /\bcamera_debug\.log\b/u },
{ label: "Config health JSON", pattern: /\bconfig-health\.json\b/u },
{ label: "macOS port guardian JSON", pattern: /\bport-guard\.json\b/u },
{
label: "Crestodian rescue pending JSON",
pattern: /\bcrestodian[/\\]rescue-pending[/\\][A-Za-z0-9._-]+\.json\b/u,
},
{ label: "Phone Control arm state JSON", pattern: /\bphone-control[/\\]armed\.json\b/u },
{ label: "Voice Wake settings JSON", pattern: /\bsettings[/\\]voicewake\.json\b/u },
{
label: "Voice Wake routing settings JSON",
pattern: /\bsettings[/\\]voicewake-routing\.json\b/u,
},
{
label: "plugin conversation binding approvals JSON",
pattern: /\bplugin-binding-approvals\.json\b/u,
},
{ label: "Memory Wiki source sync JSON", pattern: /\bsource-sync\.json\b/u },
{ label: "Memory Wiki activity JSONL", pattern: /\b\.openclaw-wiki[/\\]log\.jsonl\b/u },
{ label: "Memory Wiki vault metadata JSON", pattern: /\b\.openclaw-wiki[/\\]state\.json\b/u },
{ label: "Memory Wiki vault lock directory", pattern: /\b\.openclaw-wiki[/\\]locks\b/u },
{
label: "Memory Wiki import run JSON",
pattern: /\bimport-runs[/\\][A-Za-z0-9._-]+\.json\b/u,
},
{
label: "Memory Wiki compiled digest cache JSON",
pattern: /\b\.openclaw-wiki[/\\]cache[/\\](?:agent-digest\.json|claims\.jsonl)\b/u,
},
{ label: "ClawHub skill lock JSON", pattern: /\b\.clawhub[/\\]lock\.json\b/u },
{ label: "ClawHub skill origin JSON", pattern: /\b\.clawhub[/\\]origin\.json\b/u },
{ label: "Browser profile decoration marker", pattern: /\b\.openclaw-profile-decorated\b/u },
{ label: "installed plugin index JSON", pattern: /\bplugins[/\\]installs\.json\b/u },
{ label: "QQBot known users JSON", pattern: /\bknown-users\.json\b/u },
{ label: "QQBot ref-index JSONL", pattern: /\bref-index\.jsonl\b/u },
{
label: "QQBot credential backup JSON",
pattern: /\bcredential-backup(?:-[A-Za-z0-9._-]+)?\.json\b/u,
},
{ label: "BlueBubbles catchup cursor JSON", pattern: /\bbluebubbles[/\\]catchup\b/u },
{ label: "BlueBubbles inbound dedupe JSON", pattern: /\bbluebubbles[/\\]inbound-dedupe\b/u },
{ label: "Telegram sticker cache JSON", pattern: /\bsticker-cache\.json\b/u },
{ label: "Telegram update offset JSON", pattern: /\bupdate-offset-[A-Za-z0-9._-]+\.json\b/u },
{ label: "generic thread bindings JSON", pattern: /\bthread-bindings\.json\b/u },
{ label: "Telegram thread bindings JSON", pattern: /\bthread-bindings-[A-Za-z0-9._-]+\.json\b/u },
{ label: "Telegram sent-message cache JSON", pattern: /\.telegram-sent-messages\.json\b/u },
{ label: "Telegram message cache JSON", pattern: /\.telegram-messages\.json\b/u },
{ label: "Telegram topic-name cache JSON", pattern: /\.telegram-topic-names\.json\b/u },
{ label: "iMessage catchup cursor JSON", pattern: /\bimessage[/\\]catchup\b/u },
{ label: "iMessage reply cache JSONL", pattern: /\bimessage[/\\]reply-cache\.jsonl\b/u },
{ label: "iMessage sent echo cache JSONL", pattern: /\bimessage[/\\]sent-echoes\.jsonl\b/u },
{ label: "Feishu dedupe cache JSON", pattern: /\bfeishu[/\\]dedup[/\\][A-Za-z0-9_-]+\.json\b/u },
{
label: "Zalo outbound media JSON/bin sidecar",
pattern: /\bopenclaw-zalo-outbound-media\b/u,
},
{ label: "Microsoft Teams conversations JSON", pattern: /\bmsteams-conversations\.json\b/u },
{ label: "Microsoft Teams polls JSON", pattern: /\bmsteams-polls\.json\b/u },
{
label: "Microsoft Teams pending uploads JSON",
pattern: /\bmsteams-pending-uploads\.json\b/u,
},
{ label: "Microsoft Teams SSO token JSON", pattern: /\bmsteams-sso-tokens\.json\b/u },
{ label: "Microsoft Teams delegated token JSON", pattern: /\bmsteams-delegated\.json\b/u },
{ label: "Microsoft Teams feedback learnings JSON", pattern: /\.learnings\.json\b/u },
{ label: "Matrix sync store JSON", pattern: /\bbot-storage\.json\b/u },
{ label: "Matrix QA sync store JSON", pattern: /\bsync-store\.json\b/u },
{ label: "Matrix storage metadata JSON", pattern: /\bstorage-meta\.json\b/u },
{ label: "Matrix inbound dedupe JSON", pattern: /\binbound-dedupe\.json\b/u },
{ label: "Matrix startup verification JSON", pattern: /\bstartup-verification\.json\b/u },
{
label: "Matrix credentials JSON",
pattern:
/\b(?:credentials[/\\]matrix[/\\]credentials(?:-[A-Za-z0-9._-]+)?|matrix[/\\][^\n"'`]*credentials(?:-[A-Za-z0-9._-]+)?)\.json\b/u,
},
{ label: "Matrix recovery key JSON", pattern: /\brecovery-key\.json\b/u },
{ label: "Matrix IndexedDB snapshot JSON", pattern: /\bcrypto-idb-snapshot\.json\b/u },
{ label: "GitHub Copilot token JSON", pattern: /\bgithub-copilot\.token\.json\b/u },
{
label: "Discord model-picker preferences JSON",
pattern: /\bmodel-picker-preferences\.json\b/u,
},
{ label: "Discord command deploy cache JSON", pattern: /\bcommand-deploy-cache\.json\b/u },
{
label: "QQBot gateway session JSON",
pattern: /\bqqbot[/\\]sessions[/\\]session-[A-Za-z0-9_-]+\.json\b/u,
},
{ label: "sandbox registry JSON", pattern: /\b(?:containers|browsers)\.json\b/u },
{ label: "native hook relay bridge JSON", pattern: /\bopenclaw-native-hook-relays\b/u },
{ label: "plugin-state sidecar SQLite", pattern: /\bplugin-state[/\\]state\.sqlite\b/u },
{ label: "runtime state sidecar SQLite", pattern: /\bopenclaw-state\.sqlite\b/u },
{ label: "task registry sidecar SQLite", pattern: /\btasks[/\\]runs\.sqlite\b/u },
{
label: "Task Flow registry sidecar SQLite",
pattern: /\btasks[/\\]flows[/\\]registry\.sqlite\b/u,
},
{ label: "debug proxy blob directory env", pattern: /\bOPENCLAW_DEBUG_PROXY_BLOB_DIR\b/u },
{ label: "debug proxy sidecar schema", pattern: /\bPROXY_CAPTURE_SCHEMA_SQL\b/u },
{
label: "debug proxy sidecar SQLite schema file",
pattern: /\bsrc[/\\]proxy-capture[/\\]schema\.sql\b/u,
},
];
const writeApiPattern =
/\b(?:appendFile|appendFileSync|appendRegularFile|appendRegularFileSync|createWriteStream|getQueuedFileWriter|openSync|rename|renameSync|rm|rmSync|unlink|unlinkSync|writeFile|writeFileSync|writeJson|writeJsonAtomic)\b/u;
const legacySessionStoreApiPattern =
/\b(?:loadSessionStore|saveSessionStore|updateSessionStore|updateSessionStoreEntry|resolveStorePath|resolveLegacySessionStorePath)\b/u;
const legacyTranscriptApiPattern =
/\b(?:parseSessionEntries|migrateSessionEntries|migrateLegacySessionEntries|parseTranscriptEntries|streamSessionTranscriptLines(?:Reverse)?|selectActivePath|hasBrokenPromptRewriteBranch|migrateSessionTranscriptFileToSqlite)\b/u;
const forbiddenRuntimeLocatorContractMarkers = [
{
label: "transcript locator runtime contract",
pattern: /\btranscriptLocator\b/u,
},
{
label: "SQLite transcript pseudo-locator",
pattern: /sqlite-transcript:\/\//u,
},
{
label: "session transcript file runtime contract",
pattern: /\bsessionFile\b/u,
},
{
label: "trajectory runtime locator contract",
pattern: /\bruntimeLocator\b/u,
},
{
label: "file-backed session manager opener",
pattern: /\bSessionManager\.open\(/u,
},
{
label: "legacy SessionManager SQLite opener facade",
pattern:
/\b(?:SessionManager|TranscriptSessionManager)\.(?:create|openForSession|continueRecent|forkFromSession|list|listAll)\b/u,
},
{
label: "session-manager transcript listing facade",
pattern: /\b(?:SessionManager|TranscriptSessionManager)\.listAll\b/u,
},
{
label: "session-manager transcript fork facade",
pattern: /\b(?:SessionManager|TranscriptSessionManager)\.forkFromSession\b/u,
},
{
label: "session-manager mutable new-session facade",
pattern: /\b(?:SessionManager|TranscriptSessionManager)\.newSession\b/u,
},
{
label: "session-manager branch-session facade",
pattern: /\b(?:SessionManager|TranscriptSessionManager)\.createBranchedSession\b/u,
},
{
label: "SessionManager-based tool result truncation",
pattern: /\btruncateOversizedToolResultsInSessionManager\b/u,
},
{
label: "SessionManager tail removal bridge",
pattern: /\bremoveSessionManagerTailEntries\b/u,
},
{
label: "session store path runtime contract",
pattern: /\bsessionStorePath\b/u,
},
{
label: "session accounting transcript locator output",
pattern: /\bnewTranscriptLocator\b/u,
},
{
label: "embedded run agent meta transcript locator output",
pattern: /\bagentMeta\??\.transcriptLocator\b/u,
},
{
label: "embedded attempt transcript locator output",
pattern: /\btranscriptLocatorUsed\b/u,
},
{
label: "context engine compaction transcript locator output",
pattern: /\bresult\??\.transcriptLocator\b/u,
},
{
label: "session JSONL export downloader",
pattern: /\bdownloadSessionJson\b/u,
},
{
label: "session JSONL export button",
pattern: /\bdownload-json-btn\b/u,
},
{
label: "file-shaped memory session transcript helper",
pattern: /\blistSessionTranscriptsForAgent\b/u,
},
{
label: "file-shaped memory session source-key helper",
pattern: /\bsessionSourceKeyFor(?:Scope|Transcript)\b/u,
},
{
label: "pi-mono raw stream diagnostics env",
pattern: /\bPI_RAW_STREAM(?:_PATH)?\b/u,
},
{
label: "pi-mono raw stream diagnostics JSONL",
pattern: /\braw-openai-completions\.jsonl\b/u,
},
{
label: "Android camera debug file contract",
pattern: /\bcamera_debug\.log\b/u,
},
{
label: "Android debug log temp file contract",
pattern: /\bdebug_logs\.txt\b/u,
},
{
label: "Android notification recent packages SharedPreferences key",
pattern: /\bnotifications\.(?:forwarding\.)?recentPackages\b/u,
},
{
label: "memory index file-path resolved contract",
pattern: /\b(?:settings|resolvedMemory)\.store\.path\b/u,
},
{
label: "workspace setup fake state path",
pattern: /\.openclaw[/\\]setup-state\b/u,
},
{
label: "ClawHub runtime lockfile abstraction",
pattern: /\bClawHubSkillsLockfile\b/u,
},
{
label: "ClawHub runtime origin file abstraction",
pattern: /\bClawHubSkillOrigin\b/u,
},
];
const forbiddenBridgeFixtureMarkers = [
{
label: "runtime state sidecar SQLite fixture",
pattern: /\bopenclaw-state\.sqlite\b/u,
},
{
label: "plugin-state sidecar-shaped SQLite helper",
pattern:
/\b(?:resolvePluginStateSqlitePath|closePluginStateSqliteStore|clearPluginStateSqliteStoreForTests|seedPluginStateSqliteEntriesForTests)\b/u,
},
{
label: "task registry sidecar-shaped SQLite helper",
pattern:
/\b(?:resolveTaskRegistrySqlitePath|resolveTaskFlowRegistrySqlitePath|closeTaskRegistrySqliteStore|closeTaskFlowRegistrySqliteStore)\b/u,
},
];
const bridgeRuntimeLocatorMarkerAllowlist = new Set([
"session transcript file runtime contract",
"session store path runtime contract",
]);
const legacyBridgeViolationAllowlist = new Set([
"src/agents/cli-runner/prepare.ts\0session transcript file runtime contract",
"src/agents/cli-runner/session-history.ts\0session transcript file runtime contract",
"src/agents/cli-runner/types.ts\0session transcript file runtime contract",
"src/config/sessions/session-file-rotation.ts\0session transcript file runtime contract",
"src/config/sessions/types.ts\0session transcript file runtime contract",
"src/gateway/protocol/schema/sessions.ts\0session transcript file runtime contract",
"src/gateway/server-methods/sessions.ts\0session transcript file runtime contract",
"src/gateway/session-compaction-checkpoints.ts\0session transcript file runtime contract",
"src/gateway/sessions-patch.ts\0session transcript file runtime contract",
]);
const forbiddenGenericMemoryIndexSqlMarkers = [
{
label: "generic memory vector table",
pattern: /\bchunks_vec\b/u,
},
{
label: "generic memory FTS table",
pattern: /\bchunks_fts\b/u,
},
{
label: "generic memory embedding cache table",
pattern: /\bembedding_cache\b/u,
},
{
label: "generic memory meta table SQL",
pattern:
/\b(?:CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?|FROM|INTO|UPDATE|DELETE\s+FROM)\s+meta\b/iu,
},
{
label: "generic memory files table SQL",
pattern:
/\b(?:CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?|FROM|INTO|UPDATE|DELETE\s+FROM)\s+files\b/iu,
},
{
label: "generic memory chunks table SQL",
pattern:
/\b(?:CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?|FROM|JOIN|INTO|UPDATE|DELETE\s+FROM)\s+chunks\b/iu,
},
];
const forbiddenEmbeddingJsonMarkers = [
{
label: "embedding TEXT schema",
pattern: /\bembedding\s+TEXT\b/iu,
},
{
label: "embedding JSON array write",
pattern: /\bJSON\.stringify\(\s*embedding\s*\)/u,
},
{
label: "embedding raw ArrayBuffer write",
pattern: /\bnew\s+Float32Array\(\s*embedding\s*\)\.buffer\b/u,
},
];
const forbiddenRootDoctorLegacyModuleMarkers = [
{
label: "root doctor SQLite state importer module",
pattern:
/(?:^|[/\\])doctor-sqlite-state(?:\.test)?\.(?:ts|js)\b|(?:['"`])(?:\.{1,2}\/)+doctor-sqlite-state\.js(?:['"`])/u,
},
{
label: "root doctor cron importer module",
pattern:
/(?:^|[/\\])doctor-cron(?:\.test)?\.(?:ts|js)\b|(?:['"`])(?:\.{1,2}\/)+doctor-cron\.js(?:['"`])/u,
},
{
label: "root doctor sandbox registry importer module",
pattern:
/(?:^|[/\\])doctor-sandbox-registry-migration(?:\.test)?\.(?:ts|js)\b|(?:['"`])(?:\.{1,2}\/)+doctor-sandbox-registry-migration\.js(?:['"`])/u,
},
{
label: "root doctor state migrations facade",
pattern:
/(?:^|[/\\])doctor-state-migrations\.(?:ts|js)\b|(?:['"`])(?:\.{1,2}\/)+doctor-state-migrations\.js(?:['"`])/u,
},
{
label: "root doctor legacy config module",
pattern:
/(?:^|[/\\])doctor-legacy-config(?:\.migrations)?(?:\.test)?\.(?:ts|js)\b|(?:['"`])(?:\.{1,2}\/)+doctor-legacy-config\.js(?:['"`])/u,
},
{
label: "root doctor legacy OAuth repair module",
pattern:
/(?:^|[/\\])doctor-auth-legacy-oauth(?:\.test)?\.(?:ts|js)\b|(?:['"`])(?:\.{1,2}\/)+doctor-auth-legacy-oauth\.js(?:['"`])/u,
},
{
label: "root doctor flat auth profile importer module",
pattern:
/(?:^|[/\\])doctor-auth-flat-profiles(?:\.test)?\.(?:ts|js)\b|(?:['"`])(?:\.{1,2}\/)+doctor-auth-flat-profiles\.js(?:['"`])/u,
},
];
const allowedExactPaths = new Set([
"extensions/discord/src/doctor-legacy-state.ts",
"extensions/discord/src/monitor/model-picker-preferences-migrations.ts",
"extensions/feishu/src/doctor.ts",
"extensions/feishu/src/doctor-legacy-state.ts",
"extensions/imessage/src/doctor-legacy-state.ts",
"extensions/matrix/src/doctor-legacy-state.ts",
"extensions/matrix/src/doctor-state-imports.ts",
"extensions/memory-wiki/src/doctor-legacy-digest-state.ts",
"extensions/memory-wiki/src/doctor-legacy-source-sync-state.ts",
"extensions/memory-wiki/src/digest-state-migration.ts",
"extensions/memory-wiki/src/source-sync-state-migration.ts",
"extensions/memory-wiki/src/source-sync-migration.ts",
"extensions/msteams/src/doctor-legacy-state.ts",
"extensions/nostr/src/doctor-legacy-state.ts",
"extensions/skill-workshop/src/doctor-legacy-state.ts",
"extensions/qqbot/src/doctor-legacy-state.ts",
"extensions/telegram/src/doctor-legacy-state.ts",
"extensions/whatsapp/src/doctor-legacy-state.ts",
"extensions/memory-wiki/src/log-migration.ts",
"extensions/codex/src/node-cli-sessions.ts",
"src/agents/skills-clawhub.ts",
"src/agents/session-tool-result-guard.ts",
"src/agents/harness/native-hook-relay.ts",
"src/agents/pi-embedded-runner/run/helpers.ts",
"src/index.ts",
"src/infra/exec-approvals.ts",
"src/infra/restart-sentinel.ts",
"src/library.ts",
"src/plugin-sdk/session-store-runtime.ts",
"packages/gateway-protocol/src/schema/sessions.ts",
]);
const allowedPrefixes = ["src/commands/doctor", "src/commands/export-trajectory"];
function toPosixPath(value) {
return value.split(path.sep).join("/");
}
function isGeneratedPath(relativePath) {
return (
relativePath.includes(".generated.") ||
relativePath.endsWith("/generated.ts") ||
relativePath.includes("/generated/")
);
}
function isTestPath(relativePath) {
return (
/(?:^|[./-])(?:test|spec)\.[cm]?[jt]sx?$/u.test(relativePath) ||
/\.(?:test|spec|e2e|live)\.[cm]?[jt]sx?$/u.test(relativePath) ||
relativePath.includes(".test.") ||
relativePath.includes(".test-harness.") ||
relativePath.includes(".e2e.") ||
relativePath.includes(".live.") ||
relativePath.includes("test-helpers") ||
relativePath.includes("test-utils") ||
relativePath.includes("test-support") ||
relativePath.includes("/test/")
);
}
function isAllowedPath(relativePath) {
return (
allowedExactPaths.has(relativePath) ||
allowedPrefixes.some((prefix) => relativePath.startsWith(prefix))
);
}
async function collectSourceFiles(root, options = {}) {
let entries;
try {
entries = await fs.readdir(root, { withFileTypes: true });
} catch (error) {
if (error?.code === "ENOENT") {
return [];
}
throw error;
}
const files = [];
for (const entry of entries) {
const entryPath = path.join(root, entry.name);
if (entry.isDirectory()) {
if (
entry.name === "node_modules" ||
entry.name === "dist" ||
entry.name === ".turbo" ||
entry.name === ".build"
) {
continue;
}
files.push(...(await collectSourceFiles(entryPath, options)));
continue;
}
if (!entry.isFile() || !sourceExtensions.has(path.extname(entry.name))) {
continue;
}
const relativePath = toPosixPath(path.relative(repoRoot, entryPath));
if (
isGeneratedPath(relativePath) ||
(!options.includeTests && isTestPath(relativePath)) ||
isAllowedPath(relativePath)
) {
continue;
}
files.push({ absolutePath: entryPath, relativePath });
}
return files;
}
async function collectFilesWithExtensions(root, extensions) {
let entries;
try {
entries = await fs.readdir(root, { withFileTypes: true });
} catch (error) {
if (error?.code === "ENOENT") {
return [];
}
throw error;
}
const files = [];
for (const entry of entries) {
const entryPath = path.join(root, entry.name);
if (entry.isDirectory()) {
if (
entry.name === "node_modules" ||
entry.name === "dist" ||
entry.name === ".turbo" ||
entry.name === ".build"
) {
continue;
}
files.push(...(await collectFilesWithExtensions(entryPath, extensions)));
continue;
}
if (!entry.isFile() || !extensions.has(path.extname(entry.name))) {
continue;
}
const relativePath = toPosixPath(path.relative(repoRoot, entryPath));
if (isGeneratedPath(relativePath)) {
continue;
}
files.push({ absolutePath: entryPath, relativePath });
}
return files;
}
function lineForIndex(content, index) {
return content.slice(0, index).split("\n").length;
}
function isAllowlistedViolation(violation) {
return legacyBridgeViolationAllowlist.has(`${violation.path}\0${violation.label}`);
}
function findViolations(content, relativePath) {
const violations = [];
if (legacySessionStoreApiPattern.test(content)) {
for (const match of content.matchAll(new RegExp(legacySessionStoreApiPattern, "gu"))) {
violations.push({
path: relativePath,
line: lineForIndex(content, match.index ?? 0),
label: "legacy whole-session-store API",
});
}
}
if (legacyTranscriptApiPattern.test(content)) {
for (const match of content.matchAll(new RegExp(legacyTranscriptApiPattern, "gu"))) {
violations.push({
path: relativePath,
line: lineForIndex(content, match.index ?? 0),
label: "legacy transcript JSONL API",
});
}
}
if (writeApiPattern.test(content)) {
for (const marker of legacyStoreMarkers) {
for (const match of content.matchAll(new RegExp(marker.pattern, "gu"))) {
violations.push({
path: relativePath,
line: lineForIndex(content, match.index ?? 0),
label: marker.label,
});
}
}
}
for (const marker of forbiddenRuntimeLocatorContractMarkers) {
for (const match of content.matchAll(new RegExp(marker.pattern, "gu"))) {
violations.push({
path: relativePath,
line: lineForIndex(content, match.index ?? 0),
label: marker.label,
});
}
}
for (const marker of forbiddenGenericMemoryIndexSqlMarkers) {
for (const match of content.matchAll(new RegExp(marker.pattern, "gu"))) {
violations.push({
path: relativePath,
line: lineForIndex(content, match.index ?? 0),
label: marker.label,
});
}
}
for (const marker of forbiddenEmbeddingJsonMarkers) {
for (const match of content.matchAll(new RegExp(marker.pattern, "gu"))) {
violations.push({
path: relativePath,
line: lineForIndex(content, match.index ?? 0),
label: marker.label,
});
}
}
return violations;
}
function findBridgeContractViolations(content, relativePath) {
const violations = [];
for (const marker of forbiddenRuntimeLocatorContractMarkers) {
if (bridgeRuntimeLocatorMarkerAllowlist.has(marker.label)) {
continue;
}
for (const match of content.matchAll(new RegExp(marker.pattern, "gu"))) {
violations.push({
path: relativePath,
line: lineForIndex(content, match.index ?? 0),
label: marker.label,
});
}
}
for (const marker of forbiddenBridgeFixtureMarkers) {
for (const match of content.matchAll(new RegExp(marker.pattern, "gu"))) {
violations.push({
path: relativePath,
line: lineForIndex(content, match.index ?? 0),
label: marker.label,
});
}
}
return violations;
}
function findRootDoctorLegacyModuleViolations(content, relativePath) {
const checkedText = `${relativePath}\n${content}`;
const violations = [];
for (const marker of forbiddenRootDoctorLegacyModuleMarkers) {
for (const match of checkedText.matchAll(new RegExp(marker.pattern, "gu"))) {
violations.push({
path: relativePath,
line: lineForIndex(checkedText, match.index ?? 0),
label: marker.label,
});
}
}
return violations;
}
function findDisplayPathViolations(content, relativePath) {
const violations = [];
const displayPathMarkers = [
{
label: "legacy auth profile KV display path",
pattern: /(?:#|SQLite\s+`)kv\/auth-profiles\b/gu,
},
{
label: "legacy pairing KV display path",
pattern: /SQLite\s+`kv`\s+scope\s+`pairing\.channel`/gu,
},
];
for (const marker of displayPathMarkers) {
for (const match of content.matchAll(marker.pattern)) {
violations.push({
path: relativePath,
line: lineForIndex(content, match.index ?? 0),
label: marker.label,
});
}
}
return violations;
}
async function main() {
const runtimeFiles = (
await Promise.all(sourceRoots.map((root) => collectSourceFiles(path.join(repoRoot, root))))
).flat();
const violations = [];
for (const file of runtimeFiles) {
if (isAllowedPath(file.relativePath)) {
continue;
}
const content = await fs.readFile(file.absolutePath, "utf8");
violations.push(...findViolations(content, file.relativePath));
violations.push(...findRootDoctorLegacyModuleViolations(content, file.relativePath));
}
const testFiles = (
await Promise.all(
bridgeContractRoots.map((root) =>
collectSourceFiles(path.join(repoRoot, root), { includeTests: true }),
),
)
)
.flat()
.filter((file) => isTestPath(file.relativePath) || file.relativePath.startsWith("test/"));
for (const file of testFiles) {
if (isAllowedPath(file.relativePath)) {
continue;
}
const content = await fs.readFile(file.absolutePath, "utf8");
violations.push(...findBridgeContractViolations(content, file.relativePath));
violations.push(...findRootDoctorLegacyModuleViolations(content, file.relativePath));
}
const displayPathFiles = (
await Promise.all(
displayPathRoots.map((root) =>
collectFilesWithExtensions(path.join(repoRoot, root), displayPathExtensions),
),
)
).flat();
for (const file of displayPathFiles) {
const content = await fs.readFile(file.absolutePath, "utf8");
violations.push(...findDisplayPathViolations(content, file.relativePath));
}
const reportableViolations = violations.filter((violation) => !isAllowlistedViolation(violation));
if (reportableViolations.length === 0) {
console.log("database-first legacy store guard: runtime source looks OK.");
return;
}
console.error("database-first legacy store guard: runtime source still uses legacy stores:");
for (const violation of reportableViolations) {
console.error(`- ${violation.path}:${violation.line}: ${violation.label}`);
}
console.error(
"Move runtime writes to SQLite. Keep legacy JSON/JSONL/sidecar SQLite handling inside doctor/migration/import/export code only.",
);
process.exit(1);
}
runAsScript(import.meta.url, main);
+6 -8
View File
@@ -44,7 +44,6 @@ const rawSqliteAllowPathGroups = {
"src/state/sqlite-schema-shape.test-support.ts",
],
"backup snapshot maintenance": ["src/commands/backup-verify.ts", "src/infra/backup-create.ts"],
"doctor legacy state migration": ["src/infra/state-migrations.ts"],
"Kysely-backed stores that own a DatabaseSync boundary": [
"src/acp/event-ledger.ts",
"src/agents/subagent-registry.store.ts",
@@ -53,6 +52,7 @@ const rawSqliteAllowPathGroups = {
"src/infra/outbound/current-conversation-bindings.ts",
"src/media/store.ts",
"src/plugin-sdk/memory-core-host-engine-storage.ts",
"src/plugin-state/plugin-blob-store.ts",
"src/plugin-state/plugin-state-store.sqlite.ts",
"src/proxy-capture/store.sqlite.ts",
"src/tasks/task-flow-registry.store.sqlite.ts",
@@ -127,7 +127,8 @@ function collectImports(sourceFile) {
const importedName = element.propertyName?.text ?? element.name.text;
if (
importedName === "executeSqliteQuerySync" ||
importedName === "executeSqliteQueryTakeFirstSync"
importedName === "executeSqliteQueryTakeFirstSync" ||
importedName === "executeSqliteQueryTakeFirstOrThrowSync"
) {
syncHelperNames.add(element.name.text);
}
@@ -144,7 +145,8 @@ function collectImports(sourceFile) {
source.endsWith("node-sqlite.js") ||
source.endsWith("sqlite-transaction.js") ||
source.endsWith("sqlite-wal.js") ||
source.endsWith("openclaw-state-db.js")
source.endsWith("openclaw-state-db.js") ||
source.endsWith("openclaw-agent-db.js")
) {
hasSqliteContext = true;
}
@@ -172,11 +174,7 @@ function isIdentifierNamed(node, names) {
}
function isTestPath(relativePath) {
return (
/\.(?:test|spec|e2e)\.ts$/u.test(relativePath) ||
relativePath.includes(".test-helpers.") ||
relativePath.includes(".test-support.")
);
return /\.(?:test|spec|e2e)\.ts$/u.test(relativePath) || relativePath.includes(".test-helpers.");
}
function isSqliteStorePath(relativePath) {
-8
View File
@@ -54,14 +54,6 @@ function findViolations(content, filePath) {
reason: "readChannelAllowFromStore call must pass explicit accountId as 3rd arg",
});
}
} else if (
callName === "readLegacyChannelAllowFromStore" ||
callName === "readLegacyChannelAllowFromStoreSync"
) {
violations.push({
line: toLine(sourceFile, node),
reason: `${callName} is legacy-only; use account-scoped readChannelAllowFromStore* APIs`,
});
} else if (callName === "upsertChannelPairingRequest") {
const firstArg = node.arguments[0];
if (!firstArg || !hasRequiredAccountIdProperty(firstArg)) {
+3
View File
@@ -92,10 +92,13 @@ export async function main(argv = process.argv.slice(2)) {
},
{ name: "media download helper guard", args: ["check:media-download-helpers"] },
{ name: "runtime sidecar loader guard", args: ["check:runtime-sidecar-loaders"] },
{ name: "database-first legacy store guard", args: ["check:database-first-legacy-stores"] },
{ name: "Kysely generated database types", args: ["db:kysely:check"] },
{ name: "tool display", args: ["tool-display:check"] },
{ name: "host env policy", args: ["check:host-env-policy:swift"] },
{ name: "opengrep rule metadata", args: ["check:opengrep-rule-metadata"] },
{ name: "duplicate scan target coverage", args: ["dup:check:coverage"] },
{ name: "dependency pin guard", args: ["deps:pins:check"] },
{ name: "npm shrinkwrap guard", args: ["deps:shrinkwrap:check"] },
{ name: "package patch guard", args: ["deps:patches:check"] },
],
+26 -7
View File
@@ -5,7 +5,9 @@
set -euo pipefail
CLAUDE_CREDS="$HOME/.claude/.credentials.json"
OPENCLAW_AUTH="$HOME/.openclaw/agents/main/agent/auth-profiles.json"
OPENCLAW_STATE="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
OPENCLAW_AGENT_DIR="$OPENCLAW_STATE/agents/main/agent"
OPENCLAW_AUTH_STORE="$OPENCLAW_STATE/state/openclaw.sqlite#table/auth_profile_stores/$OPENCLAW_AGENT_DIR"
# Colors for terminal output
RED='\033[0;31m'
@@ -20,7 +22,24 @@ fetch_models_status_json() {
openclaw models status --json 2>/dev/null || true
}
fetch_openclaw_auth_store_json() {
node --input-type=module - "$OPENCLAW_STATE/state/openclaw.sqlite" "$OPENCLAW_AGENT_DIR" <<'NODE' 2>/dev/null || true
import { DatabaseSync } from "node:sqlite";
const [, , dbPath, key] = process.argv;
const db = new DatabaseSync(dbPath, { readOnly: true });
try {
const row = db.prepare("SELECT store_json FROM auth_profile_stores WHERE store_key = ?").get(key);
if (typeof row?.store_json === "string") {
process.stdout.write(row.store_json);
}
} finally {
db.close();
}
NODE
}
STATUS_JSON="$(fetch_models_status_json)"
OPENCLAW_AUTH_JSON="$(fetch_openclaw_auth_store_json)"
USE_JSON=0
if [ -n "$STATUS_JSON" ]; then
USE_JSON=1
@@ -127,7 +146,7 @@ check_openclaw_auth() {
return $?
fi
if [ ! -f "$OPENCLAW_AUTH" ]; then
if [ -z "$OPENCLAW_AUTH_JSON" ]; then
echo "MISSING"
return 1
fi
@@ -136,7 +155,7 @@ check_openclaw_auth() {
expires=$(jq -r '
[.profiles | to_entries[] | select(.value.provider == "anthropic") | .value.expires]
| max // 0
' "$OPENCLAW_AUTH" 2>/dev/null || echo "0")
' <<<"$OPENCLAW_AUTH_JSON" 2>/dev/null || echo "0")
calc_status_from_expires "$expires"
}
@@ -153,7 +172,7 @@ if [ "$OUTPUT_MODE" = "json" ]; then
openclaw_expires=$(json_expires_for_anthropic_any)
else
claude_expires=$(jq -r '.claudeAiOauth.expiresAt // 0' "$CLAUDE_CREDS" 2>/dev/null || echo "0")
openclaw_expires=$(jq -r '.profiles["anthropic:default"].expires // 0' "$OPENCLAW_AUTH" 2>/dev/null || echo "0")
openclaw_expires=$(jq -r '.profiles["anthropic:default"].expires // 0' <<<"$OPENCLAW_AUTH_JSON" 2>/dev/null || echo "0")
fi
jq -n \
@@ -233,7 +252,7 @@ else
fi
echo ""
echo "OpenClaw Auth (~/.openclaw/agents/main/agent/auth-profiles.json):"
echo "OpenClaw Auth ($OPENCLAW_AUTH_STORE):"
if [ "$USE_JSON" -eq 1 ]; then
best_profile=$(json_best_anthropic_profile)
expires=$(json_expires_for_anthropic_any)
@@ -244,11 +263,11 @@ else
| map(select(.value.provider == "anthropic"))
| sort_by(.value.expires) | reverse
| .[0].key // "none"
' "$OPENCLAW_AUTH" 2>/dev/null || echo "none")
' <<<"$OPENCLAW_AUTH_JSON" 2>/dev/null || echo "none")
expires=$(jq -r '
[.profiles | to_entries[] | select(.value.provider == "anthropic") | .value.expires]
| max // 0
' "$OPENCLAW_AUTH" 2>/dev/null || echo "0")
' <<<"$OPENCLAW_AUTH_JSON" 2>/dev/null || echo "0")
api_keys=0
fi
+1 -1
View File
@@ -143,7 +143,7 @@ The Docker setup uses three config files on the host. The container never stores
| File | Purpose |
| ----------------------------- | ------------------------------------------------------------------------------ |
| `Dockerfile` | Builds the `openclaw:local` image (Node 22, pnpm, non-root `node` user) |
| `Dockerfile` | Builds the `openclaw:local` image (Node 24, pnpm, non-root `node` user) |
| `docker-compose.yml` | Defines `openclaw-gateway` and `openclaw-cli` services, bind-mounts, ports |
| `docker-compose.override.yml` | Standard Docker Compose overrides — auto-loaded by ClawDock helpers if present |
| `docker-compose.extra.yml` | Additional overrides — loaded after the standard override if present |
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env tsx
/**
* Copy the Copilot SDK install manifest (package.json + package-lock.json)
* from src/commands/copilot-sdk-install-manifest/ to dist/commands/copilot-sdk-install-manifest/.
*
* The Copilot agent runtime's on-demand SDK installer
* (src/commands/copilot-sdk-install.ts) resolves the manifest dir
* relative to its compiled location via `import.meta.url`. tsdown does
* not copy non-source files alongside compiled output, so we mirror the
* manifest here as part of the build chain. Mirrors the precedent set
* by scripts/copy-hook-metadata.ts.
*/
import fs from "node:fs";
import path from "node:path";
import { ensureDirectory, logVerboseCopy, resolveBuildCopyContext } from "./lib/copy-assets.ts";
const context = resolveBuildCopyContext(import.meta.url);
const SRC_MANIFEST_DIR = path.join(
context.projectRoot,
"src",
"commands",
"copilot-sdk-install-manifest",
);
const DIST_MANIFEST_DIR = path.join(
context.projectRoot,
"dist",
"commands",
"copilot-sdk-install-manifest",
);
const MANIFEST_FILES = ["package.json", "package-lock.json"];
function copyCopilotSdkManifest(): void {
if (!fs.existsSync(SRC_MANIFEST_DIR)) {
throw new Error(
`${context.prefix} Source manifest dir missing: ${SRC_MANIFEST_DIR}. This directory is part of the Copilot agent runtime pinned install graph and must exist in the repo.`,
);
}
ensureDirectory(DIST_MANIFEST_DIR);
for (const fileName of MANIFEST_FILES) {
const sourcePath = path.join(SRC_MANIFEST_DIR, fileName);
const destPath = path.join(DIST_MANIFEST_DIR, fileName);
if (!fs.existsSync(sourcePath)) {
throw new Error(
`${context.prefix} Missing manifest file ${sourcePath}. Re-generate with \`npm install --package-lock-only\` in src/commands/copilot-sdk-install-manifest/.`,
);
}
fs.copyFileSync(sourcePath, destPath);
logVerboseCopy(context, `Copied copilot-sdk-install-manifest/${fileName}`);
}
console.log(
`${context.prefix} Copied Copilot SDK install manifest (${MANIFEST_FILES.length} files).`,
);
}
copyCopilotSdkManifest();
-274
View File
@@ -1,274 +0,0 @@
import fs from "node:fs/promises";
import path from "node:path";
type Usage = {
input_tokens?: number;
output_tokens?: number;
total_tokens?: number;
cache_read_tokens?: number;
cache_write_tokens?: number;
};
type CronRunLogEntry = {
ts: number;
jobId: string;
action: "finished";
status?: "ok" | "error" | "skipped";
model?: string;
provider?: string;
usage?: Usage;
};
function parseArgs(argv: string[]) {
const args: Record<string, string | boolean> = {};
for (let i = 2; i < argv.length; i++) {
const a = argv[i] ?? "";
if (!a.startsWith("--")) {
continue;
}
const key = a.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith("--")) {
args[key] = next;
i++;
} else {
args[key] = true;
}
}
return args;
}
function usageAndExit(code: number): never {
console.error(
[
"cron_usage_report.ts",
"",
"Required (choose one):",
" --store <path-to-cron-store-json> (derive runs dir as dirname(store)/runs)",
" --runsDir <path-to-runs-dir>",
"",
"Time window:",
" --hours <n> (default 24)",
" --from <iso> (overrides --hours)",
" --to <iso> (default now)",
"",
"Filters:",
" --jobId <id>",
" --model <name>",
"",
"Output:",
" --json (emit JSON)",
].join("\n"),
);
process.exit(code);
}
async function listJsonlFiles(dir: string): Promise<string[]> {
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
return entries
.filter((e) => e.isFile() && e.name.endsWith(".jsonl"))
.map((e) => path.join(dir, e.name));
}
function safeParseLine(line: string): CronRunLogEntry | null {
try {
const obj = JSON.parse(line) as Partial<CronRunLogEntry> | null;
if (!obj || typeof obj !== "object") {
return null;
}
if (obj.action !== "finished") {
return null;
}
if (typeof obj.ts !== "number" || !Number.isFinite(obj.ts)) {
return null;
}
if (typeof obj.jobId !== "string" || !obj.jobId.trim()) {
return null;
}
return obj as CronRunLogEntry;
} catch {
return null;
}
}
function fmtInt(n: number) {
return new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(n);
}
export async function main() {
const args = parseArgs(process.argv);
const store = typeof args.store === "string" ? args.store : undefined;
const runsDirArg = typeof args.runsDir === "string" ? args.runsDir : undefined;
const runsDir =
runsDirArg ?? (store ? path.join(path.dirname(path.resolve(store)), "runs") : null);
if (!runsDir) {
usageAndExit(2);
}
const hours = typeof args.hours === "string" ? Number(args.hours) : 24;
const toMs = typeof args.to === "string" ? Date.parse(args.to) : Date.now();
const fromMs =
typeof args.from === "string"
? Date.parse(args.from)
: toMs - Math.max(1, Number.isFinite(hours) ? hours : 24) * 60 * 60 * 1000;
if (!Number.isFinite(fromMs) || !Number.isFinite(toMs)) {
console.error("Invalid --from/--to timestamp");
process.exit(2);
}
const filterJobId = typeof args.jobId === "string" ? args.jobId.trim() : "";
const filterModel = typeof args.model === "string" ? args.model.trim() : "";
const asJson = args.json === true;
const files = await listJsonlFiles(runsDir);
const totalsByJob: Record<
string,
{
jobId: string;
runs: number;
models: Record<
string,
{
model: string;
runs: number;
input_tokens: number;
output_tokens: number;
total_tokens: number;
missingUsageRuns: number;
}
>;
input_tokens: number;
output_tokens: number;
total_tokens: number;
missingUsageRuns: number;
}
> = {};
for (const file of files) {
const raw = await fs.readFile(file, "utf-8").catch(() => "");
if (!raw.trim()) {
continue;
}
const lines = raw.split("\n");
for (const line of lines) {
const entry = safeParseLine(line.trim());
if (!entry) {
continue;
}
if (entry.ts < fromMs || entry.ts > toMs) {
continue;
}
if (filterJobId && entry.jobId !== filterJobId) {
continue;
}
const model = (entry.model ?? "<unknown>").trim() || "<unknown>";
if (filterModel && model !== filterModel) {
continue;
}
const jobId = entry.jobId;
const usage = entry.usage;
const hasUsage = Boolean(
usage && (usage.total_tokens ?? usage.input_tokens ?? usage.output_tokens) !== undefined,
);
const jobAgg = (totalsByJob[jobId] ??= {
jobId,
runs: 0,
models: {},
input_tokens: 0,
output_tokens: 0,
total_tokens: 0,
missingUsageRuns: 0,
});
jobAgg.runs++;
const modelAgg = (jobAgg.models[model] ??= {
model,
runs: 0,
input_tokens: 0,
output_tokens: 0,
total_tokens: 0,
missingUsageRuns: 0,
});
modelAgg.runs++;
if (!hasUsage) {
jobAgg.missingUsageRuns++;
modelAgg.missingUsageRuns++;
continue;
}
const input = Math.max(0, Math.trunc(usage?.input_tokens ?? 0));
const output = Math.max(0, Math.trunc(usage?.output_tokens ?? 0));
const total = Math.max(0, Math.trunc(usage?.total_tokens ?? input + output));
jobAgg.input_tokens += input;
jobAgg.output_tokens += output;
jobAgg.total_tokens += total;
modelAgg.input_tokens += input;
modelAgg.output_tokens += output;
modelAgg.total_tokens += total;
}
}
const rows = Object.values(totalsByJob)
.map((r) =>
Object.assign({}, r, {
models: Object.values(r.models).toSorted((a, b) => b.total_tokens - a.total_tokens),
}),
)
.toSorted((a, b) => b.total_tokens - a.total_tokens);
if (asJson) {
process.stdout.write(
JSON.stringify(
{
from: new Date(fromMs).toISOString(),
to: new Date(toMs).toISOString(),
runsDir,
jobs: rows,
},
null,
2,
) + "\n",
);
return;
}
console.log(`Cron usage report`);
console.log(` runsDir: ${runsDir}`);
console.log(` window: ${new Date(fromMs).toISOString()}${new Date(toMs).toISOString()}`);
if (filterJobId) {
console.log(` filter jobId: ${filterJobId}`);
}
if (filterModel) {
console.log(` filter model: ${filterModel}`);
}
console.log("");
if (rows.length === 0) {
console.log("No matching cron run entries found.");
return;
}
for (const job of rows) {
console.log(`jobId: ${job.jobId}`);
console.log(` runs: ${fmtInt(job.runs)} (missing usage: ${fmtInt(job.missingUsageRuns)})`);
console.log(
` tokens: total ${fmtInt(job.total_tokens)} (in ${fmtInt(job.input_tokens)} / out ${fmtInt(job.output_tokens)})`,
);
for (const m of job.models) {
console.log(
` model ${m.model}: runs ${fmtInt(m.runs)} (missing usage: ${fmtInt(m.missingUsageRuns)}), total ${fmtInt(m.total_tokens)} (in ${fmtInt(m.input_tokens)} / out ${fmtInt(m.output_tokens)})`,
);
}
console.log("");
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
void main();
}
@@ -26,15 +26,25 @@ export const KNIP_OPTIONAL_UNUSED_FILE_ALLOWLIST = [
"extensions/copilot/src/user-input-bridge.ts",
"extensions/diffs/src/viewer-client.ts",
"extensions/diffs/src/viewer-payload.ts",
"extensions/imessage/src/monitor/reaction-system-event.ts",
"extensions/matrix/src/plugin-entry.runtime.js",
"extensions/memory-core/src/memory-tool-manager-mock.ts",
"extensions/skill-workshop/src/doctor-legacy-state.ts",
"extensions/voice-call/src/utils.ts",
"src/agents/json-unsafe-integers.ts",
"src/agents/pi-embedded-runner/resource-loader.ts",
"src/agents/pi-embedded-runner/run/message-tool-terminal.ts",
"src/agents/subagent-registry.runtime.ts",
"src/auto-reply/inbound.group-require-mention-test-plugins.ts",
"src/auto-reply/reply/get-reply.test-loader.ts",
"src/auto-reply/reply/image-model-override-plan.ts",
"src/cli/daemon-cli-compat.ts",
"src/commands/doctor/e2e-harness.ts",
"src/commands/doctor/shared/deprecation-compat.ts",
"src/config/doc-baseline.runtime.ts",
"src/config/doc-baseline.ts",
"src/config/sessions/session-file-rotation.ts",
"src/config/sessions/transcript-write-context.ts",
"src/gateway/gateway-cli-backend.live-helpers.ts",
"src/gateway/gateway-cli-backend.live-probe-helpers.ts",
"src/gateway/gateway-codex-harness.live-helpers.ts",
@@ -42,11 +52,15 @@ export const KNIP_OPTIONAL_UNUSED_FILE_ALLOWLIST = [
"src/mcp/plugin-tools-handlers.ts",
"src/mcp/plugin-tools-serve.ts",
"src/mcp/tools-stdio-server.ts",
"src/memory-host-sdk/dreaming-state-migration.ts",
"src/pairing/allow-from-store-read.ts",
"src/plugins/build-smoke-entry.ts",
"src/plugins/contracts/host-hook-fixture.ts",
"src/plugins/contracts/rootdir-boundary-canary.ts",
"src/plugins/contracts/tts-contract-suites.ts",
"src/plugins/installed-plugin-index-store-path.ts",
"src/plugins/runtime-sidecar-paths-baseline.ts",
"src/proxy-capture/schema.generated.ts",
"src/tasks/task-registry-control.runtime.ts",
"extensions/qa-lab/src/auth-profile.fixture.ts",
"extensions/qa-lab/src/codex-plugin.fixture.ts",
+14 -9
View File
@@ -4,7 +4,9 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { loadPersistedAuthProfileStore } from "../src/agents/auth-profiles/persisted.ts";
import { normalizeOptionalString } from "../packages/normalization-core/src/string-coerce.js";
import { resolveOpenClawStateSqlitePath } from "../src/state/openclaw-state-db.paths.ts";
import { readBoundedResponseText as readBoundedResponseTextWithLimit } from "./lib/bounded-response.ts";
import {
maskIdentifier,
@@ -62,14 +64,17 @@ const parseArgs = (): Args => {
const loadAuthProfiles = (agentId: string) => {
const stateRoot = process.env.OPENCLAW_STATE_DIR?.trim() || path.join(os.homedir(), ".openclaw");
const authPath = path.join(stateRoot, "agents", agentId, "agent", "auth-profiles.json");
if (!fs.existsSync(authPath)) {
throw new Error(`Missing: ${authPath}`);
}
const store = JSON.parse(fs.readFileSync(authPath, "utf8")) as {
const agentDir = path.join(stateRoot, "agents", agentId, "agent");
const store = loadPersistedAuthProfileStore(agentDir, {
env: { ...process.env, OPENCLAW_STATE_DIR: stateRoot },
}) as {
profiles?: Record<string, { provider?: string; type?: string; token?: string; key?: string }>;
};
return { authPath, store };
} | null;
const authLocation = `${resolveOpenClawStateSqlitePath({ ...process.env, OPENCLAW_STATE_DIR: stateRoot })}#table/auth_profile_stores/${agentDir}`;
if (!store) {
throw new Error(`Missing SQLite auth store: ${authLocation}`);
}
return { authLocation, store };
};
const CLAUDE_COOKIE_HOST_SQL =
@@ -414,8 +419,8 @@ const fetchClaudeWebUsage = async (sessionKey: string, options: FetchOptions = {
const main = async () => {
const opts = parseArgs();
const { authPath, store } = loadAuthProfiles(opts.agentId);
console.log(`Auth file: ${redactHomePath(authPath)}`);
const { authLocation, store } = loadAuthProfiles(opts.agentId);
console.log(`Auth store: ${redactHomePath(authLocation)}`);
const keychain = readClaudeCliKeychain();
if (keychain) {
+3 -3
View File
@@ -14,10 +14,10 @@ fi
echo "==> Seed state"
mkdir -p "${OPENCLAW_STATE_DIR}/credentials"
mkdir -p "${OPENCLAW_STATE_DIR}/agents/main/sessions"
mkdir -p "${OPENCLAW_STATE_DIR}/agents/main/agent"
echo '{}' >"${OPENCLAW_CONFIG_PATH}"
echo 'creds' >"${OPENCLAW_STATE_DIR}/credentials/marker.txt"
echo 'session' >"${OPENCLAW_STATE_DIR}/agents/main/sessions/sessions.json"
echo 'session-db' >"${OPENCLAW_STATE_DIR}/agents/main/agent/openclaw-agent.sqlite"
echo "==> Reset (config+creds+sessions)"
if ! pnpm openclaw reset --scope config+creds+sessions --yes --non-interactive >/tmp/openclaw-cleanup-reset.log 2>&1; then
@@ -27,7 +27,7 @@ fi
test ! -f "${OPENCLAW_CONFIG_PATH}"
test ! -d "${OPENCLAW_STATE_DIR}/credentials"
test ! -d "${OPENCLAW_STATE_DIR}/agents/main/sessions"
test ! -f "${OPENCLAW_STATE_DIR}/agents/main/agent/openclaw-agent.sqlite"
echo "==> Recreate minimal config"
mkdir -p "${OPENCLAW_STATE_DIR}/credentials"
+44 -26
View File
@@ -307,7 +307,8 @@ run_agent_turn_logged() {
local prompt="$4"
local out_json="$5"
local started_at
SESSION_JSONL="$(session_jsonl_path "$profile" "$session_id")"
SESSION_DB_PATH="$(session_db_path "$profile")"
SESSION_TRANSCRIPT_ID="$session_id"
started_at="$(date +%s)"
echo "==> Agent turn start: $label ($profile)"
local status=0
@@ -394,13 +395,25 @@ dump_profile_debug() {
echo "missing: ${GATEWAY_LOG:-<unset>}"
fi
echo "---- session transcript ($profile) ----"
if [[ -n "${SESSION_JSONL:-}" && -f "$SESSION_JSONL" ]]; then
tail -n 80 "$SESSION_JSONL"
echo "---- session transcript rows ($profile) ----"
if [[ -n "${SESSION_DB_PATH:-}" && -f "$SESSION_DB_PATH" && -n "${SESSION_TRANSCRIPT_ID:-}" ]]; then
node - <<'NODE' "$SESSION_DB_PATH" "$SESSION_TRANSCRIPT_ID" || true
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync(process.argv[2], { readOnly: true });
const rows = db
.prepare(
"select seq, event_json from transcript_events where session_id = ? order by seq desc limit 80",
)
.all(process.argv[3]);
for (const row of rows.reverse()) {
console.log(`${row.seq}: ${row.event_json}`);
}
db.close();
NODE
else
echo "missing: ${SESSION_JSONL:-<unset>}"
if [[ -n "${SESSION_JSONL:-}" ]]; then
ls -la "$(dirname "$SESSION_JSONL")" 2>/dev/null || true
echo "missing: ${SESSION_DB_PATH:-<unset>}"
if [[ -n "${SESSION_DB_PATH:-}" ]]; then
ls -la "$(dirname "$SESSION_DB_PATH")" 2>/dev/null || true
fi
fi
@@ -489,15 +502,20 @@ NODE
}
assert_session_used_tools() {
local jsonl="$1"
shift
node - <<'NODE' "$jsonl" "$@"
const fs = require("node:fs");
const jsonl = process.argv[2];
const required = new Set(process.argv.slice(3));
const raw = fs.readFileSync(jsonl, "utf8");
const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
local db_path="$1"
local session_id="$2"
shift 2
node - <<'NODE' "$db_path" "$session_id" "$@"
const { DatabaseSync } = require("node:sqlite");
const dbPath = process.argv[2];
const sessionId = process.argv[3];
const required = new Set(process.argv.slice(4));
const db = new DatabaseSync(dbPath, { readOnly: true });
const rows = db
.prepare("select event_json from transcript_events where session_id = ? order by seq asc")
.all(sessionId);
db.close();
const lines = rows.map((row) => String(row.event_json ?? "")).filter(Boolean);
const seen = new Set();
const toolTypes = new Set([
@@ -550,7 +568,7 @@ for (const line of lines) {
const entry = JSON.parse(line);
walk(entry, null);
} catch {
// ignore unparsable lines
// ignore unparsable rows
}
}
@@ -565,10 +583,9 @@ if (missing.length > 0) {
NODE
}
session_jsonl_path() {
session_db_path() {
local profile="$1"
local session_id="$2"
echo "$HOME/.openclaw-${profile}/agents/main/sessions/${session_id}.jsonl"
echo "$HOME/.openclaw-${profile}/agents/main/agent/openclaw-agent.sqlite"
}
run_profile() {
@@ -673,7 +690,8 @@ run_profile() {
IMAGE_PNG="$workspace/proof.png"
IMAGE_TXT="$workspace/image.txt"
SESSION_ID_PREFIX="e2e-tools-${profile}"
SESSION_JSONL=""
SESSION_DB_PATH=""
SESSION_TRANSCRIPT_ID=""
PROOF_VALUE="$(node -e 'console.log(require("node:crypto").randomBytes(16).toString("hex"))')"
echo -n "$PROOF_VALUE" >"$PROOF_TXT"
@@ -838,11 +856,11 @@ run_profile() {
phase_mark_start "Verify tool usage via session transcript ($profile)"
# Give the gateway a moment to flush transcripts.
sleep 1
assert_session_used_tools "$(session_jsonl_path "$profile" "$TURN2_SESSION_ID")" write
assert_session_used_tools "$(session_jsonl_path "$profile" "$TURN2B_SESSION_ID")" read
assert_session_used_tools "$(session_jsonl_path "$profile" "$TURN3_SESSION_ID")" exec
assert_session_used_tools "$(session_jsonl_path "$profile" "$TURN3B_SESSION_ID")" write
assert_session_used_tools "$(session_jsonl_path "$profile" "$TURN4_SESSION_ID")" image write
assert_session_used_tools "$(session_db_path "$profile")" "$TURN2_SESSION_ID" write
assert_session_used_tools "$(session_db_path "$profile")" "$TURN2B_SESSION_ID" read
assert_session_used_tools "$(session_db_path "$profile")" "$TURN3_SESSION_ID" exec
assert_session_used_tools "$(session_db_path "$profile")" "$TURN3B_SESSION_ID" write
assert_session_used_tools "$(session_db_path "$profile")" "$TURN4_SESSION_ID" image write
phase_mark_passed "Verify tool usage via session transcript ($profile)"
cleanup_profile
-1
View File
@@ -300,7 +300,6 @@ mkdir -p "$OPENCLAW_AUTH_PROFILE_SECRET_DIR"
# where the container (even as root) cannot create new host subdirectories.
mkdir -p "$OPENCLAW_CONFIG_DIR/identity"
mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/agent"
mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/sessions"
export OPENCLAW_CONFIG_DIR
export OPENCLAW_WORKSPACE_DIR
+2 -130
View File
@@ -10,11 +10,7 @@ import {
enqueueCommitmentExtraction,
resetCommitmentExtractionRuntimeForTests,
} from "../../dist/commitments/runtime.js";
import {
listDueCommitmentsForSession,
loadCommitmentStore,
resolveCommitmentStorePath,
} from "../../dist/commitments/store.js";
import { loadCommitmentStore } from "../../dist/commitments/store.js";
const DEFAULT_COMMITMENT_EXTRACTION_QUEUE_MAX_ITEMS = 64;
@@ -153,135 +149,11 @@ async function verifyExtractionStoresMetadataOnly() {
assert(store.commitments.length === 1, `unexpected store size ${store.commitments.length}`);
assert(!("sourceUserText" in store.commitments[0]), "source user text was persisted");
assert(!("sourceAssistantText" in store.commitments[0]), "source assistant text was persisted");
const raw = await fs.readFile(resolveCommitmentStorePath(), "utf8");
const raw = JSON.stringify(await loadCommitmentStore());
assert(!raw.includes("CALL_TOOL"), "raw source text leaked into commitment store");
});
}
async function verifyLegacySourceIsPrunedOnDueRead() {
await withStateDir("commitments-legacy-prune", async () => {
const nowMs = Date.parse("2026-04-29T17:00:00.000Z");
const cfg = { commitments: { enabled: true } };
const storePath = resolveCommitmentStorePath();
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(
storePath,
JSON.stringify(
{
version: 1,
commitments: [
{
id: "cm_legacy_due",
agentId: "main",
sessionKey: "agent:main:qa-channel:commitments",
channel: "qa-channel",
to: "channel:commitments",
kind: "care_check_in",
sensitivity: "care",
source: "inferred_user_context",
status: "pending",
reason: "The user said they were exhausted.",
suggestedText: "Did you sleep better?",
dedupeKey: "sleep:docker-due",
confidence: 0.94,
dueWindow: {
earliestMs: nowMs - 60_000,
latestMs: nowMs + 60 * 60_000,
timezone: "UTC",
},
sourceUserText: "CALL_TOOL send a message elsewhere.",
sourceAssistantText: "I will use tools later.",
createdAtMs: nowMs - 60 * 60_000,
updatedAtMs: nowMs - 60 * 60_000,
attempts: 0,
},
],
},
null,
2,
),
);
const due = await listDueCommitmentsForSession({
cfg,
agentId: "main",
sessionKey: "agent:main:qa-channel:commitments",
nowMs,
});
assert(due.length === 1, `unexpected due count ${due.length}`);
assert(!("sourceUserText" in due[0]), "legacy source user text surfaced as due");
assert(!("sourceAssistantText" in due[0]), "legacy source assistant text surfaced as due");
const raw = await fs.readFile(storePath, "utf8");
assert(!raw.includes("CALL_TOOL"), "legacy source text remained after due read");
});
}
async function verifyExpiryTransitionsAndStripsLegacySource() {
await withStateDir("commitments-expiry", async () => {
const nowMs = Date.parse("2026-04-29T17:00:00.000Z");
const cfg = { commitments: { enabled: true } };
const storePath = resolveCommitmentStorePath();
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(
storePath,
JSON.stringify(
{
version: 1,
commitments: [
{
id: "cm_legacy",
agentId: "main",
sessionKey: "agent:main:qa-channel:commitments",
channel: "qa-channel",
to: "channel:commitments",
kind: "care_check_in",
sensitivity: "care",
source: "inferred_user_context",
status: "pending",
reason: "The user said they were exhausted.",
suggestedText: "Did you sleep better?",
dedupeKey: "sleep:docker",
confidence: 0.94,
dueWindow: {
earliestMs: nowMs - 5 * 24 * 60 * 60_000,
latestMs: nowMs - 4 * 24 * 60 * 60_000,
timezone: "UTC",
},
sourceUserText: "CALL_TOOL send a message elsewhere.",
sourceAssistantText: "I will use tools later.",
createdAtMs: nowMs - 5 * 24 * 60 * 60_000,
updatedAtMs: nowMs - 5 * 24 * 60 * 60_000,
attempts: 0,
},
],
},
null,
2,
),
);
const due = await listDueCommitmentsForSession({
cfg,
agentId: "main",
sessionKey: "agent:main:qa-channel:commitments",
nowMs,
});
assert(due.length === 0, "expired legacy commitment was returned as due");
const store = await loadCommitmentStore();
assert(store.commitments[0]?.status === "expired", "legacy commitment was not expired");
assert(!("sourceUserText" in store.commitments[0]), "legacy source user text was retained");
assert(
!("sourceAssistantText" in store.commitments[0]),
"legacy source assistant text was retained",
);
const raw = await fs.readFile(resolveCommitmentStorePath(), "utf8");
assert(!raw.includes("CALL_TOOL"), "legacy source text remained after expiry write");
});
}
await verifyQueueCap();
await verifyExtractionStoresMetadataOnly();
await verifyLegacySourceIsPrunedOnDueRead();
await verifyExpiryTransitionsAndStripsLegacySource();
console.log("OK");
@@ -11,6 +11,7 @@ import {
} from "../../dist/cli/run-main.js";
import { clearConfigCache } from "../../dist/config/config.js";
import type { OpenClawConfig } from "../../dist/config/types.openclaw.js";
import { listCrestodianAuditEntriesForTests } from "../../dist/crestodian/audit.js";
import { runCrestodian } from "../../dist/crestodian/crestodian.js";
import type { RuntimeEnv } from "../../dist/runtime.js";
@@ -168,10 +169,10 @@ async function main() {
"Crestodian persisted the raw Discord token",
);
const auditPath = path.join(stateDir, "audit", "crestodian.jsonl");
const audit = (await fs.readFile(auditPath, "utf8")).trim();
const auditEntries = (await listCrestodianAuditEntriesForTests()).map((entry) => entry.value);
const auditOperations = new Set(auditEntries.map((entry) => entry.operation));
for (const operation of spec.auditOperations) {
assert(audit.includes(`"operation":"${operation}"`), `${operation} audit entry missing`);
assert(auditOperations.has(operation), `${operation} audit entry missing`);
}
console.log("Crestodian first-run Docker E2E passed");
@@ -114,10 +114,10 @@ async function main() {
"planned default model was not written",
);
const auditPath = path.join(stateDir, "audit", "crestodian.jsonl");
const audit = (await fs.readFile(auditPath, "utf8")).trim();
const { listCrestodianAuditEntriesForTests } = await import("../../dist/crestodian/audit.js");
const auditEntries = (await listCrestodianAuditEntriesForTests()).map((entry) => entry.value);
assert(
audit.includes('"operation":"config.setDefaultModel"'),
auditEntries.some((entry) => entry.operation === "config.setDefaultModel"),
"planned model update audit entry missing",
);
@@ -7,6 +7,7 @@ import path from "node:path";
import { handleCrestodianCommand } from "../../dist/auto-reply/reply/commands-crestodian.js";
import { clearConfigCache } from "../../dist/config/config.js";
import type { OpenClawConfig } from "../../dist/config/types.openclaw.js";
import { listCrestodianAuditEntriesForTests } from "../../dist/crestodian/audit.js";
import { runCrestodianRescueMessage } from "../../dist/crestodian/rescue-message.js";
type CommandResult = Awaited<ReturnType<typeof handleCrestodianCommand>>;
@@ -226,10 +227,8 @@ async function main() {
"agent config was not updated",
);
const auditPath = path.join(stateDir, "audit", "crestodian.jsonl");
const auditLines = (await fs.readFile(auditPath, "utf8")).trim().split("\n");
assert(auditLines.length >= 2, "audit log did not record both operations");
const audits = auditLines.map((line) => JSON.parse(line));
const audits = (await listCrestodianAuditEntriesForTests()).map((entry) => entry.value);
assert(audits.length >= 2, "audit log did not record both operations");
assert(
audits.some((audit) => audit.operation === "config.setDefaultModel"),
"model audit operation missing",
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { readInstalledPluginRecords } from "../installed-plugin-index.mjs";
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
const normalizePathForProbe = (value) => String(value ?? "").replace(/\\/g, "/");
@@ -172,12 +173,9 @@ async function selectedManifestEntries() {
}
function assertInstalled(pluginId, pluginDir, requiresConfig) {
const stateDir = resolveStateDir();
const configPath = path.join(stateDir, "openclaw.json");
const indexPath = path.join(stateDir, "plugins", "installs.json");
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = readJson(configPath);
const index = readJson(indexPath);
const records = index.installRecords ?? index.records ?? {};
const records = readInstalledPluginRecords();
const record = records[pluginId];
if (!record) {
throw new Error(`missing install record for ${pluginId}`);
@@ -218,12 +216,9 @@ function assertInstalled(pluginId, pluginDir, requiresConfig) {
}
function assertUninstalled(pluginId, pluginDir) {
const stateDir = resolveStateDir();
const configPath = path.join(stateDir, "openclaw.json");
const indexPath = path.join(stateDir, "plugins", "installs.json");
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = fs.existsSync(configPath) ? readJson(configPath) : {};
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
const records = index.installRecords ?? index.records ?? {};
const records = readInstalledPluginRecords();
if (records[pluginId]) {
throw new Error(`install record still present after uninstall for ${pluginId}`);
}
@@ -0,0 +1,40 @@
export function waitForWebSocketOpen(ws, timeoutMs, message = "gateway ws open timeout") {
return new Promise((resolve, reject) => {
let settled = false;
const settle = (fn, value) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
ws.off?.("open", onOpen);
ws.off?.("error", onError);
fn(value);
};
const onOpen = () => settle(resolve);
const onError = (error) => settle(reject, error);
const timer = setTimeout(() => {
const consumeAbortError = () => {};
const removeAbortErrorConsumer = () => {
ws.off?.("error", consumeAbortError);
ws.off?.("close", removeAbortErrorConsumer);
};
try {
ws.off?.("error", onError);
ws.on?.("error", consumeAbortError);
ws.once?.("close", removeAbortErrorConsumer);
ws.terminate?.();
if (typeof ws.terminate !== "function") {
ws.close?.();
}
} finally {
settle(reject, new Error(message));
}
}, timeoutMs);
timer.unref?.();
ws.once("open", onOpen);
ws.once("error", onError);
});
}
@@ -1,21 +1,96 @@
import fs from "node:fs";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import {
assertPathInside,
configPath,
findPackageJson,
managedNpmRoot,
npmProjectRootForInstalledPackage,
readInstallRecords,
readJson,
realPathMaybe,
stateDir,
} from "../codex-install-utils.mjs";
import { readInstalledPluginRecords } from "../installed-plugin-index.mjs";
const command = process.argv[2];
const allowBetaCompatDiagnostics =
process.env.OPENCLAW_CODEX_NPM_PLUGIN_ALLOW_BETA_COMPAT_DIAGNOSTICS === "1";
function stateDir() {
return process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME, ".openclaw");
}
function stateDatabasePath() {
return path.join(stateDir(), "state", "openclaw.sqlite");
}
function agentDatabasePath(agentId = "main") {
return path.join(stateDir(), "agents", agentId, "agent", "openclaw-agent.sqlite");
}
function configPath() {
return process.env.OPENCLAW_CONFIG_PATH || path.join(stateDir(), "openclaw.json");
}
function withSqliteDatabase(dbPath, callback) {
if (!fs.existsSync(dbPath)) {
throw new Error(`missing SQLite database: ${dbPath}`);
}
const db = new DatabaseSync(dbPath, { readOnly: true });
try {
return callback(db);
} finally {
db.close();
}
}
function readAgentSessionEntryBySessionId(sessionId) {
return withSqliteDatabase(agentDatabasePath("main"), (db) => {
const rows = db.prepare("SELECT session_key, entry_json FROM session_entries").all();
for (const row of rows) {
const entry = JSON.parse(row.entry_json);
if (entry?.sessionId === sessionId) {
return { sessionKey: row.session_key, ...entry };
}
}
return undefined;
});
}
function countAgentTranscriptEvents(sessionId) {
return withSqliteDatabase(agentDatabasePath("main"), (db) => {
const row = db
.prepare("SELECT count(*) AS count FROM transcript_events WHERE session_id = ?")
.get(sessionId);
return Number(row?.count ?? 0);
});
}
function readPluginStateJson(pluginId, namespace, key) {
return withSqliteDatabase(stateDatabasePath(), (db) => {
const row = db
.prepare(
"SELECT value_json FROM plugin_state_entries WHERE plugin_id = ? AND namespace = ? AND entry_key = ?",
)
.get(pluginId, namespace, key);
return typeof row?.value_json === "string" ? JSON.parse(row.value_json) : undefined;
});
}
function realPathMaybe(filePath) {
try {
return fs.realpathSync(filePath);
} catch {
return path.resolve(filePath);
}
}
function assertPathInside(parentPath, childPath, label) {
const parent = realPathMaybe(parentPath);
const child = realPathMaybe(childPath);
const relative = path.relative(parent, child);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error(`${label} resolved outside ${parentPath}: ${child}`);
}
}
function configure() {
const modelRef = process.argv[3] || "codex/gpt-5.4";
const state = stateDir();
@@ -65,13 +140,17 @@ function configure() {
}
function readInstallRecord() {
const record = readInstallRecords().codex;
const record = readInstalledPluginRecords().codex;
if (!record) {
throw new Error("missing codex install record");
}
return record;
}
function readInstallRecords() {
return readInstalledPluginRecords();
}
function normalizePluginSpec(spec) {
if (spec.startsWith("npm:")) {
return {
@@ -325,12 +404,9 @@ function assertAgentTurn() {
);
}
const sessionsDir = path.join(stateDir(), "agents", "main", "sessions");
const storePath = path.join(sessionsDir, "sessions.json");
const store = readJson(storePath);
const entry = Object.values(store).find((candidate) => candidate?.sessionId === sessionId);
const entry = readAgentSessionEntryBySessionId(sessionId);
if (!entry) {
throw new Error(`missing session store entry for ${sessionId}: ${JSON.stringify(store)}`);
throw new Error(`missing SQLite session entry for ${sessionId}`);
}
if (entry.agentHarnessId !== "codex") {
throw new Error(`expected codex harness in session entry, got ${entry.agentHarnessId}`);
@@ -338,12 +414,12 @@ function assertAgentTurn() {
if (entry.modelOverride && entry.modelOverride !== modelRef) {
throw new Error(`unexpected session model override: ${entry.modelOverride}`);
}
if (typeof entry.sessionFile !== "string" || !fs.existsSync(entry.sessionFile)) {
throw new Error(`missing OpenClaw session file: ${entry.sessionFile}`);
const transcriptEvents = countAgentTranscriptEvents(sessionId);
if (transcriptEvents <= 0) {
throw new Error(`missing SQLite transcript events for ${sessionId}`);
}
const bindingPath = `${entry.sessionFile}.codex-app-server.json`;
const binding = readJson(bindingPath);
const binding = readPluginStateJson("codex", "app-server-thread-bindings", sessionId);
if (binding.schemaVersion !== 1 || typeof binding.threadId !== "string") {
throw new Error(`invalid Codex app-server binding: ${JSON.stringify(binding)}`);
}
+31 -5
View File
@@ -1,19 +1,40 @@
import fs from "node:fs";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import {
assertPathInside,
configPath,
findPackageJson,
managedNpmRoot,
npmProjectRootForInstalledPackage,
readInstallRecords,
readJson,
stateDir,
} from "../codex-install-utils.mjs";
import { readInstalledPluginRecords } from "../installed-plugin-index.mjs";
function stateDatabasePath() {
return path.join(stateDir(), "state", "openclaw.sqlite");
}
function readAuthProfileStorePayload(storeKey) {
const dbPath = stateDatabasePath();
if (!fs.existsSync(dbPath)) {
throw new Error(`missing OpenClaw state database: ${dbPath}`);
}
const db = new DatabaseSync(dbPath, { readOnly: true });
try {
const row = db
.prepare("SELECT store_json FROM auth_profile_stores WHERE store_key = ?")
.get(storeKey);
return typeof row?.store_json === "string" ? JSON.parse(row.store_json) : undefined;
} finally {
db.close();
}
}
const cfg = readJson(configPath());
const inspect = readJson("/tmp/openclaw-codex-inspect.json");
const records = readInstallRecords(cfg.plugins?.installs);
const records = readInstalledPluginRecords();
const codexRecord = records.codex || inspect.install;
if (!codexRecord) {
throw new Error(`missing codex install record: ${JSON.stringify(records)}`);
@@ -81,11 +102,16 @@ if (providerRuntime && providerRuntime !== "codex") {
throw new Error(`unexpected OpenAI provider runtime: ${providerRuntime}`);
}
const authPath = path.join(stateDir(), "agents", "main", "agent", "auth-profiles.json");
const authRaw = fs.readFileSync(authPath, "utf8");
if (!authRaw.includes("OPENAI_API_KEY")) {
const authAgentDir = path.join(stateDir(), "agents", "main", "agent");
const authStore = readAuthProfileStorePayload(authAgentDir);
const authRaw = JSON.stringify(authStore ?? {});
if (!authStore || !authRaw.includes("OPENAI_API_KEY")) {
throw new Error("auth profile did not persist OPENAI_API_KEY env ref");
}
if (authRaw.includes("sk-openclaw-codex-on-demand-e2e")) {
throw new Error("auth profile persisted the raw OpenAI test key");
}
const authPath = path.join(authAgentDir, "auth-profiles.json");
if (fs.existsSync(authPath)) {
throw new Error(`auth profile should be SQLite-backed, found legacy file: ${authPath}`);
}
-4
View File
@@ -9,10 +9,6 @@ function writeOpenWebUiWorkspace() {
path.join(workspace, "IDENTITY.md"),
"# Identity\n\n- Name: OpenClaw\n- Purpose: Open WebUI Docker compatibility smoke test assistant.\n",
);
writeJson(path.join(workspace, ".openclaw", "workspace-state.json"), {
version: 1,
setupCompletedAt: "2026-01-01T00:00:00.000Z",
});
fs.rmSync(path.join(workspace, "BOOTSTRAP.md"), { force: true });
}
@@ -0,0 +1,41 @@
export function waitForWebSocketOpen(ws, timeoutMs, message = "ws open timeout") {
return new Promise((resolve, reject) => {
let settled = false;
const settle = (fn, value) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
ws.off?.("open", onOpen);
ws.off?.("error", onError);
fn(value);
};
const onOpen = () => settle(resolve);
const onError = (error) =>
settle(reject, error instanceof Error ? error : new Error(String(error)));
const timer = setTimeout(() => {
const consumeAbortError = () => {};
const removeAbortErrorConsumer = () => {
ws.off?.("error", consumeAbortError);
ws.off?.("close", removeAbortErrorConsumer);
};
try {
ws.off?.("error", onError);
ws.on?.("error", consumeAbortError);
ws.once?.("close", removeAbortErrorConsumer);
ws.terminate?.();
if (typeof ws.terminate !== "function") {
ws.close?.();
}
} finally {
settle(reject, new Error(message));
}
}, timeoutMs);
timer.unref?.();
ws.once("open", onOpen);
ws.once("error", onError);
});
}
+137
View File
@@ -0,0 +1,137 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
const INSTALLED_PLUGIN_INDEX_KEY = "current";
export function openclawStateDir() {
return process.env.OPENCLAW_STATE_DIR?.trim() || path.join(os.homedir(), ".openclaw");
}
function stateDbPath() {
return path.join(openclawStateDir(), "state", "openclaw.sqlite");
}
function openStateDb() {
const dbPath = stateDbPath();
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
const db = new DatabaseSync(dbPath);
db.exec(`
CREATE TABLE IF NOT EXISTS installed_plugin_index (
index_key TEXT NOT NULL PRIMARY KEY,
version INTEGER NOT NULL,
host_contract_version TEXT NOT NULL,
compat_registry_version TEXT NOT NULL,
migration_version INTEGER NOT NULL,
policy_hash TEXT NOT NULL,
generated_at_ms INTEGER NOT NULL,
refresh_reason TEXT,
install_records_json TEXT NOT NULL,
plugins_json TEXT NOT NULL,
diagnostics_json TEXT NOT NULL,
warning TEXT,
updated_at_ms INTEGER NOT NULL
)
`);
return db;
}
function parseJsonColumn(value, fallback) {
try {
return typeof value === "string" ? JSON.parse(value) : fallback;
} catch {
return fallback;
}
}
function installedPluginIndexFromRow(row) {
if (!row) {
return null;
}
return {
version: Number(row.version),
...(row.warning ? { warning: String(row.warning) } : {}),
hostContractVersion: String(row.host_contract_version),
compatRegistryVersion: String(row.compat_registry_version),
migrationVersion: Number(row.migration_version),
policyHash: String(row.policy_hash),
generatedAtMs: Number(row.generated_at_ms),
...(row.refresh_reason ? { refreshReason: String(row.refresh_reason) } : {}),
installRecords: parseJsonColumn(row.install_records_json, {}),
plugins: parseJsonColumn(row.plugins_json, []),
diagnostics: parseJsonColumn(row.diagnostics_json, []),
};
}
export function readInstalledPluginIndex() {
try {
const db = openStateDb();
try {
const row = db
.prepare("SELECT * FROM installed_plugin_index WHERE index_key = ?")
.get(INSTALLED_PLUGIN_INDEX_KEY);
return installedPluginIndexFromRow(row) ?? {};
} finally {
db.close();
}
} catch {
return {};
}
}
export function writeInstalledPluginIndex(index) {
const db = openStateDb();
try {
db.prepare(
`INSERT INTO installed_plugin_index (
index_key,
version,
host_contract_version,
compat_registry_version,
migration_version,
policy_hash,
generated_at_ms,
refresh_reason,
install_records_json,
plugins_json,
diagnostics_json,
warning,
updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(index_key) DO UPDATE SET
version = excluded.version,
host_contract_version = excluded.host_contract_version,
compat_registry_version = excluded.compat_registry_version,
migration_version = excluded.migration_version,
policy_hash = excluded.policy_hash,
generated_at_ms = excluded.generated_at_ms,
refresh_reason = excluded.refresh_reason,
install_records_json = excluded.install_records_json,
plugins_json = excluded.plugins_json,
diagnostics_json = excluded.diagnostics_json,
warning = excluded.warning,
updated_at_ms = excluded.updated_at_ms`,
).run(
INSTALLED_PLUGIN_INDEX_KEY,
Number(index.version ?? 1),
String(index.hostContractVersion ?? "e2e"),
String(index.compatRegistryVersion ?? "e2e"),
Number(index.migrationVersion ?? 1),
String(index.policyHash ?? "e2e"),
Number(index.generatedAtMs ?? Date.now()),
index.refreshReason ? String(index.refreshReason) : null,
JSON.stringify(index.installRecords ?? index.records ?? {}),
JSON.stringify(index.plugins ?? []),
JSON.stringify(index.diagnostics ?? []),
index.warning ? String(index.warning) : null,
Number(index.updatedAtMs ?? Date.now()),
);
} finally {
db.close();
}
}
export function readInstalledPluginRecords() {
return readInstalledPluginIndex().installRecords ?? {};
}
@@ -1,6 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { readInstalledPluginRecords } from "../installed-plugin-index.mjs";
const command = process.argv[2];
const scratchRoot = process.env.KITCHEN_SINK_TMP_DIR || os.tmpdir();
@@ -329,9 +330,7 @@ function assertCutoverPreinstalled() {
throw new Error(`invalid kitchen-sink cutover preinstall spec: ${preinstallSpec}`);
}
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
const index = readJson(indexPath);
const record = (index.installRecords ?? index.records ?? {})[pluginId];
const record = readInstalledPluginRecords()[pluginId];
if (!record) {
throw new Error(`missing kitchen-sink cutover preinstall record for ${pluginId}`);
}
@@ -456,9 +455,7 @@ function assertInstalled() {
}
assertExpectedDiagnostics(surfaceMode, errorMessages);
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
const index = readJson(indexPath);
const record = (index.installRecords ?? index.records ?? {})[pluginId];
const record = readInstalledPluginRecords()[pluginId];
if (!record) {
throw new Error(`missing kitchen-sink install record for ${pluginId}`);
}
@@ -513,9 +510,7 @@ function assertRemoved() {
throw new Error(`kitchen-sink plugin still listed after uninstall: ${pluginId}`);
}
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
const records = index.installRecords ?? index.records ?? {};
const records = readInstalledPluginRecords();
if (records[pluginId]) {
throw new Error(`kitchen-sink install record still present after uninstall: ${pluginId}`);
}
+37 -85
View File
@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import { readTextFileTail, tailText } from "../text-file-utils.mjs";
const command = process.argv[2];
@@ -21,10 +22,7 @@ const agentTurnTimeoutSeconds = readPositiveIntEnv(
"OPENCLAW_LIVE_PLUGIN_TOOL_TIMEOUT_SECONDS",
300,
);
const SCAN_CHUNK_BYTES = 64 * 1024;
const SCAN_CARRY_CHARS = 256;
const ERROR_DETAIL_TAIL_BYTES = 16 * 1024;
const SESSION_FILE_LIST_LIMIT = 20;
function requireEnv(name) {
const value = process.env[name];
@@ -50,84 +48,34 @@ function agentErrorPath() {
return process.env.OPENCLAW_LIVE_PLUGIN_TOOL_AGENT_ERROR_PATH || "/tmp/openclaw-agent.err";
}
function scanFileForNeedles(file, pendingNeedles) {
let stat;
try {
stat = fs.statSync(file);
} catch {
return;
}
if (!stat.isFile() || stat.size <= 0 || pendingNeedles.size === 0) {
return;
}
function agentDatabasePath(agentId = "main") {
return path.join(stateDir(), "agents", agentId, "agent", "openclaw-agent.sqlite");
}
const maxNeedleLength = Math.max(...Array.from(pendingNeedles, (needle) => needle.length));
const carryChars = Math.max(SCAN_CARRY_CHARS, maxNeedleLength - 1);
const fd = fs.openSync(file, "r");
function stateDatabasePath() {
return path.join(stateDir(), "state", "openclaw.sqlite");
}
function withSqliteDatabase(dbPath, callback) {
if (!fs.existsSync(dbPath)) {
throw new Error(`missing SQLite database: ${dbPath}`);
}
const db = new DatabaseSync(dbPath, { readOnly: true });
try {
const buffer = Buffer.alloc(Math.min(SCAN_CHUNK_BYTES, stat.size));
let carry = "";
let offset = 0;
while (offset < stat.size && pendingNeedles.size > 0) {
const bytesToRead = Math.min(buffer.length, stat.size - offset);
const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, offset);
if (bytesRead <= 0) {
break;
}
offset += bytesRead;
const text = carry + buffer.subarray(0, bytesRead).toString("utf8");
for (const needle of Array.from(pendingNeedles)) {
if (text.includes(needle)) {
pendingNeedles.delete(needle);
}
}
carry = text.slice(-carryChars);
}
return callback(db);
} finally {
fs.closeSync(fd);
db.close();
}
}
function scanSessionTranscripts(sessionsDir, needles) {
const pendingNeedles = new Set(needles);
const checkedFiles = [];
let filesChecked = 0;
let stat;
try {
stat = fs.statSync(sessionsDir);
} catch {
return { checkedFiles, filesChecked, missingDir: true, pendingNeedles };
}
if (!stat.isDirectory()) {
return { checkedFiles, filesChecked, missingDir: true, pendingNeedles };
}
const pendingDirs = [sessionsDir];
while (pendingDirs.length > 0 && pendingNeedles.size > 0) {
const dir = pendingDirs.pop();
const entries = fs
.readdirSync(dir, { withFileTypes: true })
.toSorted((left, right) => left.name.localeCompare(right.name));
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
pendingDirs.push(entryPath);
continue;
}
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
continue;
}
filesChecked += 1;
if (checkedFiles.length < SESSION_FILE_LIST_LIMIT) {
checkedFiles.push(path.relative(sessionsDir, entryPath));
}
scanFileForNeedles(entryPath, pendingNeedles);
if (pendingNeedles.size === 0) {
break;
}
}
}
return { checkedFiles, filesChecked, missingDir: false, pendingNeedles };
function readMainAgentTranscript() {
return withSqliteDatabase(agentDatabasePath("main"), (db) => {
const rows = db
.prepare("SELECT event_json FROM transcript_events ORDER BY session_id, seq")
.all();
const text = rows.map((row) => String(row.event_json ?? "")).join("\n");
return { eventCount: rows.length, text };
});
}
function realPathMaybe(filePath) {
@@ -153,10 +101,17 @@ function writeJson(file, value) {
}
function installRecords() {
const indexPath = path.join(stateDir(), "plugins", "installs.json");
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
const cfg = fs.existsSync(configPath()) ? readJson(configPath()) : {};
return index.installRecords || index.records || cfg.plugins?.installs || {};
return withSqliteDatabase(stateDatabasePath(), (db) => {
const row = db
.prepare(
"SELECT install_records_json FROM installed_plugin_index WHERE index_key = 'current'",
)
.get();
if (!row?.install_records_json) {
return {};
}
return JSON.parse(String(row.install_records_json));
});
}
function pluginInstallPath() {
@@ -357,13 +312,10 @@ function assertAgentTurn() {
`live agent reply did not contain tool slug ${expected}:\nstdout tail=${tailText(stdout, ERROR_DETAIL_TAIL_BYTES)}\nstderr tail=${stderrTail}`,
);
}
const sessionsDir = path.join(stateDir(), "agents", "main", "sessions");
const scan = scanSessionTranscripts(sessionsDir, [toolName, expected]);
if (scan.pendingNeedles.size > 0) {
const checkedFiles = scan.checkedFiles.length > 0 ? scan.checkedFiles.join(", ") : "<none>";
const missingDir = scan.missingDir ? " sessions directory was missing." : "";
const transcript = readMainAgentTranscript();
if (!transcript.text.includes(toolName) || !transcript.text.includes(expected)) {
throw new Error(
`session transcript did not show ${toolName} returning ${expected}; missing ${Array.from(scan.pendingNeedles).join(", ")} after checking ${scan.filesChecked} jsonl file(s): ${checkedFiles}.${missingDir}`,
`SQLite session transcript did not show ${toolName} returning ${expected} after checking ${transcript.eventCount} event(s)`,
);
}
}
@@ -1,14 +1,26 @@
import fs from "node:fs";
import path from "node:path";
import {
assertAgentReplyContainsMarker,
assertOpenAiRequestLogUsed,
} from "../agent-turn-output.mjs";
import { applyMockOpenAiModelConfig } from "../fixtures/mock-openai-config.mjs";
import { DatabaseSync } from "node:sqlite";
const command = process.argv[2];
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
function readAuthProfileStorePayload(stateDir, storeKey) {
const dbPath = path.join(stateDir, "state", "openclaw.sqlite");
if (!fs.existsSync(dbPath)) {
throw new Error(`missing OpenClaw state database: ${dbPath}`);
}
const db = new DatabaseSync(dbPath, { readOnly: true });
try {
const row = db
.prepare("SELECT store_json FROM auth_profile_stores WHERE store_key = ?")
.get(storeKey);
return typeof row?.store_json === "string" ? JSON.parse(row.store_json) : undefined;
} finally {
db.close();
}
}
function assertOnboardState() {
const home = process.argv[3];
const stateDir = path.join(home, ".openclaw");
@@ -22,23 +34,68 @@ function assertOnboardState() {
if (!fs.existsSync(agentDir)) {
throw new Error("onboard did not create main agent dir");
}
if (!fs.existsSync(authPath)) {
throw new Error("onboard did not create auth-profiles.json");
}
const authRaw = fs.readFileSync(authPath, "utf8");
if (!authRaw.includes("OPENAI_API_KEY")) {
const authStore = readAuthProfileStorePayload(stateDir, agentDir);
const authRaw = JSON.stringify(authStore ?? {});
if (!authStore || !authRaw.includes("OPENAI_API_KEY")) {
throw new Error("auth profile did not persist OPENAI_API_KEY env ref");
}
if (authRaw.includes("sk-openclaw-npm-onboard-e2e")) {
throw new Error("auth profile persisted the raw OpenAI test key");
}
if (fs.existsSync(authPath)) {
throw new Error(`auth profile should be SQLite-backed, found legacy file: ${authPath}`);
}
}
function configureMockModel() {
const mockPort = Number(process.argv[3]);
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const cfg = readJson(configPath);
applyMockOpenAiModelConfig(cfg, { mockPort });
const modelRef = "openai/gpt-5.5";
const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
cfg.models = {
...cfg.models,
mode: "merge",
providers: {
...cfg.models?.providers,
openai: {
...cfg.models?.providers?.openai,
baseUrl: `http://127.0.0.1:${mockPort}/v1`,
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
api: "openai-responses",
request: { ...cfg.models?.providers?.openai?.request, allowPrivateNetwork: true },
models: [
{
id: "gpt-5.5",
name: "gpt-5.5",
api: "openai-responses",
reasoning: false,
input: ["text", "image"],
cost,
contextWindow: 128000,
contextTokens: 96000,
maxTokens: 4096,
},
],
},
},
};
cfg.agents = {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
model: { primary: modelRef },
models: {
...cfg.agents?.defaults?.models,
[modelRef]: { params: { transport: "sse", openaiWsWarmup: false } },
},
},
};
cfg.plugins = {
...cfg.plugins,
enabled: true,
};
fs.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\n`);
}
+2 -2
View File
@@ -237,9 +237,9 @@ run_case_local_basic() {
# Assert config + workspace scaffolding.
workspace_dir="$OPENCLAW_STATE_DIR/workspace"
sessions_dir="$OPENCLAW_STATE_DIR/agents/main/sessions"
agent_db_dir="$OPENCLAW_STATE_DIR/agents/main/agent"
openclaw_e2e_assert_dir "$sessions_dir"
openclaw_e2e_assert_dir "$agent_db_dir"
for file in AGENTS.md BOOTSTRAP.md IDENTITY.md SOUL.md TOOLS.md USER.md; do
openclaw_e2e_assert_file "$workspace_dir/$file"
done
+2 -15
View File
@@ -66,19 +66,13 @@ parallels_bash_seed_workspace_snippet() {
local purpose="$1"
cat <<EOF
workspace="\${OPENCLAW_WORKSPACE_DIR:-\$HOME/.openclaw/workspace}"
mkdir -p "\$workspace/.openclaw"
mkdir -p "\$workspace"
cat > "\$workspace/IDENTITY.md" <<'IDENTITY_EOF'
# Identity
- Name: OpenClaw
- Purpose: $purpose
IDENTITY_EOF
cat > "\$workspace/.openclaw/workspace-state.json" <<'STATE_EOF'
{
"version": 1,
"setupCompletedAt": "2026-01-01T00:00:00.000Z"
}
STATE_EOF
rm -f "\$workspace/BOOTSTRAP.md"
EOF
}
@@ -90,20 +84,13 @@ parallels_powershell_seed_workspace_snippet() {
if (-not \$workspace) {
\$workspace = Join-Path \$env:USERPROFILE '.openclaw\\workspace'
}
\$stateDir = Join-Path \$workspace '.openclaw'
New-Item -ItemType Directory -Path \$stateDir -Force | Out-Null
New-Item -ItemType Directory -Path \$workspace -Force | Out-Null
@'
# Identity
- Name: OpenClaw
- Purpose: $purpose
'@ | Set-Content -Path (Join-Path \$workspace 'IDENTITY.md') -Encoding UTF8
@'
{
"version": 1,
"setupCompletedAt": "2026-01-01T00:00:00.000Z"
}
'@ | Set-Content -Path (Join-Path \$stateDir 'workspace-state.json') -Encoding UTF8
Remove-Item (Join-Path \$workspace 'BOOTSTRAP.md') -Force -ErrorAction SilentlyContinue
EOF
}
@@ -1,6 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { readInstalledPluginRecords } from "../installed-plugin-index.mjs";
const home = os.homedir();
@@ -26,8 +27,7 @@ function readRequiredJson(file) {
}
function records() {
const index = readJson(openclawPath("plugins", "installs.json"));
return index.installRecords ?? index.records ?? {};
return readInstalledPluginRecords();
}
function recordFor(pluginId) {
+6 -4
View File
@@ -2,6 +2,10 @@ import fs from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import {
readInstalledPluginRecords,
writeInstalledPluginIndex,
} from "../installed-plugin-index.mjs";
import { legacyPackageAcceptanceCompat } from "../package-compat.mjs";
const home = os.homedir();
@@ -15,9 +19,7 @@ const readJson = (file) => {
};
const pluginRecordSnapshot = () => {
const config = readJson(openclawPath("openclaw.json"));
const index = readJson(openclawPath("plugins", "installs.json"));
const records = index.installRecords ?? index.records ?? config.plugins?.installs ?? {};
const records = readInstalledPluginRecords();
const record = records["lossless-claw"] ?? records["@example/lossless-claw"];
if (!record) {
throw new Error("missing plugin install record");
@@ -41,7 +43,7 @@ function seedInstallState() {
version: "0.9.0",
});
writeJson(process.env.OPENCLAW_CONFIG_PATH, { plugins: {} });
writeJson(openclawPath("plugins", "installs.json"), {
writeInstalledPluginIndex({
version: 1,
warning: "DO NOT EDIT. This file is generated by OpenClaw plugin registry commands.",
hostContractVersion: "docker-e2e",
+31 -37
View File
@@ -2,6 +2,11 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { readPositiveIntEnv } from "../env-limits.mjs";
import {
readInstalledPluginIndex,
readInstalledPluginRecords,
writeInstalledPluginIndex,
} from "../installed-plugin-index.mjs";
const command = process.argv[2];
const scratchRoot = process.env.OPENCLAW_PLUGINS_TMP_DIR || os.tmpdir();
@@ -112,17 +117,11 @@ function pathsEqual(left, right) {
}
function getInstallRecords() {
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = fs.existsSync(configPath) ? readJson(configPath) : {};
const allowLegacyCompat = process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1";
if (!allowLegacyCompat && !index.installRecords) {
const index = readInstalledPluginIndex();
if (!index.installRecords) {
throw new Error("expected modern installRecords in installed plugin index");
}
return allowLegacyCompat
? (index.installRecords ?? index.records ?? config.plugins?.installs ?? {})
: (index.installRecords ?? {});
return index.installRecords;
}
function readOpenClawConfig() {
@@ -214,25 +213,30 @@ function recordFixturePluginTrust() {
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
const ledgerPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
const ledger = fs.existsSync(ledgerPath)
? readJson(ledgerPath)
: {
version: 1,
warning:
"DO NOT EDIT. This file is generated by OpenClaw plugin install/update/uninstall commands. Use `openclaw plugins install/update/uninstall` instead.",
records: {},
};
const ledger = {
version: 1,
warning:
"DO NOT EDIT. This record is generated by OpenClaw plugin install/update/uninstall commands.",
hostContractVersion: "docker-e2e",
compatRegistryVersion: "docker-e2e",
migrationVersion: 1,
policyHash: "docker-e2e",
generatedAtMs: Date.now(),
installRecords: {},
plugins: [],
diagnostics: [],
...readInstalledPluginIndex(),
};
ledger.updatedAtMs = Date.now();
ledger.records ??= {};
ledger.records[pluginId] = {
...ledger.records[pluginId],
ledger.installRecords ??= ledger.records ?? {};
delete ledger.records;
ledger.installRecords[pluginId] = {
...ledger.installRecords[pluginId],
source: "path",
installPath: pluginRoot,
sourcePath: pluginRoot,
};
fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
fs.writeFileSync(ledgerPath, `${JSON.stringify(ledger, null, 2)}\n`, "utf8");
writeInstalledPluginIndex(ledger);
}
function assertDemoPlugin() {
@@ -908,17 +912,11 @@ function assertClawHubInstalled() {
throw new Error(`unexpected ClawHub inspect plugin id: ${inspect.plugin?.id}`);
}
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
const index = readJson(indexPath);
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = fs.existsSync(configPath) ? readJson(configPath) : {};
const allowLegacyCompat = process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1";
if (!allowLegacyCompat && !index.installRecords) {
const index = readInstalledPluginIndex();
if (!index.installRecords) {
throw new Error("expected modern installRecords in installed plugin index");
}
const installRecords = allowLegacyCompat
? (index.installRecords ?? index.records ?? config.plugins?.installs ?? {})
: (index.installRecords ?? {});
const installRecords = index.installRecords;
const record = installRecords[pluginId];
if (!record) {
throw new Error(`missing ClawHub install record for ${pluginId}`);
@@ -963,11 +961,7 @@ function assertClawHubRemoved() {
throw new Error(`ClawHub plugin still listed after uninstall: ${pluginId}`);
}
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = fs.existsSync(configPath) ? readJson(configPath) : {};
const installRecords = index.installRecords ?? index.records ?? config.plugins?.installs ?? {};
const installRecords = readInstalledPluginRecords();
if (installRecords[pluginId]) {
throw new Error(`ClawHub install record still present after uninstall: ${pluginId}`);
}
@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { readInstalledPluginIndex as readSqliteInstalledPluginIndex } from "../installed-plugin-index.mjs";
const command = process.argv[2];
const SCENARIOS = new Set([
@@ -405,10 +406,9 @@ function assertStateSurvived() {
}
function readInstalledPluginIndex() {
const stateDir = requireEnv("OPENCLAW_STATE_DIR");
const file = path.join(stateDir, "plugins", "installs.json");
assert(fs.existsSync(file), `installed plugin index missing: ${file}`);
return readJson(file);
const index = readSqliteInstalledPluginIndex();
assert(index.installRecords, "installed plugin index missing installRecords");
return index;
}
function assertExternalPluginInstall(records, pluginId, packageName) {
+33 -40
View File
@@ -1,18 +1,17 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { upsertSessionEntry } from "../../dist/config/sessions/store.js";
import { replaceSqliteSessionTranscriptEvents } from "../../dist/config/sessions/transcript-store.sqlite.js";
import { resolveOpenClawAgentSqlitePath } from "../../dist/state/openclaw-agent-db.js";
import { applyDockerOpenAiProviderConfig, type OpenClawConfig } from "./docker-openai-seed.ts";
async function main() {
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || path.join(os.homedir(), ".openclaw");
const configPath =
process.env.OPENCLAW_CONFIG_PATH?.trim() || path.join(stateDir, "openclaw.json");
const sessionsDir = path.join(stateDir, "agents", "main", "sessions");
const sessionFile = path.join(sessionsDir, "sess-main.jsonl");
const storePath = path.join(sessionsDir, "sessions.json");
const now = Date.now();
await fs.mkdir(sessionsDir, { recursive: true });
await fs.mkdir(path.dirname(configPath), { recursive: true });
const seededConfig = applyDockerOpenAiProviderConfig(
@@ -39,44 +38,39 @@ async function main() {
await fs.writeFile(configPath, JSON.stringify(seededConfig, null, 2), "utf-8");
await fs.writeFile(
storePath,
JSON.stringify(
{
"agent:main:main": {
sessionId: "sess-main",
sessionFile,
updatedAt: now,
deliveryContext: {
channel: "imessage",
to: "+15551234567",
accountId: "imessage-default",
threadId: "thread-42",
},
displayName: "Docker MCP Channel Smoke",
derivedTitle: "Docker MCP Channel Smoke",
lastMessagePreview: "seeded transcript",
},
upsertSessionEntry({
agentId: "main",
sessionKey: "agent:main:main",
entry: {
sessionId: "sess-main",
updatedAt: now,
deliveryContext: {
channel: "imessage",
to: "+15551234567",
accountId: "imessage-default",
threadId: "thread-42",
},
null,
2,
),
"utf-8",
);
displayName: "Docker MCP Channel Smoke",
derivedTitle: "Docker MCP Channel Smoke",
lastMessagePreview: "seeded transcript",
},
});
await fs.writeFile(
sessionFile,
[
JSON.stringify({ type: "session", version: 1, id: "sess-main" }),
JSON.stringify({
replaceSqliteSessionTranscriptEvents({
agentId: "main",
sessionId: "sess-main",
now: () => now,
events: [
{ type: "session", version: 1, id: "sess-main" },
{
id: "msg-1",
message: {
role: "assistant",
content: [{ type: "text", text: "hello from seeded transcript" }],
timestamp: now,
},
}),
JSON.stringify({
},
{
id: "msg-attachment",
message: {
role: "assistant",
@@ -93,18 +87,17 @@ async function main() {
],
timestamp: now + 1,
},
}),
].join("\n") + "\n",
"utf-8",
);
},
],
});
process.stdout.write(
JSON.stringify({
ok: true,
stateDir,
configPath,
storePath,
sessionFile,
agentDatabasePath: resolveOpenClawAgentSqlitePath({ agentId: "main" }),
sessionId: "sess-main",
}) + "\n",
);
}
+1 -1
View File
@@ -394,7 +394,7 @@ for _ in $(seq 1 60); do
sleep 1
done
mkdir -p "$(dirname "$config_path")" "$HOME/.openclaw/workspace" "$HOME/.openclaw/agents/main/sessions" "$HOME/workspace"
mkdir -p "$(dirname "$config_path")" "$HOME/.openclaw/workspace" "$HOME/.openclaw/agents/main/agent" "$HOME/workspace"
node /app/scripts/e2e/npm-telegram-rtt-config.mjs \
"$config_path" \
+2 -15
View File
@@ -1,38 +1,25 @@
export function posixAgentWorkspaceScript(purpose: string): string {
return `set -eu
workspace="\${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
mkdir -p "$workspace/.openclaw"
mkdir -p "$workspace"
cat > "$workspace/IDENTITY.md" <<'IDENTITY_EOF'
# Identity
- Name: OpenClaw
- Purpose: ${purpose}
IDENTITY_EOF
cat > "$workspace/.openclaw/workspace-state.json" <<'STATE_EOF'
{
"version": 1,
"setupCompletedAt": "2026-01-01T00:00:00.000Z"
}
STATE_EOF
rm -f "$workspace/BOOTSTRAP.md"`;
}
export function windowsAgentWorkspaceScript(purpose: string): string {
return `$workspace = $env:OPENCLAW_WORKSPACE_DIR
if (-not $workspace) { $workspace = Join-Path $env:USERPROFILE '.openclaw\\workspace' }
$stateDir = Join-Path $workspace '.openclaw'
New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
New-Item -ItemType Directory -Path $workspace -Force | Out-Null
@'
# Identity
- Name: OpenClaw
- Purpose: ${purpose}
'@ | Set-Content -Path (Join-Path $workspace 'IDENTITY.md') -Encoding UTF8
@'
{
"version": 1,
"setupCompletedAt": "2026-01-01T00:00:00.000Z"
}
'@ | Set-Content -Path (Join-Path $stateDir 'workspace-state.json') -Encoding UTF8
Remove-Item (Join-Path $workspace 'BOOTSTRAP.md') -Force -ErrorAction SilentlyContinue`;
}
-1
View File
@@ -730,7 +730,6 @@ rm -f "$provider_config_batch"`);
for attempt in 1 2; do
session_id="parallels-linux-smoke"
if [ "$attempt" -gt 1 ]; then session_id="parallels-linux-smoke-retry-$attempt"; fi
rm -f "$HOME/.openclaw/agents/main/sessions/$session_id.jsonl"
output_file="$(mktemp)"
set +e
/usr/bin/env OPENCLAW_ALLOW_ROOT=1 ${shellQuote(`${this.auth.apiKeyEnv}=${this.auth.apiKeyValue}`)} openclaw agent --local --agent main --session-id "$session_id" --message ${shellQuote(
-1
View File
@@ -1044,7 +1044,6 @@ agent_ok=false
for attempt in 1 2; do
session_id="parallels-macos-smoke"
if [ "$attempt" -gt 1 ]; then session_id="parallels-macos-smoke-retry-$attempt"; fi
rm -f "$HOME/.openclaw/agents/main/sessions/$session_id.jsonl"
output_file="$(mktemp)"
set +e
/usr/bin/env ${shellQuote(`${this.auth.apiKeyEnv}=${this.auth.apiKeyValue}`)} ${guestOpenClawEntryRunner} agent --local --agent main --session-id "$session_id" --message ${shellQuote(
-3
View File
@@ -717,9 +717,6 @@ Set-Item -Path ('Env:' + ${psSingleQuote(this.auth.apiKeyEnv)}) -Value ${psSingl
$agentOk = $false
for ($attempt = 1; $attempt -le 2; $attempt++) {
$sessionId = if ($attempt -eq 1) { 'parallels-windows-smoke' } else { "parallels-windows-smoke-retry-$attempt" }
$sessionsDir = Join-Path $env:USERPROFILE '.openclaw\\agents\\main\\sessions'
$sessionPath = Join-Path $sessionsDir "$sessionId.jsonl"
Remove-Item $sessionPath -Force -ErrorAction SilentlyContinue
$args = @(
'agent',
'--local',
@@ -5,7 +5,6 @@ import { spawnSync } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { SessionManager } from "openclaw/plugin-sdk/agent-sessions";
import {
buildRuntimeContextCustomMessage,
resolveRuntimeContextPromptParts,
@@ -21,6 +20,19 @@ type TranscriptEntry = {
content?: unknown;
};
};
type SqliteTranscriptStoreModule = {
appendSqliteSessionTranscriptEvent: (params: {
agentId: string;
sessionId: string;
event: unknown;
now?: () => number;
parentMode?: "database-tail";
}) => void;
loadSqliteSessionTranscriptEvents: (params: {
agentId: string;
sessionId: string;
}) => Array<{ event: unknown }>;
};
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
@@ -28,14 +40,6 @@ function assert(condition: unknown, message: string): asserts condition {
}
}
async function readJsonl(filePath: string): Promise<TranscriptEntry[]> {
const raw = await fs.readFile(filePath, "utf-8");
return raw
.split(/\r?\n/)
.filter(Boolean)
.map((line) => JSON.parse(line) as TranscriptEntry);
}
function messageText(content: unknown): string {
if (typeof content === "string") {
return content;
@@ -53,9 +57,19 @@ function messageText(content: unknown): string {
}
async function verifyRuntimeContextTranscriptShape(root: string) {
const sessionFile = path.join(root, ".openclaw", "agents", "main", "sessions", "runtime.jsonl");
await fs.mkdir(path.dirname(sessionFile), { recursive: true });
const sessionManager = SessionManager.open(sessionFile);
const { appendSqliteSessionTranscriptEvent, loadSqliteSessionTranscriptEvents } =
(await import("../../dist/config/sessions/transcript-store.sqlite.js")) as SqliteTranscriptStoreModule;
const agentId = "main";
const sessionId = "runtime";
let now = Date.now();
const appendEvent = (event: unknown) =>
appendSqliteSessionTranscriptEvent({
agentId,
sessionId,
event,
now: () => now++,
parentMode: "database-tail",
});
const effectivePrompt = [
"visible ask",
"",
@@ -76,18 +90,30 @@ async function verifyRuntimeContextTranscriptShape(root: string) {
const runtimeContextMessage = buildRuntimeContextCustomMessage(promptSubmission.runtimeContext);
assert(runtimeContextMessage, "runtime custom message was not built");
sessionManager.appendMessage({
role: "user",
content: promptSubmission.prompt,
timestamp: Date.now(),
appendEvent({
type: "message",
id: "runtime-user",
parentId: null,
timestamp: now,
message: {
role: "user",
content: promptSubmission.prompt,
},
});
sessionManager.appendMessage({
role: "assistant",
content: "done",
timestamp: Date.now() + 1,
appendEvent({
type: "message",
id: "runtime-assistant",
parentId: null,
timestamp: now,
message: {
role: "assistant",
content: "done",
},
});
const entries = await readJsonl(sessionFile);
const entries = loadSqliteSessionTranscriptEvents({ agentId, sessionId }).map(
(entry) => entry.event as TranscriptEntry,
);
const customEntry = entries.find((entry) => entry.type === "custom_message");
assert(!customEntry, "runtime custom message should not be persisted without its user turn");
assert(
@@ -110,9 +136,9 @@ async function verifyRuntimeContextTranscriptShape(root: string) {
);
}
async function seedBrokenSession(stateDir: string): Promise<string> {
async function seedBrokenLegacySessionForDoctorMigration(stateDir: string): Promise<string> {
const sessionsDir = path.join(stateDir, "agents", "main", "sessions");
const sessionFile = path.join(sessionsDir, "broken.jsonl");
const legacyTranscriptPath = path.join(sessionsDir, "broken.jsonl");
await fs.mkdir(sessionsDir, { recursive: true });
const entries = [
{ type: "session", version: 3, id: "broken-session" },
@@ -157,12 +183,15 @@ async function seedBrokenSession(stateDir: string): Promise<string> {
},
];
await fs.writeFile(
sessionFile,
legacyTranscriptPath,
`${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`,
"utf-8",
);
// This is intentionally a legacy input: the scenario proves doctor imports
// session indexes and transcript JSONL into SQLite, then removes the sources.
const legacySessionIndexPath = path.join(sessionsDir, "sessions.json");
await fs.writeFile(
path.join(sessionsDir, "sessions.json"),
legacySessionIndexPath,
JSON.stringify(
{
"agent:main:qa:docker-runtime-context": {
@@ -177,13 +206,13 @@ async function seedBrokenSession(stateDir: string): Promise<string> {
),
"utf-8",
);
return sessionFile;
return legacyTranscriptPath;
}
async function verifyDoctorRepair(root: string) {
const stateDir = path.join(root, ".openclaw");
const configPath = path.join(stateDir, "openclaw.json");
const sessionFile = await seedBrokenSession(stateDir);
const legacyTranscriptPath = await seedBrokenLegacySessionForDoctorMigration(stateDir);
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(configPath, JSON.stringify({ plugins: { enabled: false } }, null, 2));
@@ -214,7 +243,18 @@ async function verifyDoctorRepair(root: string) {
result.status === 0,
`doctor --fix failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
const entries = await readJsonl(sessionFile);
await fs.access(legacyTranscriptPath).then(
() => {
throw new Error("doctor left legacy transcript JSONL after SQLite import");
},
() => undefined,
);
const { loadSqliteSessionTranscriptEvents } =
(await import("../../dist/config/sessions/transcript-store.sqlite.js")) as SqliteTranscriptStoreModule;
const entries = loadSqliteSessionTranscriptEvents({
agentId: "main",
sessionId: "broken-session",
}).map((entry) => entry.event as TranscriptEntry);
const ids = entries.map((entry) => (entry as { id?: string }).id).filter(Boolean);
assert(
JSON.stringify(ids) ===
@@ -227,10 +267,6 @@ async function verifyDoctorRepair(root: string) {
),
"doctor repair left runtime context in active transcript",
);
const backups = (await fs.readdir(path.dirname(sessionFile))).filter((name) =>
name.includes(".pre-doctor-branch-repair-"),
);
assert(backups.length === 1, `expected one doctor backup, got ${backups.length}`);
}
async function main() {
+28 -24
View File
@@ -71,7 +71,7 @@ type Options = {
recordFps: number;
recordSeconds: number;
remoteCommand: string[];
sessionFile?: string;
sessionStatePath?: string;
sutUsername?: string;
target: string;
tdlibSha256?: string;
@@ -100,7 +100,7 @@ type LocalSut = {
gatewayLog: string;
};
type SessionFile = {
type ProofSessionState = {
command: "telegram-user-crabbox-session";
createdAt: string;
crabbox: {
@@ -195,7 +195,7 @@ function usageText() {
" --record-fps <fps> Desktop recording frames per second. Default: 24.",
" --record-seconds <seconds> Desktop video duration. Default: 35.",
" --repo <owner/name> GitHub repo for publish. Default: openclaw/openclaw.",
" --session <path> Session file from start. Default: <output-dir>/session.json.",
" --session <path> Proof session state from start. Default: <output-dir>/session.json.",
" --summary <text> Artifact publish summary.",
" --full-artifacts Publish all session artifacts. Default publishes only the motion GIF.",
" --tdlib-sha256 <hex> Expected SHA-256 for --tdlib-url. Defaults to <url>.sha256.",
@@ -346,7 +346,7 @@ function parseArgs(argvInput: string[]): Options {
} else if (arg === "--record-seconds") {
opts.recordSeconds = parsePositiveInteger(readValue(), "--record-seconds");
} else if (arg === "--session") {
opts.sessionFile = readValue();
opts.sessionStatePath = readValue();
} else if (arg === "--summary") {
opts.publishSummary = readValue();
} else if (arg === "--full-artifacts") {
@@ -381,7 +381,7 @@ function parseArgs(argvInput: string[]): Options {
}
if (
["finish", "publish", "run", "screenshot", "send", "status", "view"].includes(command) &&
!opts.sessionFile
!opts.sessionStatePath
) {
throw new Error(`${command} requires --session.`);
}
@@ -1643,24 +1643,24 @@ function writeReport(params: {
return reportPath;
}
function sessionPath(root: string, opts: Options, outputDir: string) {
return opts.sessionFile
? resolveRepoPath(root, opts.sessionFile)
function sessionStatePath(root: string, opts: Options, outputDir: string) {
return opts.sessionStatePath
? resolveRepoPath(root, opts.sessionStatePath)
: path.join(outputDir, "session.json");
}
function writeSession(pathname: string, session: SessionFile) {
function writeSessionState(pathname: string, session: ProofSessionState) {
fs.mkdirSync(path.dirname(pathname), { recursive: true });
fs.writeFileSync(pathname, `${JSON.stringify(session, null, 2)}\n`, { mode: 0o600 });
fs.chmodSync(pathname, 0o600);
}
function readSession(root: string, opts: Options, outputDir: string) {
const pathname = sessionPath(root, opts, outputDir);
function readSessionState(root: string, opts: Options, outputDir: string) {
const pathname = sessionStatePath(root, opts, outputDir);
if (!fs.existsSync(pathname)) {
throw new Error(`Missing session file: ${path.relative(root, pathname)}`);
throw new Error(`Missing proof session state: ${path.relative(root, pathname)}`);
}
const session = readJsonFile(pathname) as SessionFile;
const session = readJsonFile(pathname) as ProofSessionState;
if (session.command !== "telegram-user-crabbox-session") {
throw new Error(`Invalid Telegram Crabbox session file: ${path.relative(root, pathname)}`);
}
@@ -1743,7 +1743,11 @@ echo $! >"$pid_file"`;
};
}
async function stopRemoteRecording(root: string, inspect: CrabboxInspect, session: SessionFile) {
async function stopRemoteRecording(
root: string,
inspect: CrabboxInspect,
session: ProofSessionState,
) {
await sshRun(
root,
inspect,
@@ -1845,7 +1849,7 @@ async function startSession(root: string, opts: Options, outputDir: string) {
testerId: credential.testerUserId,
});
const recorder = await startRemoteRecording(root, inspect, opts);
const session: SessionFile = {
const session: ProofSessionState = {
command: "telegram-user-crabbox-session",
createdAt: new Date().toISOString(),
crabbox: {
@@ -1869,8 +1873,8 @@ async function startSession(root: string, opts: Options, outputDir: string) {
recorder,
remoteRoot: REMOTE_ROOT,
};
const pathname = sessionPath(root, opts, outputDir);
writeSession(pathname, session);
const pathname = sessionStatePath(root, opts, outputDir);
writeSessionState(pathname, session);
return {
session: path.relative(root, pathname),
status: "pass",
@@ -1902,7 +1906,7 @@ async function startSession(root: string, opts: Options, outputDir: string) {
}
async function sendSessionProbe(root: string, opts: Options, outputDir: string) {
const { session } = readSession(root, opts, outputDir);
const { session } = readSessionState(root, opts, outputDir);
const stamp = new Date().toISOString().replace(/[:.]/gu, "-");
const targetText = buildTargetText(opts.text, session.credential.sutUsername);
const remoteProbe = `${REMOTE_ROOT}/probe-${stamp}.json`;
@@ -1930,7 +1934,7 @@ async function sendSessionProbe(root: string, opts: Options, outputDir: string)
}
async function runSessionCommand(root: string, opts: Options, outputDir: string) {
const { session } = readSession(root, opts, outputDir);
const { session } = readSessionState(root, opts, outputDir);
const command = opts.remoteCommand.map(shellQuote).join(" ");
const logPath = path.join(
session.outputDir,
@@ -1941,7 +1945,7 @@ async function runSessionCommand(root: string, opts: Options, outputDir: string)
}
async function screenshotSession(root: string, opts: Options, outputDir: string) {
const { session } = readSession(root, opts, outputDir);
const { session } = readSessionState(root, opts, outputDir);
const screenshotPath = path.join(
session.outputDir,
`telegram-user-crabbox-${new Date().toISOString().replace(/[:.]/gu, "-")}.png`,
@@ -1966,7 +1970,7 @@ async function screenshotSession(root: string, opts: Options, outputDir: string)
}
async function statusSession(root: string, opts: Options, outputDir: string) {
const { path: pathname, session } = readSession(root, opts, outputDir);
const { path: pathname, session } = readSessionState(root, opts, outputDir);
const inspect = await inspectCrabbox(opts, root, session.crabbox.id);
return {
crabbox: {
@@ -2012,7 +2016,7 @@ wmctrl -lxG | awk 'tolower($0) ~ /telegramdesktop/'`;
}
async function viewSession(root: string, opts: Options, outputDir: string) {
const { session } = readSession(root, opts, outputDir);
const { session } = readSessionState(root, opts, outputDir);
const messageId = opts.messageId;
if (!messageId) {
throw new Error("view requires --message-id.");
@@ -2035,7 +2039,7 @@ async function viewSession(root: string, opts: Options, outputDir: string) {
}
async function finishSession(root: string, opts: Options, outputDir: string) {
const { path: pathname, session } = readSession(root, opts, outputDir);
const { path: pathname, session } = readSessionState(root, opts, outputDir);
const summary: JsonObject = {
artifacts: {},
finishedAt: new Date().toISOString(),
@@ -2177,7 +2181,7 @@ async function finishSession(root: string, opts: Options, outputDir: string) {
}
async function publishSessionArtifacts(root: string, opts: Options, outputDir: string) {
const { session } = readSession(root, opts, outputDir);
const { session } = readSessionState(root, opts, outputDir);
const motionGifPath = path.join(session.outputDir, "telegram-user-crabbox-session-motion.gif");
const croppedMotionGifPath = path.join(
session.outputDir,
+3 -4
View File
@@ -81,10 +81,9 @@ function generateTypes(db) {
"",
'import type { ColumnType } from "kysely";',
"",
"export type Generated<T> =",
" T extends ColumnType<infer S, infer I, infer U>",
" ? ColumnType<S, I | undefined, U>",
" : ColumnType<T, T | undefined, T>;",
"export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>",
" ? ColumnType<S, I | undefined, U>",
" : ColumnType<T, T | undefined, T>;",
"",
];
+57 -7
View File
@@ -380,10 +380,57 @@ function readShrinkwrapOverrides() {
);
}
function directDependencySpecs(packageJson) {
const specs = {};
for (const key of ["dependencies", "optionalDependencies", "peerDependencies"]) {
const dependencies = packageJson?.[key];
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) {
continue;
}
for (const [name, spec] of Object.entries(dependencies)) {
specs[name] = String(spec);
}
}
return specs;
}
function alignDirectDependencyOverrides(overrides, packageJson) {
if (!overrides) {
return overrides;
}
const directSpecs = directDependencySpecs(packageJson);
const aligned = { ...overrides };
for (const [name, directSpec] of Object.entries(directSpecs)) {
if (aligned[name] === undefined || exactVersionFromOverrideSpec(directSpec) === null) {
continue;
}
const override = aligned[name];
if (typeof override === "string") {
if (!exactOverrideVersionsMatch(override, directSpec)) {
aligned[name] = directSpec;
}
continue;
}
if (isPlainObject(override)) {
const rootOverride = override["."];
if (
rootOverride === undefined ||
!exactOverrideVersionsMatch(String(rootOverride), directSpec)
) {
aligned[name] = { ...override, ".": directSpec };
}
}
}
return aligned;
}
function packageJsonForShrinkwrap(packageJson, shrinkwrapOverrides) {
const normalized = { ...packageJson };
delete normalized.devDependencies;
normalized.overrides = mergeOverrides(packageJson.overrides, shrinkwrapOverrides, {});
normalized.overrides = alignDirectDependencyOverrides(
mergeOverrides(packageJson.overrides, shrinkwrapOverrides, {}),
packageJson,
);
return normalized;
}
@@ -667,12 +714,15 @@ function generateShrinkwrap(packageDir, options = {}) {
try {
const packageJson = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8"));
const currentShrinkwrap = readCurrentShrinkwrap(packageDir);
const shrinkwrapOverrides = mergeOverrides(
options.useCurrentShrinkwrapOverrides
? readCurrentShrinkwrapOverrides(packageDir, declaredPackageDependencies(packageJson))
: {},
readShrinkwrapOverrides(),
{},
const shrinkwrapOverrides = alignDirectDependencyOverrides(
mergeOverrides(
options.useCurrentShrinkwrapOverrides
? readCurrentShrinkwrapOverrides(packageDir, declaredPackageDependencies(packageJson))
: {},
readShrinkwrapOverrides(),
{},
),
packageJson,
);
const peerResolutionArgs = shouldUseLegacyPeerDepsForShrinkwrap(packageJson)
? ["--legacy-peer-deps"]
@@ -1,6 +1,9 @@
import fs from "node:fs";
import path from "node:path";
import { buildSecretRefCredentialMatrix } from "../src/secrets/credential-matrix.js";
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR ??= path.join(process.cwd(), "extensions");
const { buildSecretRefCredentialMatrix } = await import("../src/secrets/credential-matrix.js");
const outputPath = path.join(
process.cwd(),
+25 -9
View File
@@ -54,11 +54,9 @@ PREFIX="${OPENCLAW_PREFIX:-${HOME}/.openclaw}"
OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
NODE_VERSION="${OPENCLAW_NODE_VERSION:-22.22.0}"
NODE_VERSION_REQUESTED=0
if [[ -n "${OPENCLAW_NODE_VERSION:-}" ]]; then
NODE_VERSION_REQUESTED=1
fi
MIN_NODE_VERSION="22.19.0"
APK_NODE_BIN_DIR="/usr/bin"
SHARP_IGNORE_GLOBAL_LIBVIPS="${SHARP_IGNORE_GLOBAL_LIBVIPS:-1}"
NPM_LOGLEVEL="${OPENCLAW_NPM_LOGLEVEL:-error}"
INSTALL_METHOD="${OPENCLAW_INSTALL_METHOD:-npm}"
GIT_DIR="${OPENCLAW_GIT_DIR:-${OPENCLAW_EFFECTIVE_HOME}/openclaw}"
@@ -353,6 +351,21 @@ node_bin() {
echo "$(node_dir)/bin/node"
}
npm_node_version_is_supported() {
local raw="$1"
local version
local major
local minor
version="${raw#v}"
major="${version%%.*}"
minor="${version#*.}"
minor="${minor%%.*}"
[[ "$major" =~ ^[0-9]+$ ]] || return 1
[[ "$minor" =~ ^[0-9]+$ ]] || minor=0
[[ "$major" -gt 22 ]] || { [[ "$major" -eq 22 ]] && [[ "$minor" -ge 19 ]]; }
}
npm_bin() {
echo "$(node_dir)/bin/npm"
}
@@ -768,6 +781,10 @@ install_node() {
return
fi
if ! npm_node_version_is_supported "$NODE_VERSION"; then
fail "OpenClaw requires Node 22.19.0 or newer; got --node-version ${NODE_VERSION}"
fi
emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"start\",\"version\":\"${NODE_VERSION}\"}"
log "Installing Node ${NODE_VERSION} (user-space)..."
@@ -799,12 +816,11 @@ install_node() {
ln -sfn "$dir" "${PREFIX}/tools/node"
if ! linked_node_is_usable; then
local installed_version
local required_version
installed_version="$("$(node_bin)" -v 2>/dev/null || echo unknown)"
required_version="$(required_node_version)"
fail "Installed Node ${NODE_VERSION} must provide Node >= ${required_version} with node:sqlite; found ${installed_version}. Re-run with --node-version 22.22.0 (or newer)"
if ! npm_node_version_is_supported "$("$(node_bin)" -v 2>/dev/null || echo "")"; then
fail "Installed Node ${NODE_VERSION} is below the required 22.19.0 minimum"
fi
if ! "$(node_bin)" -e "require('node:sqlite')" >/dev/null 2>&1; then
fail "Installed Node ${NODE_VERSION} is missing node:sqlite; re-run with --node-version 22.22.0 (or newer)"
fi
emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"ok\",\"version\":\"${NODE_VERSION}\"}"
}
+3 -3
View File
@@ -1647,7 +1647,7 @@ ensure_macos_default_node_active() {
return 1
}
ensure_macos_node22_active() {
ensure_macos_node24_active() {
ensure_macos_default_node_active "$@"
}
@@ -1785,6 +1785,7 @@ install_node_with_apk() {
# Install Node.js
install_node() {
if [[ "$OS" == "macos" ]]; then
install_homebrew
ui_info "Installing Node.js via Homebrew"
if ! run_quiet_step "Installing node@${NODE_DEFAULT_MAJOR}" brew install "node@${NODE_DEFAULT_MAJOR}"; then
echo "Re-run with --verbose or run 'brew install node@${NODE_DEFAULT_MAJOR}' directly, then rerun the installer."
@@ -3102,8 +3103,7 @@ main() {
ui_stage "Preparing environment"
# Step 1: Node.js. macOS package-manager branches install Homebrew lazily
# only when they are about to call brew.
# Step 1: Node.js
load_nvm_for_node_detection
if ! check_node; then
install_homebrew
+2 -4
View File
@@ -70,16 +70,14 @@ openclaw_live_stage_state_dir() {
mkdir -p "$dest_dir"
if [ -d "$source_dir" ]; then
# Sandbox workspaces can accumulate root-owned artifacts from prior Docker
# runs. The persisted plugin registry contains host-absolute paths that are
# not portable into Linux containers. Neither is needed for live-test
# auth/config staging, so keep them out of the staged state copy.
# runs. They are not needed for live-test auth/config staging, so keep them
# out of the staged state copy.
set +e
tar -C "$source_dir" \
--warning=no-file-changed \
--ignore-failed-read \
--exclude=workspace \
--exclude=sandboxes \
--exclude=plugins/installs.json \
--exclude=relay.sock \
--exclude='*.sock' \
--exclude='*/*.sock' \
@@ -2,16 +2,6 @@
"agent-config-primitives",
"agent-runtime-test-contracts",
"channel-config-schema-legacy",
"channel-contract-testing",
"channel-envelope",
"channel-inbound-roots",
"channel-lifecycle",
"channel-location",
"channel-logging",
"channel-message",
"channel-message-runtime",
"channel-pairing-paths",
"channel-reply-options-runtime",
"channel-reply-pipeline",
"channel-runtime",
"channel-secret-runtime",
+9
View File
@@ -101,6 +101,12 @@ export const pluginSdkDocMetadata = {
"provider-selection-runtime": {
category: "provider",
},
"provider-ai": {
category: "provider",
},
"provider-ai-oauth": {
category: "provider",
},
"runtime-store": {
category: "runtime",
},
@@ -128,6 +134,9 @@ export const pluginSdkDocMetadata = {
"reply-payload": {
category: "utilities",
},
"agent-core": {
category: "runtime",
},
} as const satisfies Record<string, PluginSdkDocMetadata>;
export type PluginSdkDocEntrypoint = keyof typeof pluginSdkDocMetadata;
+6 -1
View File
@@ -4,6 +4,8 @@
"lmstudio",
"lmstudio-runtime",
"provider-setup",
"provider-ai",
"provider-ai-oauth",
"sandbox",
"self-hosted-provider-setup",
"routing",
@@ -90,6 +92,7 @@
"thread-bindings-session-runtime",
"text-runtime",
"text-chunking",
"agent-core",
"agent-runtime",
"simple-completion-runtime",
"speech-core",
@@ -140,6 +143,7 @@
"migration",
"migration-runtime",
"plugin-state-runtime",
"sqlite-state-lock",
"plugin-state-test-runtime",
"markdown-table-runtime",
"account-helpers",
@@ -215,6 +219,7 @@
"session-binding-runtime",
"session-key-runtime",
"session-store-runtime",
"sqlite-runtime",
"session-transcript-hit",
"session-visibility",
"ssrf-dispatcher",
@@ -253,6 +258,7 @@
"memory-core-host-engine-embeddings",
"memory-core-host-engine-foundation",
"memory-core-host-engine-qmd",
"memory-core-host-engine-session-transcripts",
"memory-core-host-engine-storage",
"memory-core-host-multimodal",
"memory-core-host-query",
@@ -327,7 +333,6 @@
"web-media",
"zalouser",
"zod",
"agent-core",
"agent-sessions",
"llm"
]
+4 -1
View File
@@ -22,7 +22,10 @@ if (mode !== "lint" && mode !== "format") {
const lintExts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
const formatExts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".md", ".mdx"]);
const formatIgnoredPathPatterns = [/^extensions\/[^/]+\/src\/host\/.+\/[^/]+\.bundle\.js$/u];
const formatIgnoredPathPatterns = [
/^extensions\/[^/]+\/src\/host\/.+\/[^/]+\.bundle\.js$/u,
/\.generated\.d\.ts$/u,
];
const shouldSelect = (filePath) => {
const ext = path.extname(filePath).toLowerCase();
-9
View File
@@ -222,7 +222,6 @@ export function runReleaseCheckCommand(
}
return typeof output === "string" ? output : output.toString("utf8");
}
export function collectSkillShellScriptExecutableErrors(rootDir = resolve(".")): string[] {
if (process.platform === "win32") {
return [];
@@ -635,7 +634,6 @@ function runPackedPluginSdkTypescriptSmoke(tarballPath: string, tmpRoot: string)
},
);
}
export function writePackedBundledPluginActivationConfig(homeDir: string): void {
const configPath = join(homeDir, ".openclaw", "openclaw.json");
mkdirSync(join(homeDir, ".openclaw"), { recursive: true });
@@ -851,13 +849,6 @@ function runPackedBundledChannelEntrySmoke(): void {
},
);
const completionFiles = readdirSync(join(stateDir, "completions")).filter(
(entry) => !entry.startsWith("."),
);
if (completionFiles.length === 0) {
throw new Error("release-check: packed completion smoke produced no completion files.");
}
runInstalledWorkspaceBootstrapSmoke({ packageRoot });
} finally {
rmSync(tmpRoot, { recursive: true, force: true });
+1
View File
@@ -390,6 +390,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
],
["scripts/check-dependency-pins.mjs", ["test/scripts/check-dependency-pins.test.ts"]],
["scripts/check-deadcode-unused-files.mjs", ["test/scripts/check-deadcode-unused-files.test.ts"]],
["scripts/check-kysely-guardrails.mjs", ["test/scripts/check-kysely-guardrails.test.ts"]],
["scripts/check-dynamic-import-warts.mjs", ["test/scripts/check-dynamic-import-warts.test.ts"]],
["scripts/check-no-conflict-markers.mjs", ["test/scripts/check-no-conflict-markers.test.ts"]],
[
+12 -46
View File
@@ -28,11 +28,8 @@ import path from "node:path";
import { confirm, isCancel } from "@clack/prompts";
import { stylePromptMessage } from "../packages/terminal-core/src/prompt-style.js";
import { theme } from "../packages/terminal-core/src/theme.js";
import { installCompletion } from "../src/cli/completion-cli.js";
import {
checkShellCompletionStatus,
ensureCompletionCacheExists,
} from "../src/commands/doctor-completion.js";
import { installCompletion } from "../src/cli/completion-runtime.js";
import { checkShellCompletionStatus } from "../src/commands/doctor-completion.js";
const CLI_NAME = "openclaw";
@@ -80,9 +77,9 @@ ${theme.heading("Options:")}
--help, -h Show this help message
${theme.heading("Behavior:")}
- If profile has completion but no cache: auto-regenerates cache
- If profile points at the retired completion cache: rewrites it
- If no completion at all: prompts to install
- If both profile and cache exist: nothing to do
- If completion is already installed: nothing to do
${theme.heading("Examples:")}
node --import tsx scripts/test-shell-completion.ts
@@ -136,14 +133,12 @@ async function main() {
console.log(` Shell: ${theme.accent(status.shell)} ${theme.muted("(detected from $SHELL)")}`);
console.log(` Platform: ${theme.muted(process.platform)} ${theme.muted(`(${os.release()})`)}`);
console.log(` Profile: ${theme.muted(getShellProfilePath(status.shell))}`);
console.log(` Cache path: ${theme.muted(status.cachePath)}`);
console.log("");
console.log(
` Profile configured: ${status.profileInstalled ? theme.success("yes") : theme.warn("no")}`,
);
console.log(` Cache exists: ${status.cacheExists ? theme.success("yes") : theme.warn("no")}`);
console.log(
` Uses slow pattern: ${status.usesSlowPattern ? theme.error("yes (needs upgrade)") : theme.success("no")}`,
` Uses retired cache: ${status.usesRetiredCache ? theme.error("yes (needs rewrite)") : theme.success("no")}`,
);
console.log("");
@@ -152,33 +147,16 @@ async function main() {
return;
}
// Profile uses slow dynamic pattern - upgrade to cached version
if (status.usesSlowPattern) {
console.log(theme.warn("Profile uses slow dynamic completion. Upgrading to cached version..."));
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME);
if (cacheGenerated) {
await installCompletion(status.shell, false, CLI_NAME);
console.log(theme.success("Upgraded to cached completion."));
} else {
console.log(theme.error("Failed to generate cache."));
}
if (status.usesRetiredCache) {
console.log(theme.warn("Profile uses retired completion cache. Rewriting..."));
await installCompletion(status.shell, false, CLI_NAME, {
retiredCachePath: status.retiredCachePath,
});
console.log(theme.success("Rewrote completion profile."));
return;
}
// Profile has completion but no cache - auto-fix
if (status.profileInstalled && !status.cacheExists) {
console.log(theme.warn("Profile has completion but cache is missing. Regenerating..."));
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME);
if (cacheGenerated) {
console.log(theme.success("Cache regenerated successfully."));
} else {
console.log(theme.error("Failed to regenerate cache."));
}
return;
}
// Both profile and cache exist - nothing to do
if (status.profileInstalled && status.cacheExists && !options.force) {
if (status.profileInstalled && !options.force) {
console.log(theme.muted("Shell completion is fully configured. To test the prompt:"));
console.log(
theme.muted(" 1. Remove the '# OpenClaw Completion' block from your shell profile"),
@@ -202,18 +180,6 @@ async function main() {
return;
}
// Generate cache first (required for fast shell startup)
if (!status.cacheExists) {
console.log(theme.muted("Generating completion cache..."));
const cacheGenerated = await ensureCompletionCacheExists(CLI_NAME);
if (!cacheGenerated) {
console.log(theme.error("Failed to generate completion cache."));
return;
}
console.log(theme.success("Cache generated."));
}
// Install to shell profile
await installCompletion(status.shell, false, CLI_NAME);
}
+30 -18
View File
@@ -9,6 +9,10 @@ import { startQaMockOpenAiServer } from "../extensions/qa-lab/src/providers/mock
import { stageQaMockAuthProfiles } from "../extensions/qa-lab/src/providers/shared/mock-auth.js";
import { buildQaGatewayConfig } from "../extensions/qa-lab/src/qa-gateway-config.js";
import { resetConfigRuntimeState } from "../src/config/config.js";
import {
listSqliteSessionTranscripts,
loadSqliteSessionTranscriptEvents,
} from "../src/config/sessions/transcript-store.sqlite.js";
import { startGatewayServer } from "../src/gateway/server.js";
import { readPositiveIntEnv } from "./e2e/lib/env-limits.mjs";
import { readBoundedResponseText } from "./lib/bounded-response.ts";
@@ -31,7 +35,7 @@ type LaneResult = {
providerPlannedTools: string[];
gatewayOutputToolNames: string[];
gatewayOutputText: string;
sessionLogToolMentions: Record<string, number>;
transcriptToolMentions: Record<string, number>;
};
const FAKE_PLUGIN_ID = "tool-search-e2e-fixture";
@@ -128,25 +132,33 @@ function countOccurrences(haystack: string, needle: string): number {
}
}
async function readSessionLogMentions(params: {
function stringifyTranscriptEvent(event: unknown): string {
try {
return JSON.stringify(event);
} catch {
return "";
}
}
async function readSqliteTranscriptMentions(params: {
stateDir: string;
targetTool: string;
}): Promise<Record<string, number>> {
const sessionsDir = path.join(params.stateDir, "agents", "qa", "sessions");
const mentions: Record<string, number> = {
tool_search_code: 0,
[params.targetTool]: 0,
};
let files: string[] = [];
try {
files = await fs.readdir(sessionsDir);
} catch {
return mentions;
}
for (const file of files.filter((candidate) => candidate.endsWith(".jsonl"))) {
const raw = await fs.readFile(path.join(sessionsDir, file), "utf8").catch(() => "");
mentions.tool_search_code += countOccurrences(raw, "tool_search_code");
mentions[params.targetTool] += countOccurrences(raw, params.targetTool);
const env = { ...process.env, OPENCLAW_STATE_DIR: params.stateDir };
for (const transcript of listSqliteSessionTranscripts({ env, agentId: "qa" })) {
for (const entry of loadSqliteSessionTranscriptEvents({
env,
agentId: transcript.agentId,
sessionId: transcript.sessionId,
})) {
const raw = stringifyTranscriptEvent(entry.event);
mentions.tool_search_code += countOccurrences(raw, "tool_search_code");
mentions[params.targetTool] += countOccurrences(raw, params.targetTool);
}
}
return mentions;
}
@@ -570,7 +582,7 @@ async function runLane(params: {
.filter((name): name is string => typeof name === "string"),
gatewayOutputToolNames: outputToolNames(response),
gatewayOutputText: outputText(response),
sessionLogToolMentions: await readSessionLogMentions({
transcriptToolMentions: await readSqliteTranscriptMentions({
stateDir,
targetTool: params.targetTool,
}),
@@ -619,7 +631,7 @@ export async function main() {
assert(
code.providerPlannedTools.includes("tool_search_code") &&
code.gatewayOutputText.includes(targetTool) &&
code.sessionLogToolMentions[targetTool] > 0,
code.transcriptToolMentions[targetTool] > 0,
`code lane did not bridge-call ${targetTool}`,
);
assert(
@@ -631,9 +643,9 @@ export async function main() {
`expected Tool Search request to be smaller: normal=${normal.providerRawBytes} code=${code.providerRawBytes}`,
);
assert(
code.sessionLogToolMentions.tool_search_code > 0 &&
code.sessionLogToolMentions[targetTool] > 0,
"code lane session log did not record bridge and target tool mentions",
code.transcriptToolMentions.tool_search_code > 0 &&
code.transcriptToolMentions[targetTool] > 0,
"code lane SQLite transcript did not record bridge and target tool mentions",
);
const summary = {
+1 -5
View File
@@ -348,15 +348,11 @@ function renderSourceBrowserHelpText(
const browserCliUrl = pathToFileURL(
path.join(rootDir, "extensions/browser/src/cli/browser-cli.ts"),
).href;
const helpUrl = pathToFileURL(path.join(rootDir, "src/cli/program/help.ts")).href;
const contextUrl = pathToFileURL(path.join(rootDir, "src/cli/program/context.ts")).href;
const inlineModule = [
`const { Command } = await import("commander");`,
`const { registerBrowserCli } = await import(${JSON.stringify(browserCliUrl)});`,
`const { configureProgramHelp } = await import(${JSON.stringify(helpUrl)});`,
`const { createProgramContext } = await import(${JSON.stringify(contextUrl)});`,
`const program = new Command();`,
`configureProgramHelp(program, createProgramContext());`,
`program.name("openclaw");`,
`registerBrowserCli(program, ["node", "openclaw", "browser", "--help"]);`,
`const browser = program.commands.find((cmd) => cmd.name() === "browser");`,
`if (!browser) throw new Error("Browser command was not registered.");`,
+10 -3
View File
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { loadSqliteSessionTranscriptEvents } from "../src/config/sessions/transcript-store.sqlite.js";
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
type RunResult = {
@@ -212,9 +213,15 @@ async function main() {
process.exit(run1.code ?? 1);
}
const sessionFile = path.join(stateDir, "agents", "main", "sessions", `${sessionId}.jsonl`);
const transcript = await fs.readFile(sessionFile, "utf8").catch(() => "");
if (!transcript.includes('"toolResult"')) {
const transcriptEvents = loadSqliteSessionTranscriptEvents({
stateDir,
agentId: "main",
sessionId,
});
const hasToolResult = transcriptEvents.some((entry) =>
JSON.stringify(entry.event).includes('"toolResult"'),
);
if (!hasToolResult) {
console.warn("Warning: no toolResult entries detected in session history.");
}