mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
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:
@@ -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({
|
||||
|
||||
@@ -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}).`);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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"] },
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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();
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" \
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;",
|
||||
"",
|
||||
];
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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"]],
|
||||
[
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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.");`,
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user