Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Knight
2f7bf4c025 fix: warn about stale OpenAI keys with Codex auth 2026-05-26 07:15:27 +10:00
2030 changed files with 36726 additions and 72956 deletions

View File

@@ -1,88 +0,0 @@
---
name: agent-transcript
description: "Add a redacted agent transcript section to GitHub PR or issue bodies during OpenClaw agent-created PR/issue workflows."
---
# Agent Transcript
Best-effort local-only provenance for OpenClaw PR/issue bodies. Use during agent-created GitHub PR or issue workflows before creating/updating the body.
## Contract
- Never use network. Session discovery reads local agent logs only.
- Never upload raw logs. Render sanitized Markdown first.
- Always ask the user before adding transcript logs to a GitHub PR/issue body.
- Tell the user sanitized session logs help reviewers and can make PRs easier to prioritize.
- Offer a local HTML preview before insertion. If the user wants preview, open it and wait for confirmation before adding the section.
- Fail closed on unresolved secrets, private keys, browser/session/cookie details, or auth URLs.
- Drop system/developer prompts, raw tool outputs, reasoning, env, cookies, tokens, and broad local paths.
- Keep user prompts, assistant visible decisions, terse tool summaries, and test/proof outcomes.
- Remove session turns unrelated to the PR/issue work. Use the PR/issue title, branch name, changed files, and stated goal as scope; omit earlier/later unrelated tasks even when they are in the same session log.
- Best effort only: PR/issue creation must continue if no safe transcript is found.
- Add the `## Agent Transcript` section only when inserting a real transcript. Never add a placeholder transcript heading or text such as "A sanitized local transcript preview was generated but not included."
- Use a collapsed `<details>` section and update existing markers instead of duplicating sections.
## Helper
```bash
.agents/skills/agent-transcript/scripts/agent-transcript --help
```
Find a likely local session:
```bash
.agents/skills/agent-transcript/scripts/agent-transcript find \
--query "$PR_TITLE $BRANCH_OR_PR_URL" \
--cwd "$PWD" \
--since-days 14
```
`find` scans the newest 400 matching local JSONL logs by default across Codex, Claude, Pi, and OpenClaw agent sessions. Use `--max-files N` for a wider local search.
Render a PR/issue body section:
```bash
.agents/skills/agent-transcript/scripts/agent-transcript render \
--session "$SESSION_JSONL" \
--out /tmp/agent-transcript.md
```
Preview one candidate session locally:
```bash
.agents/skills/agent-transcript/scripts/agent-transcript preview \
--session "$SESSION_JSONL" \
--out /tmp/agent-transcript-preview.html
open /tmp/agent-transcript-preview.html
```
Append/update a body file before `gh pr create --body-file` or connector PR creation:
```bash
.agents/skills/agent-transcript/scripts/agent-transcript append-body \
--body /tmp/pr-body.md \
--session "$SESSION_JSONL" \
--out /tmp/pr-body.with-transcript.md
```
## PR/Issue Workflow
1. Draft the normal PR/issue body first.
2. Run `find` with title, branch, PR URL/number if known, and cwd.
3. If a high-confidence session is found, ask:
`Include a redacted agent transcript? It helps reviewers and can make the PR easier to prioritize. I can open a local preview first.`
4. If the user wants preview, run `preview`, open the HTML with `open`, and wait for confirmation.
5. Before insertion, trim unrelated session turns from the generated section. Keep only turns that explain this PR/issue's goal, implementation choices, files, tests, proof, blockers, and final outcome.
6. If the user approves, run `append-body`.
7. Use the enriched body file for creation/update.
8. If no safe session is found, say nothing and continue without transcript. If the user declines, continue without transcript and do not add any transcript placeholder section.
## Review Artifacts
For manual audits across many PR/session candidates, create a local HTML preview from a local JSON file. This is for maintainers only and is not part of the PR/issue workflow:
```bash
.agents/skills/agent-transcript/scripts/agent-transcript html \
--prs /tmp/recent-prs.json \
--out /tmp/agent-transcript-preview.html
```

View File

@@ -1,683 +0,0 @@
#!/usr/bin/env node
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
const MARKER_START = "<!-- agent-transcript:start -->";
const MARKER_END = "<!-- agent-transcript:end -->";
const DEFAULT_MAX_CHARS = 50000;
const DEFAULT_ENTRY_MAX_CHARS = 6000;
function usage() {
console.log(`Usage:
agent-transcript find --query TEXT [--cwd PATH] [--since-days N] [--max-files N] [--root PATH...]
agent-transcript render --session FILE [--out FILE] [--max-chars N] [--entry-max-chars N] [--title TEXT] [--url URL]
agent-transcript preview --session FILE [--out FILE] [--max-chars N] [--entry-max-chars N] [--title TEXT] [--url URL]
agent-transcript append-body --body FILE --session FILE [--out FILE] [--max-chars N] [--entry-max-chars N]
agent-transcript html --prs FILE [--out FILE] [--since-days N] [--min-score N] [--root PATH...] [--exclude-session FILE...]
Local-only. No network calls.`);
}
function parseArgs(argv) {
const args = { _: [] };
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (!arg.startsWith("--")) {
args._.push(arg);
continue;
}
const key = arg.slice(2);
const next = argv[i + 1];
if (next == null || next.startsWith("--")) {
args[key] = true;
continue;
}
i++;
if (args[key] == null) args[key] = next;
else if (Array.isArray(args[key])) args[key].push(next);
else args[key] = [args[key], next];
}
return args;
}
function asArray(value) {
if (value == null) return [];
return Array.isArray(value) ? value : [value];
}
function homePath(...parts) {
return path.join(os.homedir(), ...parts);
}
function openClawSessionRoots() {
const stateDir = process.env.OPENCLAW_STATE_DIR || homePath(".openclaw");
const agentsDir = path.join(stateDir, "agents");
if (!fs.existsSync(agentsDir)) return [];
try {
const roots = fs
.readdirSync(agentsDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.flatMap((entry) => {
const agentDir = path.join(agentsDir, entry.name);
return [
path.join(agentDir, "sessions"),
path.join(agentDir, "agent", "sessions"),
path.join(agentDir, "agent", "codex-home", "sessions"),
];
})
.filter((root) => fs.existsSync(root));
return [...new Set(roots)];
} catch {
return [];
}
}
function defaultRoots() {
return [
homePath(".codex", "sessions"),
homePath(".claude", "projects"),
homePath(".pi", "agent", "sessions"),
...openClawSessionRoots(),
];
}
function walkJsonl(root, sinceMs, out = []) {
if (!root || !fs.existsSync(root)) return out;
const stat = fs.statSync(root);
if (stat.isFile()) {
if (root.endsWith(".jsonl") && stat.mtimeMs >= sinceMs) out.push(root);
return out;
}
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
if (entry.name === "node_modules" || entry.name === ".git") continue;
const file = path.join(root, entry.name);
if (entry.isDirectory()) walkJsonl(file, sinceMs, out);
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
const entryStat = fs.statSync(file);
if (entryStat.mtimeMs >= sinceMs) out.push(file);
}
}
return out;
}
function readJsonl(file, maxLines = 12000) {
const text = fs.readFileSync(file, "utf8");
const lines = text.split(/\n+/).filter(Boolean).slice(0, maxLines);
const rows = [];
for (const line of lines) {
try {
rows.push(JSON.parse(line));
} catch {
rows.push({ type: "unparsed", text: line });
}
}
return rows;
}
function stringContent(value) {
if (value == null) return "";
if (typeof value === "string") return value;
if (Array.isArray(value)) return value.map(stringContent).filter(Boolean).join("\n");
if (typeof value === "object") {
if (typeof value.text === "string") return value.text;
if (typeof value.content === "string") return value.content;
if (typeof value.message === "string") return value.message;
if (Array.isArray(value.content)) return stringContent(value.content);
if (value.type === "text" && value.text) return String(value.text);
}
return "";
}
function detectAgent(file, rows) {
if (file.includes(`${path.sep}.codex${path.sep}`)) return "codex";
if (file.includes(`${path.sep}.claude${path.sep}`)) return "claude";
if (file.includes(`${path.sep}.pi${path.sep}`)) return "pi";
if (
file.includes(`${path.sep}.openclaw${path.sep}`) ||
(file.includes(`${path.sep}agents${path.sep}`) && file.includes(`${path.sep}sessions${path.sep}`))
) {
return "openclaw";
}
if (rows.some((row) => row?.type === "session_meta" || row?.type === "response_item")) return "codex";
if (rows.some((row) => row?.sessionId && row?.userType)) return "claude";
return "agent";
}
function eventText(row) {
if (row?.type === "event_msg") {
const payload = row.payload || {};
return stringContent(payload.message || payload.text_elements || payload.content);
}
if (row?.type === "response_item") {
const payload = row.payload || {};
return stringContent(payload.content || payload.summary || payload.arguments || payload.output);
}
if (row?.message) return stringContent(row.message);
if (row?.content) return stringContent(row.content);
if (row?.text) return stringContent(row.text);
return "";
}
function eventRole(row) {
if (row?.type === "event_msg") {
const type = row.payload?.type;
if (type === "user_message") return "user";
if (type === "agent_message") return "assistant";
if (type === "token_count" || type === "task_started" || type === "task_complete") return null;
if (type === "web_search_end") return "web";
}
if (row?.type === "response_item") {
const payload = row.payload || {};
if (payload.type === "function_call") return "tool";
if (payload.type === "function_call_output") return "tool_output";
if (payload.type === "reasoning") return null;
if (payload.type === "web_search_call") return "web";
if (payload.role === "user") return "user";
if (payload.role === "assistant") return "assistant";
}
if (row?.type === "user") return "user";
if (row?.type === "assistant") return "assistant";
if (row?.message?.role === "user") return "user";
if (row?.message?.role === "assistant") return "assistant";
if (row?.type === "tool_result" || row?.type === "tool_use") return "tool";
return null;
}
function hasSetupBlob(text) {
return (
text.includes("<INSTRUCTIONS>") ||
text.includes("# AGENTS.MD") ||
text.includes("Knowledge cutoff:") ||
text.includes("You are Codex") ||
/\byour instructions\b/i.test(text) ||
/\binstructions absorbed\b/i.test(text) ||
/\bAGENTS\.md\b/i.test(text)
);
}
function redact(input, stats) {
let s = String(input ?? "");
const rules = [
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED_PRIVATE_KEY]"],
[/sk-[A-Za-z0-9_-]{20,}/g, "[REDACTED_OPENAI_KEY]"],
[/(gh[pousr]_[A-Za-z0-9_]{20,})/g, "[REDACTED_GITHUB_TOKEN]"],
[/(AKIA[0-9A-Z]{16})/g, "[REDACTED_AWS_KEY]"],
[/eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}/g, "[REDACTED_JWT]"],
[/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{16,}/gi, "[REDACTED_AUTH_HEADER]"],
[/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[REDACTED_EMAIL]"],
[/\b(?:\+?\d[\d .()-]{7,}\d)\b/g, "[REDACTED_PHONE]"],
[/\/Users\/[^\s`"'>)]+/g, "[LOCAL_PATH]"],
[/~\/[^\s`"'>)]+/g, "[HOME_PATH]"],
[/([?&](?:token|key|secret|signature|sig|access_token|auth)=)[^\s`"'>&]+/gi, "$1[REDACTED]"],
];
for (const [re, repl] of rules) {
const before = s;
s = s.replace(re, repl);
if (s !== before) stats.redactions++;
}
return s;
}
function unsafe(text) {
const patterns = [
/-----BEGIN [A-Z ]*PRIVATE KEY-----/,
/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{16,}/i,
/\b(?:user_session|_gh_sess|__Host-user_session_same_site|GH_SESSION_TOKEN)\b/i,
/\b(?:GITHUB_TOKEN|GH_TOKEN|OPENAI_API_KEY|ANTHROPIC_API_KEY)\b/,
/\/upload\/policies\/assets|uploadToken|authenticity_token/i,
];
return patterns.filter((pattern) => pattern.test(text)).map((pattern) => String(pattern));
}
function normalizeEntry(role, text, stats, options = {}) {
let t = redact(text, stats).replace(/\n{3,}/g, "\n\n").trim();
if (!t) return null;
if (hasSetupBlob(t)) t = "[instructions recap omitted; policy/config text, not task dialogue]";
if (unsafe(t).length) t = "[omitted: browser/session/auth internals; not useful for public PR transcript]";
const entryMaxChars = Number(options.entryMaxChars || options["entry-max-chars"] || DEFAULT_ENTRY_MAX_CHARS);
if (t.length > entryMaxChars) {
t = `${t.slice(0, entryMaxChars).trimEnd()}\n...[truncated ${t.length - entryMaxChars} chars]`;
}
return `[${role}]\n${t}`;
}
function entryRole(entry) {
const match = entry.match(/^\[([^\]]+)\]\n/);
return match ? match[1] : null;
}
function entryBody(entry) {
return entry.replace(/^\[[^\]]+\]\n/, "");
}
function coalesceEntries(entries) {
const coalesced = [];
for (const entry of entries) {
const role = entryRole(entry);
const body = entryBody(entry);
const last = coalesced[coalesced.length - 1];
if (!last || !role || entryRole(last) !== role || role === "tool summary") {
coalesced.push(entry);
continue;
}
const lastBody = entryBody(last);
if (lastBody === body || lastBody.includes(body)) continue;
if (body.includes(lastBody)) {
coalesced[coalesced.length - 1] = `[${role}]\n${body}`;
continue;
}
coalesced[coalesced.length - 1] = `[${role}]\n${lastBody}\n\n${body}`;
}
return coalesced;
}
function toolFamily(name) {
const normalized = String(name).toLowerCase();
if (
/(read|fetch|open|list|find|search|grep|rg|sed|cat|head|tail|jq|wc|status|diff|show|view|snapshot|screenshot)/.test(
normalized,
)
) {
return "read";
}
if (/(write|edit|patch|apply|create|update|append|save|comment|fill|click|type|navigate|upload)/.test(normalized)) {
return "write";
}
if (/(exec|command|shell|run|test|build|lint|format|install|pnpm|npm|node|git|gh|ssh)/.test(normalized)) {
return "execute";
}
if (/(web|http|fetch|browser|chrome|github|dropbox|notion|gmail|calendar)/.test(normalized)) {
return "network";
}
return "other";
}
function shellFamily(command) {
const cmd = String(command || "").trim();
if (!cmd) return "execute";
if (
/^(rg|grep|sed|cat|head|tail|jq|wc|ls|find|pwd|git (status|diff|show|log|blame)|gh (pr|issue|api|run|repo|auth) (view|list|status)|test |stat |ps |which |command -v )\b/.test(
cmd,
)
) {
return "read";
}
if (/^(open |chmod |mkdir |touch |cp |mv |kill |git add|git commit|git push|gh pr create|gh issue create)\b/.test(cmd)) {
return "write";
}
if (/^(node|npm|pnpm|bun|python|python3|ruby|tsx|tsgo|make|cargo|go test|swift|xcodebuild)\b/.test(cmd)) {
return "execute";
}
if (/^(ssh|curl|wget|tailscale|nc )\b/.test(cmd)) return "network";
return "execute";
}
function toolCallFamily(row) {
const name = row.payload?.name || row.name || row.message?.name || row.type || "tool";
if (name === "exec_command") {
try {
const args = JSON.parse(row.payload?.arguments || "{}");
return shellFamily(args.cmd);
} catch {
return "execute";
}
}
if (name === "apply_patch") return "write";
if (name === "write_stdin") return "execute";
return toolFamily(name);
}
function compactToolSummary(familyCounts, dropped) {
const families = new Map();
for (const [family, count] of familyCounts.entries()) {
families.set(family, (families.get(family) || 0) + count);
}
const ordered = ["read", "write", "execute", "network", "other"]
.map((family) => [family, families.get(family) || 0])
.filter(([, count]) => count > 0)
.map(([family, count]) => `${count} ${family}`);
const calls = ordered.length ? ordered.join(", ") : "0 tool";
return `${calls}; raw tool outputs dropped: ${dropped}`;
}
function recountEntries(stats, entries) {
stats.rawEntries = stats.entries;
stats.entries = entries.length;
stats.user = entries.filter((entry) => entry.startsWith("[user]\n")).length;
stats.assistant = entries.filter((entry) => entry.startsWith("[assistant]\n")).length;
}
function renderSession(file, options = {}) {
const rows = readJsonl(file);
const agent = detectAgent(file, rows);
const stats = {
agent,
entries: 0,
user: 0,
assistant: 0,
toolCalls: 0,
toolOutputsDropped: 0,
web: 0,
redactions: 0,
omittedUnsafe: 0,
};
const toolCounts = new Map();
const items = [];
const seenEntries = new Set();
const hasEventDialogue = rows.some((row) => {
const type = row?.type === "event_msg" ? row.payload?.type : null;
return type === "user_message" || type === "agent_message";
});
for (const row of rows) {
const role = eventRole(row);
if (!role) continue;
if (hasEventDialogue && row.type === "response_item" && (role === "user" || role === "assistant")) {
continue;
}
if (role === "tool_output") {
stats.toolOutputsDropped++;
continue;
}
if (role === "tool") {
const family = toolCallFamily(row);
toolCounts.set(family, (toolCounts.get(family) || 0) + 1);
stats.toolCalls++;
continue;
}
if (role === "web") {
stats.web++;
continue;
}
const before = eventText(row);
const entry = normalizeEntry(role, before, stats, options);
if (!entry) continue;
const dedupeKey = entry.replace(/\s+/g, " ").trim();
if (seenEntries.has(dedupeKey)) continue;
seenEntries.add(dedupeKey);
if (entry.includes("[omitted: browser/session/auth internals")) stats.omittedUnsafe++;
items.push(entry);
stats.entries++;
if (role === "user") stats.user++;
if (role === "assistant") stats.assistant++;
}
if (toolCounts.size) {
items.push(`[tool summary]\n${compactToolSummary(toolCounts, stats.toolOutputsDropped)}`);
stats.entries++;
}
const renderedItems = coalesceEntries(items);
recountEntries(stats, renderedItems);
const maxChars = Number(options.maxChars || DEFAULT_MAX_CHARS);
let joined = renderedItems.join("\n\n");
if (joined.length > maxChars) joined = `${joined.slice(0, maxChars).trimEnd()}\n\n...[transcript truncated to ${maxChars} chars]`;
const headerBits = [options.title, options.url].filter(Boolean).join(" | ");
const unsafeAfter = unsafe(joined);
const safe = unsafeAfter.length === 0;
const markdown = `${MARKER_START}
## Agent Transcript
<details>
<summary>Redacted ${agent} session transcript${headerBits ? `: ${redact(headerBits, stats)}` : ""}</summary>
\`\`\`\`text
source: [LOCAL_SESSION]
redaction: local paths, emails, phone-shaped strings, token-shaped strings, auth headers, auth query params
omitted: raw tool outputs, system/developer prompts, local paths, secrets, browser/session/auth details
stats: ${JSON.stringify(stats)}
${joined}
\`\`\`\`
</details>
${MARKER_END}
`;
return { file, agent, safe, unsafeAfter, stats, markdown };
}
function readBoundedText(file, maxBytes = 220000) {
const fd = fs.openSync(file, "r");
try {
const stat = fs.fstatSync(fd);
if (stat.size <= maxBytes) {
const buffer = Buffer.alloc(stat.size);
fs.readSync(fd, buffer, 0, stat.size, 0);
return buffer.toString("utf8");
}
const half = Math.floor(maxBytes / 2);
const head = Buffer.alloc(half);
const tail = Buffer.alloc(half);
fs.readSync(fd, head, 0, half, 0);
fs.readSync(fd, tail, 0, half, Math.max(0, stat.size - half));
return `${head.toString("utf8")}\n[...middle omitted for scan...]\n${tail.toString("utf8")}`;
} finally {
fs.closeSync(fd);
}
}
function sessionScanRecord(file, maxBytes) {
const stat = fs.statSync(file);
const agent = detectAgent(file, []);
return {
file,
agent,
mtime: new Date(stat.mtimeMs).toISOString(),
haystack: `${file}\n${readBoundedText(file, maxBytes)}`.toLowerCase(),
};
}
function scoreScanRecord(record, terms, cwd) {
const haystack = record.haystack;
let score = 0;
const reasons = [];
for (const term of terms) {
const normalized = term.toLowerCase().trim();
if (normalized.length < 3) continue;
if (haystack.includes(normalized)) {
score += Math.min(20, Math.max(3, Math.floor(normalized.length / 3)));
reasons.push(normalized.slice(0, 80));
}
}
if (cwd) {
const cwdLower = cwd.toLowerCase();
if (haystack.includes(cwdLower) || record.file.toLowerCase().includes(cwdLower.replaceAll("/", "-"))) {
score += 8;
reasons.push("cwd");
}
}
return { file: record.file, score, reasons, mtime: record.mtime, agent: record.agent };
}
function recentFiles(files, maxFiles) {
return files
.map((file) => {
try {
return { file, mtimeMs: fs.statSync(file).mtimeMs };
} catch {
return null;
}
})
.filter(Boolean)
.sort((a, b) => b.mtimeMs - a.mtimeMs)
.slice(0, maxFiles)
.map((entry) => entry.file);
}
function candidateFiles(roots, terms, sinceMs, options = {}) {
return recentFiles(roots.flatMap((root) => walkJsonl(root, sinceMs)), Number(options["max-files"] || 400));
}
function findSessions(options) {
const sinceDays = Number(options["since-days"] || 14);
const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
const roots = asArray(options.root).length ? asArray(options.root) : defaultRoots();
const query = String(options.query || "");
const terms = query
.split(/\s+/)
.concat(query.match(/https?:\/\/\S+/g) || [])
.filter(Boolean);
const files = candidateFiles(roots, terms, sinceMs, options);
const scanBytes = Number(options["scan-bytes"] || 60000);
const results = files
.map((file) => scoreScanRecord(sessionScanRecord(file, scanBytes), terms, options.cwd))
.filter((result) => result.score > 0)
.sort((a, b) => b.score - a.score || b.mtime.localeCompare(a.mtime))
.slice(0, Number(options.limit || 10));
return results;
}
function sessionScanRecords(options) {
const sinceDays = Number(options["since-days"] || 14);
const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
const roots = asArray(options.root).length ? asArray(options.root) : defaultRoots();
const excluded = new Set(asArray(options["exclude-session"]).map((file) => path.resolve(file)));
return roots
.flatMap((root) => walkJsonl(root, sinceMs))
.filter((file) => !excluded.has(path.resolve(file)))
.map((file) => sessionScanRecord(file, Number(options["scan-bytes"] || 90000)));
}
function replaceSection(body, section) {
const start = body.indexOf(MARKER_START);
const end = body.indexOf(MARKER_END);
if (start !== -1 && end !== -1 && end > start) {
return `${body.slice(0, start).trimEnd()}\n\n${section.trim()}\n\n${body.slice(end + MARKER_END.length).trimStart()}`;
}
return `${body.trimEnd()}\n\n${section.trim()}\n`;
}
function escapeHtml(text) {
return String(text)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function htmlDocument(records) {
const rows = records
.map((record) => `<section>
<h2><a href="${escapeHtml(record.url || "")}">${escapeHtml(record.title || record.url || "PR")}</a></h2>
<p><code>${escapeHtml(record.session ? "[LOCAL_SESSION]" : "no session")}</code> score: ${escapeHtml(record.score ?? "")} safe: ${escapeHtml(record.safe ?? "")}</p>
<pre>${escapeHtml(record.markdown || record.error || "")}</pre>
</section>`)
.join("\n");
return `<!doctype html>
<meta charset="utf-8">
<title>Agent Transcript Preview</title>
<style>
body{font:14px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:32px;color:#1f2328;background:#fff}
section{border-top:1px solid #d0d7de;padding:24px 0}
h1,h2{line-height:1.2}
pre{white-space:pre-wrap;background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:16px;overflow:auto}
code{background:#f6f8fa;padding:2px 4px;border-radius:4px}
a{color:#0969da}
</style>
<h1>Agent Transcript Preview</h1>
${rows}
`;
}
function singlePreviewDocument(record) {
return htmlDocument([record]);
}
function readPrs(file) {
const raw = fs.readFileSync(file, "utf8");
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : parsed.items || parsed.prs || [];
}
function main() {
const [command, ...rest] = process.argv.slice(2);
const args = parseArgs(rest);
if (!command || command === "--help" || command === "-h" || args.help) {
usage();
return;
}
if (command === "find") {
console.log(JSON.stringify(findSessions(args), null, 2));
return;
}
if (command === "render") {
if (!args.session) throw new Error("--session is required");
const rendered = renderSession(args.session, args);
if (!rendered.safe) throw new Error(`unsafe transcript after redaction: ${rendered.unsafeAfter.join(", ")}`);
if (args.out) fs.writeFileSync(args.out, rendered.markdown);
else process.stdout.write(rendered.markdown);
return;
}
if (command === "preview") {
if (!args.session) throw new Error("--session is required");
const rendered = renderSession(args.session, args);
if (!rendered.safe) throw new Error(`unsafe transcript after redaction: ${rendered.unsafeAfter.join(", ")}`);
const output = singlePreviewDocument({
title: args.title || "Agent Transcript Preview",
url: args.url || "",
session: args.session,
safe: rendered.safe,
markdown: rendered.markdown,
});
if (args.out) fs.writeFileSync(args.out, output);
else process.stdout.write(output);
return;
}
if (command === "append-body") {
if (!args.body || !args.session) throw new Error("--body and --session are required");
const rendered = renderSession(args.session, args);
if (!rendered.safe) throw new Error(`unsafe transcript after redaction: ${rendered.unsafeAfter.join(", ")}`);
const body = fs.readFileSync(args.body, "utf8");
const next = replaceSection(body, rendered.markdown);
if (args.out) fs.writeFileSync(args.out, next);
else process.stdout.write(next);
return;
}
if (command === "html") {
if (!args.prs) throw new Error("--prs is required");
const records = [];
const scanRecords = sessionScanRecords(args);
const minScore = Number(args["min-score"] || 50);
for (const pr of readPrs(args.prs)) {
const query = [pr.url, pr.number ? `#${pr.number}` : "", pr.number, pr.title, pr.headRefName, pr.headRefName || pr.branch]
.filter(Boolean)
.join(" ");
const terms = query
.split(/\s+/)
.concat(query.match(/https?:\/\/\S+/g) || [])
.filter(Boolean);
const [candidate] = scanRecords
.map((record) => scoreScanRecord(record, terms, args.cwd))
.filter((result) => result.score >= minScore)
.sort((a, b) => b.score - a.score || b.mtime.localeCompare(a.mtime));
if (!candidate) {
records.push({ ...pr, error: "No local session match found." });
continue;
}
try {
const rendered = renderSession(candidate.file, { ...args, title: pr.title, url: pr.url });
records.push({
...pr,
session: candidate.file,
score: candidate.score,
safe: rendered.safe,
markdown: rendered.markdown,
});
} catch (error) {
records.push({ ...pr, session: candidate.file, score: candidate.score, error: String(error) });
}
}
const output = htmlDocument(records);
if (args.out) fs.writeFileSync(args.out, output);
else process.stdout.write(output);
return;
}
usage();
process.exitCode = 2;
}
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}

View File

@@ -26,9 +26,8 @@ Use when:
- If a review-triggered fix changes code, rerun focused tests and rerun the structured review helper.
- For security-audit suppression changes, verify accepted findings remain auditable: suppressed findings stay in structured output, active output keeps an unsuppressible suppression notice, and aggregate findings cannot hide unrelated active risk.
- Never switch or override the requested review engine/model. If the review hits model capacity, retry the same command a few times with the same engine/model.
- Be patient with large bundles. Structured review can take up to 30 minutes while the model call is active, especially with Codex tools or web search.
- Treat heartbeat lines like `review still running: ... elapsed=... pid=...` as healthy progress, not a hang. Let the helper continue while heartbeats are advancing.
- Do not kill a review just because it has been quiet for 2-5 minutes, or because it is still running under the 30-minute window. Inspect the process only after missing multiple expected heartbeats, after 30 minutes, or after an obviously failed subprocess; prefer letting the same helper command finish.
- Be patient with large bundles. Structured review can be silent for several minutes while the model call is active, especially with Codex tools or web search. Treat `review still running: ... elapsed=... pid=...` as healthy progress, not a hang.
- Do not kill a review just because it has been quiet for 2-5 minutes. Inspect the process only after multiple heartbeat intervals or an obviously failed subprocess; prefer letting the same helper command finish.
- Tools are useful in review mode. The helper allows read-only inspection tools and web search by default so reviewers can check dependency contracts, upstream docs, and current behavior.
- Security perspective is always included, but it should not cripple legitimate functionality. Report security findings only when the change creates a concrete, actionable risk or removes an important safety check.
- Do not invoke built-in `codex review`, nested reviewers, or reviewer panels from inside the review. The helper builds one bundle, calls one selected engine, validates one structured result, and stops.

View File

@@ -149,7 +149,7 @@ pnpm crabbox:run -- \
--ttl 240m \
--timing-json \
--shell -- \
"pnpm test:changed"
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test:changed"
```
Full suite:
@@ -160,7 +160,7 @@ pnpm crabbox:run -- \
--ttl 240m \
--timing-json \
--shell -- \
"pnpm test"
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test"
```
Focused rerun:
@@ -171,7 +171,7 @@ pnpm crabbox:run -- \
--ttl 240m \
--timing-json \
--shell -- \
"pnpm test <path-or-filter>"
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test <path-or-filter>"
```
Read the JSON summary. Useful fields:
@@ -206,7 +206,7 @@ node scripts/crabbox-wrapper.mjs run \
--ttl 240m \
--timing-json \
-- \
corepack pnpm check:changed
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 OPENCLAW_TESTBOX=1 OPENCLAW_TESTBOX_REMOTE_RUN=1 pnpm check:changed
```
Read the JSON summary and the Testbox line. Useful fields:
@@ -544,14 +544,14 @@ If brokered AWS cannot dispatch, sync, attach, or stop, retry once with
```sh
pnpm crabbox:run -- --debug --timing-json -- \
pnpm test:changed
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test:changed
```
Full suite:
```sh
pnpm crabbox:run -- --debug --timing-json -- \
pnpm test
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test
```
Auth fallback, only when `blacksmith` says auth is missing:
@@ -591,7 +591,7 @@ Minimal Blacksmith-backed Crabbox run, from repo root:
```sh
pnpm crabbox:run -- --provider blacksmith-testbox --timing-json -- \
corepack pnpm test:changed
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test:changed
```
Use direct Blacksmith only when Crabbox is the broken layer and you are
@@ -617,7 +617,7 @@ provider deliberately.
```sh
pnpm crabbox:warmup -- --class beast --market on-demand --idle-timeout 90m
pnpm crabbox:hydrate -- --id <cbx_id-or-slug>
pnpm crabbox:run -- --id <cbx_id-or-slug> --timing-json --shell -- "pnpm test:changed"
pnpm crabbox:run -- --id <cbx_id-or-slug> --timing-json --shell -- "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test:changed"
pnpm crabbox:stop -- <cbx_id-or-slug>
```

View File

@@ -89,11 +89,11 @@ Reject:
- if unwritable or wrong shape, create own PR and preserve useful contributor credit
- if no PR exists, create one
- add regression test when it fits
- release-note context for user-facing fixes in PR body or commit message; credit human reporter/contributor when known
- changelog for user-facing fixes; thank credited human reporter/contributor
6. Review, refresh, and publish:
- rebase or otherwise refresh the PR branch on current `origin/main`
- resolve drift, including newly exposed CI failures, rather than counting the PR as ready
- do not add `CHANGELOG.md` during normal sweep PRs; release automation generates it from PRs and commits
- changelog-only conflicts are routine on busy `main`; resolve them mechanically when already refreshing, but do not treat them as a real code conflict, a reason to reject the PR, or evidence that the branch needs extra fixup beyond the changelog entry order
- left-test the rebased head with the smallest meaningful local/Testbox/live command that proves the bug
- run `$autoreview` until no accepted/actionable findings remain before creating, updating, or presenting the PR URL
- create/update PR with real body and proof fields

View File

@@ -139,12 +139,12 @@ Issue triage is review/prove/patch-local by default:
2. Fix only issues that are easy, high-confidence, and narrowly owned by the implicated path.
3. Add focused regression proof when practical.
4. Stop with the dirty diff, touched files, and test/gate output for maintainer review.
5. After maintainer approval to ship, make one commit per accepted fix, with release-note context in the PR body or commit message when user-facing.
5. After maintainer approval to ship, make one commit per accepted fix, with its own changelog entry when user-facing.
6. Pull/rebase, push, then comment and close only the issues that were fixed or explicitly triaged closed.
Do not batch unrelated issue fixes into one commit. Do not publish, comment, close, or label during the review/prove phase.
Missing `CHANGELOG.md` is not a PR review finding or merge blocker. If landing/fixing a user-visible change, make sure the PR body or commit message captures the release-note context; never ask or block solely on it.
Missing changelog is not a PR review finding or merge blocker. If landing/fixing a user-visible change, add/update changelog automatically when practical; never ask or block solely on it.
Only list candidates that pass all gates:
@@ -244,8 +244,9 @@ gh search issues --repo openclaw/openclaw --match title,body --limit 50 \
## Follow PR review and landing hygiene
- Never mention release-note bookkeeping in review-only output. It is landing
or release-generation mechanics, not a correctness finding.
- Never mention merge conflicts that are relatively easy to resolve, such as
`CHANGELOG.md` entries, in review-only output. These are landing mechanics,
not correctness findings.
- If bot review conversations exist on your PR, address them and resolve them yourself once fixed.
- Leave a review conversation unresolved only when reviewer or maintainer judgment is still needed.
- Before landing any PR with non-trivial code changes, run `$autoreview` until no accepted/actionable findings remain, unless equivalent manual review already covered it, the change is trivial/docs-only, or the user opts out.

View File

@@ -5,19 +5,22 @@ description: "Run or recover OpenClaw macOS release signing, notarization, appca
# OpenClaw Mac Release
Use with `$release-openclaw-maintainer`, `$release-openclaw-ci`, `$one-password`, and `$release-private` if it exists when stable macOS assets, private mac preflight, notarization, appcast promotion, or mac release recovery is involved.
Use with `$release-openclaw-maintainer`, `$release-openclaw-ci`, and `$one-password` when stable macOS assets, private mac preflight, notarization, appcast promotion, or mac release recovery is involved.
## Credentials
- Resolve Peter-owned ASC item refs, key ids, issuer ids, and service-token provenance from `$release-private`.
- Canonical ASC item: vault `Molty`, title `API Key - App Store Connect - Personal - Release`.
- Fields: `private_key_p8`, `key_id`, `issuer_id`.
- Current known good key id: `AKVLXW849T`.
- Legacy mirror: vault `Private`, title `API Key - App Store Connect - Personal`; keep it synced for older refs.
- Stale/revoked key symptom: `xcrun notarytool submit` fails with `HTTP status code: 401. Unauthenticated`.
- Validate candidate ASC credentials with `xcrun notarytool history` before setting GitHub secrets.
## 1Password
- Use `$one-password`: all `op` work inside one persistent tmux session, no secret output.
- Use the service-token guidance from `$release-private` when available.
- Prefer `OP_SERVICE_ACCOUNT_TOKEN` from `~/.profile` for Molty reads.
- Do not assume `MOLTY_OP_SERVICE_ACCOUNT_TOKEN` is alive; it has previously pointed at a deleted service account.
- If a service token fails, run status-only checks: token present/length and `op whoami`; never print token values.
- If desktop app auth is needed but Touch ID is unavailable, set `OP_BIOMETRIC_UNLOCK_ENABLED=false` for the manual `op account add --signin` path.

View File

@@ -5,7 +5,7 @@ description: Prepare or verify OpenClaw stable/beta releases, changelogs, releas
# OpenClaw Release Maintainer
Use this skill for release and publish-time workflow. Load `$release-private` if it exists before resolving Peter-owned credential locators or private host topology. Keep ordinary development changes and GHSA-specific advisory work outside this skill.
Use this skill for release and publish-time workflow. Keep ordinary development changes and GHSA-specific advisory work outside this skill.
## Respect release guardrails
@@ -23,8 +23,7 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
green. Then branch from that commit so regular development can continue on
`main` while release validation runs.
- Before release branching, commit any dirty files in coherent groups, push,
pull/rebase, then generate `CHANGELOG.md` on `main` from merged PRs and all
direct commits since the last reachable release tag. Commit/push/pull that
pull/rebase, then run `/changelog` on `main` and commit/push/pull that
changelog rewrite immediately before creating the release branch.
- During release planning, inspect both `src/plugins/compat/registry.ts` and
`src/commands/doctor/shared/deprecation-compat.ts` before branching and again
@@ -69,8 +68,8 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
or clawgrit reports. Report regressions explicitly. A major regression is a
release blocker unless the operator waives it or the data clearly proves
infrastructure noise.
- Generate the changelog before version/tag preparation so the top changelog
section is deduped and ordered by user impact.
- Use `/changelog` before version/tag preparation so the top changelog section
is deduped and ordered by user impact.
- Do not create beta-specific `CHANGELOG.md` headings. Beta releases use the
stable base version section, for example `v2026.4.20-beta.1` uses
`## 2026.4.20` release notes.
@@ -137,25 +136,11 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
## Build changelog-backed release notes
- `CHANGELOG.md` is release-owned. Normal PRs and direct `main` fixes should
not edit it.
- Before release branching or tagging, rewrite the target `CHANGELOG.md`
section from history, not existing notes. Use the last reachable stable or
beta release tag as the base, then inspect every commit through the target
release SHA.
- Include both merged PR commits and direct commits on `main`. Direct commits
matter: infer notes from their subject, body, touched files, linked issues,
tests, and nearby code when no PR body exists.
- Prefer PR bodies, issue links, review proof, and commit bodies over commit
subjects alone. If a commit fixed an issue directly, the commit body should
name the user-visible behavior, affected surface, issue ref, and credited
reporter/contributor when known.
- Treat missing context as a release-note audit gap: inspect the diff and linked
issue, draft the best accurate entry, and note the uncertainty for maintainer
review rather than inventing impact.
- Add missed user-facing changes, remove internal-only noise, dedupe overlapping
PR/direct-commit entries, and sort each section from most to least interesting
for users.
section from commit history, not just from existing notes: scan commits since
the last reachable release tag, add missed user-facing changes, dedupe
overlapping entries, and sort each section from most to least interesting for
users.
- Changelog entries should be user-facing, not internal release-process notes.
- GitHub release and prerelease bodies must use the full matching
`CHANGELOG.md` version section, not highlights or an excerpt. When creating
@@ -436,7 +421,7 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- Hard rule: never run `op` directly in the main agent shell during release
work. Any 1Password CLI use must happen inside that tmux session so prompts
and alerts are contained and observable.
- Use `$release-private` for the npm credentials and OTP item.
- Use the 1Password item `op://Private/Npmjs` for npm credentials and OTP.
Do not print passwords, tokens, or OTPs to the transcript; send them through
tmux buffers, env vars scoped to the tmux command, or `expect` with
`log_user 0`.

View File

@@ -5,7 +5,7 @@ description: "OpenClaw Tideclaw alpha/nightly release automation: isolated branc
# Nightly Release
Use for Tideclaw/OpenClaw alpha/nightly release automation, manual alpha triggers, beta prep, release-branch repair, and post-release forward-port. Load `$release-private` if it exists before using Tideclaw host paths, cron ids, or Discord routing ids.
Use for Tideclaw/OpenClaw alpha/nightly release automation, manual alpha triggers, beta prep, release-branch repair, and post-release forward-port.
## Policy
@@ -36,7 +36,7 @@ This is good for auditability if commits are clearly machine-authored and gated
- Branch prefix: `tideclaw/alpha/`
- Branch name: `tideclaw/alpha/YYYY-MM-DD-HHMMZ`
- Base: current `origin/main` SHA at trigger time.
- State file: resolve from `$release-private` on the Tideclaw host.
- State file: `/root/tideclaw-workspace/.tideclaw-alpha-state.json`
- Release tag: `vYYYY.M.D-alpha.N`
- npm dist-tag: `alpha`
@@ -44,7 +44,7 @@ Do not reuse old alpha branches for a new run. If rerunning the same base SHA, c
## Start
1. Work in the Tideclaw host checkout from `$release-private`.
1. Work in `/root/tideclaw-workspace/openclaw`.
2. Fetch first:
```bash
@@ -66,8 +66,7 @@ git switch -c "$BRANCH" "$BASE_SHA"
Manual trigger:
```bash
CRON_ID="<from release-private>"
OPENCLAW_ALLOW_ROOT=1 openclaw cron run "$CRON_ID" --expect-final --timeout 21600000
OPENCLAW_ALLOW_ROOT=1 openclaw cron run 8cfc76f6-d1b4-4608-85ec-1a659e76c0cd --expect-final --timeout 21600000
```
## Discord Alpha Trigger
@@ -89,7 +88,7 @@ Rules:
3. Follow the normal alpha workflow: reuse prior fixes, run local checks, fix on the alpha branch, run release CI, publish alpha after green gates, then forward-port reusable fixes via fixes-only PR.
4. If another alpha/beta/stable release run is already active, report the active branch/run and stop.
5. `#maintainers` trigger requires an explicit Tideclaw mention; do not react to unmentioned release chatter there.
6. Resolve Discord role/user ids and live host hotfix notes from `$release-private`.
6. Discord currently resolves `@Tideclaw` in `#maintainers` to managed role `<@&1505299068748824609>`. The live host hotfix treats that role mention as addressing bot user `1505297171623186432`; if the hotfix is absent, the message is dropped before this skill runs and must be resent after fixing preflight.
## Discord Beta Trigger
@@ -120,7 +119,7 @@ Rules:
Before running checks, mine recent Tideclaw alpha branches for fixes already made during previous release attempts:
1. Read the Tideclaw state file from `$release-private` for the last successful alpha branch and fix commit SHAs.
1. Read `/root/tideclaw-workspace/.tideclaw-alpha-state.json` for the last successful alpha branch and fix commit SHAs.
2. List recent remote branches:
```bash
@@ -225,7 +224,7 @@ Release is not done until all are true:
- Release body links npm version page, registry tarball, integrity, and CI/proof.
- `npm view openclaw@<version>` shows the exact version, dist-tag `alpha`, tarball, integrity, and publish time.
- Installed/package smoke follows repo release docs.
- The Tideclaw state file from `$release-private` records version, tag, base SHA, branch, fix commit SHAs, workflow run IDs, npm integrity, and timestamp.
- `/root/tideclaw-workspace/.tideclaw-alpha-state.json` records version, tag, base SHA, branch, fix commit SHAs, workflow run IDs, npm integrity, and timestamp.
Final Discord summary in `#releases`:

View File

@@ -123,14 +123,14 @@ runs:
shell: bash
run: |
set -euo pipefail
bash scripts/ci-docker-pull-retry.sh "${OPENCLAW_DOCKER_E2E_BARE_IMAGE}"
docker pull "${OPENCLAW_DOCKER_E2E_BARE_IMAGE}"
- name: Pull shared functional Docker E2E image
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_functional_image == '1'
shell: bash
run: |
set -euo pipefail
bash scripts/ci-docker-pull-retry.sh "${OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE}"
docker pull "${OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE}"
- name: Validate Docker E2E credentials
if: inputs.hydrate-artifacts == 'true'

View File

@@ -26,23 +26,11 @@ inputs:
runs:
using: composite
steps:
- name: Normalize container toolcache
shell: bash
run: |
set -euo pipefail
if [[ -d /__t && ! -e /opt/hostedtoolcache ]]; then
mkdir -p /opt
ln -s /__t /opt/hostedtoolcache
fi
- name: Setup Node.js
shell: bash
env:
REQUESTED_NODE_VERSION: ${{ inputs.node-version }}
run: |
set -euo pipefail
source "$GITHUB_ACTION_PATH/../setup-pnpm-store-cache/ensure-node.sh"
openclaw_ensure_node "$REQUESTED_NODE_VERSION"
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
check-latest: false
- name: Setup pnpm
uses: ./.github/actions/setup-pnpm-store-cache
@@ -52,10 +40,9 @@ runs:
- name: Setup Bun
if: inputs.install-bun == 'true'
shell: bash
run: |
set -euo pipefail
npm install -g bun@1.3.13
uses: oven-sh/setup-bun@v2.2.0
with:
bun-version: "1.3.13"
- name: Runtime versions
shell: bash

View File

@@ -14,7 +14,7 @@ inputs:
required: false
default: ""
use-actions-cache:
description: Whether actions/cache should cache the pnpm store.
description: Whether pnpm/action-setup should cache the pnpm store.
required: false
default: "true"
outputs:
@@ -47,42 +47,12 @@ runs:
openclaw_ensure_node "$requested_node"
- name: Setup pnpm from packageManager
shell: bash
env:
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
PACKAGE_MANAGER_FILE: ${{ inputs.package-manager-file }}
run: |
set -euo pipefail
package_manager="$(node -e "const fs = require('node:fs'); const path = require('node:path'); const pkg = JSON.parse(fs.readFileSync(path.resolve(process.argv[1]), 'utf8')); process.stdout.write(pkg.packageManager || '')" "$PACKAGE_MANAGER_FILE")"
case "$package_manager" in
pnpm@*) ;;
*)
echo "::error::Expected packageManager to pin pnpm, got '${package_manager:-<empty>}'"
exit 1
;;
esac
corepack enable
corepack prepare "$package_manager" --activate
- name: Resolve pnpm store path
id: pnpm-store
if: ${{ inputs.use-actions-cache == 'true' && runner.os != 'Windows' }}
shell: bash
run: |
set -euo pipefail
store_path="$(pnpm store path --silent)"
node -e "require('node:fs').mkdirSync(process.argv[1], { recursive: true })" "$store_path"
echo "path=$store_path" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
if: ${{ inputs.use-actions-cache == 'true' && runner.os != 'Windows' }}
uses: actions/cache@v5
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ inputs.node-version }}-${{ hashFiles(inputs.lockfile-path) }}
restore-keys: |
pnpm-store-${{ runner.os }}-${{ inputs.node-version }}-
pnpm-store-${{ runner.os }}-
package_json_file: ${{ inputs.package-manager-file }}
run_install: false
cache: ${{ inputs.use-actions-cache }}
cache_dependency_path: ${{ inputs.lockfile-path }}
- name: Record pnpm version
id: pnpm-version

View File

@@ -28,17 +28,9 @@ openclaw_active_node_version() {
openclaw_prepend_node_bin() {
local node_bin_dir="$1"
local shell_node_bin_dir="$node_bin_dir"
if command -v cygpath >/dev/null 2>&1; then
shell_node_bin_dir="$(cygpath -u "$node_bin_dir" 2>/dev/null || printf '%s' "$node_bin_dir")"
fi
export PATH="$shell_node_bin_dir:$PATH"
export PATH="$node_bin_dir:$PATH"
if [[ -n "${GITHUB_PATH:-}" ]]; then
local github_node_bin_dir="$shell_node_bin_dir"
if command -v cygpath >/dev/null 2>&1; then
github_node_bin_dir="$(cygpath -w "$shell_node_bin_dir" 2>/dev/null || printf '%s' "$shell_node_bin_dir")"
fi
echo "$github_node_bin_dir" >> "$GITHUB_PATH"
echo "$node_bin_dir" >> "$GITHUB_PATH"
fi
hash -r
}
@@ -51,7 +43,6 @@ openclaw_find_toolcache_node() {
"${RUNNER_TOOL_CACHE:-}" \
"${AGENT_TOOLSDIRECTORY:-}" \
"${ACTIONS_RUNNER_TOOL_CACHE:-}" \
"${OPENCLAW_CONTAINER_TOOL_CACHE:-/__t}" \
"/opt/hostedtoolcache" \
"/home/runner/_work/_tool" \
"/Users/runner/hostedtoolcache" \
@@ -77,56 +68,6 @@ openclaw_find_toolcache_node() {
return 1
}
openclaw_resolve_node_download_version() {
local requested_node="$1"
if [[ "$requested_node" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
[[ "$requested_node" == v* ]] && printf '%s\n' "$requested_node" || printf 'v%s\n' "$requested_node"
return 0
fi
local prefix="${requested_node#v}"
prefix="${prefix%%[xX]*}"
prefix="v${prefix}"
[[ "$prefix" == *. ]] || prefix="${prefix}."
curl -fsSL https://nodejs.org/dist/index.json |
OPENCLAW_NODE_PREFIX="$prefix" python3 -c 'import json, os, sys
prefix = os.environ["OPENCLAW_NODE_PREFIX"]
for item in json.load(sys.stdin):
version = item.get("version", "")
if version.startswith(prefix):
print(version)
break
'
}
openclaw_node_download_platform() {
local os_name arch_name
os_name="$(uname -s)"
arch_name="$(uname -m)"
case "$os_name:$arch_name" in
Linux:x86_64) printf 'linux-x64\n' ;;
Linux:aarch64 | Linux:arm64) printf 'linux-arm64\n' ;;
Darwin:x86_64) printf 'darwin-x64\n' ;;
Darwin:arm64) printf 'darwin-arm64\n' ;;
*)
return 1
;;
esac
}
openclaw_download_node() {
local requested_node="$1"
local version platform archive_url install_root
version="$(openclaw_resolve_node_download_version "$requested_node")"
platform="$(openclaw_node_download_platform)" || return 1
install_root="${RUNNER_TEMP:-/tmp}/openclaw-node-${version}-${platform}"
archive_url="https://nodejs.org/dist/${version}/node-${version}-${platform}.tar.xz"
mkdir -p "$install_root"
echo "Downloading Node ${version} from ${archive_url}"
curl -fsSL "$archive_url" | tar -xJ -C "$install_root" --strip-components=1
openclaw_prepend_node_bin "$install_root/bin"
}
openclaw_ensure_node() {
local requested_node="${1:-}"
requested_node="${requested_node#v}"
@@ -145,8 +86,6 @@ openclaw_ensure_node() {
if [[ -n "$node_bin" ]]; then
echo "Using Node $("$node_bin" -p 'process.versions.node') from $node_bin"
openclaw_prepend_node_bin "$(dirname "$node_bin")"
else
openclaw_download_node "$requested_node" || true
fi
active_node_version="$(openclaw_active_node_version)"

View File

@@ -12,7 +12,7 @@ Hard limits:
- Do not change production code, tests, package metadata, generated baselines, lockfiles, or CI config.
- Keep changes minimal and factual.
- Use "plugin/plugins" in user-facing docs/UI/changelog; `extensions/` is only the internal workspace layout.
- Do not add `CHANGELOG.md` entries during normal docs work. Capture user-facing release-note context in the PR body or commit message instead.
- Do not add a changelog entry unless the docs update describes a user-facing behavior/API change from the triggering commit.
Allowed paths:

View File

@@ -61,7 +61,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \

View File

@@ -59,7 +59,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
@@ -132,5 +132,6 @@ jobs:
- name: Run Testbox
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: success()
continue-on-error: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -76,16 +76,13 @@ jobs:
android_matrix: ${{ steps.manifest.outputs.android_matrix }}
steps:
- name: Checkout
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_REF: ${{ inputs.target_ref || github.sha }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_REF}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.target_ref || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: true
submodules: false
- name: Resolve checkout SHA
id: checkout_ref
@@ -302,16 +299,13 @@ jobs:
PRE_COMMIT_HOME: .cache/pre-commit-security-fast
steps:
- name: Checkout
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_REF: ${{ inputs.target_ref || github.sha }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_REF}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.target_ref || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: true
submodules: false
- name: Ensure security base commit
if: github.event_name != 'workflow_dispatch'
@@ -341,20 +335,22 @@ jobs:
fi
echo "PRE_COMMIT_CONFIG_PATH=$trusted_config" >> "$GITHUB_ENV"
- name: Resolve Python runtime
- name: Setup Python
id: setup-python
run: |
set -euo pipefail
python3 --version
version="$(python3 - <<'PY'
import platform
print(platform.python_version())
PY
)"
echo "python-version=${version}" >> "$GITHUB_OUTPUT"
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Restore pre-commit cache
uses: actions/cache@v5
with:
path: .cache/pre-commit-security-fast
key: pre-commit-security-fast-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
pre-commit-security-fast-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-
- name: Install pre-commit
run: python3 -m pip install --disable-pip-version-check pre-commit==4.2.0
run: python -m pip install --disable-pip-version-check pre-commit==4.2.0
- name: Detect committed private keys
run: pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" --all-files detect-private-key
@@ -387,12 +383,10 @@ jobs:
pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" zizmor --files "${workflow_files[@]}"
- name: Setup Node.js
env:
REQUESTED_NODE_VERSION: "24.x"
run: |
set -euo pipefail
source .github/actions/setup-pnpm-store-cache/ensure-node.sh
openclaw_ensure_node "$REQUESTED_NODE_VERSION"
uses: actions/setup-node@v6
with:
node-version: "24.x"
check-latest: false
- name: Audit production dependencies
run: node scripts/pre-commit/pnpm-audit-prod.mjs --audit-level=high
@@ -417,6 +411,7 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -432,10 +427,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -518,24 +513,7 @@ jobs:
run: pnpm test:build:singleton
- name: Check CLI startup memory
shell: bash
run: |
set +e
pnpm test:startup:memory
status=$?
if [[ -f .artifacts/startup-memory/summary.md ]]; then
cat .artifacts/startup-memory/summary.md >> "$GITHUB_STEP_SUMMARY"
fi
exit "$status"
- name: Upload startup memory report
if: always()
uses: actions/upload-artifact@v7
with:
name: startup-memory
path: .artifacts/startup-memory/
if-no-files-found: ignore
retention-days: 7
run: pnpm test:startup:memory
- name: Run built artifact checks
id: built_artifact_checks
@@ -641,6 +619,7 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -656,10 +635,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -727,6 +706,7 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -742,10 +722,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -807,6 +787,7 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -822,10 +803,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -884,6 +865,7 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -899,10 +881,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -959,6 +941,7 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -974,10 +957,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1081,6 +1064,7 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1096,10 +1080,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1211,6 +1195,7 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1226,10 +1211,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1360,6 +1345,7 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1375,10 +1361,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1405,13 +1391,12 @@ jobs:
install-bun: "false"
- name: Checkout ClawHub docs source
run: |
set -euo pipefail
git init clawhub-source
git -C clawhub-source config gc.auto 0
git -C clawhub-source remote add origin "https://github.com/openclaw/clawhub.git"
git -C clawhub-source fetch --no-tags --depth=1 origin "+HEAD:refs/remotes/origin/checkout"
git -C clawhub-source checkout --detach refs/remotes/origin/checkout
uses: actions/checkout@v6
with:
repository: openclaw/clawhub
path: clawhub-source
fetch-depth: 1
persist-credentials: true
- name: Check docs
env:
@@ -1427,16 +1412,11 @@ jobs:
timeout-minutes: 20
steps:
- name: Checkout
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: true
submodules: false
- name: Setup Python
uses: actions/setup-python@v6
@@ -1475,16 +1455,11 @@ jobs:
matrix: ${{ fromJson(needs.preflight.outputs.checks_windows_matrix) }}
steps:
- name: Checkout
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: true
submodules: false
- name: Try to exclude workspace from Windows Defender (best-effort)
shell: pwsh
@@ -1506,12 +1481,10 @@ jobs:
}
- name: Setup Node.js
env:
REQUESTED_NODE_VERSION: "24.x"
run: |
set -euo pipefail
source .github/actions/setup-pnpm-store-cache/ensure-node.sh
openclaw_ensure_node "$REQUESTED_NODE_VERSION"
uses: actions/setup-node@v6
with:
node-version: 24.x
check-latest: false
- name: Setup pnpm
uses: ./.github/actions/setup-pnpm-store-cache
@@ -1575,16 +1548,11 @@ jobs:
matrix: ${{ fromJson(needs.preflight.outputs.macos_node_matrix) }}
steps:
- name: Checkout
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: true
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
@@ -1621,16 +1589,11 @@ jobs:
timeout-minutes: 20
steps:
- name: Checkout
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: true
submodules: false
- name: Install XcodeGen / SwiftLint / SwiftFormat
run: brew install xcodegen swiftlint swiftformat
@@ -1730,6 +1693,7 @@ jobs:
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
@@ -1745,10 +1709,10 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1

View File

@@ -141,13 +141,7 @@ jobs:
if ! command -v docker >/dev/null 2>&1; then
echo "docker not found; installing fallback engine"
curl --fail --show-error --location \
--connect-timeout "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_CONNECT_TIMEOUT_SECONDS:-15}" \
--max-time "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_TIMEOUT_SECONDS:-300}" \
--retry "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRIES:-3}" \
--retry-delay "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRY_DELAY_SECONDS:-5}" \
--retry-all-errors \
https://get.docker.com | sudo sh
curl -fsSL https://get.docker.com | sudo sh
fi
if command -v systemctl >/dev/null 2>&1; then
@@ -172,12 +166,7 @@ jobs:
esac
buildx_version="${DOCKER_BUILDX_VERSION:-v0.15.1}"
mkdir -p "$HOME/.docker/cli-plugins"
curl --fail --show-error --location \
--connect-timeout "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_CONNECT_TIMEOUT_SECONDS:-15}" \
--max-time "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_TIMEOUT_SECONDS:-300}" \
--retry "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRIES:-3}" \
--retry-delay "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRY_DELAY_SECONDS:-5}" \
--retry-all-errors \
curl -fsSL \
"https://github.com/docker/buildx/releases/download/${buildx_version}/buildx-${buildx_version}.linux-${buildx_arch}" \
-o "$HOME/.docker/cli-plugins/docker-buildx"
chmod 0755 "$HOME/.docker/cli-plugins/docker-buildx"
@@ -318,13 +307,7 @@ jobs:
if ! command -v docker >/dev/null 2>&1; then
echo "docker not found; installing fallback engine"
curl --fail --show-error --location \
--connect-timeout "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_CONNECT_TIMEOUT_SECONDS:-15}" \
--max-time "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_TIMEOUT_SECONDS:-300}" \
--retry "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRIES:-3}" \
--retry-delay "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRY_DELAY_SECONDS:-5}" \
--retry-all-errors \
https://get.docker.com | sudo sh
curl -fsSL https://get.docker.com | sudo sh
fi
if command -v systemctl >/dev/null 2>&1; then
@@ -349,12 +332,7 @@ jobs:
esac
buildx_version="${DOCKER_BUILDX_VERSION:-v0.15.1}"
mkdir -p "$HOME/.docker/cli-plugins"
curl --fail --show-error --location \
--connect-timeout "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_CONNECT_TIMEOUT_SECONDS:-15}" \
--max-time "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_TIMEOUT_SECONDS:-300}" \
--retry "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRIES:-3}" \
--retry-delay "${OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRY_DELAY_SECONDS:-5}" \
--retry-all-errors \
curl -fsSL \
"https://github.com/docker/buildx/releases/download/${buildx_version}/buildx-${buildx_version}.linux-${buildx_arch}" \
-o "$HOME/.docker/cli-plugins/docker-buildx"
chmod 0755 "$HOME/.docker/cli-plugins/docker-buildx"

View File

@@ -245,7 +245,7 @@ jobs:
DOCKER_BUILDKIT: "1"
run: |
set -euo pipefail
timeout --kill-after=30s 35m docker build \
timeout --foreground --kill-after=30s 35m docker build \
--target runtime-assets \
--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \
.
@@ -287,7 +287,7 @@ jobs:
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"Sorry. Your account was suspended"* ]]; then
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
@@ -417,7 +417,7 @@ jobs:
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"Sorry. Your account was suspended"* ]]; then
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
@@ -557,7 +557,7 @@ jobs:
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"Sorry. Your account was suspended"* ]]; then
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
@@ -859,7 +859,7 @@ jobs:
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"Sorry. Your account was suspended"* ]]; then
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
@@ -975,7 +975,7 @@ jobs:
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"Sorry. Your account was suspended"* ]]; then
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
@@ -1089,29 +1089,6 @@ jobs:
run: |
set -euo pipefail
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"Sorry. Your account was suspended"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
release_check_blocking_job() {
case "$1" in
"resolve_target" | \
@@ -1168,7 +1145,7 @@ jobs:
fi
local run_json status conclusion url attempt head_sha
run_json="$(gh_with_retry run view "$run_id" --json status,conclusion,url,attempt,headSha,jobs)"
run_json="$(gh run view "$run_id" --json status,conclusion,url,attempt,headSha,jobs)"
status="$(jq -r '.status' <<< "$run_json")"
conclusion="$(jq -r '.conclusion' <<< "$run_json")"
url="$(jq -r '.url' <<< "$run_json")"
@@ -1215,7 +1192,7 @@ jobs:
fi
local run_json row
run_json="$(gh_with_retry run view "$run_id" --json status,conclusion,url,createdAt,updatedAt,headSha)"
run_json="$(gh run view "$run_id" --json status,conclusion,url,createdAt,updatedAt,headSha)"
row="$(
jq -r --arg label "$label" '
def ts: fromdateiso8601;
@@ -1251,7 +1228,7 @@ jobs:
echo
echo "### Slowest jobs: ${label}"
echo
gh_with_retry run view "$run_id" --json jobs --jq '
gh run view "$run_id" --json jobs --jq '
def ts: fromdateiso8601;
"| Job | Result | Minutes |",
"| --- | --- | ---: |",
@@ -1268,7 +1245,7 @@ jobs:
echo
echo "### Longest queues: ${label}"
echo
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq ".jobs[] | @json" | jq -sr '
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq ".jobs[] | @json" | jq -sr '
def ts: fromdateiso8601;
"| Job | Result | Queue minutes | Run minutes |",
"| --- | --- | ---: | ---: |",
@@ -1297,7 +1274,7 @@ jobs:
fi
local run_json status conclusion artifacts_json
run_json="$(gh_with_retry run view "$run_id" --json status,conclusion,url,jobs)"
run_json="$(gh run view "$run_id" --json status,conclusion,url,jobs)"
status="$(jq -r '.status' <<< "$run_json")"
conclusion="$(jq -r '.conclusion' <<< "$run_json")"
if [[ "$status" == "completed" && "$conclusion" == "success" ]]; then
@@ -1320,7 +1297,7 @@ jobs:
echo
echo "Artifacts:"
artifacts_json="$(
gh_with_retry api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts?per_page=100" 2>/dev/null || true
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts?per_page=100" 2>/dev/null || true
)"
if [[ -n "${artifacts_json// }" ]]; then
jq -r '
@@ -1494,7 +1471,6 @@ jobs:
PLUGIN_PRERELEASE_RUN_ID: ${{ needs.plugin_prerelease.outputs.run_id }}
RELEASE_CHECKS_RUN_ID: ${{ needs.release_checks.outputs.run_id }}
NPM_TELEGRAM_RUN_ID: ${{ needs.npm_telegram.outputs.run_id }}
PERFORMANCE_RUN_ID: ${{ needs.performance.outputs.run_id }}
run: |
set -euo pipefail
manifest_dir="${RUNNER_TEMP}/full-release-validation"

View File

@@ -121,7 +121,7 @@ jobs:
# builder stalls; an explicit buildx invocation fails closed instead.
- name: Build root Dockerfile smoke image
run: |
timeout --kill-after=30s 45m docker buildx build \
timeout 45m docker buildx build \
--progress=plain \
--load \
--build-arg OPENCLAW_EXTENSIONS=matrix \
@@ -132,7 +132,7 @@ jobs:
- name: Run root Dockerfile CLI smoke
run: |
timeout --kill-after=30s 20m docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc '
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc '
which openclaw &&
openclaw --version &&
node -e "
@@ -163,7 +163,7 @@ jobs:
- name: Smoke test Dockerfile with matrix extension build arg
run: |
timeout --kill-after=30s 20m docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc '
docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc '
which openclaw &&
openclaw --version &&
node -e "
@@ -235,7 +235,7 @@ jobs:
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
run: |
set -euo pipefail
if timeout --kill-after=30s 180s docker pull "$IMAGE_REF"; then
if timeout 180s docker pull "$IMAGE_REF"; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Using existing root Dockerfile smoke image: \`$IMAGE_REF\`" >> "$GITHUB_STEP_SUMMARY"
else
@@ -256,7 +256,7 @@ jobs:
env:
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
run: |
timeout --kill-after=30s 45m docker buildx build \
timeout 45m docker buildx build \
--progress=plain \
--push \
--build-arg OPENCLAW_EXTENSIONS=matrix \
@@ -320,13 +320,13 @@ jobs:
- name: Pull root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: timeout --kill-after=30s 600s docker pull "$IMAGE_REF"
run: timeout 600s docker pull "$IMAGE_REF"
- name: Run root Dockerfile CLI smoke
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: |
timeout --kill-after=30s 20m docker run --rm --entrypoint sh "$IMAGE_REF" -lc '
docker run --rm --entrypoint sh "$IMAGE_REF" -lc '
which openclaw &&
openclaw --version &&
node -e "
@@ -359,7 +359,7 @@ jobs:
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: |
timeout --kill-after=30s 20m docker run --rm --entrypoint sh "$IMAGE_REF" -lc '
docker run --rm --entrypoint sh "$IMAGE_REF" -lc '
which openclaw &&
openclaw --version &&
node -e "
@@ -426,7 +426,7 @@ jobs:
- name: Pull root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: timeout --kill-after=30s 600s docker pull "$IMAGE_REF"
run: timeout 600s docker pull "$IMAGE_REF"
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
@@ -435,7 +435,7 @@ jobs:
- name: Build installer smoke image
run: |
timeout --kill-after=30s 20m docker buildx build \
timeout 20m docker buildx build \
--progress=plain \
--load \
-t openclaw-install-smoke:local \
@@ -444,7 +444,7 @@ jobs:
- name: Build installer non-root image
run: |
timeout --kill-after=30s 20m docker buildx build \
timeout 20m docker buildx build \
--progress=plain \
--load \
-t openclaw-install-nonroot:local \
@@ -475,22 +475,13 @@ jobs:
- name: Run Rocky Linux installer smoke
run: |
timeout --kill-after=30s 20m docker run --rm \
timeout 20m docker run --rm \
-e OPENCLAW_NO_ONBOARD=1 \
-e OPENCLAW_NO_PROMPT=1 \
-v "$PWD/scripts/install.sh:/tmp/install.sh:ro" \
rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6 \
bash -lc 'dnf install -y -q ca-certificates tar gzip xz findutils which sudo >/dev/null && bash /tmp/install.sh --install-method npm --version latest --no-onboard --no-prompt --verify && openclaw --version'
- name: Run Rocky Linux CLI installer smoke
run: |
timeout --kill-after=30s 20m docker run --rm \
-e OPENCLAW_NO_ONBOARD=1 \
-e OPENCLAW_NO_PROMPT=1 \
-v "$PWD/scripts/install-cli.sh:/tmp/install-cli.sh:ro" \
rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6 \
bash -lc 'dnf install -y -q ca-certificates tar gzip xz findutils which sudo >/dev/null && bash /tmp/install-cli.sh --prefix /tmp/openclaw-cli --version latest --no-onboard && /tmp/openclaw-cli/bin/openclaw --version'
bun_global_install_smoke:
needs: [preflight, root_dockerfile_image]
if: needs.preflight.outputs.run_full_install_smoke == 'true' && needs.preflight.outputs.run_bun_global_install_smoke == 'true'
@@ -512,7 +503,7 @@ jobs:
- name: Pull root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: timeout --kill-after=30s 600s docker pull "$IMAGE_REF"
run: timeout 600s docker pull "$IMAGE_REF"
- name: Setup Node environment for Bun smoke
uses: ./.github/actions/setup-node-env

View File

@@ -48,7 +48,6 @@ env:
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
CRABBOX_REF: main
CRABBOX_CAPACITY_REGIONS: eu-west-1,eu-west-2,eu-central-1,us-east-1,us-west-2
MANTIS_OUTPUT_DIR: .artifacts/qa-e2e/mantis/telegram-desktop-proof
jobs:
@@ -423,7 +422,7 @@ jobs:
{
printf '%s\n' 'Defaults env_keep += "CODEX_HOME CODEX_INTERNAL_ORIGINATOR_OVERRIDE"'
printf '%s\n' 'Defaults env_keep += "BASELINE_REF BASELINE_SHA CANDIDATE_REF CANDIDATE_SHA"'
printf '%s\n' 'Defaults env_keep += "CRABBOX_ACCESS_CLIENT_ID CRABBOX_ACCESS_CLIENT_SECRET CRABBOX_COORDINATOR CRABBOX_COORDINATOR_TOKEN CRABBOX_LEASE_ID CRABBOX_PROVIDER CRABBOX_CAPACITY_REGIONS"'
printf '%s\n' 'Defaults env_keep += "CRABBOX_ACCESS_CLIENT_ID CRABBOX_ACCESS_CLIENT_SECRET CRABBOX_COORDINATOR CRABBOX_COORDINATOR_TOKEN CRABBOX_LEASE_ID CRABBOX_PROVIDER"'
printf '%s\n' 'Defaults env_keep += "GH_TOKEN MANTIS_CANDIDATE_TRUST MANTIS_INSTRUCTIONS MANTIS_OUTPUT_DIR MANTIS_PR_NUMBER"'
printf '%s\n' 'Defaults env_keep += "OPENCLAW_BUILD_PRIVATE_QA OPENCLAW_ENABLE_PRIVATE_QA_CLI OPENCLAW_QA_CONVEX_SECRET_CI OPENCLAW_QA_CONVEX_SITE_URL OPENCLAW_QA_CREDENTIAL_OWNER_ID OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN"'
printf '%s\n' 'Defaults env_keep += "OPENCLAW_TELEGRAM_USER_CRABBOX_BIN OPENCLAW_TELEGRAM_USER_CRABBOX_PROVIDER OPENCLAW_TELEGRAM_USER_DRIVER_SCRIPT OPENCLAW_TELEGRAM_USER_PROOF_CMD"'
@@ -452,7 +451,6 @@ jobs:
CRABBOX_ACCESS_CLIENT_SECRET: ${{ secrets.CRABBOX_ACCESS_CLIENT_SECRET }}
CRABBOX_COORDINATOR: ${{ secrets.CRABBOX_COORDINATOR || secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }}
CRABBOX_COORDINATOR_TOKEN: ${{ secrets.CRABBOX_COORDINATOR_TOKEN || secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN }}
CRABBOX_CAPACITY_REGIONS: ${{ env.CRABBOX_CAPACITY_REGIONS }}
CRABBOX_LEASE_ID: ${{ needs.resolve_request.outputs.lease_id }}
CRABBOX_PROVIDER: ${{ needs.resolve_request.outputs.crabbox_provider }}
GH_TOKEN: ${{ github.token }}
@@ -494,11 +492,8 @@ jobs:
exit 0
fi
status=0
mapfile -d '' session_files < <(sudo find .artifacts/qa-e2e -name session.json -type f -print0)
mapfile -d '' session_files < <(sudo find .artifacts/qa-e2e -path '*/telegram-user-crabbox/*/session.json' -type f -print0)
for session_file in "${session_files[@]}"; do
if ! sudo -u codex node -e 'const fs = require("fs"); const session = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.exit(session.command === "telegram-user-crabbox-session" ? 0 : 1);' "$session_file"; then
continue
fi
lease_file="${session_file%/session.json}/.session/lease.json"
if [[ ! -f "$lease_file" ]]; then
continue
@@ -513,11 +508,8 @@ jobs:
status=1
fi
done
mapfile -d '' lease_files < <(sudo find .artifacts/qa-e2e -path '*/.session/lease.json' -type f -print0)
mapfile -d '' lease_files < <(sudo find .artifacts/qa-e2e -path '*/telegram-user-crabbox/*/.session/lease.json' -type f -print0)
for lease_file in "${lease_files[@]}"; do
if ! sudo -u codex node -e 'const fs = require("fs"); const lease = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.exit(lease.kind === "telegram-user" ? 0 : 1);' "$lease_file"; then
continue
fi
if ! sudo -u codex env \
OPENCLAW_QA_CONVEX_SECRET_CI="$OPENCLAW_QA_CONVEX_SECRET_CI" \
OPENCLAW_QA_CONVEX_SITE_URL="$OPENCLAW_QA_CONVEX_SITE_URL" \

View File

@@ -553,15 +553,6 @@ jobs:
use-actions-cache: "false"
- name: Download candidate artifact
id: download_candidate
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: openclaw-cross-os-release-checks-candidate-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/candidate
- name: Retry candidate artifact download
if: ${{ steps.download_candidate.outcome == 'failure' }}
uses: actions/download-artifact@v8
with:
name: openclaw-cross-os-release-checks-candidate-${{ github.run_id }}
@@ -569,38 +560,11 @@ jobs:
- name: Download baseline artifact
if: ${{ matrix.suite == 'packaged-upgrade' }}
id: download_baseline
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: openclaw-cross-os-release-checks-baseline-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/baseline
- name: Retry baseline artifact download
if: ${{ matrix.suite == 'packaged-upgrade' && steps.download_baseline.outcome == 'failure' }}
uses: actions/download-artifact@v8
with:
name: openclaw-cross-os-release-checks-baseline-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/baseline
- name: Verify release-check inputs
shell: bash
env:
CANDIDATE_TGZ: ${{ runner.temp }}/openclaw-cross-os-release-checks/candidate/${{ needs.prepare.outputs.candidate_file_name }}
BASELINE_TGZ: ${{ runner.temp }}/openclaw-cross-os-release-checks/baseline/${{ needs.prepare.outputs.baseline_file_name }}
OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}
SUITE: ${{ matrix.suite }}
run: |
mkdir -p "${OUTPUT_DIR}"
if [[ ! -f "${CANDIDATE_TGZ}" ]]; then
echo "::error::candidate artifact missing: ${CANDIDATE_TGZ}"
exit 1
fi
if [[ "${SUITE}" == "packaged-upgrade" ]] && [[ ! -f "${BASELINE_TGZ}" ]]; then
echo "::error::baseline artifact missing: ${BASELINE_TGZ}"
exit 1
fi
- name: Run cross-OS release checks
shell: bash
env:
@@ -651,8 +615,7 @@ jobs:
if [[ -f "${SUMMARY_PATH}" ]]; then
cat "${SUMMARY_PATH}" >> "$GITHUB_STEP_SUMMARY"
else
mkdir -p "$(dirname "${SUMMARY_PATH}")"
echo "No summary generated." | tee "${SUMMARY_PATH}" >> "$GITHUB_STEP_SUMMARY"
echo "No summary generated." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload release-check artifacts

View File

@@ -102,11 +102,6 @@ on:
- beta
- stable
- full
use_github_hosted_runners:
description: Use GitHub-hosted runners instead of Blacksmith runners
required: false
default: false
type: boolean
advisory:
description: Treat failures as advisory for the caller
required: false
@@ -213,11 +208,6 @@ on:
required: false
default: stable
type: string
use_github_hosted_runners:
description: Use GitHub-hosted runners instead of Blacksmith runners
required: false
default: true
type: boolean
secrets:
OPENAI_API_KEY:
required: false
@@ -484,7 +474,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'live-cache')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: 20
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
@@ -521,7 +511,7 @@ jobs:
set -euo pipefail
for attempt in 1 2; do
echo "live-cache attempt ${attempt}/2"
if timeout --kill-after=30s 8m pnpm test:live:cache; then
if timeout --foreground --kill-after=30s 8m pnpm test:live:cache; then
exit 0
fi
if [[ "$attempt" == "2" ]]; then
@@ -534,7 +524,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_repo_e2e && inputs.live_suite_filter == ''
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ inputs.release_test_profile == 'full' && 90 || 60 }}
env:
OPENCLAW_VITEST_MAX_WORKERS: "2"
@@ -566,7 +556,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_repo_e2e && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'openshell-e2e')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
@@ -640,7 +630,7 @@ jobs:
if: inputs.include_release_path_suites && inputs.docker_lanes == ''
name: Docker E2E (${{ matrix.label }})
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
@@ -931,7 +921,7 @@ jobs:
needs: validate_selected_ref
if: inputs.docker_lanes != ''
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 5
outputs:
groups_json: ${{ steps.groups.outputs.groups_json }}
@@ -960,7 +950,7 @@ jobs:
if: inputs.docker_lanes != ''
name: Docker E2E targeted lanes (${{ matrix.group.label }})
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 60
strategy:
fail-fast: false
@@ -1192,7 +1182,7 @@ jobs:
if: inputs.include_openwebui && !inputs.include_release_path_suites && inputs.docker_lanes == ''
name: Docker E2E (openwebui)
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 60
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
@@ -1318,7 +1308,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_release_path_suites || inputs.include_openwebui || inputs.docker_lanes != ''
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: ${{ inputs.release_test_profile == 'full' && 90 || 60 }}
permissions:
actions: read
@@ -1434,7 +1424,7 @@ jobs:
fi
echo "Validating Docker E2E package tarball: $target"
started_at="$(date +%s)"
timeout --kill-after=30s 5m node scripts/check-openclaw-package-tarball.mjs "$target"
timeout --foreground 5m node scripts/check-openclaw-package-tarball.mjs "$target"
finished_at="$(date +%s)"
echo "Docker E2E package tarball validation finished in $((finished_at - started_at))s."
digest="$(sha256sum "$target" | awk '{print $1}')"
@@ -1561,7 +1551,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_live_suites && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'live-') || startsWith(inputs.live_suite_filter, 'docker-live-models'))
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 60
permissions:
contents: read
@@ -1634,7 +1624,7 @@ jobs:
needs: [validate_selected_ref, prepare_live_test_image]
if: inputs.include_live_suites && inputs.live_model_providers == '' && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'docker-live-models')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 45
strategy:
fail-fast: false
@@ -1778,14 +1768,14 @@ jobs:
- name: Run Docker live model sweep
if: contains(matrix.profiles, inputs.release_test_profile)
run: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-models-docker.sh
run: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-models-docker.sh
validate_live_models_docker_targeted:
name: Docker live models (selected providers)
needs: [validate_selected_ref, prepare_live_test_image]
if: inputs.include_live_suites && inputs.live_model_providers != '' && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'docker-live-models')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 45
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
@@ -1953,13 +1943,13 @@ jobs:
done
- name: Run Docker live model sweep
run: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-models-docker.sh
run: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-models-docker.sh
validate_live_provider_suites:
needs: validate_selected_ref
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || (startsWith(inputs.live_suite_filter, 'native-live-') && !startsWith(inputs.live_suite_filter, 'native-live-extensions-media') && inputs.live_suite_filter != 'native-live-extensions-a-k'))
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
@@ -2261,7 +2251,6 @@ jobs:
env:
OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}
OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }}
shell: bash
run: |
set +e
bash .release-harness/scripts/ci-live-command-retry.sh
@@ -2281,7 +2270,7 @@ jobs:
needs: [validate_selected_ref, prepare_live_test_image]
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'live-'))
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
@@ -2289,32 +2278,32 @@ jobs:
include:
- suite_id: live-gateway-docker
label: Docker live gateway OpenAI
command: OPENCLAW_LIVE_GATEWAY_THINKING=low OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=300000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: beta minimum stable full
- suite_id: live-gateway-anthropic-docker
label: Docker live gateway Anthropic
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic OPENCLAW_LIVE_GATEWAY_MODELS=anthropic/claude-sonnet-4-6 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full
- suite_id: live-gateway-google-docker
label: Docker live gateway Google
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview,google/gemini-3-flash-preview OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview,google/gemini-3-flash-preview OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full
- suite_id: live-gateway-minimax-docker
label: Docker live gateway MiniMax
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full
- suite_id: live-gateway-advisory-docker-deepseek-fireworks
suite_group: live-gateway-advisory-docker
label: Docker live gateway advisory DeepSeek/Fireworks
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=deepseek,fireworks OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=deepseek,fireworks OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
advisory: true
@@ -2322,7 +2311,7 @@ jobs:
- suite_id: live-gateway-advisory-docker-opencode-openrouter
suite_group: live-gateway-advisory-docker
label: Docker live gateway advisory OpenCode/OpenRouter
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go,openrouter OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go,openrouter OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
advisory: true
@@ -2330,32 +2319,32 @@ jobs:
- suite_id: live-gateway-advisory-docker-xai-zai
suite_group: live-gateway-advisory-docker
label: Docker live gateway advisory xAI/Z.ai
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=xai,zai OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=xai,zai OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
advisory: true
profiles: full
- suite_id: live-cli-backend-docker
label: Docker live CLI backend
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 45m bash .release-harness/scripts/test-live-cli-backend-docker.sh
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 45m bash .release-harness/scripts/test-live-cli-backend-docker.sh
timeout_minutes: 50
profile_env_only: false
profiles: stable full
- suite_id: live-acp-bind-docker
label: Docker live ACP bind
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 45m bash .release-harness/scripts/test-live-acp-bind-docker.sh
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 45m bash .release-harness/scripts/test-live-acp-bind-docker.sh
timeout_minutes: 50
profile_env_only: false
profiles: stable full
- suite_id: live-codex-harness-docker
label: Docker live Codex harness
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 35m bash .release-harness/scripts/test-live-codex-harness-docker.sh
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-codex-harness-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full
- suite_id: live-subagent-announce-docker
label: Docker live subagent announce
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --kill-after=30s 20m bash .release-harness/scripts/test-live-subagent-announce-docker.sh
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 20m bash .release-harness/scripts/test-live-subagent-announce-docker.sh
timeout_minutes: 25
profile_env_only: false
profiles: stable full
@@ -2480,7 +2469,6 @@ jobs:
env:
OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}
OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }}
shell: bash
run: |
set +e
bash .release-harness/scripts/ci-live-command-retry.sh
@@ -2500,7 +2488,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'native-live-extensions-media') || inputs.live_suite_filter == 'native-live-extensions-a-k')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
container:
image: ghcr.io/openclaw/openclaw-live-media-runner:ubuntu-24.04
credentials:
@@ -2668,7 +2656,6 @@ jobs:
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-extensions-media-video' && startsWith(matrix.suite_id, 'native-live-extensions-media-video-')))
env:
OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }}
shell: bash
run: |
set +e
${{ matrix.command }}

View File

@@ -307,36 +307,7 @@ jobs:
exit 1
fi
report_md="${report_json%.json}.md"
effective_status="$status"
if [[ "$FAIL_ON_REGRESSION" == "true" && "$status" != "0" ]]; then
if REPORT_JSON="$report_json" node <<'NODE'
const fs = require("node:fs");
const report = JSON.parse(fs.readFileSync(process.env.REPORT_JSON, "utf8"));
const statuses = report.summary?.statuses ?? {};
const nonPassStatuses = Object.entries(statuses)
.filter(([status, count]) => status !== "PASS" && Number(count) > 0);
const baselineRegressionCount =
Number(report.baseline?.comparison?.regressionCount ?? report.gate?.baseline?.regressionCount ?? 0);
const gate = report.gate;
const toleratedPartial =
gate?.verdict === "PARTIAL" &&
Number(gate.blockingCount ?? 0) === 0 &&
baselineRegressionCount === 0 &&
nonPassStatuses.length === 0;
if (!toleratedPartial) {
process.exit(1);
}
NODE
then
effective_status=0
{
echo "Kova returned a partial release-gate verdict for filtered performance coverage, but all selected scenarios passed and no baseline regression was reported."
echo
} >> "$GITHUB_STEP_SUMMARY"
fi
fi
echo "status=$status" >> "$GITHUB_OUTPUT"
echo "effective_status=$effective_status" >> "$GITHUB_OUTPUT"
echo "report_json=$report_json" >> "$GITHUB_OUTPUT"
echo "report_md=$report_md" >> "$GITHUB_OUTPUT"
@@ -373,43 +344,8 @@ jobs:
EOF
cat "$summary_path" >> "$GITHUB_STEP_SUMMARY"
if [[ "$FAIL_ON_REGRESSION" == "true" && "$effective_status" != "0" ]]; then
exit "$effective_status"
fi
- name: Fetch previous source performance baseline
if: ${{ steps.lane.outputs.run == 'true' && matrix.lane == 'mock-provider' && steps.clawgrit.outputs.present == 'true' }}
env:
CLAWGRIT_REPORTS_TOKEN: ${{ secrets.CLAWGRIT_REPORTS_TOKEN }}
shell: bash
run: |
set -euo pipefail
reports_root=".artifacts/clawgrit-baseline"
mkdir -p "$reports_root"
git -C "$reports_root" init -b main
git -C "$reports_root" remote add origin "https://x-access-token:${CLAWGRIT_REPORTS_TOKEN}@github.com/openclaw/clawgrit-reports.git"
if ! git -C "$reports_root" fetch --depth=1 origin main; then
echo "No previous source performance baseline could be fetched." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
git -C "$reports_root" checkout -B main FETCH_HEAD
ref_slug="$(printf '%s' "${TESTED_REF}" | tr -c 'A-Za-z0-9._-' '-')"
pointer="${reports_root}/openclaw-performance/${ref_slug}/latest-mock-provider.json"
if [[ ! -f "$pointer" ]]; then
echo "No previous source performance baseline exists for ${TESTED_REF}." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if ! latest_path="$(node -e "const fs=require('node:fs'); const data=JSON.parse(fs.readFileSync(process.argv[1],'utf8')); const value=String(data.path || ''); if (!/^openclaw-performance\\/[A-Za-z0-9._-]+\\/[0-9]+-[0-9]+\\/mock-provider$/u.test(value)) process.exit(1); process.stdout.write(value);" "$pointer")"; then
echo "Previous source performance baseline pointer is invalid." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
baseline_source="${reports_root}/${latest_path}/source"
if [[ -d "$baseline_source" ]]; then
baseline_source="$(realpath "$baseline_source")"
echo "SOURCE_PERF_BASELINE_DIR=$baseline_source" >> "$GITHUB_ENV"
echo "Using source performance baseline: ${latest_path}/source" >> "$GITHUB_STEP_SUMMARY"
else
echo "Previous source performance baseline has no source directory." >> "$GITHUB_STEP_SUMMARY"
if [[ "$FAIL_ON_REGRESSION" == "true" && "$status" != "0" ]]; then
exit "$status"
fi
- name: Run OpenClaw source performance probes
@@ -423,7 +359,7 @@ jobs:
fi
mkdir -p "$SOURCE_PERF_DIR/mock-hello"
if ! node -e "const fs=require('node:fs'); const scripts=require('./package.json').scripts||{}; process.exit(scripts['test:gateway:cpu-scenarios'] && scripts['test:extensions:memory'] && scripts.openclaw && fs.existsSync('scripts/bench-cli-startup.ts') && fs.existsSync('scripts/profile-extension-memory.mjs') ? 0 : 1)"; then
if ! node -e "const fs=require('node:fs'); const scripts=require('./package.json').scripts||{}; process.exit(scripts['test:gateway:cpu-scenarios'] && scripts.openclaw && fs.existsSync('scripts/bench-cli-startup.ts') ? 0 : 1)"; then
cat > "$SOURCE_PERF_DIR/index.md" <<EOF
# OpenClaw Source Performance
@@ -435,7 +371,7 @@ jobs:
- Tested ref: ${TESTED_REF}
- Tested SHA: ${TESTED_SHA}
- Required scripts: test:gateway:cpu-scenarios, test:extensions:memory, openclaw, scripts/bench-cli-startup.ts, scripts/profile-extension-memory.mjs
- Required scripts: test:gateway:cpu-scenarios, openclaw, scripts/bench-cli-startup.ts
EOF
cat "$SOURCE_PERF_DIR/index.md" >> "$GITHUB_STEP_SUMMARY"
exit 0
@@ -455,9 +391,6 @@ jobs:
--startup-case fiftyPlugins \
--startup-case fiftyStartupLazyPlugins
pnpm test:extensions:memory \
-- --json "$SOURCE_PERF_DIR/extension-memory.json"
for run_index in $(seq 1 "$source_runs"); do
run_dir="$SOURCE_PERF_DIR/mock-hello/run-$(printf '%03d' "$run_index")"
pnpm openclaw qa suite \
@@ -527,13 +460,9 @@ jobs:
cleanup_gateway
trap - EXIT
summary_args=(node "$PERFORMANCE_HELPER_DIR/scripts/openclaw-performance-source-summary.mjs" \
node "$PERFORMANCE_HELPER_DIR/scripts/openclaw-performance-source-summary.mjs" \
--source-dir "$SOURCE_PERF_DIR" \
--output "$SOURCE_PERF_DIR/index.md")
if [[ -n "${SOURCE_PERF_BASELINE_DIR:-}" && -d "$SOURCE_PERF_BASELINE_DIR" ]]; then
summary_args+=(--baseline-source-dir "$SOURCE_PERF_BASELINE_DIR")
fi
"${summary_args[@]}"
--output "$SOURCE_PERF_DIR/index.md"
cat "$SOURCE_PERF_DIR/index.md" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -344,7 +344,7 @@ jobs:
OPENCLAW_EXTENSION_BATCH_PARALLEL: 2
OPENCLAW_VITEST_MAX_WORKERS: 1
OPENCLAW_EXTENSION_BATCH: ${{ matrix.extensions_csv }}
run: pnpm test:extensions:batch "$OPENCLAW_EXTENSION_BATCH" -- --exclude extensions/codex/src/app-server/run-attempt.test.ts
run: pnpm test:extensions:batch "$OPENCLAW_EXTENSION_BATCH"
plugin-prerelease-inspector:
permissions:

View File

@@ -42,7 +42,7 @@ jobs:
run: |
set -euo pipefail
timeout --kill-after=30s 5m docker build -t openclaw-sandbox-smoke-base:bookworm-slim - <<'EOF'
docker build -t openclaw-sandbox-smoke-base:bookworm-slim - <<'EOF'
FROM debian:bookworm-slim
RUN useradd --create-home --shell /bin/bash sandbox
USER sandbox
@@ -63,5 +63,5 @@ jobs:
FINAL_USER=sandbox \
scripts/sandbox-common-setup.sh
u="$(timeout --kill-after=30s 2m docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc 'id -un')"
u="$(docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc 'id -un')"
test "$u" = "sandbox"

View File

@@ -38,4 +38,4 @@ jobs:
install-bun: "false"
- name: Run TUI PTY tests
run: timeout --kill-after=30s 120s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts
run: timeout 120s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts

View File

@@ -75,14 +75,14 @@ jobs:
- name: install.sh in Docker
run: |
timeout --kill-after=30s 20m docker run --rm \
docker run --rm \
-v "$PWD/scripts/install.sh:/tmp/install.sh:ro" \
node:24-bookworm-slim \
bash -lc 'bash /tmp/install.sh --version latest && openclaw --version'
- name: install-cli.sh in Docker
run: |
timeout --kill-after=30s 20m docker run --rm \
docker run --rm \
-e OPENCLAW_NO_ONBOARD=1 \
-e OPENCLAW_NO_PROMPT=1 \
-v "$PWD/scripts/install-cli.sh:/tmp/install-cli.sh:ro" \

View File

@@ -26,16 +26,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
uses: actions/checkout@v6
- name: Fail on tabs in workflow files
run: |
@@ -67,16 +58,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
uses: actions/checkout@v6
- name: Install actionlint
shell: bash
@@ -108,16 +90,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
uses: actions/checkout@v6
- name: Setup Node environment
uses: ./.github/actions/setup-node-env

View File

@@ -27,7 +27,7 @@ Skills own workflows; root owns hard policy and routing.
- For PRs that add, remove, or change config/default surfaces with possible compatibility, upgrade, provider/plugin, operator, setup, startup, or fallback impact, ClawSweeper review should emit a `reviewMetrics` entry when practical. The metric should name the count and direction of the changes, such as added, changed, or removed config/default surfaces, and explain why the metric matters before merge. When the metric indicates concrete merge risk, also surface the concern in `risks`, use `mergeRiskLabels` when the risk matches the label rubric, make `bestSolution` name the desired pre-merge state, and ensure `labelJustifications` explain the specific reason rather than restating the label.
- Review whole decision surfaces, not only the touched runtime, provider, channel, harness, plugin seam, or context path. Check sibling Codex/Pi-style runtimes, provider/model routing, channel delivery, gateway/protocol, plugin SDK, and context-management paths when relevant.
- One-sided fixes need sibling-surface proof, an explanation for why siblings are unaffected, or explicit follow-up work.
- Changelog findings: see Docs / Changelog.
- User-facing `fix`, `feat`, and `perf` changes need `CHANGELOG.md` before landing; contributor PR authors are not blocked solely on maintainer-owned changelog work. Never request thanks for bot/forbidden handles: `@openclaw`, `@clawsweeper`, `@codex`, `@steipete`.
- Public ClawSweeper comments prefer `https://docs.openclaw.ai/...` when a public docs page exists; structured evidence still cites repo files, lines, SHAs.
- Findings need current source, shipped/current behavior, tests/CI evidence, and dependency contract proof when dependency-backed behavior is involved. Validation is judged against touched and sibling surfaces plus this file's commands; real behavior proof matters for user-visible changes, with Telegram/Desktop proof for Telegram-visible behavior when feasible.
- Prefer findings for concrete behavior regressions, missing changed-surface proof, owner-boundary violations, security/API contract issues, or docs/config mismatches.
@@ -58,17 +58,9 @@ Skills own workflows; root owns hard policy and routing.
- Externalizing a bundled plugin: update package excludes, official catalogs, docs, tests, and prove core runtime paths resolve installed plugin roots before root-dep removal.
- Runtime reads canonical config only. No silent compat for old/malformed config keys. If a config change invalidates existing files, add a matching `openclaw doctor --fix` migration. Core/auth config repairs live in core doctor; plugin-owned config repairs live in that plugin's doctor contract (`legacyConfigRules` / `normalizeCompatibilityConfig`).
- Fix shape: default to clean bounded refactor, not smallest patch. Move ownership to right boundary; delete stale abstractions, duplicate policy, dead branches, wrappers, fallback stacks.
- Fix observed local failures with generic product rules; do not hardcode names, ids, log phrases, or user examples in prod code unless they are an explicit contract.
- Tests may use observed examples, but prod literals need a short contract reason.
- Compatibility is opt-in. "Shipped" means reachable from a release Git tag; main/GitHub/PR/unreleased code is not shipped.
- Refactor default: one canonical path. Delete the old path unless user explicitly wants compat or the shipped public contract is obvious and cited.
- Keep old behavior only for an explicit public API/config/plugin SDK/data contract, tagged upgrade path, security/migration boundary, dependency contract, or observed prod state.
- If unsure, ask before preserving compat. Do not keep aliases, shims, fallback stacks, stale names, or obsolete tests just in case.
- Tests alone do not make internals contracts. If compat stays, name the contract and migration/removal plan in code, test, or PR.
- Lean code is a goal. No internal shims, aliases, legacy names, broad fallbacks, or defensive branches just to reduce diff or handle unrealistic edge cases.
- Handle real production states, tagged upgrade paths, security boundaries, and dependency contracts. Public/hostile/observed malformed input gets care; hypothetical malformed input does not.
- Deprecate shipped public contracts only.
- Plugin SDK exception: shipped external API gets new API first plus named compat/deprecation, small tests/docs if useful, removal plan.
- Handle real production states, shipped upgrade paths, security boundaries, and dependency contracts. Public/hostile/observed malformed input gets care; hypothetical malformed input does not.
- Public plugin SDK/API is the compat exception. New API first, old path only via named compat/deprecation metadata, docs, warnings when useful, tests for old+new, planned removal.
- Migrate internal/bundled callers to modern API in the same change. Do not let internal compat become permanent architecture.
- Channels are implementation under `src/channels/**`; plugin authors get SDK seams. Providers own auth/catalog/runtime hooks; core owns generic loop.
- Hot paths should carry prepared facts forward: provider id, model ref, channel id, target, capability family, attachment class. Do not rediscover with broad plugin/provider/channel/capability loaders.
@@ -76,8 +68,7 @@ Skills own workflows; root owns hard policy and routing.
- Gateway/plugin metadata is process-stable: installs, manifests, catalogs, generated paths, bundled metadata. Changes require restart or explicit owner reload/install/doctor flow.
- Runtime hot paths: no freshness polling (`stat`/`realpath`/JSON reread/hash). Reuse current snapshots, install records, discovery, lookup tables, root scopes, resolved paths.
- Process-local metadata caches ok when lifecycle-owned and bounded/single-slot. Freshness exceptions need named owner + tests.
- Inline comments: preserve reviewer context at the code site. Use for cross-path/state invariants, platform/dependency caps, deterministic ordering, compact encoded state, lifecycle ordering, ownership boundaries, session/id adoption, queue-depth symmetry, fallbacks, or intentional caller differences.
- Comment shape: 1-3 short lines; state why the branch/helper exists, what contract it protects, and the bad outcome if removed. Cite nearby constants/helpers when useful. No syntax narration, PR/user-specific lore, or obvious mechanics.
- Inline code comments: brief notes for tricky, bug-prone, or previously buggy logic.
- Gateway protocol changes: additive first; incompatible needs versioning/docs/client follow-through.
- Protocol version bumps: explicit owner confirmation only; never automatic/generated.
- Config contract: exported types, schema/help, metadata, baselines, docs aligned. Retired public keys stay retired; compat in raw migration/doctor only.
@@ -89,6 +80,7 @@ Skills own workflows; root owns hard policy and routing.
- Runtime: Node 22.19+; Node 24 recommended. Keep Node + Bun paths working.
- Package manager/runtime: repo defaults only. No swaps without approval.
- Install: `pnpm install` (keep Bun lock/patches aligned if touched).
- Sharp/Homebrew libvips source-build fail: `SHARP_IGNORE_GLOBAL_LIBVIPS=1 pnpm install`.
- CLI: `pnpm openclaw ...` or `pnpm dev`; build: `pnpm build`.
- Tests in a normal source checkout: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
- Tests in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm test*`; use `node scripts/run-vitest.mjs <path-or-filter>` for tiny explicit-file proof, or Crabbox/Testbox for anything broader.
@@ -125,6 +117,7 @@ Skills own workflows; root owns hard policy and routing.
- Do not leave associated issues open for hypothetical future repros. Close with rationale; ask for a new issue or reopen only if concrete new evidence appears. Close comment states: decision, why, supported alternative, and what evidence would change the decision.
- PR review answer: bug/behavior, URL(s), affected surface, provenance for regressions when traceable, best-fix judgment, evidence from code/tests/CI/current or shipped behavior.
- Issue/PR final answer: last line is the full GitHub URL.
- Changelog: PR landings/fixes need one unless pure test/internal. Do not mention missing changelog as a review finding; Codex handles it during fix/landing.
- PR verification: before merge, post exact local commands, CI/Testbox run IDs, before/after proof when used, and known proof gaps.
- Issue fixed on `main` with proof: comment proof + commit/PR, then close.
- After landing or requested close/sweep: search duplicates; comment proof + canonical commit/PR/release before closing.
@@ -132,10 +125,8 @@ Skills own workflows; root owns hard policy and routing.
- `ship` that fixes an issue: after push, comment proof + commit link, then close the issue.
- GH comments with backticks, `$`, or shell snippets: use heredoc/body file, not inline double-quoted `--body`.
- PR create: real body required. Include Summary + Verification; mention refs, behavior, and proof.
- PR create/refresh: keep PR branches takeover-ready. Use a branch maintainers can push to, or for fork PRs ensure `maintainer_can_modify` / GitHub's `Allow edits by maintainers` is enabled unless explicitly told otherwise or GitHub's Actions/secrets warning makes that unsafe.
- GitHub issue/PR create: read `$agent-transcript`; ask about sanitized transcript logs when available.
- Real behavior proof section is parsed. Use exact `field: value` labels: `Behavior addressed`, `Real environment tested`, `Exact steps or command run after this patch`, `Evidence after fix`, `Observed result after fix`, `What was not tested`.
- PR artifacts/screenshots: attach to PR/comment/external artifact store. Never push screenshots, videos, proof images, or proof assets to OpenClaw or any product repo branch, including temp artifact branches. Use Crabbox artifact publishing plus the manifest URL. Do not commit `.github/pr-assets`.
- PR artifacts/screenshots: attach to PR/comment/external artifact store. Do not commit `.github/pr-assets`.
- CI polling: exact SHA, relevant checks only, minimal fields. Skip routine noise (`Auto response`, `Labeler`, docs agents, performance/stale). Logs only after failure/completion or concrete need.
- Maintainers: may skip/ignore `Real behavior proof` when local tests or Crabbox verified behavior; record proof in PR verification.
- `/landpr`: use `~/.codex/prompts/landpr.md`; do not idle on `auto-response` or `check-docs`.
@@ -159,13 +150,10 @@ Skills own workflows; root owns hard policy and routing.
- Inline simple one-use objects/spreads when clearer. Extract only when it removes duplication or hard logic.
- Tests prove behavior/regressions, not every internal branch.
- For non-trivial refactors, check `git diff --numstat` before closeout. If LOC grew, trim or explain why.
- Prefer existing narrow helpers over repeated casts/guards. Add local helpers when 2+ nearby call sites share real boundary logic.
- Prefer ctor parameter properties for injected deps/config. Do not ban them for erasable-syntax purity.
- Prefer `satisfies` for registries/config maps; derive types from schemas when a runtime schema already exists.
- Table-drive repetitive tests when it reduces code and keeps failure names clear.
- Dynamic import: no static+dynamic import for same prod module. Use `*.runtime.ts` lazy boundary. After edits: `pnpm build`; check `[INEFFECTIVE_DYNAMIC_IMPORT]`.
- Cycles: keep `pnpm check:import-cycles` + architecture/madge green.
- Classes: no prototype mixins/mutations. Prefer inheritance/composition. Tests prefer per-instance stubs.
- Comments: brief, only non-obvious logic.
- Split files around ~700 LOC when clarity/testability improves.
- Naming: **OpenClaw** product/docs; `openclaw` CLI/package/path/config.
- English: American spelling.
@@ -187,9 +175,9 @@ Skills own workflows; root owns hard policy and routing.
- Use `$technical-documentation` for docs writing/review. Docs change with behavior/API.
- Codex harness upgrade (`extensions/codex/package.json` `@openai/codex`): refresh `docs/plugins/codex-harness.md` model snapshot from the new harness `model/list`.
- Docs final answers: include relevant full `https://docs.openclaw.ai/...` URL(s). If issue/PR work too, GitHub URL last.
- `CHANGELOG.md`: release-owned. Do not edit for normal PRs, direct `main` fixes, or `ship it`; only explicit release/changelog generation may rewrite it. Do not ask contributors/agents for changelog edits.
- User-facing `fix`/`feat`/`perf`: put release-note context in PR body, squash message, or direct commit: behavior, surface, issue/PR refs, credited human author/reporter.
- Release generation: derive `CHANGELOG.md` from merged PRs + all direct `main` commits. Entries: active `### Changes`/`### Fixes`, single-line, thank credited humans; never thank bots/forbidden handles: `@openclaw`, `@clawsweeper`, `@codex`, `@steipete`.
- Changelog entries: active version `### Changes`/`### Fixes`; single-line bullets only.
- Contributor PR authors should not edit `CHANGELOG.md`; maintainer/AI adds entries during landing/merge.
- Contributor-facing changelog entries thank credited human `@author`. Never thank bots, `@openclaw`, `@clawsweeper`, or `@steipete`; if unknown, omit thanks.
## Git
@@ -198,7 +186,7 @@ Skills own workflows; root owns hard policy and routing.
- No manual stash/autostash unless explicit. No branch/worktree changes unless requested.
- `main`: no merge commits; rebase on latest `origin/main` before push. After one green run plus clean rebase sanity, do not chase moving `main` with repeated full gates.
- User says `commit`: your changes only. `commit all`: all changes in grouped chunks. `push`: may `git pull --rebase` first.
- User says `ship it`: commit intended changes, pull --rebase, push.
- User says `ship it`: changelog if needed, commit intended changes, pull --rebase, push.
- Do not delete/rename unexpected files; ask if blocking, else ignore.
- Bulk PR close/reopen >5: ask with count/scope.

View File

@@ -6,116 +6,36 @@ Docs: https://docs.openclaw.ai
### Changes
- Voice: expose shared realtime turn-context tracking through the realtime voice SDK and reuse it for Discord speaker attribution and wake-name context recovery.
- Voice: reuse shared realtime output activity tracking in Google Meet command and node audio bridges, including recent-output checks for local barge-in detection.
- Voice: expose shared realtime output activity tracking through the realtime voice SDK and reuse it for Discord playback activity and barge-in decisions.
- Voice: expose shared realtime consult question matching, speakable-result extraction, and alias-aware forced-consult coordination through the realtime voice SDK, then reuse it in Gateway Talk, Voice Call, and Discord voice paths.
- Voice: share activation-name matching and consult-transcript screening through the realtime voice SDK so Discord, browser voice, and meeting surfaces can reuse one implementation.
- Cron: default `cron.maxConcurrentRuns` to 8 so scheduled automations and their isolated agent turns can make progress in parallel without explicit configuration.
- QA-Lab: add `qa coverage --match <query>` so focused proof selection can discover matching scenarios from existing metadata before running live or remote lanes.
- Control UI: add an ephemeral Activity tab for sanitized live tool activity summaries without persisting raw telemetry. Fixes #12831. Thanks @BunsDev.
- Build: include `ui:build` in the `full` and `ciArtifacts` profiles of `scripts/build-all.mjs` so `pnpm build` always rebuilds `dist/control-ui` after `tsdown` cleans `dist`, removing the second-command requirement and the missing-asset failure mode for source/runtime installs and CI artifact uploads. (#85206)
- Migrate: import supported Hermes, OpenCode, and Codex auth credentials into OpenClaw auth profiles when credential migration is selected, with explicit opt-out and non-interactive controls. (#85667) Thanks @fuller-stack-dev.
- iOS: improve Talk mode with direct realtime voice sessions, compact toolbar status, and responsive voice waveform feedback. (#86355) Thanks @ngutman.
- Media: replace the Sharp image backend with Photon for metadata, resizing, EXIF orientation, and PNG alpha-preserving optimization so OpenClaw no longer installs Sharp or the WhatsApp Jimp fallback for image processing. (#86437)
### Fixes
- Telegram/network: treat `ENETDOWN` as a transient pre-connect network failure so Telegram sends, gateway unhandled-rejection handling, and cron network retries follow the same recovery path as sibling network outages. (#86762) Thanks @TurboTheTurtle.
- Agents/sessions: include visibility metadata on restricted `sessions_list` results so scoped counts are clearly reported without widening access or exposing hidden-session counts. (#86944) Thanks @ferminquant.
- Gateway/DNS: validate wide-area discovery domains before deriving zone paths or writing zone files, so invalid `discovery.wideArea.domain` and `dns setup --domain` values fail with a DNS-name diagnostic instead of falling through to unrelated configuration errors. Thanks @mmaps.
- Agents/BTW: route fallback side-question streams through the embedded stream resolver so Anthropic-compatible MiniMax requests use the same capped transport as normal chat. (#86312) Thanks @neeravmakwana.
- Telegram: treat `/command@TargetBot` bot-command entities as explicit mentions for the addressed bot so `requireMention` groups no longer drop targeted commands or captions. Fixes #84462. (#86553) Thanks @luoyanglang.
- CI: bound Docker/Bash E2E tarball npm installs with `OPENCLAW_E2E_NPM_INSTALL_TIMEOUT` so package, onboarding, plugin, and upgrade lanes fail instead of hanging on a stuck npm install.
- CI: keep `OPENCLAW_TESTBOX=1 pnpm check:changed` delegating to Blacksmith Testbox through Crabbox without forwarding local Testbox or worker env into the remote command.
- CI: send KILL after the TERM grace period for manual checkout fetch timeouts so stuck Testbox and workflow checkout retries cannot hang behind a wedged `git fetch`.
- iMessage: thread current channel/account inbound attachment roots into the image tool so iMessage-saved attachments under `~/Library/Messages/Attachments` (including the wildcard `/Users/*/Library/Messages/Attachments` root) are read through the existing inbound path policy instead of being rejected as `path-not-allowed`. Literal `localRoots` stays workspace-scoped. Fixes #30170. (#86569)
- QQ Bot: respect `OPENCLAW_HOME` for outbound media path resolution so `<qqmedia>` sends no longer silently fail when `HOME` and `OPENCLAW_HOME` differ (Docker / multi-user hosts). Persisted QQ Bot data (sessions, known users, refs) stays anchored on the OS home for upgrade compatibility. Fixes #83562. Thanks @sliverp.
- Update: report the primary malformed `openclaw.extensions` payload error without adding a duplicate missing-main diagnostic. (#86596) Thanks @ferminquant.
- Control UI: keep host-local Markdown file paths inert while preserving app-relative links. (#86620) Thanks @BryanTegomoh.
- Gateway: dampen repeated unauthenticated device-required probes per URL while preserving explicit-auth and paired recovery paths. (#86575) Thanks @ferminquant.
- IRC: store inbound channel routes with the canonical `channel:#name` target and join transient channel sends before writing. (#85906) Thanks @Kailigithub.
- Usage: surface unknown all-zero model pricing as missing cost entries instead of a confident `$0` total. (#85882) Thanks @MichaelZelbel.
- Agents/Codex: honor yolo app-server approval policy only for the full `never` plus `danger-full-access` case. (#85909) Thanks @earlvanze.
- Gateway/Gmail: clear Gmail watcher renewal intervals on re-entry so hot reloads do not leak lifecycle timers. (#82947) Thanks @SebTardif.
- Logging: exit cleanly on broken stdout/stderr pipes without masking existing failure exit codes. (#80059) Thanks @pavelzak.
- Gateway/security: escape transcript metadata field names while extracting oversized session line prefixes. (#85934) Thanks @SebTardif.
- Plugins/security: validate manifest model pattern regexes with the safe-regex compiler so unsafe patterns are ignored before matching. (#86046) Thanks @SebTardif.
- Discord: route gateway metadata REST lookups through the configured Discord proxy so proxied accounts do not fall back to direct `discord.com` connections before opening the WebSocket. Fixes #80227. Thanks @Clivilwalker.
- Agents/media: hydrate current-turn image attachments from filename-derived MIME types so active vision can see generated or forwarded images whose source omitted an image content type. (#84812) Thanks @marchpure.
- Agents/fs: point workspace-only scratch-path guidance at in-workspace temp directories while keeping host-root writes rejected by the tool guard. (#86501) Thanks @tianxiaochannel-oss88.
- Agents/media: keep async cron media completions scoped to their run session while preserving direct delivery for stale generated-media success and failure notifications. (#86529) Thanks @ai-hpc.
- Gateway: emit plugin `session_end`/`session_start` hooks when `agent.send` rotates or replaces a session id, keeping hook lifecycle state aligned with `sessions.changed` notifications. Fixes #83507. (#85875) Thanks @brokemac79.
- OpenShell/SSH: reject malformed generated exec commands before sandbox/session setup so unresolved workflow placeholders fail fast instead of reaching the remote shell. Fixes #72373. Thanks @brokemac79.
- Google: stop normalizing `gemini-3.1-flash-lite` to the retired preview endpoint and update Flash Lite alias guidance to the GA model id. Fixes #86151. (#86240) Thanks @SebTardif.
- Installer: make Alpine apk installs cover Git, verify the Node runtime floor, try `nodejs-current`, and report Alpine version guidance when repositories only provide older Node packages.
- Agents/status: prefer the active Claude CLI OAuth auth label over an unused Anthropic env API-key label for equivalent runtime aliases. Fixes #80184. (#86570) Thanks @brokemac79.
- Agents/media: send direct fallback for generated media still missing after an active requester wake fails. (#85489) Thanks @fuller-stack-dev.
- Agents: derive overflow compaction budgets from provider-reported and synthetic over-budget token counts so confirmed context overflows compact before retrying. (#70473) Thanks @fuller-stack-dev.
- Agents/Codex: recover Codex context-window prompt errors through overflow compaction and surface reset guidance when recovery is exhausted. (#85542) Thanks @fuller-stack-dev.
- Agents/Codex: allow Codex app-server runs to bootstrap from `CODEX_API_KEY` or `OPENAI_API_KEY` when no Codex auth profile is configured.
- Agents/Codex: keep selected Codex runtime routing on OpenAI-Codex while preserving direct OpenAI API-key compaction fallback. (#86408) Thanks @funmerlin and @VACInc.
- Agent transcript: include OpenClaw agent session logs when finding local transcript candidates.
- Crabbox: bootstrap raw AWS macOS shell commands wrapped in absolute `time` paths so RSS probes can run Node and pnpm on fresh macOS runners.
- Crabbox: bootstrap raw AWS macOS shell commands even when setup statements precede Node or pnpm usage.
- TUI/local: skip unnecessary secret resolution, gateway model catalog loading, bootstrap, and skill scans in explicit local-model runs so startup reaches the model request faster.
- Sessions/doctor: load large session stores without clone amplification during read-only doctor checks and reclaim stale `sessions.json.*.tmp` sidecars. Fixes #56827. Thanks @openperf.
- Tests: clean successful plugin gateway gauntlet isolated temp roots while keeping an explicit preservation switch for failed/debug runs.
- Plugins/perf: reuse derived plugin metadata snapshots for the lifetime of the process so reply-time skill setup no longer rescans plugin metadata on every turn.
- Discord/OpenAI voice: keep wake-name master consults using the current speaker context after ignored ambient transcripts and shorten the default capture silence grace.
- Doctor: skip redundant Gateway restart prompts when a recent supervisor restart leaves the Gateway healthy. Fixes #86518. (#86533) Thanks @liaoyl830.
- Cron: restore suspended cron lanes to the configured/default concurrency instead of falling back to one after quota or circuit-breaker auto-resume.
- Gateway: keep session-only Control UI tool-start mirrors flowing during diagnostic queue pressure instead of silently dropping non-terminal tool updates.
- Agents/memory: return optional not-found context for missing date-only daily memory reads instead of logging benign first-run `ENOENT` failures. Fixes #82928. Thanks @galiniliev.
- Discord: merge streamed text captions into following media block replies so captions and attachments send as one message. (#86487) Thanks @neeravmakwana.
- Gateway: avoid sending duplicate tool-event frames to Control UI connections that are subscribed by both run and session.
- Discord/OpenAI voice: accept broader edge-position fuzzy wake-name transcripts while keeping ambient speech gated.
- Discord/OpenAI voice: accept longer leading wake-name mistranscripts such as "Open Club" for OpenClaw.
- Agents/OpenAI-compatible: stop ModelStudio-compatible chat requests before sending system/tool-only payloads that have no usable user or assistant turn. (#86177) Thanks @TurboTheTurtle.
- Gateway/plugins: reuse plugin package realpath checks while building installed plugin indexes so startup avoids repeated filesystem resolution work.
- Kilo Gateway: send string `stop` sequences as arrays so Kilo accepts OpenAI-compatible chat completions. (#86461) Thanks @SebTardif.
- Discord/OpenAI voice: accept leading fuzzy wake-name transcripts such as "Monty" or "Moti" for a Molty agent while keeping ambient speech gated.
- Media understanding: convert HEIC and HEIF images to JPEG before image description providers run so iPhone photos work in direct and configured image-description flows. (#86037)
- Agents: release embedded-attempt session locks from outer teardown so post-prompt exceptions cannot wedge later requests behind `SessionWriteLockTimeoutError`. Fixes #86014. Thanks @openperf.
- Discord/OpenAI voice: rotate Realtime sessions at provider max duration without logging the expected session-expiry event as an error.
- Sessions: skip metadata-only entries during QMD-slugified session lookup so one incomplete row does not block transcript hit resolution. (#86327) Thanks @abnershang.
- Agents/media: derive bundled plugin local-media trust from plugin tool metadata instead of importing the full plugin registry on subscription paths. (#84409) Thanks @samzong.
- Image tool: keep config-backed custom-provider API keys usable for auto-discovered vision models, including deferred image-tool execution without env keys or auth profiles. (#85733)
- Memory/local embeddings: run local GGUF embeddings in an isolated worker sidecar and degrade to configured fallback or keyword search on worker failure so native embedding crashes do not take down the Gateway. (#85348) Thanks @osolmaz.
- Gateway: clear the runtime config snapshot before `SIGUSR1` in-process restarts so config changes survive the next gateway loop. (#86388) Thanks @XuZehan-iCenter.
- Models: show OAuth delegation markers as configured `models.json` auth while keeping runtime route usability checks strict. (#86378) Thanks @rohitjavvadi.
- Cron: seed active scheduled and manual cron task rows with a progress summary so status surfaces do not look blank while jobs run. (#86313) Thanks @ferminquant.
- Cron: preserve unsupported persisted cron payload rows during routine store writes while keeping those rows non-runnable. Fixes #84922. (#86415) Thanks @IWhatsskill.
- Updater: exclude prerelease git tags from stable channel resolution so source updates do not check out newer alpha/rc/preview/canary tags. (#86260) Thanks @stevenepalmer.
- Security/Audit: flag webhook `hooks.token` reuse of active Gateway password auth in `openclaw security audit` while keeping password-mode startup compatibility. (#84338) Thanks @coygeek.
- QQBot: derive the outbound reply watchdog from configured agent and provider timeouts so slow local model replies are not cut off at five minutes. Fixes #85267. (#85271) Thanks @SymbolStar.
- Agents/heartbeat: stop heartbeat turns after the first valid `heartbeat_respond` so repeated response loops do not burn tokens. (#86357) Thanks @udaymanish6.
- Tasks: keep retained lost tasks out of default status health counts, explain their cleanup window during maintenance, and prune lost task records after 24 hours instead of the general 7-day terminal retention.
- Memory-core: keep REM dreaming focused on live light-staged memories and mark staged entries as considered so old recall history no longer dominates fresh candidates. (#86302) Thanks @SebTardif.
- Memory: abort sync instead of downgrading an existing semantic vector index to FTS-only when the configured embedding provider is temporarily unavailable. (#85704) Thanks @yaaboo-gif.
- Telegram: propagate forum topic names through the account-scoped topic cache for native command context and topic create/edit actions. (#86299) Thanks @SebTardif.
- Slack: keep downloaded read-only files out of reply media so Slack file reads do not echo files back to the conversation. (#86318) Thanks @neeravmakwana.
- Cron: accept leading-plus relative durations such as `+5m` for one-shot `--at` schedules. (#86341) Thanks @mushuiyu886.
- Agents/media: preserve async-started media tool metadata so background generation starts no longer surface generic incomplete-turn warnings while replay stays unsafe. (#85933) Thanks @fuller-stack-dev.
- Docker E2E: dedupe scheduler lane resources so npm/service package lanes are not over-counted and serialized unnecessarily.
- QA/diagnostics: add a collector-backed OpenTelemetry smoke lane, make the OTLP payload leak check scenario-aware, and keep source QA builds from failing on optional dependency imports resolved through pnpm's temp module path.
- Crabbox: bootstrap Git metadata for sparse remote changed gates so raw synced workspaces can run `pnpm check:changed` from the intended diff.
- xAI/LM Studio: avoid buffering ordinary bracketed or `final` prose until stream completion while watching for plain-text tool-call fallbacks.
- Doctor: warn and continue when the cron job store exists but cannot be read so later health checks still run. Fixes #86102. (#86384) Thanks @1052326311.
- Discord: suppress a bot's previous reply body and referenced media from prompt context when a user replies to that bot message, while keeping reply metadata for routing. (#86238) Thanks @fuller-stack-dev.
- Discord: restore bare numeric channel IDs for outbound message-tool sends while keeping explicit DM targets unambiguous. (#86571) Thanks @joshavant.
- Docker E2E: avoid rebuilding the Control UI twice while preparing the shared OpenClaw package tarball for package-backed scenario runs.
- Tests: avoid rebuilding the Control UI twice during the installer Docker smoke now that `pnpm build` includes `ui:build`.
- Tests: give QA config mutation RPCs enough native Windows budget to finish gateway config writes and restart settle after hot scenario runs.
- Tests: keep the gateway restart-inflight QA scenario focused on restart recovery on native Windows by allowing expected embedded prompt handoff errors and using the Windows-safe timeout budget.
- QA-Lab: make the synthetic OpenAI provider honor generic `reply exactly:` directives after required kickoff reads so restart-recovery scenarios do not fall through to generic repo-summary prose.
- Gateway: abort active `agent` RPC runs during forced restart shutdown so stale in-process turns cannot keep writing a session after the Gateway lifecycle restarts.
- Crabbox: sync clean sparse worktrees through a temporary full checkout even when reusing an existing lease so tracked build-time files are not omitted.
- Build: route `scripts/ui.js` through the shared pnpm runner and keep Control UI chunking helpers in sparse-included source so native Windows Corepack builds can produce `dist/control-ui`.
- Tests: give the memory fallback QA scenario enough turn budget to exercise native Windows gateway runs instead of failing on the client timeout while the mock agent is still dispatching.
- Tests: collect QA gateway CPU/RSS metrics on native Windows and give the channel baseline enough turn budget to report slow gateway runs instead of timing out before proof.
- Install/update: bypass npm `min-release-age` policies with `--min-release-age=0` instead of `--before` so hosted installers keep working on npm versions that reject the combined config. (#84749) Thanks @TeodoroRodrigo.
- Diagnostics: reclaim wedged session lanes when stale active-run bookkeeping blocks queued work despite no forward progress. Fixes #85639. Thanks @openperf.
- WebChat: keep message-tool replies visible in the chat while still summarizing internal tool results for the model. Fixes #86347. Thanks @shakkernerd.
- Gateway/perf: fail startup benchmark samples when the Gateway process exits before benchmark teardown, including signal deaths after readiness probes.
- Gateway/perf: fail restart benchmark samples when the Gateway exits before benchmark teardown, including clean exits and signal deaths after successful restart probes.
@@ -127,23 +47,18 @@ Docs: https://docs.openclaw.ai
- Checks: keep intentional Knip unused-file findings optional so full CI and sparse proof workspaces stay aligned.
- Docker: restore writable `~/.config` in runtime images. Fixes #85968. Thanks @hkoessler and @Bartok9.
- Plugin SDK: keep legacy root diagnostic subscriptions connected when built plugin SDK aliases resolve diagnostic helpers through a separate module graph.
- Diagnostics: export alertable OTel and Prometheus signals for blocked tools, model failover, stale sessions, liveness warnings, oversized payloads, and webhook ingress while fixing shared OTLP endpoints with query strings.
- Tests: normalize macOS canonical temp paths in exec allowlists, fs-safe trash assertions, installed plugin matching, Telegram topic-name stores, and built ACPX MCP server expectations so native macOS proof runners cover the intended behavior.
- Codex/app-server: preserve message-tool-only source reply delivery mode on active runs so sub-agent completion wakeups can steer the active Codex turn instead of being rejected. (#86287) Thanks @ferminquant.
- Tests: sample the Windows kitchen-sink RPC gateway directly and serialize RSS probes so native runs keep the memory guard active.
- Tests: normalize bundled plugin lifecycle probe paths and state-root lookup so native Windows release sweeps accept valid packaged plugin installs.
- Agents/Claude CLI: route live native Bash permission requests through OpenClaw exec policy so Claude turns no longer stall on `control_request`, and document that OpenClaw exec policy is authoritative. Fixes #80819. (#86330, from #81971) Thanks @guthirry and @sallyom.
- Security audit: warn when YOLO OpenClaw exec policy overrides a restrictive raw Claude `--permission-mode` for managed live sessions. (#86557) Thanks @sallyom.
- Config: keep benign legacy metadata write anomalies out of default doctor and config command output while preserving explicit anomaly logging for diagnostics.
- Codex: log when implicit app-server `never` approvals are promoted for OpenClaw tool policy, including whether the trigger was a `before_tool_call` hook or trusted tool policy.
- Codex harness: make subscription usage-limit errors without reset times explain that OpenClaw cannot determine the reset and point users to wait until Codex is available, use another Codex account, or switch to another configured model/provider. Thanks @amknight.
- Models/Codex: warn when stale OpenAI API-key auth remains beside Codex OAuth routes, and skip importing Codex `OPENAI_API_KEY` values when ChatGPT OAuth is present. Thanks @amknight.
- Google Vertex: support production ADC modes such as Workload Identity Federation, service-account credentials, and metadata-server ADC for the native Vertex transport. (#83971) Thanks @damianFelixPago.
- Telegram: route normal `[telegram][diag]` polling diagnostics through `runtime.log` while keeping non-diag warnings and persistence failures on `runtime.error`, so healthy polling startup no longer looks like an error. Fixes #82957. (#82958) Thanks @galiniliev.
- Providers/Ollama: strip inline Kimi cloud reasoning prefixes from streamed and final visible replies while keeping ordinary Kimi answers append-only. (#86286) Thanks @jason-allen-oneal.
- Gateway: require Talk secret authority before setup-code handoff can include Talk secrets. (#85690) Thanks @ngutman.
- Agents: keep fallback error reporting scoped to the active model candidate so stale prior-provider quota/auth text is not reported for later fallback attempts. (#86134) thanks @zhangguiping-xydt.
- iMessage: dedupe watcher startup when `channels.imessage.accounts` lists both `default` and a named account that point at the same local Messages source, so the gateway no longer spawns two `imsg rpc` processes or doubles inbound replies; the dedupe is scoped to watcher startup, leaving duplicate accounts addressable for outbound sends, status, and capability listings, and `openclaw doctor` flags the redundant account with a rebinding hint. Fixes #65141. (#86705) Thanks @swang430.
## 2026.5.25
@@ -192,7 +107,6 @@ Docs: https://docs.openclaw.ai
- Crabbox: install Corepack shims into the writable hydration `PNPM_HOME` so local AWS runner hydration no longer tries to overwrite `/usr/local/bin/pnpm`.
- Live tests: fail Gateway live model sweeps when selected coverage is lost to timeouts or stale high-signal filters instead of reporting false missing-profile coverage, and pin Docker OpenAI gateway coverage to the current `gpt-5.5` lane.
- Tests: fail Docker resource-ceiling checks when stats samples or configured limits are invalid instead of silently reporting zero peaks.
- Auth/Codex: emit a one-shot actionable `log.warn` from the embedded legacy Codex OAuth sidecar loader when the only available seed lives in the macOS Keychain, naming `openclaw doctor --fix` and macOS Keychain instead of letting the credential silently fall through to a downstream `No API key found for provider "openai-codex"`. Thanks @romneyda.
- Agents: fail closed when provider-less session models match multiple provider-prefixed runtime policies so CLI runtime routing no longer depends on config order. (#85970) Thanks @potterdigital.
- Control UI/agents: keep collapsed tool rows readable without early ellipses, preserve raw expanded tool details, and make post-compaction AGENTS.md reinjection opt-in to avoid duplicated project context. Fixes #45649 and #45488. Thanks @BunsDev.
@@ -241,7 +155,6 @@ Docs: https://docs.openclaw.ai
- Gateway/plugins: reuse a compatible Gateway startup plugin registry during dispatch so safe plugin dispatches avoid redundant registry loading. (#84324) Thanks @ai-hpc.
- Plugins/SDK: add a general `embeddingProviders` capability contract and registration API so embeddings can become a reusable provider surface outside memory-specific adapters.
- Dependencies: refresh provider, plugin, UI, and tooling packages, update `protobufjs` to 8.4.0 to clear the current npm advisory, and carry the Claude ACP completion patch forward to `@agentclientprotocol/claude-agent-acp` 0.36.1.
- ACPX: bump the bundled ACP backend to `acpx` 0.10.0 for session export/import support.
- Agents/tools: remove the old sender-owner tool gating path so configured tools stay visible for trusted sessions while command and channel-action auth still carry real sender identity.
- QA-Lab: add curated mock JSONL replay fixtures and first-drift reporting for runtime-parity audits. (#80323, refs #80176) Thanks @100yenadmin.
- QA-Lab: add a QA bus tool-trace visibility scenario for sanitized tool-call assertions.
@@ -261,7 +174,6 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI/update: allow package-manager-managed hardlinked package roots during global update swaps while keeping generic plugin, hook, and dependency-free install moves fail-closed. (#85569) Thanks @ai-hpc.
- Gateway/update: avoid fetching unrelated tags during dev-channel git updates so moved release tags do not block branch-based updates. (#84737) Thanks @rubencu.
- CLI/update: suppress the expected future-config warning while an old update parent hands off to the freshly installed post-core process.
- MiniMax: store OAuth token expiry as an absolute millisecond timestamp so OAuth profiles no longer appear expired on every request. (#83480) Thanks @NianJiuZst.
@@ -2173,7 +2085,6 @@ Docs: https://docs.openclaw.ai
- Telegram/groups: include the recent local chat window and nearby reply-target window as generic inbound context so stale reply ancestry does not overshadow the live group conversation.
- Plugins/Nix: allow externally configured plugin roots under `/nix/store` to load in `OPENCLAW_NIX_MODE=1` while keeping normal external plugin hardlink rejection unchanged. Thanks @joshp123.
- Nextcloud Talk: include the required bot `response` feature in setup, explain missing `--feature response` on rejected sends, and surface missing response capability in doctor/status checks. Fixes #78935. (#79657) Thanks @joshavant.
- Cron/diagnostics: emit the existing `message.queued`, `session.state` (processing/idle), and `message.processed` lifecycle events for isolated-cron agent turns in `runCronIsolatedAgentTurn`, matching the dispatch and embedded-runner paths so subscribers (diagnostics OTLP, OTel exporters, custom observability plugins) get per-run session attribution instead of bucketing isolated cron LLM calls under static fallback ids. Events are gated on `isDiagnosticsEnabled(cfg)` so the documented `diagnostics.enabled: false` master toggle continues to silence the recorder. (#79214) Thanks @arniesaha.
- fix(discord): gate user allowlist name resolution [AI]. (#79002) Thanks @pgondhi987.
- fix(msteams): gate startup user allowlist resolution [AI]. (#79003) Thanks @pgondhi987.
- Infra/fetch-timeout: pass `operation` and `url` context to `buildTimeoutAbortSignal` from the music-generate reference fetch and the Matrix guarded redirect transport, so the `fetch timeout reached; aborting operation` warning carries actionable structured fields instead of a bare line. Fixes #79195. Thanks @pandadev66.

View File

@@ -107,7 +107,6 @@ For coordinated change sets that genuinely need more than 20 PRs, join the **#cl
- Test locally with your OpenClaw instance
- External PRs must include a filled **Real behavior proof** section in the PR body. Show the real setup you tested, the exact command or steps you ran after the patch, after-fix evidence, the observed result, and anything you did not test. Screenshots, recordings, terminal screenshots, console output, copied live output, linked artifacts, and redacted runtime logs all count. Unit tests, mocks, snapshots, lint, typechecks, and CI are useful but do not satisfy this requirement by themselves. Maintainers may apply `proof: override` only when the proof gate should not apply.
- Keep PRs takeover-ready: open them from a branch maintainers can push to. For fork PRs, leave GitHub's **Allow edits by maintainers** option enabled so maintainers can finish urgent fixes, changelog entries, or merge prep when needed. If GitHub shows **Allow edits and access to secrets by maintainers**, enable it only when that workflow/secrets access is acceptable and say so in the PR.
- Do not edit `CHANGELOG.md` in contributor PRs. Maintainers or ClawSweeper add the changelog entry when landing user-facing changes.
- Run tests: `pnpm build && pnpm check && pnpm test`
- For iterative local commits, `scripts/committer --fast "message" <files...>` passes `FAST_COMMIT=1` through to the pre-commit hook so it skips the repo-wide `pnpm check`. Only use it when you've already run equivalent targeted validation for the touched surface.

View File

@@ -98,7 +98,7 @@ These are frequently reported but are typically closed with no code change:
- Reports that treat `POST /tools/invoke` under shared-secret bearer auth (`gateway.auth.mode="token"` or `"password"`) as a narrower per-request/per-scope authorization surface. That endpoint is designed as the same trusted-operator HTTP boundary: shared-secret bearer auth is full operator access there, narrower `x-openclaw-scopes` values do not reduce that path, and owner-only tool policy follows the shared-secret operator contract.
- Reports that only show differences in heuristic detection/parity (for example obfuscation-pattern detection on one exec path but not another, such as `node.invoke -> system.run` parity gaps) without demonstrating bypass of auth, approvals, allowlist enforcement, sandboxing, or other documented trust boundaries.
- Reports that only show an ACP tool can indirectly execute, mutate, orchestrate sessions, or reach another tool/runtime without demonstrating bypass of ACP prompt/approval, allowlist enforcement, sandboxing, or another documented trust boundary. ACP silent approval is intentionally limited to narrow readonly classes; parity-only indirect-command findings are hardening, not vulnerabilities.
- Reports that only show untrusted media bytes reaching a maintained native decoder dependency (for example image codec libraries such as libheif) without proving the shipped dependency version is vulnerable and demonstrating crash, memory corruption, data exposure, or a boundary bypass through OpenClaw. JavaScript header sniffing and image dimension fast-paths are preflight/UX checks, not the security boundary for native decoder correctness.
- Reports that only show untrusted media bytes reaching a maintained native decoder dependency (for example Sharp/libvips/libheif) without proving the shipped dependency version is vulnerable and demonstrating crash, memory corruption, data exposure, or a boundary bypass through OpenClaw. JavaScript header sniffing and image dimension fast-paths are preflight/UX checks, not the security boundary for native decoder correctness.
- Reports whose only impact is transient extra memory, CPU, or allocation work from decoding, base64 expansion, media transcoding, serialization, or other format conversion after the input was already accepted under OpenClaw's configured size/trust limits, including base64 decode-before-size-estimate findings. These are performance issues, not vulnerabilities, unless the report demonstrates unauthenticated amplification, bypass of configured limits, crash/process termination, persistent resource exhaustion, data exposure, or another documented boundary bypass.
- ReDoS/DoS claims that require trusted operator configuration input (for example catastrophic regex in `sessionFilter` or `logging.redactPatterns`) without a trust-boundary bypass.
- Archive/install extraction claims that require pre-existing local filesystem priming in trusted state (for example planting symlink/hardlink aliases under destination directories such as skills/tools paths) without showing an untrusted path that can create/control that primitive.

View File

@@ -65,8 +65,8 @@ android {
applicationId = "ai.openclaw.app"
minSdk = 31
targetSdk = 36
versionCode = 2026052601
versionName = "2026.5.26"
versionCode = 2026052501
versionName = "2026.5.25"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")

View File

@@ -1,9 +1,5 @@
# OpenClaw iOS Changelog
## 2026.5.26 - 2026-05-26
Maintenance update for the current OpenClaw release.
## 2026.5.25 - 2026-05-25
Maintenance update for the current OpenClaw release.

View File

@@ -2,8 +2,8 @@
// Source of truth: apps/ios/version.json
// Generated by scripts/ios-sync-versioning.ts.
OPENCLAW_IOS_VERSION = 2026.5.26
OPENCLAW_MARKETING_VERSION = 2026.5.26
OPENCLAW_IOS_VERSION = 2026.5.25
OPENCLAW_MARKETING_VERSION = 2026.5.25
OPENCLAW_BUILD_VERSION = 1
#include? "../build/Version.xcconfig"

View File

@@ -85,8 +85,6 @@ struct TalkToolbarTray: View {
var isSpeaking: Bool
var isUserSpeechDetected: Bool
var permissionState: TalkGatewayPermissionState
var voiceModeTitle: String
var voiceModeSubtitle: String?
var onEnableTalk: () -> Void
var onStopTalk: () -> Void
@@ -137,13 +135,6 @@ struct TalkToolbarTray: View {
.foregroundStyle(.secondary)
.lineLimit(1)
}
if let voiceModeText = self.voiceModeText {
Text(voiceModeText)
.font(.caption2.weight(.semibold))
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
Spacer(minLength: 0)
@@ -202,22 +193,7 @@ struct TalkToolbarTray: View {
}
.accessibilityElement(children: .combine)
.accessibilityLabel("Talk Mode")
.accessibilityValue(self.accessibilityValue)
}
private var accessibilityValue: String {
if let voiceModeText {
return "\(self.state.title), \(self.subtitle), \(voiceModeText)"
}
return "\(self.state.title), \(self.subtitle)"
}
private var voiceModeText: String? {
guard !self.state.prefersPermissionCopy else { return nil }
let title = self.voiceModeTitle.trimmingCharacters(in: .whitespacesAndNewlines)
guard !title.isEmpty, title != "Not loaded" else { return nil }
let subtitle = (self.voiceModeSubtitle ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
return subtitle.isEmpty ? title : "\(title)\(subtitle)"
.accessibilityValue("\(self.state.title), \(self.subtitle)")
}
private var subtitle: String {

View File

@@ -519,8 +519,6 @@ private struct CanvasContent: View {
isSpeaking: self.appModel.talkMode.isSpeaking,
isUserSpeechDetected: self.appModel.talkMode.isUserSpeechDetected,
permissionState: self.appModel.talkMode.gatewayTalkPermissionState,
voiceModeTitle: self.appModel.talkMode.gatewayTalkVoiceModeTitle,
voiceModeSubtitle: self.appModel.talkMode.gatewayTalkVoiceModeSubtitle,
onEnableTalk: {
self.showTalkPermissionPrompt = true
},

View File

@@ -612,7 +612,7 @@ struct SettingsTab: View {
private var shouldShowRealtimeVoicePicker: Bool {
let providerSelection = TalkModeProviderSelection.resolved(self.talkProviderSelectionRaw)
return providerSelection == .openAIRealtime
|| self.appModel.talkMode.gatewayTalkUsesRealtime
|| self.appModel.talkMode.gatewayTalkUsesRealtimeRelay
}
private func talkVoiceSettingsView() -> AnyView {
@@ -633,16 +633,6 @@ struct SettingsTab: View {
}
}
}
LabeledContent("Voice Mode") {
VStack(alignment: .trailing, spacing: 2) {
Text(self.appModel.talkMode.gatewayTalkVoiceModeTitle)
if let subtitle = self.appModel.talkMode.gatewayTalkVoiceModeSubtitle {
Text(subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
LabeledContent(
"Active Provider",
value: self.appModel.talkMode.gatewayTalkProviderLabel)

View File

@@ -3,107 +3,9 @@ import OpenClawKit
enum TalkModeExecutionMode {
case native
case realtimeClient
case realtimeRelay
}
struct TalkVoiceModeDescriptor: Equatable {
let title: String
let subtitle: String?
let providerId: String?
let modelId: String?
let voiceId: String?
let transport: String?
let isRealtime: Bool
var accessibilityValue: String {
if let subtitle, !subtitle.isEmpty {
return "\(self.title), \(subtitle)"
}
return self.title
}
}
enum TalkVoiceModeDescriptorBuilder {
static func build(
providerId: String,
providerLabel: String,
modelId: String?,
voiceId: String?,
transport: String?,
isRealtime: Bool) -> TalkVoiceModeDescriptor
{
let normalizedProvider = providerId.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let trimmedModel = Self.trimmed(modelId)
let trimmedVoice = Self.trimmed(voiceId)
let trimmedTransport = Self.trimmed(transport)
let title = if isRealtime, normalizedProvider == "openai", trimmedModel == "gpt-realtime-2" {
"GPT Realtime 2.0"
} else if isRealtime, normalizedProvider == "openai" {
"OpenAI Realtime"
} else if isRealtime {
providerLabel.isEmpty ? "Realtime Voice" : providerLabel
} else if normalizedProvider == "system" {
"iOS System Voice"
} else {
providerLabel.isEmpty ? "Talk Voice" : providerLabel
}
var details: [String] = []
if isRealtime, normalizedProvider != "openai", !providerLabel.isEmpty, providerLabel != title {
details.append(providerLabel)
}
if let trimmedTransport {
details.append(Self.transportLabel(trimmedTransport))
}
if let trimmedModel, title != "GPT Realtime 2.0" || trimmedModel != "gpt-realtime-2" {
details.append(trimmedModel)
}
if let trimmedVoice {
details.append(Self.voiceLabel(trimmedVoice))
}
return TalkVoiceModeDescriptor(
title: title,
subtitle: details.isEmpty ? nil : details.joined(separator: ""),
providerId: normalizedProvider.isEmpty ? nil : normalizedProvider,
modelId: trimmedModel,
voiceId: trimmedVoice,
transport: trimmedTransport,
isRealtime: isRealtime)
}
private static func trimmed(_ value: String?) -> String? {
let trimmed = (value ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
private static func voiceLabel(_ voice: String) -> String {
TalkModeRealtimeVoiceSelection.voices.contains(voice)
? TalkModeRealtimeVoiceSelection.label(for: voice)
: voice
}
private static func transportLabel(_ transport: String) -> String {
switch transport.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
case "webrtc":
"Native WebRTC"
case "gateway-relay":
"Gateway Relay"
case "provider-websocket":
"Provider WebSocket"
case "managed-room":
"Managed Room"
case "native":
"Native"
case let value where !value.isEmpty:
value
default:
"Native"
}
}
}
enum TalkModeProviderSelection: String, CaseIterable, Identifiable {
case gatewayDefault = "gateway"
case nativeElevenLabs = "elevenlabs"
@@ -210,9 +112,8 @@ enum TalkModeGatewayConfigParser {
let defaultVoiceId = Self.firstString(activeConfig, keys: ["voiceId", "voice"])
let defaultOutputFormat = Self.firstString(activeConfig, keys: ["outputFormat"])
let realtime = talk?["realtime"]?.dictionaryValue
let realtimeProviders = realtime?["providers"]?.dictionaryValue
let realtimeProvider = Self.firstString(realtime, keys: ["provider"])
?? Self.singleRealtimeProviderId(realtimeProviders)
let realtimeProviders = realtime?["providers"]?.dictionaryValue
let realtimeProviderConfig = Self.realtimeProviderConfig(
providers: realtimeProviders,
provider: realtimeProvider)
@@ -263,24 +164,12 @@ enum TalkModeGatewayConfigParser {
let mode = Self.firstString(realtime, keys: ["mode"])?.lowercased()
let transport = Self.firstString(realtime, keys: ["transport"])?.lowercased()
let brain = Self.firstString(realtime, keys: ["brain"])?.lowercased()
guard mode == "realtime", brain == nil || brain == "agent-consult" else {
return .native
}
if transport == "gateway-relay" {
if mode == "realtime", transport == "gateway-relay", brain == nil || brain == "agent-consult" {
return .realtimeRelay
}
if transport == nil || transport == "webrtc" {
return .realtimeClient
}
return .native
}
private static func singleRealtimeProviderId(_ providers: [String: AnyCodable]?) -> String? {
guard let providers, providers.count == 1 else { return nil }
let provider = providers.keys.first?.trimmingCharacters(in: .whitespacesAndNewlines)
return provider?.isEmpty == false ? provider : nil
}
private static func realtimeProviderConfig(
providers: [String: AnyCodable]?,
provider: String?) -> [String: AnyCodable]?

View File

@@ -116,14 +116,10 @@ final class TalkModeManager: NSObject {
var gatewayTalkDefaultVoiceId: String?
var gatewayTalkProviderLabel: String = "Not loaded"
var gatewayTalkTransportLabel: String = "Not loaded"
var gatewayTalkUsesRealtime: Bool = false
var gatewayTalkUsesRealtimeRelay: Bool = false
var gatewayTalkRealtimeProviderLabel: String?
var gatewayTalkRealtimeModelId: String?
var gatewayTalkRealtimeVoiceId: String?
var gatewayTalkVoiceModeTitle: String = "Not loaded"
var gatewayTalkVoiceModeSubtitle: String?
var gatewayTalkVoiceModeAccessibilityValue: String = "Not loaded"
var gatewayTalkPermissionState: TalkGatewayPermissionState = .unknown
private enum CaptureMode {
@@ -149,7 +145,6 @@ final class TalkModeManager: NSObject {
private var recognitionTask: SFSpeechRecognitionTask?
private var silenceTask: Task<Void, Never>?
private var realtimeSession: TalkRealtimeWebRTCSession?
private var realtimeRelaySession: RealtimeTalkRelaySession?
private var prefetchedRealtimeSession: TalkRealtimeClientSession?
private var realtimePrefetchTask: Task<Void, Never>?
@@ -172,14 +167,6 @@ final class TalkModeManager: NSObject {
private var realtimeProvider: String?
private var realtimeModelId: String?
private var realtimeVoiceId: String?
private var configuredVoiceModeDescriptor = TalkVoiceModeDescriptor(
title: "Not loaded",
subtitle: nil,
providerId: nil,
modelId: nil,
voiceId: nil,
transport: nil,
isRealtime: false)
private var apiKey: String?
private var voiceAliases: [String: String] = [:]
private var interruptOnSpeech: Bool = true
@@ -317,13 +304,8 @@ final class TalkModeManager: NSObject {
GatewayDiagnostics.log("talk.timeline manager start blocked gateway permission")
return
}
if self.realtimeWebRTCEnabled {
let started = self.executionMode == .realtimeRelay
? await self.startRealtimeRelayIfAvailable()
: await self.startRealtimeIfAvailable()
if started {
return
}
if self.realtimeWebRTCEnabled, await self.startRealtimeIfAvailable() {
return
}
let speechOk = await Self.requestSpeechPermission()
@@ -437,7 +419,6 @@ final class TalkModeManager: NSObject {
if let realtimeSession {
realtimeSession.cancelResponse()
}
self.realtimeRelaySession?.cancelOutput()
self.stopSpeaking()
}
@@ -1065,68 +1046,9 @@ final class TalkModeManager: NSObject {
}
}
private func startRealtimeRelayIfAvailable() async -> Bool {
guard let gateway else { return false }
GatewayDiagnostics.log("talk.timeline realtime relay start attempt sessionKey=\(self.mainSessionKey)")
let startedAt = Self.nowSeconds()
let relaySession = RealtimeTalkRelaySession(
gateway: gateway,
options: RealtimeTalkRelaySession.Options(
sessionKey: self.mainSessionKey,
provider: self.realtimeProvider,
model: self.realtimeModelId,
voice: self.realtimeVoiceId),
pcmPlayer: self.pcmPlayer,
onStatus: { [weak self] status in
guard let self else { return }
self.statusText = status
self.isListening = status.localizedCaseInsensitiveContains("listening")
if status.localizedCaseInsensitiveContains("thinking") {
self.isListening = false
self.isSpeaking = false
self.isUserSpeechDetected = false
}
},
onSpeakingChanged: { [weak self] speaking in
guard let self else { return }
self.isSpeaking = speaking
if speaking {
self.isListening = false
}
})
self.realtimeRelaySession = relaySession
do {
try Self.configureAudioSession()
try await relaySession.start()
guard self.realtimeRelaySession === relaySession, self.isEnabled else {
relaySession.stop()
return true
}
self.isListening = true
self.captureMode = .continuous
GatewayDiagnostics.log(
"talk.timeline realtime relay start ready elapsedMs=\(Self.elapsedMs(since: startedAt))")
return true
} catch {
guard self.realtimeRelaySession === relaySession, self.isEnabled else {
relaySession.stop()
return true
}
self.realtimeRelaySession = nil
GatewayDiagnostics.log(
"talk.timeline realtime relay start failed elapsedMs=\(Self.elapsedMs(since: startedAt)) "
+ "error=\(error.localizedDescription)")
return false
}
}
func prefetchRealtimeSessionIfReady(reason: String) async {
guard self.gatewayConnected,
self.realtimeSession == nil,
self.realtimeRelaySession == nil,
!self.isEnabled
else { return }
guard self.realtimeWebRTCEnabled, self.executionMode != .realtimeRelay else { return }
guard self.gatewayConnected, self.realtimeSession == nil, !self.isEnabled else { return }
guard self.realtimeWebRTCEnabled else { return }
guard self.gatewayTalkPermissionState == .ready else { return }
guard self.consumePrefetchedRealtimeSession(peekOnly: true) == nil else { return }
guard self.realtimePrefetchTask == nil else { return }
@@ -1194,8 +1116,6 @@ final class TalkModeManager: NSObject {
private func stopRealtimeSession() {
self.realtimeSession?.stop()
self.realtimeSession = nil
self.realtimeRelaySession?.stop()
self.realtimeRelaySession = nil
}
private func subscribeChatIfNeeded(sessionKey: String) async {
@@ -1385,14 +1305,6 @@ final class TalkModeManager: NSObject {
if canUseElevenLabs, let voiceId, let apiKey {
GatewayDiagnostics.log("talk tts: provider=elevenlabs voiceId=\(voiceId)")
let modelId = directive?.modelId ?? self.currentModelId ?? self.defaultModelId
self.applyVoiceModeDescriptor(TalkVoiceModeDescriptorBuilder.build(
providerId: "elevenlabs",
providerLabel: Self.displayName(forProvider: "elevenlabs"),
modelId: modelId,
voiceId: voiceId,
transport: "native",
isRealtime: false))
let desiredOutputFormat = (directive?.outputFormat ?? self.defaultOutputFormat)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let requestedOutputFormat = (desiredOutputFormat?.isEmpty == false) ? desiredOutputFormat : nil
@@ -1403,6 +1315,7 @@ final class TalkModeManager: NSObject {
"talk output_format unsupported for local playback: \(requestedOutputFormat, privacy: .public)")
}
let modelId = directive?.modelId ?? self.currentModelId ?? self.defaultModelId
if let modelId {
GatewayDiagnostics.log("talk tts: modelId=\(modelId)")
}
@@ -1471,13 +1384,6 @@ final class TalkModeManager: NSObject {
} else {
self.logger.warning("tts unavailable; falling back to system voice (missing key or voiceId)")
GatewayDiagnostics.log("talk tts: provider=system (missing key or voiceId)")
self.applyVoiceModeDescriptor(TalkVoiceModeDescriptorBuilder.build(
providerId: "system",
providerLabel: Self.displayName(forProvider: "system"),
modelId: nil,
voiceId: language,
transport: "native",
isRealtime: false))
if self.interruptOnSpeech {
do {
try self.startRecognition()
@@ -1494,14 +1400,6 @@ final class TalkModeManager: NSObject {
"tts failed: \(error.localizedDescription, privacy: .public); falling back to system voice")
GatewayDiagnostics.log("talk tts: provider=system (error) msg=\(error.localizedDescription)")
do {
let language = ElevenLabsTTSClient.validatedLanguage(directive?.language)
self.applyVoiceModeDescriptor(TalkVoiceModeDescriptorBuilder.build(
providerId: "system",
providerLabel: Self.displayName(forProvider: "system"),
modelId: nil,
voiceId: language,
transport: "native",
isRealtime: false))
if self.interruptOnSpeech {
do {
try self.startRecognition()
@@ -1511,6 +1409,7 @@ final class TalkModeManager: NSObject {
}
}
self.statusText = "Speaking (System)…"
let language = ElevenLabsTTSClient.validatedLanguage(directive?.language)
try await TalkSystemSpeechSynthesizer.shared.speak(text: cleaned, language: language)
} catch {
self.statusText = "Speak failed: \(error.localizedDescription)"
@@ -1520,7 +1419,6 @@ final class TalkModeManager: NSObject {
self.stopRecognition()
self.isSpeaking = false
self.restoreConfiguredVoiceModeDescriptor()
}
private func stopSpeaking(storeInterruption: Bool = true) {
@@ -1543,7 +1441,6 @@ final class TalkModeManager: NSObject {
TalkSystemSpeechSynthesizer.shared.stop()
self.cancelIncrementalSpeech()
self.isSpeaking = false
self.restoreConfiguredVoiceModeDescriptor()
}
private func shouldInterrupt(with transcript: String) -> Bool {
@@ -2359,10 +2256,6 @@ extension TalkModeManager {
"OpenAI"
case "google":
"Google"
case "system":
"iOS System Voice"
case "realtime":
"Realtime Voice"
case let provider where !provider.isEmpty:
provider
default:
@@ -2370,36 +2263,6 @@ extension TalkModeManager {
}
}
private func applyVoiceModeDescriptor(_ descriptor: TalkVoiceModeDescriptor, persistAsConfigured: Bool = false) {
if persistAsConfigured {
self.configuredVoiceModeDescriptor = descriptor
}
self.gatewayTalkVoiceModeTitle = descriptor.title
self.gatewayTalkVoiceModeSubtitle = descriptor.subtitle
self.gatewayTalkVoiceModeAccessibilityValue = descriptor.accessibilityValue
}
private func restoreConfiguredVoiceModeDescriptor() {
self.applyVoiceModeDescriptor(self.configuredVoiceModeDescriptor)
}
private func buildConfiguredVoiceModeDescriptor(
provider: String,
providerLabel: String,
modelId: String?,
voiceId: String?,
transport: String,
isRealtime: Bool) -> TalkVoiceModeDescriptor
{
TalkVoiceModeDescriptorBuilder.build(
providerId: provider,
providerLabel: providerLabel,
modelId: modelId,
voiceId: voiceId,
transport: transport,
isRealtime: isRealtime)
}
private func ensureTalkConfigLoadedForStart() async {
if self.gatewayTalkConfigLoaded || self.gatewayTalkPermissionState.isApprovalRequestInProgress {
GatewayDiagnostics.log(
@@ -2478,7 +2341,7 @@ extension TalkModeManager {
realtimeProvider = realtimeProvider ?? "openai"
realtimeModelId = realtimeModelId ?? Self.defaultRealtimeModelIdFallback
}
let usesRealtimeConfig = activeProvider != Self.defaultTalkProvider || executionMode != .native
let usesRealtimeConfig = activeProvider != Self.defaultTalkProvider || executionMode == .realtimeRelay
self.activeTalkProvider = activeProvider
self.executionMode = executionMode
self.realtimeWebRTCEnabled = usesRealtimeConfig
@@ -2514,31 +2377,14 @@ extension TalkModeManager {
}
self.gatewayTalkDefaultVoiceId = usesRealtimeConfig ? realtimeVoiceId : self.defaultVoiceId
self.gatewayTalkDefaultModelId = usesRealtimeConfig ? realtimeModelId : self.defaultModelId
let providerLabel = providerSelection == .gatewayDefault
self.gatewayTalkProviderLabel = providerSelection == .gatewayDefault
? Self.displayName(forProvider: activeProvider)
: providerSelection.label
let usesRealtimeRelay = executionMode == .realtimeRelay
let transport = usesRealtimeConfig ? (usesRealtimeRelay ? "gateway-relay" : "webrtc") : "native"
let transportLabel = usesRealtimeRelay ? "Gateway Relay" : (usesRealtimeConfig ? "Native WebRTC" : "Native")
self.gatewayTalkProviderLabel = providerLabel
self.gatewayTalkUsesRealtime = usesRealtimeConfig
self.gatewayTalkUsesRealtimeRelay = usesRealtimeRelay
self.gatewayTalkTransportLabel = transportLabel
self.gatewayTalkUsesRealtimeRelay = false
self.gatewayTalkTransportLabel = usesRealtimeConfig ? "Native WebRTC" : "Native"
self.gatewayTalkRealtimeProviderLabel = realtimeProvider.map { Self.displayName(forProvider: $0) }
self.gatewayTalkRealtimeModelId = realtimeModelId
self.gatewayTalkRealtimeVoiceId = realtimeVoiceId
let voiceModeProvider = usesRealtimeConfig ? (realtimeProvider ?? "realtime") : activeProvider
let voiceModeLabel = usesRealtimeConfig
? Self.displayName(forProvider: voiceModeProvider)
: Self.displayName(forProvider: activeProvider)
let voiceModeDescriptor = self.buildConfiguredVoiceModeDescriptor(
provider: voiceModeProvider,
providerLabel: voiceModeLabel,
modelId: usesRealtimeConfig ? realtimeModelId : self.defaultModelId,
voiceId: usesRealtimeConfig ? realtimeVoiceId : self.defaultVoiceId,
transport: transport,
isRealtime: usesRealtimeConfig)
self.applyVoiceModeDescriptor(voiceModeDescriptor, persistAsConfigured: true)
self.gatewayTalkApiKeyConfigured = gatewayOwnedVoiceProvider || (self.apiKey?.isEmpty == false)
self.gatewayTalkConfigLoaded = true
self.talkConfigLoadedAt = Date()
@@ -2570,19 +2416,10 @@ extension TalkModeManager {
self.realtimeVoiceId = nil
self.gatewayTalkProviderLabel = "Not loaded"
self.gatewayTalkTransportLabel = "Not loaded"
self.gatewayTalkUsesRealtime = false
self.gatewayTalkUsesRealtimeRelay = false
self.gatewayTalkRealtimeProviderLabel = nil
self.gatewayTalkRealtimeModelId = nil
self.gatewayTalkRealtimeVoiceId = nil
self.applyVoiceModeDescriptor(TalkVoiceModeDescriptor(
title: "Not loaded",
subtitle: nil,
providerId: nil,
modelId: nil,
voiceId: nil,
transport: nil,
isRealtime: false), persistAsConfigured: true)
self.defaultModelId = Self.defaultModelIdFallback
if !self.modelOverrideActive {
self.currentModelId = self.defaultModelId

View File

@@ -48,46 +48,6 @@ import Testing
#expect(parsed.realtimeVoiceId == "marin")
}
@Test func infersRealtimeProviderWhenProviderMapHasSingleEntry() {
let config: [String: Any] = [
"talk": [
"realtime": [
"mode": "realtime",
"transport": "webrtc",
"providers": [
"openai": [
"model": "gpt-realtime-2",
],
],
],
],
]
let parsed = TalkModeGatewayConfigParser.parse(
config: config,
defaultProvider: "elevenlabs",
defaultModelIdFallback: "eleven_v3",
defaultRealtimeModelIdFallback: "gpt-realtime-2",
defaultSilenceTimeoutMs: 900)
#expect(parsed.executionMode == .realtimeClient)
#expect(parsed.realtimeProvider == "openai")
#expect(parsed.realtimeModelId == "gpt-realtime-2")
}
@Test func formatsGenericRealtimeVoiceModeWithoutNativeProviderFallback() {
let descriptor = TalkVoiceModeDescriptorBuilder.build(
providerId: "realtime",
providerLabel: "Realtime Voice",
modelId: "gpt-realtime-2",
voiceId: nil,
transport: "webrtc",
isRealtime: true)
#expect(descriptor.title == "Realtime Voice")
#expect(descriptor.subtitle == "Native WebRTC • gpt-realtime-2")
}
@Test func defaultsOpenAIRealtimeModelWhenProviderOmitsModel() {
let config: [String: Any] = [
"talk": [
@@ -119,60 +79,7 @@ import Testing
#expect(TalkModeRealtimeVoiceSelection.resolvedOverride("unknown") == nil)
}
@Test func formatsOpenAIRealtimeVoiceMode() {
let descriptor = TalkVoiceModeDescriptorBuilder.build(
providerId: "openai",
providerLabel: "OpenAI",
modelId: "gpt-realtime-2",
voiceId: "marin",
transport: "webrtc",
isRealtime: true)
#expect(descriptor.title == "GPT Realtime 2.0")
#expect(descriptor.subtitle == "Native WebRTC • Marin")
#expect(descriptor.accessibilityValue == "GPT Realtime 2.0, Native WebRTC • Marin")
}
@Test func formatsGatewayRelayRealtimeVoiceMode() {
let descriptor = TalkVoiceModeDescriptorBuilder.build(
providerId: "google",
providerLabel: "Google Live Voice",
modelId: "gemini-live-2.5-flash-preview",
voiceId: nil,
transport: "gateway-relay",
isRealtime: true)
#expect(descriptor.title == "Google Live Voice")
#expect(descriptor.subtitle == "Gateway Relay • gemini-live-2.5-flash-preview")
}
@Test func formatsElevenLabsVoiceMode() {
let descriptor = TalkVoiceModeDescriptorBuilder.build(
providerId: "elevenlabs",
providerLabel: "ElevenLabs",
modelId: "eleven_v3",
voiceId: "voice-id",
transport: "native",
isRealtime: false)
#expect(descriptor.title == "ElevenLabs")
#expect(descriptor.subtitle == "Native • eleven_v3 • voice-id")
}
@Test func formatsSystemVoiceFallbackMode() {
let descriptor = TalkVoiceModeDescriptorBuilder.build(
providerId: "system",
providerLabel: "iOS System Voice",
modelId: nil,
voiceId: "en-US",
transport: "native",
isRealtime: false)
#expect(descriptor.title == "iOS System Voice")
#expect(descriptor.subtitle == "Native • en-US")
}
@Test func usesRealtimeClientModeForWebRTCTransport() {
@Test func leavesNativeModeWhenRealtimeTransportIsNotGatewayRelay() {
let config: [String: Any] = [
"talk": [
"realtime": [
@@ -190,7 +97,7 @@ import Testing
defaultRealtimeModelIdFallback: "gpt-realtime-2",
defaultSilenceTimeoutMs: 900)
#expect(parsed.executionMode == .realtimeClient)
#expect(parsed.executionMode == .native)
}
@Test func detectsPCMFormatRejectionFromElevenLabsError() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1,3 +1,3 @@
{
"version": "2026.5.26"
"version": "2026.5.25"
}

View File

@@ -6,6 +6,7 @@ import Foundation
enum HostEnvSecurityPolicy {
static let blockedInheritedKeys: Set<String> = [
"_JAVA_OPTIONS",
"AMQP_URL",
"ANSIBLE_CALLBACK_PLUGINS",
"ANSIBLE_COLLECTIONS_PATH",
@@ -30,11 +31,12 @@ enum HostEnvSecurityPolicy {
"AZURE_CLIENT_SECRET",
"BASH_ENV",
"BROWSER",
"BUNDLE_GEMFILE",
"BUN_CONFIG_REGISTRY",
"BUNDLE_GEMFILE",
"BZR_EDITOR",
"BZR_PLUGIN_PATH",
"BZR_SSH",
"C_INCLUDE_PATH",
"CARGO_BUILD_RUSTC",
"CARGO_BUILD_RUSTC_WRAPPER",
"CARGO_HOME",
@@ -44,8 +46,8 @@ enum HostEnvSecurityPolicy {
"CGO_CFLAGS",
"CGO_LDFLAGS",
"CLASSPATH",
"CMAKE_CXX_COMPILER",
"CMAKE_C_COMPILER",
"CMAKE_CXX_COMPILER",
"CMAKE_TOOLCHAIN_FILE",
"COMPOSER_HOME",
"CONFIG_SHELL",
@@ -56,7 +58,6 @@ enum HostEnvSecurityPolicy {
"CPLUS_INCLUDE_PATH",
"CURL_HOME",
"CXX",
"C_INCLUDE_PATH",
"DATABASE_URL",
"DENO_DIR",
"DOTNET_ADDITIONAL_DEPS",
@@ -74,8 +75,6 @@ enum HostEnvSecurityPolicy {
"GEM_HOME",
"GEM_PATH",
"GH_TOKEN",
"GITHUB_TOKEN",
"GITLAB_TOKEN",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_ASKPASS",
"GIT_COMMON_DIR",
@@ -96,6 +95,8 @@ enum HostEnvSecurityPolicy {
"GIT_SSL_NO_VERIFY",
"GIT_TEMPLATE_DIR",
"GIT_WORK_TREE",
"GITHUB_TOKEN",
"GITLAB_TOKEN",
"GLIBC_TUNABLES",
"GOENV",
"GOFLAGS",
@@ -144,8 +145,8 @@ enum HostEnvSecurityPolicy {
"PERL5DBCMD",
"PERL5LIB",
"PERL5OPT",
"PHPRC",
"PHP_INI_SCAN_DIR",
"PHPRC",
"PIP_CONFIG_FILE",
"PIP_EXTRA_INDEX_URL",
"PIP_FIND_LINKS",
@@ -159,17 +160,17 @@ enum HostEnvSecurityPolicy {
"PYTHONPATH",
"PYTHONSTARTUP",
"PYTHONUSERBASE",
"R_ENVIRON",
"R_ENVIRON_USER",
"R_LIBS_USER",
"R_PROFILE",
"R_PROFILE_USER",
"REDIS_URL",
"RUBYLIB",
"RUBYOPT",
"RUBYSHELL",
"RUSTC_WRAPPER",
"RUSTFLAGS",
"R_ENVIRON",
"R_ENVIRON_USER",
"R_LIBS_USER",
"R_PROFILE",
"R_PROFILE_USER",
"SBT_OPTS",
"SHELL",
"SHELLOPTS",
@@ -191,8 +192,7 @@ enum HostEnvSecurityPolicy {
"VIRTUAL_ENV",
"VISUAL",
"WGETRC",
"YARN_RC_FILENAME",
"_JAVA_OPTIONS"
"YARN_RC_FILENAME"
]
static let blockedInheritedPrefixes: [String] = [
@@ -202,6 +202,7 @@ enum HostEnvSecurityPolicy {
]
static let blockedKeys: Set<String> = [
"_JAVA_OPTIONS",
"ANT_OPTS",
"BASH_ENV",
"BROWSER",
@@ -212,8 +213,8 @@ enum HostEnvSecurityPolicy {
"CARGO_BUILD_RUSTC_WRAPPER",
"CATALINA_OPTS",
"CC",
"CMAKE_CXX_COMPILER",
"CMAKE_C_COMPILER",
"CMAKE_CXX_COMPILER",
"CMAKE_TOOLCHAIN_FILE",
"CONFIG_SHELL",
"CONFIG_SITE",
@@ -274,14 +275,14 @@ enum HostEnvSecurityPolicy {
"PYTHONBREAKPOINT",
"PYTHONHOME",
"PYTHONPATH",
"RUBYLIB",
"RUBYOPT",
"RUBYSHELL",
"RUSTC_WRAPPER",
"R_ENVIRON",
"R_ENVIRON_USER",
"R_PROFILE",
"R_PROFILE_USER",
"RUBYLIB",
"RUBYOPT",
"RUBYSHELL",
"RUSTC_WRAPPER",
"SBT_OPTS",
"SHELL",
"SHELLOPTS",
@@ -290,8 +291,7 @@ enum HostEnvSecurityPolicy {
"SVN_EDITOR",
"SVN_SSH",
"VAGRANT_VAGRANTFILE",
"VIMINIT",
"_JAVA_OPTIONS"
"VIMINIT"
]
static let blockedOverrideKeys: Set<String> = [
@@ -321,8 +321,9 @@ enum HostEnvSecurityPolicy {
"AZURE_AUTH_LOCATION",
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"BUNDLE_GEMFILE",
"BUN_CONFIG_REGISTRY",
"BUNDLE_GEMFILE",
"C_INCLUDE_PATH",
"CARGO_BUILD_RUSTC_WRAPPER",
"CARGO_HOME",
"CFLAGS",
@@ -335,7 +336,6 @@ enum HostEnvSecurityPolicy {
"CPLUS_INCLUDE_PATH",
"CURL_CA_BUNDLE",
"CURL_HOME",
"C_INCLUDE_PATH",
"DATABASE_URL",
"DENO_DIR",
"DOCKER_CERT_PATH",
@@ -347,8 +347,6 @@ enum HostEnvSecurityPolicy {
"GEM_HOME",
"GEM_PATH",
"GH_TOKEN",
"GITHUB_TOKEN",
"GITLAB_TOKEN",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_ASKPASS",
"GIT_COMMON_DIR",
@@ -364,6 +362,8 @@ enum HostEnvSecurityPolicy {
"GIT_SSL_CAPATH",
"GIT_SSL_NO_VERIFY",
"GIT_WORK_TREE",
"GITHUB_TOKEN",
"GITLAB_TOKEN",
"GOENV",
"GOFLAGS",
"GONOPROXY",
@@ -378,8 +378,8 @@ enum HostEnvSecurityPolicy {
"HGRCPATH",
"HISTFILE",
"HOME",
"HTTPS_PROXY",
"HTTP_PROXY",
"HTTPS_PROXY",
"KUBECONFIG",
"LDFLAGS",
"LESSCLOSE",
@@ -391,10 +391,10 @@ enum HostEnvSecurityPolicy {
"MANPAGER",
"MFLAGS",
"MONGODB_URI",
"NO_PROXY",
"NODE_AUTH_TOKEN",
"NODE_EXTRA_CA_CERTS",
"NODE_TLS_REJECT_UNAUTHORIZED",
"NO_PROXY",
"NPM_TOKEN",
"OBJC_INCLUDE_PATH",
"OPENSSL_CONF",
@@ -402,8 +402,8 @@ enum HostEnvSecurityPolicy {
"PAGER",
"PERL5DB",
"PERL5DBCMD",
"PHPRC",
"PHP_INI_SCAN_DIR",
"PHPRC",
"PIP_CONFIG_FILE",
"PIP_EXTRA_INDEX_URL",
"PIP_FIND_LINKS",
@@ -413,11 +413,11 @@ enum HostEnvSecurityPolicy {
"PROMPT_COMMAND",
"PYTHONSTARTUP",
"PYTHONUSERBASE",
"R_LIBS_USER",
"REDIS_URL",
"REQUESTS_CA_BUNDLE",
"RUSTC_WRAPPER",
"RUSTFLAGS",
"R_LIBS_USER",
"SSH_ASKPASS",
"SSH_AUTH_SOCK",
"SSL_CERT_DIR",

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.5.26</string>
<string>2026.5.25</string>
<key>CFBundleVersion</key>
<string>2026052600</string>
<string>2026052500</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -499,45 +499,6 @@
"lines"
]
},
"transcripts": {
"emoji": "📝",
"title": "Transcripts",
"actions": {
"start": {
"label": "start",
"detailKeys": [
"providerId",
"accountId",
"guildId",
"channelId",
"title"
]
},
"stop": {
"label": "stop",
"detailKeys": [
"sessionId"
]
},
"status": {
"label": "status"
},
"import": {
"label": "import",
"detailKeys": [
"providerId",
"title",
"speakerLabel"
]
},
"summarize": {
"label": "summarize",
"detailKeys": [
"sessionId"
]
}
}
},
"web_search": {
"emoji": "🔎",
"title": "Web Search",

View File

@@ -64,7 +64,6 @@ const rootBundledPluginRuntimeDependencies = [
"@grammyjs/transformer-throttler",
"@homebridge/ciao",
"@mozilla/readability",
"@silvia-odwyer/photon-node",
"@slack/bolt",
"@slack/types",
"@slack/web-api",
@@ -163,6 +162,7 @@ const config = {
entry: [
"index.html!",
"src/main.ts!",
"src/build/chunking.ts!",
"vite.config.ts!",
"vitest*.ts!",
],

View File

@@ -1,4 +1,4 @@
1d2c2fa07a2d3c4d046d2defe2eb48b27011be25e75db205b19e3da37e9fd0a0 config-baseline.json
5b12e247f4375de2d454802d3af188a840851dd69e9d15a1a92a4815c7ef7d97 config-baseline.core.json
c766614db5c416910fb6cdd454efb0738779af80ddd58a4fb06d8b1ca6484ce2 config-baseline.channel.json
e8e71a715bd33405280b5a9f6b6e788abed636fba66ec5f3a4f9b9a768a1637f config-baseline.json
003183db53a41905c540f37b1d30b3006ef8906915e13eac844b643cd210fdfe config-baseline.core.json
859b021f65400df22c95ae55b074cf26c83d3a0bfadb3fceeaca522f6ea391ae config-baseline.channel.json
74441e331aabb3026784c148d4ee5ce3f489a15ed87ffd9b7ba0c5e2a7bc93be config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
1f1824af61c8885360ff99dad3fe1d7b75565e540cdef57474e9f220f9876f78 plugin-sdk-api-baseline.json
4f29099d81398cb76331b618c39d298b3c9398efd84291dfb93c2098cb4ae443 plugin-sdk-api-baseline.jsonl
390681a3d97af8c004db89ead136bd6cff693af5a0ddfe86a8e3c55a29a077eb plugin-sdk-api-baseline.json
8dfaf69ee3d0a946bfdd1d8d97ef85262824d52c20854249f900db61f2a7f7b4 plugin-sdk-api-baseline.jsonl

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="420" viewBox="0 0 800 420" role="img" aria-label="padel-cli availability output">
<rect width="800" height="420" rx="24" fill="#0b0f14" />
<rect x="24" y="24" width="752" height="372" rx="18" fill="#0f172a" stroke="#263246" stroke-width="2" />
<text x="48" y="72" fill="#9ca3af" font-size="18" font-family="Fragment Mono, ui-monospace, SFMono-Regular, Menlo, monospace">
<tspan x="48" dy="0">$ padel search --location "Barcelona" --date 2026-01-08 --time 18:00-22:00</tspan>
<tspan x="48" dy="30" fill="#e5e7eb">Available courts (3):</tspan>
<tspan x="48" dy="28" fill="#e5e7eb">- Vall d'Hebron 19:00 Court 2 (90m) EUR 34</tspan>
<tspan x="48" dy="28" fill="#e5e7eb">- Badalona 20:30 Court 1 (60m) EUR 28</tspan>
<tspan x="48" dy="28" fill="#e5e7eb">- Gracia 21:00 Court 4 (90m) EUR 36</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 897 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="420" viewBox="0 0 800 420" role="img" aria-label="GoHome Roborock status output">
<rect width="800" height="420" rx="24" fill="#0b0f14" />
<rect x="24" y="24" width="752" height="372" rx="18" fill="#111827" stroke="#263246" stroke-width="2" />
<text x="48" y="72" fill="#9ca3af" font-size="18" font-family="Fragment Mono, ui-monospace, SFMono-Regular, Menlo, monospace">
<tspan x="48" dy="0">$ gohome roborock status --device "Living Room"</tspan>
<tspan x="48" dy="30" fill="#e5e7eb">Device: Roborock Q Revo</tspan>
<tspan x="48" dy="28" fill="#e5e7eb">State: cleaning (zone)</tspan>
<tspan x="48" dy="28" fill="#e5e7eb">Battery: 78%</tspan>
<tspan x="48" dy="28" fill="#e5e7eb">Dustbin: 42%</tspan>
<tspan x="48" dy="28" fill="#e5e7eb">Water tank: 61%</tspan>
<tspan x="48" dy="28" fill="#e5e7eb">Last clean: 2026-01-06 19:42</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 947 B

View File

@@ -436,7 +436,7 @@ Model override note:
cron: {
enabled: true,
store: "~/.openclaw/cron/jobs.json",
maxConcurrentRuns: 8,
maxConcurrentRuns: 1,
retry: {
maxAttempts: 3,
backoffMs: [60000, 120000, 300000],
@@ -449,7 +449,7 @@ Model override note:
}
```
`maxConcurrentRuns` limits both scheduled cron dispatch and isolated agent-turn execution, and defaults to 8. Isolated cron agent turns use the queue's dedicated `cron-nested` execution lane internally, so raising this value lets independent cron LLM runs progress in parallel instead of only starting their outer cron wrappers. The shared non-cron `nested` lane is not widened by this setting.
`maxConcurrentRuns` limits both scheduled cron dispatch and isolated agent-turn execution. Isolated cron agent turns use the queue's dedicated `cron-nested` execution lane internally, so raising this value lets independent cron LLM runs progress in parallel instead of only starting their outer cron wrappers. The shared non-cron `nested` lane is not widened by this setting.
The runtime state sidecar is derived from `cron.store`: a `.json` store such as `~/clawd/cron/jobs.json` uses `~/clawd/cron/jobs-state.json`, while a store path without a `.json` suffix appends `-state.json`.

View File

@@ -102,7 +102,7 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat
<Accordion title="Notify defaults for cron and media">
Main-session cron tasks use `silent` notify policy by default - they create records for tracking but do not generate notifications. Isolated cron tasks also default to `silent` but are more visible because they run in their own session.
Session-backed `image_generate`, `music_generate`, and `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished media itself. Generated-media completion events require message-tool delivery: the agent must send the finished media with the `message` tool, then reply `NO_REPLY`. If the requester session is no longer active or its active wake fails, and the completion agent misses some or all generated media, OpenClaw sends an idempotent direct fallback with only the missing media to the original channel target.
Session-backed `image_generate`, `music_generate`, and `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished media itself. Generated-media completion events require message-tool delivery: the agent must send the finished media with the `message` tool, then reply `NO_REPLY`. If the requester session is no longer active and the completion agent misses some or all generated media, OpenClaw sends an idempotent direct fallback with only the missing media to the original channel target.
</Accordion>
<Accordion title="Concurrent media-generation guardrail">

View File

@@ -1234,7 +1234,7 @@ Notes:
- In `stt-tts` mode, STT uses `tools.media.audio`; `voice.model` does not affect transcription.
- In realtime modes, `voice.realtime.provider`, `voice.realtime.model`, and `voice.realtime.voice` configure the realtime audio session. For OpenAI Realtime 2 plus the Codex brain, use `voice.realtime.model: "gpt-realtime-2"` and `voice.model: "openai-codex/gpt-5.5"`.
- Realtime voice modes include small `IDENTITY.md`, `USER.md`, and `SOUL.md` profile files in the realtime provider instructions by default so fast direct turns keep the same identity, user grounding, and persona as the routed OpenClaw agent. Set `voice.realtime.bootstrapContextFiles` to a subset to customize this, or `[]` to disable it. The supported realtime bootstrap files are limited to those profile files; `AGENTS.md` stays in the normal agent context. The injected profile context does not replace `openclaw_agent_consult` for workspace work, current facts, memory lookup, or tool-backed actions.
- In OpenAI `agent-proxy` realtime mode, set `voice.realtime.requireWakeName: true` to keep Discord realtime voice silent until a transcript starts or ends with a wake name. Configured wake names must be one or two words. If `voice.realtime.wakeNames` is unset, OpenClaw uses the routed agent `name` plus `OpenClaw`, falling back to the agent id plus `OpenClaw`. Wake-name gating disables realtime provider auto-response, routes accepted turns through the OpenClaw agent consult path, and gives a short spoken acknowledgement when a leading wake name is recognized from partial transcription before the final transcript arrives.
- In OpenAI `agent-proxy` realtime mode, set `voice.realtime.requireWakeName: true` to keep Discord realtime voice silent until a transcript contains a wake name. If `voice.realtime.wakeNames` is unset, OpenClaw uses the routed agent `name` plus `OpenClaw`, falling back to the agent id plus `OpenClaw`. Wake-name gating disables realtime provider auto-response and routes accepted turns through the OpenClaw agent consult path.
- The OpenAI realtime provider accepts current Realtime 2 event names and legacy Codex-compatible aliases for output audio and transcript events, so compatible provider snapshots can drift without dropping assistant audio.
- `voice.realtime.bargeIn` controls whether Discord speaker-start events interrupt active realtime playback. If unset, it follows the realtime provider's input-audio interruption setting.
- `voice.realtime.minBargeInAudioEndMs` controls the minimum assistant playback duration before an OpenAI realtime barge-in truncates audio. Default: `250`. Set `0` for immediate interruption in low-echo rooms, or raise it for echo-heavy speaker setups.
@@ -1247,12 +1247,12 @@ Notes:
- `voice.allowedChannels` is an optional residency allowlist. Leave it unset to allow `/vc join` into any authorized Discord voice channel. When set, `/vc join`, startup auto-join, and bot voice-state moves are restricted to the listed `{ guildId, channelId }` entries. Set it to an empty array to deny all Discord voice joins. If Discord moves the bot outside the allowlist, OpenClaw leaves that channel and rejoins the configured auto-join target when one is available.
- `voice.daveEncryption` and `voice.decryptionFailureTolerance` pass through to `@discordjs/voice` join options.
- `@discordjs/voice` defaults are `daveEncryption=true` and `decryptionFailureTolerance=24` if unset.
- OpenClaw uses the bundled `libopus-wasm` codec for Discord voice receive and realtime raw PCM playback. It ships a pinned libopus WebAssembly build and does not require native opus addons.
- OpenClaw defaults to the pure-JS `opusscript` decoder for Discord voice receive. The optional native `@discordjs/opus` package is ignored by the repo pnpm install policy so normal installs, Docker lanes, and unrelated tests do not compile a native addon. Dedicated voice-performance hosts can opt in with `OPENCLAW_DISCORD_OPUS_DECODER=native` after installing the native addon.
- `voice.connectTimeoutMs` controls the initial `@discordjs/voice` Ready wait for `/vc join` and auto-join attempts. Default: `30000`.
- `voice.reconnectGraceMs` controls how long OpenClaw waits for a disconnected voice session to begin reconnecting before destroying it. Default: `15000`.
- In `stt-tts` mode, voice playback does not stop just because another user starts speaking. To avoid feedback loops, OpenClaw ignores new voice capture while TTS is playing; speak after playback finishes for the next turn. Realtime modes forward speaker starts as barge-in signals to the realtime provider.
- In realtime modes, echo from speakers into an open mic can look like barge-in and interrupt playback. For echo-heavy Discord rooms, set `voice.realtime.providers.openai.interruptResponseOnInputAudio: false` to keep OpenAI from auto-interrupting on input audio. Add `voice.realtime.bargeIn: true` if you still want Discord speaker-start events to interrupt active playback. The OpenAI realtime bridge ignores playback truncations shorter than `voice.realtime.minBargeInAudioEndMs` as likely echo/noise and logs them as skipped instead of clearing Discord playback.
- `voice.captureSilenceGraceMs` controls how long OpenClaw waits after Discord reports a speaker has stopped before finalizing that audio segment for STT. Default: `2000`; raise this if Discord splits normal pauses into choppy partial transcripts.
- `voice.captureSilenceGraceMs` controls how long OpenClaw waits after Discord reports a speaker has stopped before finalizing that audio segment for STT. Default: `2500`; raise this if Discord splits normal pauses into choppy partial transcripts.
- When ElevenLabs is the selected TTS provider, Discord voice playback uses streaming TTS and starts from the provider response stream. Providers without streaming support fall back to the synthesized temp-file path.
- OpenClaw also watches receive decrypt failures and auto-recovers by leaving/rejoining the voice channel after repeated failures in a short window.
- If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)` after updating, collect a dependency report and logs. The bundled `@discordjs/voice` line includes the upstream padding fix from discord.js PR #11449, which closed discord.js issue #11419.
@@ -1301,11 +1301,22 @@ Choose between the join modes:
- Use `autoJoin` for fixed-room bots that should be present even when no tracked user is in voice.
- Use `/vc join` for one-off joins or rooms where automatic voice presence would be surprising.
Discord voice codec:
Native opus setup for source checkouts:
- Voice receive logs show `discord voice: opus decoder: libopus-wasm`.
- Realtime playback encodes raw 48 kHz stereo PCM to Opus with the same bundled `libopus-wasm` package before handing packets to `@discordjs/voice`.
- File and provider-stream playback transcodes to raw 48 kHz stereo PCM with ffmpeg, then uses `libopus-wasm` for the Opus packet stream sent to Discord.
```bash
pnpm install
mise exec node@22 -- pnpm discord:opus:install
```
Use Node 22 for the gateway when you want the upstream macOS arm64 prebuilt native addon. If you use another Node runtime, the opt-in installer may need a local `node-gyp` source-build toolchain.
After installing the native addon, start the Gateway with:
```bash
OPENCLAW_DISCORD_OPUS_DECODER=native pnpm gateway:watch
```
Verbose voice logs should show `discord voice: opus decoder: @discordjs/opus`. Without the env opt-in, or if the native addon is missing or cannot load on the host, OpenClaw logs `discord voice: opus decoder: opusscript` and keeps receiving voice through the pure-JS fallback.
STT plus TTS pipeline:

View File

@@ -442,13 +442,6 @@ See [ACP Agents](/tools/acp-agents) for shared ACP binding behavior.
Each account can override fields such as `cliPath`, `dbPath`, `allowFrom`, `groupPolicy`, `mediaMaxMb`, history settings, and attachment root allowlists.
</Accordion>
<Accordion title="Direct-message history">
Set `channels.imessage.dmHistoryLimit` to seed new direct-message sessions with recent decoded `imsg` history for that conversation. Use `channels.imessage.dms["<sender>"].historyLimit` for per-sender overrides, including `0` to disable history for a sender.
iMessage DM history is fetched on demand from `imsg`. Leaving `dmHistoryLimit` unset disables global DM history seeding, but a positive per-sender `channels.imessage.dms["<sender>"].historyLimit` still enables seeding for that sender.
</Accordion>
</AccordionGroup>
## Media, chunking, and delivery targets

View File

@@ -306,21 +306,6 @@ Config:
- `minimal`/`extensive` enables agent reactions and sets the guidance level.
- Per-account overrides: `channels.signal.accounts.<id>.actions.reactions`, `channels.signal.accounts.<id>.reactionLevel`.
## Approval reactions
Signal exec and plugin approval prompts use the top-level `approvals.exec` and
`approvals.plugin` routing blocks. Signal does not have a
`channels.signal.execApprovals` block.
- `👍` approves once.
- `👎` denies.
- Use `/approve <id> allow-always` when a request offers persistent approval.
Approval reaction resolution requires explicit Signal approvers from
`channels.signal.allowFrom`, `channels.signal.defaultTo`, or the matching account-level fields.
Direct same-chat exec approval prompts can still suppress the duplicate local `/approve` fallback
without explicit approvers; no-approver group approvals keep the local fallback visible.
## Delivery targets (CLI/cron)
- DMs: `signal:+15551234567` (or plain E.164).

View File

@@ -522,7 +522,6 @@ Ack reactions are gated by `reactionLevel` — they are suppressed when `reactio
Behavior notes:
- sent immediately after inbound is accepted (pre-reply)
- if `ackReaction` is present without `emoji`, WhatsApp uses the routed agent's identity emoji, falling back to "👀"; omit `ackReaction` or set `emoji: ""` to send no ack reaction
- failures are logged but do not block normal reply delivery
- group mode `mentions` reacts on mention-triggered turns; group activation `always` acts as bypass for this check
- WhatsApp uses `channels.whatsapp.ackReaction` (legacy `messages.ackReaction` is not used here)
@@ -549,7 +548,6 @@ Set `messages.statusReactions.enabled: true` to let WhatsApp replace the ack rea
Behavior notes:
- `channels.whatsapp.ackReaction` still controls whether status reactions are eligible for direct messages and groups.
- The queued status reaction uses the same effective ack emoji as plain ack reactions.
- WhatsApp has one bot reaction slot per message, so lifecycle updates replace the current reaction in place.
- `messages.removeAckAfterReply: true` clears the final status reaction after the configured done/error hold.
- Tool emoji categories include `tool`, `coding`, `web`, `deploy`, `build`, and `concierge`.

View File

@@ -43,7 +43,7 @@ OpenClaw CI runs on every push to `main` and every pull request. The `preflight`
GitHub may mark superseded jobs as `cancelled` when a newer push lands on the same PR or `main` ref. Treat that as CI noise unless the newest run for the same ref is also failing. Matrix jobs use `fail-fast: false`, and `build-artifacts` reports embedded channel, core-support-boundary, and gateway-watch failures directly instead of queuing tiny verifier jobs. The automatic CI concurrency key is versioned (`CI-v7-*`) so a GitHub-side zombie in an old queue group cannot indefinitely block newer main runs. Manual full-suite runs use `CI-manual-v1-*` and do not cancel in-progress runs.
The `ci-timings-summary` job uploads a compact `ci-timings-summary` artifact for each non-draft CI run. It records wall time, queue time, slowest jobs, and failed jobs for the current run, so CI health checks do not need to scrape the full Actions payload repeatedly. The `build-artifacts` job also runs the blocking startup-memory smoke and uploads a `startup-memory` artifact with per-command RSS values for `--help`, `status --json`, and `gateway status`.
The `ci-timings-summary` job uploads a compact `ci-timings-summary` artifact for each non-draft CI run. It records wall time, queue time, slowest jobs, and failed jobs for the current run, so CI health checks do not need to scrape the full Actions payload repeatedly.
## Real behavior proof
@@ -157,8 +157,6 @@ node scripts/ci-run-timings.mjs --latest-main # ignore issue/comment noise and c
node scripts/ci-run-timings.mjs --recent 10 # compare recent successful main CI runs
pnpm test:perf:groups --full-suite --allow-failures --output .artifacts/test-perf/baseline-before.json
pnpm test:perf:groups:compare .artifacts/test-perf/baseline-before.json .artifacts/test-perf/after-agent.json
pnpm test:startup:memory
pnpm test:extensions:memory -- --json .artifacts/openclaw-performance/source/mock-provider/extension-memory.json
pnpm perf:kova:summary --report .artifacts/kova/reports/mock-provider/report.json --output .artifacts/kova/summary.md
```
@@ -180,7 +178,7 @@ The workflow installs OCM from a pinned release and Kova from `openclaw/Kova` at
- `mock-deep-profile`: CPU/heap/trace profiling for startup, gateway, and agent-turn hotspots.
- `live-openai-candidate`: a real OpenAI `openai/gpt-5.5` agent turn, skipped when `OPENAI_API_KEY` is unavailable.
The mock-provider lane also runs OpenClaw-native source probes after the Kova pass: gateway boot timing and memory across default, hook, and 50-plugin startup cases; bundled plugin import RSS, repeated mock-OpenAI `channel-chat-baseline` hello loops, and CLI startup commands against the booted gateway. When the previous published mock-provider source report is available for the tested ref, the source summary compares current RSS and heap values against that baseline and marks large RSS increases as `watch`. The source probe Markdown summary lives at `source/index.md` in the report bundle, with raw JSON beside it.
The mock-provider lane also runs OpenClaw-native source probes after the Kova pass: gateway boot timing and memory across default, hook, and 50-plugin startup cases; repeated mock-OpenAI `channel-chat-baseline` hello loops; and CLI startup commands against the booted gateway. The source probe Markdown summary lives at `source/index.md` in the report bundle, with raw JSON beside it.
Every lane uploads GitHub artifacts. When `CLAWGRIT_REPORTS_TOKEN` is configured, the workflow also commits `report.json`, `report.md`, bundles, `index.md`, and source-probe artifacts into `openclaw/clawgrit-reports` under `openclaw-performance/<tested-ref>/<run-id>-<attempt>/<lane>/`. The current tested-ref pointer is written as `openclaw-performance/<tested-ref>/latest-<lane>.json`.
@@ -505,7 +503,7 @@ The `Docs Agent` workflow is an event-driven Codex maintenance lane for keeping
### Test Performance Agent
The `Test Performance Agent` workflow is an event-driven Codex maintenance lane for slow tests. It has no pure schedule: a successful non-bot push CI run on `main` can trigger it, but it skips if another workflow-run invocation already ran or is running that UTC day. Manual dispatch bypasses that daily activity gate. The lane builds a full-suite grouped Vitest performance report, lets Codex make only small coverage-preserving test performance fixes instead of broad refactors, then reruns the full-suite report and rejects changes that reduce the passing baseline test count. The grouped report records per-config wall time and max RSS on Linux and macOS, so the before/after comparison surfaces test memory deltas beside duration deltas. If the baseline has failing tests, Codex may fix only obvious failures and the after-agent full-suite report must pass before anything is committed. When `main` advances before the bot push lands, the lane rebases the validated patch, reruns `pnpm check:changed`, and retries the push; conflicting stale patches are skipped. It uses GitHub-hosted Ubuntu so the Codex action can keep the same drop-sudo safety posture as the docs agent.
The `Test Performance Agent` workflow is an event-driven Codex maintenance lane for slow tests. It has no pure schedule: a successful non-bot push CI run on `main` can trigger it, but it skips if another workflow-run invocation already ran or is running that UTC day. Manual dispatch bypasses that daily activity gate. The lane builds a full-suite grouped Vitest performance report, lets Codex make only small coverage-preserving test performance fixes instead of broad refactors, then reruns the full-suite report and rejects changes that reduce the passing baseline test count. If the baseline has failing tests, Codex may fix only obvious failures and the after-agent full-suite report must pass before anything is committed. When `main` advances before the bot push lands, the lane rebases the validated patch, reruns `pnpm check:changed`, and retries the push; conflicting stale patches are skipped. It uses GitHub-hosted Ubuntu so the Codex action can keep the same drop-sudo safety posture as the docs agent.
### Duplicate PRs After Merge
@@ -576,7 +574,7 @@ pnpm crabbox:run -- --provider blacksmith-testbox \
--ttl 240m \
--timing-json \
--shell -- \
"corepack pnpm check:changed"
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm check:changed"
```
Focused test rerun:
@@ -591,7 +589,7 @@ pnpm crabbox:run -- --provider blacksmith-testbox \
--ttl 240m \
--timing-json \
--shell -- \
"corepack pnpm test <path-or-filter>"
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test <path-or-filter>"
```
Full suite:
@@ -606,7 +604,7 @@ pnpm crabbox:run -- --provider blacksmith-testbox \
--ttl 240m \
--timing-json \
--shell -- \
"corepack pnpm test"
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test"
```
Read the final JSON summary. The useful fields are `provider`, `leaseId`, `syncDelegated`, `exitCode`, `commandMs`, and `totalMs`. One-shot Blacksmith-backed Crabbox runs should stop the Testbox automatically; if a run is interrupted or cleanup is unclear, inspect live boxes and stop only the boxes you created:
@@ -641,7 +639,7 @@ Escalate to owned Crabbox capacity only when Blacksmith is down, quota-limited,
CRABBOX_CAPACITY_REGIONS=eu-west-1,eu-west-2,eu-central-1,us-east-1,us-west-2 \
pnpm crabbox:warmup -- --provider aws --class standard --market on-demand --idle-timeout 90m
pnpm crabbox:hydrate -- --id <cbx_id-or-slug>
pnpm crabbox:run -- --id <cbx_id-or-slug> --timing-json --shell -- "pnpm check:changed"
pnpm crabbox:run -- --id <cbx_id-or-slug> --timing-json --shell -- "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm check:changed"
pnpm crabbox:stop -- <cbx_id-or-slug>
```

View File

@@ -30,12 +30,12 @@ Use the setup commands by intent:
| Models and inference | [`models`](/cli/models) · [`infer`](/cli/infer) · `capability` (alias for [`infer`](/cli/infer)) · [`memory`](/cli/memory) · [`commitments`](/cli/commitments) · [`wiki`](/cli/wiki) |
| Network and nodes | [`directory`](/cli/directory) · [`nodes`](/cli/nodes) · [`devices`](/cli/devices) · [`node`](/cli/node) |
| Runtime and sandbox | [`approvals`](/cli/approvals) · `exec-policy` (see [`approvals`](/cli/approvals)) · [`sandbox`](/cli/sandbox) · [`tui`](/cli/tui) · `chat`/`terminal` (aliases for [`tui --local`](/cli/tui)) · [`browser`](/cli/browser) |
| Automation | [`cron`](/cli/cron) · [`tasks`](/cli/tasks) · [`hooks`](/cli/hooks) · [`webhooks`](/cli/webhooks) · [`transcripts`](/cli/transcripts) |
| Automation | [`cron`](/cli/cron) · [`tasks`](/cli/tasks) · [`hooks`](/cli/hooks) · [`webhooks`](/cli/webhooks) |
| Discovery and docs | [`dns`](/cli/dns) · [`docs`](/cli/docs) |
| Pairing and channels | [`pairing`](/cli/pairing) · [`qr`](/cli/qr) · [`channels`](/cli/channels) |
| Security and plugins | [`security`](/cli/security) · [`secrets`](/cli/secrets) · [`skills`](/cli/skills) · [`plugins`](/cli/plugins) · [`proxy`](/cli/proxy) |
| Legacy aliases | [`daemon`](/cli/daemon) (gateway service) · [`clawbot`](/cli/clawbot) (namespace) |
| Plugins (optional) | [`path`](/cli/path) · [`policy`](/cli/policy) · [`voicecall`](/cli/voicecall) (if installed) |
| Plugins (optional) | [`meeting-notes`](/cli/meeting-notes) · [`path`](/cli/path) · [`policy`](/cli/policy) · [`voicecall`](/cli/voicecall) (if installed) |
## Global flags
@@ -128,7 +128,7 @@ openclaw [--dev] [--profile <name>] <command>
status
index
search
transcripts
meeting-notes
list
show
path

View File

@@ -1,17 +1,18 @@
---
summary: "CLI reference for `openclaw transcripts` (list, show, and locate stored transcripts)"
summary: "CLI reference for `openclaw meeting-notes` (list, show, and locate stored meeting notes)"
read_when:
- You want to read stored transcript summaries from the terminal
- You need the path to a transcripts markdown summary
- You are debugging the core transcripts storage layout
title: "Transcripts CLI"
- You want to read stored meeting note summaries from the terminal
- You need the path to a meeting notes markdown summary
- You are debugging the meeting-notes plugin storage layout
title: "Meeting Notes CLI"
---
# `openclaw transcripts`
# `openclaw meeting-notes`
Inspect transcripts written by OpenClaw's core `transcripts` tool. This CLI is
read-only; capture, import, and summarization are owned by the agent tool and
configured auto-start sources.
Inspect meeting notes written by the external `meeting-notes` plugin. This CLI
is read-only and is available when that plugin is installed or loaded from
source. Capture, import, and summarization are owned by the `meeting_notes`
agent tool and by configured auto-start sources.
Use the CLI when you want to find yesterday's notes, open the Markdown file in
an editor, feed a transcript to another tool, or debug where a session landed on
@@ -20,7 +21,7 @@ disk. It does not start or stop capture.
Artifacts live under the OpenClaw state directory:
```text
$OPENCLAW_STATE_DIR/transcripts/YYYY-MM-DD/<session>/
$OPENCLAW_STATE_DIR/meeting-notes/YYYY-MM-DD/<session>/
metadata.json
transcript.jsonl
summary.json
@@ -34,17 +35,17 @@ session directory is a safe filesystem segment derived from the session id.
## Commands
```bash
openclaw transcripts list
openclaw transcripts show <session>
openclaw transcripts show YYYY-MM-DD/<session>
openclaw transcripts path <session>
openclaw transcripts path YYYY-MM-DD/<session>
openclaw transcripts path <session> --dir
openclaw transcripts path <session> --metadata
openclaw transcripts path <session> --transcript
openclaw transcripts list --json
openclaw transcripts show <session> --json
openclaw transcripts path <session> --json
openclaw meeting-notes list
openclaw meeting-notes show <session>
openclaw meeting-notes show YYYY-MM-DD/<session>
openclaw meeting-notes path <session>
openclaw meeting-notes path YYYY-MM-DD/<session>
openclaw meeting-notes path <session> --dir
openclaw meeting-notes path <session> --metadata
openclaw meeting-notes path <session> --transcript
openclaw meeting-notes list --json
openclaw meeting-notes show <session> --json
openclaw meeting-notes path <session> --json
```
- `list`: list stored sessions, date-qualified selector, start time, title, and `summary.md` path.
@@ -56,7 +57,7 @@ openclaw transcripts path <session> --json
- `--json`: print machine-readable output.
When a human session id repeats across days, use the date-qualified selector
from `list`, for example `openclaw transcripts show 2026-05-22/standup`.
from `list`, for example `openclaw meeting-notes show 2026-05-22/standup`.
Default session ids include a timestamp and random suffix; configure fixed
session ids only when they are unique within the day.
@@ -65,7 +66,7 @@ session ids only when they are unique within the day.
`list` prints one session per line:
```text
2026-05-22/standup 2026-05-22T09:00:00.000Z Weekly standup /Users/alex/.openclaw/transcripts/2026-05-22/standup/summary.md
2026-05-22/standup 2026-05-22T09:00:00.000Z Weekly standup /Users/alex/.openclaw/meeting-notes/2026-05-22/standup/summary.md
```
The output is tab-separated. The columns are selector, start time, title, and
@@ -90,13 +91,13 @@ and whether that file exists.
## Many meetings per day
Transcripts groups sessions by date, then by session id. Ten meetings on one
Meeting Notes groups sessions by date, then by session id. Ten meetings on one
day become ten sibling folders:
```text
~/.openclaw/transcripts/2026-05-22/
transcript-2026-05-22T09-00-00-000Z-a1b2c3d4/
transcript-2026-05-22T10-30-00-000Z-b2c3d4e5/
~/.openclaw/meeting-notes/2026-05-22/
meeting-2026-05-22T09-00-00-000Z-a1b2c3d4/
meeting-2026-05-22T10-30-00-000Z-b2c3d4e5/
standup/
```
@@ -111,41 +112,7 @@ write `summary.md` immediately after import. A session can still appear in
or metadata was written before any utterances arrived.
Use `path <session> --transcript` to inspect the append-only transcript, and use
the `transcripts` tool action `summarize` to regenerate the Markdown summary.
the `meeting_notes` tool action `summarize` to regenerate the Markdown summary.
## Configuration
Transcript capture is opt-in because live sources can join and record meeting
audio. Enable the tool with top-level `transcripts.enabled`:
```json
{
"transcripts": {
"enabled": true,
"maxUtterances": 2000
}
}
```
Configure auto-start sources with `transcripts.autoStart` in `openclaw.json`.
Each entry is enabled by being present; omit an entry to disable that source.
```json
{
"transcripts": {
"enabled": true,
"autoStart": [
{
"providerId": "discord-voice",
"guildId": "1234567890",
"channelId": "2345678901"
},
{
"providerId": "slack-huddle",
"accountId": "workspace",
"channelId": "C123"
}
]
}
}
```
See [Meeting Notes](/plugins/meeting-notes) for configuration, auto-start, and
source-provider details.

View File

@@ -172,22 +172,6 @@ present in `policy.jsonc`. The observed state is existing OpenClaw config or
workspace metadata; policy reports drift but does not rewrite runtime behavior
unless a repair path is explicitly available and enabled.
Agent-specific policy overlays keep broad `tools.*` and `agents.workspace`
posture global, then let named scope blocks add stricter normal policy sections
for explicit `agentIds` under `scopes.<scopeName>`. The initial scoped
sections are `tools` and `agents.workspace`; sandbox and ingress can use the
same container once their evidence is attributable to an agent. Scoped fields
carry strictness metadata such as allowlist subset, denylist superset, required
boolean, and exact-list semantics so future policy-file conformance can reuse
the same rule inventory instead of guessing. The overlay is additive: global
claims still run, and a scoped claim can emit its own finding against the same
observed config. See [Agent-scoped policy overlays](/plan/policy-agent-scoped-overlays).
Every scope present in `policy.jsonc` must be valid and enforceable. Scopes
currently require `agentIds`, and that selector supports only `tools.*` and
`agents.workspace.*`. If an `agentIds` entry is not present in `agents.list[]`,
the scoped rule is evaluated against the inherited global/default posture for
that runtime agent id instead of being skipped.
#### Channels
| Policy field | Observed state | Use when |
@@ -266,7 +250,6 @@ that runtime agent id instead of being skipped.
| `tools.exec.requireAsk` | `tools.exec.ask` and per-agent exec ask mode | Require approval posture such as `always`. |
| `tools.exec.allowHosts` | `tools.exec.host` and per-agent exec host routing | Allow only exec host routing modes such as `sandbox`. |
| `tools.elevated.allow` | `tools.elevated.enabled` and per-agent elevated posture | Set to `false` to require elevated tool mode to stay disabled. |
| `tools.alsoAllow.expected` | `tools.alsoAllow` and per-agent `tools.alsoAllow` | Require exact `alsoAllow` entries and report missing or unexpected additive tool grants. |
| `tools.denyTools` | `tools.deny` and `agents.list[].tools.deny` | Require configured tool deny lists to include tool ids or groups such as `group:runtime` and `group:fs`. |
Run policy-only checks during authoring:
@@ -550,8 +533,6 @@ Policy currently verifies:
| `policy/tools-exec-ask-unapproved` | Exec ask mode is outside the policy allowlist. |
| `policy/tools-exec-host-unapproved` | Exec host routing is outside the policy allowlist. |
| `policy/tools-elevated-enabled` | Elevated tool mode is enabled when policy denies it. |
| `policy/tools-also-allow-missing` | A configured `alsoAllow` list is missing an entry required by policy. |
| `policy/tools-also-allow-unexpected` | A configured `alsoAllow` list includes an entry not expected by policy. |
| `policy/tools-required-deny-missing` | A global or per-agent tool deny list does not include a required denied tool. |
| `policy/secrets-unmanaged-provider` | A config SecretRef references a provider not declared under `secrets.providers`. |
| `policy/secrets-denied-provider-source` | A config secret provider or SecretRef uses a source denied by policy. |

View File

@@ -32,19 +32,7 @@ This is for cooperative/shared inbox hardening. A single Gateway shared by mutua
It also emits `security.trust_model.multi_user_heuristic` when config suggests likely shared-user ingress (for example open DM/group policy, configured group targets, or wildcard sender rules), and reminds you that OpenClaw is a personal-assistant trust model by default.
For intentional shared-user setups, the audit guidance is to sandbox all sessions, keep filesystem access workspace-scoped, and keep personal/private identities or credentials off that runtime.
It also warns when small models (`<=300B`) are used without sandboxing and with web/browser tools enabled.
For webhook ingress, it warns when:
- `hooks.token` reuses an active Gateway shared-secret auth value (`gateway.auth.token` / `OPENCLAW_GATEWAY_TOKEN` or `gateway.auth.password` / `OPENCLAW_GATEWAY_PASSWORD`)
- `hooks.token` is short
- `hooks.path="/"`
- `hooks.defaultSessionKey` is unset
- `hooks.allowedAgentIds` is unrestricted
- request `sessionKey` overrides are enabled
- overrides are enabled without `hooks.allowedSessionKeyPrefixes`
If Gateway password auth is supplied only at startup, pass the same value to `openclaw security audit --auth password --password <password>` so the audit can check it against `hooks.token`.
Password-mode reuse is an audit finding for compatibility; rotate one of the secrets instead of expecting Gateway startup to reject that configuration.
For webhook ingress, it warns when `hooks.token` reuses the Gateway token, when `hooks.token` is short, when `hooks.path="/"`, when `hooks.defaultSessionKey` is unset, when `hooks.allowedAgentIds` is unrestricted, when request `sessionKey` overrides are enabled, and when overrides are enabled without `hooks.allowedSessionKeyPrefixes`.
It also warns when sandbox Docker settings are configured while sandbox mode is off, when `gateway.nodes.denyCommands` uses ineffective pattern-like/unknown entries (exact node command-name matching only, not shell-text filtering), when `gateway.nodes.allowCommands` explicitly enables dangerous node commands, when global `tools.profile="minimal"` is overridden by agent tool profiles, when write/edit tools are disabled but `exec` is still available without a constraining sandbox filesystem boundary, when open groups expose runtime/filesystem tools without sandbox/workspace guards, and when installed plugin tools may be reachable under permissive tool policy.
It also flags `gateway.allowRealIpFallback=true` (header-spoofing risk if proxies are misconfigured) and `discovery.mdns.mode="full"` (metadata leakage via mDNS TXT records).
It also warns when sandbox browser uses Docker `bridge` network without `sandbox.browser.cdpSourceRange`.

View File

@@ -106,14 +106,14 @@ The byte guard requires `truncateAfterCompaction: true`. Without transcript rota
### Successor transcripts
When `agents.defaults.compaction.truncateAfterCompaction` is enabled, OpenClaw does not rewrite the existing transcript in place. It creates a new active successor transcript from the compaction summary, preserved state, and unsummarized tail, then records checkpoint metadata that points branch/restore flows at that compacted successor.
When `agents.defaults.compaction.truncateAfterCompaction` is enabled, OpenClaw does not rewrite the existing transcript in place. It creates a new active successor transcript from the compaction summary, preserved state, and unsummarized tail, then keeps the previous JSONL as the archived checkpoint source.
Successor transcripts also drop exact duplicate long user turns that arrive
inside a short retry window, so channel retry storms are not carried into the
next active transcript after compaction.
OpenClaw no longer writes separate `.checkpoint.*.jsonl` copies for new
compactions. Existing legacy checkpoint files can still be used while referenced
and are pruned by normal session cleanup.
Pre-compaction checkpoints are retained only while they stay below OpenClaw's
checkpoint size cap; oversized active transcripts still compact, but OpenClaw
skips the large debug snapshot instead of doubling disk usage.
### Compaction notices

View File

@@ -99,7 +99,7 @@ OpenClaw ships with the pi-ai catalog. These providers require **no** `models.pr
- Use `params.serviceTier` when you want an explicit tier instead of the shared `/fast` toggle
- Hidden OpenClaw attribution headers (`originator`, `version`, `User-Agent`) apply only on native OpenAI traffic to `api.openai.com`, not generic OpenAI-compatible proxies
- Native OpenAI routes also keep Responses `store`, prompt-cache hints, and OpenAI reasoning-compat payload shaping; proxy routes do not
- `openai/gpt-5.3-codex-spark` is intentionally suppressed in OpenClaw because live OpenAI API requests reject it; use `openai-codex/gpt-5.3-codex-spark` only when the Codex catalog exposes it for your account
- `openai/gpt-5.3-codex-spark` is intentionally suppressed in OpenClaw because live OpenAI API requests reject it and the current Codex catalog does not expose it
```json5
{
@@ -150,7 +150,6 @@ Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so Ope
- For the common subscription plus native Codex runtime route, sign in with `openai-codex` auth but configure `openai/gpt-5.5`; OpenAI agent turns select Codex by default.
- Use provider/model `agentRuntime.id: "pi"` only when you want a compatibility route through PI; otherwise keep `openai/gpt-5.5` on the default Codex harness.
- `openai-codex/gpt-*` refs remain a legacy PI route. Prefer `openai/gpt-5.5` on the native Codex runtime for new agent config, and run `openclaw doctor --fix` when you want to migrate old `openai-codex/*` refs to canonical `openai/*` refs.
- `openai-codex/gpt-5.3-codex-spark` remains available only through Codex catalog discovery when the signed-in account advertises it; direct `openai/*` and Azure refs for that model stay suppressed.
```json5
{

View File

@@ -93,26 +93,13 @@ That script starts a local OTLP/HTTP receiver, runs the `otel-trace-smoke` QA
scenario with the `diagnostics-otel` plugin enabled, then asserts traces,
metrics, and logs are exported. It decodes the exported protobuf trace spans
and checks the release-critical shape:
`openclaw.run`, `openclaw.harness.run`, a latest GenAI semantic-convention
model-call span, `openclaw.context.assembled`, and `openclaw.message.delivery`
must be present. The smoke forces
`OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`, so the model-call
span must use the `{gen_ai.operation.name} {gen_ai.request.model}` name;
`openclaw.run`, `openclaw.harness.run`, `openclaw.model.call`,
`openclaw.context.assembled`, and `openclaw.message.delivery` must be present;
model calls must not export `StreamAbandoned` on successful turns; raw diagnostic IDs and
`openclaw.content.*` attributes must stay out of the trace. The raw OTLP
payloads must not contain the prompt sentinel, response sentinel, or QA session
key. It writes `otel-smoke-summary.json` next to the QA suite artifacts.
For a collector-backed OpenTelemetry smoke, run:
```bash
pnpm qa:otel:collector-smoke
```
That lane puts a real OpenTelemetry Collector Docker container in front of the
same local receiver. Use it when changing endpoint wiring, collector
compatibility, or OTLP export behavior that the in-process receiver could mask.
For the protected Prometheus scrape smoke, run:
```bash
@@ -131,13 +118,6 @@ To run both observability smokes back to back, use:
pnpm qa:observability:smoke
```
For the collector-backed OpenTelemetry lane plus the protected Prometheus scrape
smoke, use:
```bash
pnpm qa:observability:collector-smoke
```
Observability QA stays source-checkout only. The npm tarball intentionally omits
QA Lab, so package Docker release lanes do not run `qa` commands. Use
`pnpm qa:otel:smoke`, `pnpm qa:prometheus:smoke`, or

View File

@@ -49,12 +49,10 @@ effective tool list.
token counts, and timestamps. Filter by kind (`main`, `group`, `cron`, `hook`,
`node`), exact `label`, exact `agentId`, search text, or recency
(`activeMinutes`). When you need mailbox-style triage, it can also ask for a
visibility-scoped derived title, a last-message preview snippet, or bounded recent
messages on each row. Derived titles and previews are produced only for sessions
the caller can already see under the configured session tool visibility policy, so
unrelated sessions stay hidden. When visibility is restricted, `sessions_list`
returns optional `visibility` metadata showing the effective mode and a warning that
results may be scope-limited.
visibility-scoped derived title, a last-message preview snippet, or bounded
recent messages on each row. Derived titles and previews are produced only for
sessions the caller can already see under the configured session tool
visibility policy, so unrelated sessions stay hidden.
`sessions_history` fetches the conversation transcript for a specific session.
By default, tool results are excluded -- pass `includeTools: true` to see them.

View File

@@ -50,50 +50,6 @@ Disable all flags:
OPENCLAW_DIAGNOSTICS=0
```
`OPENCLAW_DIAGNOSTICS=0` is a process-level disable override: it disables
flags from both env and config for that process.
## Profiling flags
Profiler flags enable targeted timing spans without raising global logging
levels. They are disabled by default.
Enable all profiler-gated spans for one gateway run:
```bash
OPENCLAW_DIAGNOSTICS=profiler openclaw gateway run
```
Enable only reply-dispatch profiler spans:
```bash
OPENCLAW_DIAGNOSTICS=reply.profiler openclaw gateway run
```
Enable only Codex app-server startup/tool/thread profiler spans:
```bash
OPENCLAW_DIAGNOSTICS=codex.profiler openclaw gateway run
```
Enable profiler flags from config:
```json
{
"diagnostics": {
"flags": ["reply.profiler", "codex.profiler"]
}
}
```
Restart the gateway after changing config flags. To disable a profiler flag,
remove it from `diagnostics.flags` and restart. To temporarily disable every
diagnostics flag even when config enables profiler flags, start the process with:
```bash
OPENCLAW_DIAGNOSTICS=0 openclaw gateway run
```
## Timeline artifacts
The `timeline` flag writes structured startup and runtime timing events for

View File

@@ -1212,6 +1212,7 @@
"plugins/codex-native-plugins",
"plugins/codex-computer-use",
"plugins/google-meet",
"plugins/meeting-notes",
"plugins/webhooks",
"plugins/admin-http-rpc",
"plugins/voice-call",

View File

@@ -170,15 +170,13 @@ the prompt. Skill env/API key overrides are still applied by OpenClaw to the
child process environment for the run.
Claude CLI also has its own noninteractive permission mode. OpenClaw maps that
to the existing exec policy instead of adding Claude-specific policy config.
For OpenClaw-managed Claude live sessions, the effective OpenClaw exec policy is
authoritative: YOLO (`tools.exec.security: "full"` and
`tools.exec.ask: "off"`) launches Claude with
`--permission-mode bypassPermissions`, while restrictive effective exec policy
launches Claude with `--permission-mode default`. Per-agent
`agents.list[].tools.exec` settings override global `tools.exec` for that
agent. Raw Claude backend args may still include `--permission-mode`, but live
Claude launches normalize that flag to match the effective OpenClaw exec policy.
to the existing exec policy instead of adding Claude-specific config: when the
effective requested exec policy is YOLO (`tools.exec.security: "full"` and
`tools.exec.ask: "off"`), OpenClaw adds `--permission-mode bypassPermissions`.
Per-agent `agents.list[].tools.exec` settings override global `tools.exec` for
that agent. To force a different Claude mode, set explicit raw backend args
such as `--permission-mode default` or `--permission-mode acceptEdits` under
`agents.defaults.cliBackends.claude-cli.args` and matching `resumeArgs`.
The bundled Anthropic `claude-cli` backend also maps OpenClaw `/think` levels
to Claude Code's native `--effort` flag for non-off levels. `minimal` and

View File

@@ -235,6 +235,7 @@ Shared defaults for bounded runtime context surfaces.
contextLimits: {
memoryGetMaxChars: 12000,
memoryGetDefaultLines: 120,
toolResultMaxChars: 16000,
postCompactionMaxChars: 1800,
},
},
@@ -246,12 +247,8 @@ Shared defaults for bounded runtime context surfaces.
metadata and continuation notice are added.
- `memoryGetDefaultLines`: default `memory_get` line window when `lines` is
omitted.
- `toolResultMaxChars`: advanced live tool-result ceiling used for persisted
results and overflow recovery. Leave unset for the model-context auto cap:
`16000` chars below 100K tokens, `32000` chars at 100K+ tokens, and `64000`
chars at 200K+ tokens. The effective cap is still limited to about 30% of the
model context window. `openclaw doctor --deep` prints the effective cap, and
doctor warns only when an explicit override is stale or has no effect.
- `toolResultMaxChars`: live tool-result cap used for persisted results and
overflow recovery.
- `postCompactionMaxChars`: AGENTS.md excerpt cap used during post-compaction
refresh injection.
@@ -266,6 +263,7 @@ from `agents.defaults.contextLimits`.
defaults: {
contextLimits: {
memoryGetMaxChars: 12000,
toolResultMaxChars: 16000,
},
},
list: [
@@ -273,7 +271,7 @@ from `agents.defaults.contextLimits`.
id: "tiny-local",
contextLimits: {
memoryGetMaxChars: 6000,
toolResultMaxChars: 8000, // advanced ceiling for this agent
toolResultMaxChars: 8000,
},
},
],
@@ -505,16 +503,16 @@ Time format in system prompt. Default: `auto` (OS preference).
**Built-in alias shorthands** (only apply when the model is in `agents.defaults.models`):
| Alias | Model |
| ------------------- | ------------------------------- |
| `opus` | `anthropic/claude-opus-4-6` |
| `sonnet` | `anthropic/claude-sonnet-4-6` |
| `gpt` | `openai/gpt-5.5` |
| `gpt-mini` | `openai/gpt-5.4-mini` |
| `gpt-nano` | `openai/gpt-5.4-nano` |
| `gemini` | `google/gemini-3.1-pro-preview` |
| `gemini-flash` | `google/gemini-3-flash-preview` |
| `gemini-flash-lite` | `google/gemini-3.1-flash-lite` |
| Alias | Model |
| ------------------- | -------------------------------------- |
| `opus` | `anthropic/claude-opus-4-6` |
| `sonnet` | `anthropic/claude-sonnet-4-6` |
| `gpt` | `openai/gpt-5.5` |
| `gpt-mini` | `openai/gpt-5.4-mini` |
| `gpt-nano` | `openai/gpt-5.4-nano` |
| `gemini` | `google/gemini-3.1-pro-preview` |
| `gemini-flash` | `google/gemini-3-flash-preview` |
| `gemini-flash-lite` | `google/gemini-3.1-flash-lite-preview` |
Your configured aliases always win over defaults.
@@ -577,7 +575,7 @@ Replace the entire OpenClaw-assembled system prompt with a fixed string. Set at
### `agents.defaults.promptOverlays`
Provider-independent prompt overlays applied by model family on OpenClaw-assembled prompt surfaces. GPT-5-family model ids receive the shared behavior contract across PI/provider routes; `personality` controls only the friendly interaction-style layer. Native Codex app-server routes keep Codex-owned base/model instructions instead of this OpenClaw GPT-5 overlay, and OpenClaw disables Codex's built-in personality for native threads.
Provider-independent prompt overlays applied by model family on OpenClaw-assembled prompt surfaces. GPT-5-family model ids receive the shared behavior contract across PI/provider routes; `personality` controls only the friendly interaction-style layer. Native Codex app-server routes keep Codex-owned base/model/personality instructions instead of this OpenClaw GPT-5 overlay.
```json5
{

View File

@@ -357,9 +357,6 @@ Default: `tree` (current session + sessions spawned by it, such as subagents).
- `agent`: any session belonging to the current agent id (can include other users if you run per-sender sessions under the same agent id).
- `all`: any session. Cross-agent targeting still requires `tools.agentToAgent`.
- Sandbox clamp: when the current session is sandboxed and `agents.defaults.sandbox.sessionToolsVisibility="spawned"`, visibility is forced to `tree` even if `tools.sessions.visibility="all"`.
- When not `all`, `sessions_list` includes a compact `visibility` field
describing the effective mode and a warning that some sessions may be
omitted outside the current scope.
</Accordion>
</AccordionGroup>

View File

@@ -385,7 +385,7 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
cron: {
enabled: true,
store: "~/.openclaw/cron/cron.json",
maxConcurrentRuns: 8, // default; cron dispatch + isolated cron agent-turn execution
maxConcurrentRuns: 2, // cron dispatch + isolated cron agent-turn execution
sessionRetention: "24h",
runLog: {
maxBytes: "2mb",

View File

@@ -687,8 +687,7 @@ Query-string hook tokens are rejected.
Validation and safety notes:
- `hooks.enabled=true` requires a non-empty `hooks.token`.
- `hooks.token` must be distinct from `gateway.auth.token` / `OPENCLAW_GATEWAY_TOKEN`; reusing the Gateway token fails startup validation.
- `openclaw security audit` also flags `hooks.token` reuse of active Gateway password auth (`gateway.auth.password` / `OPENCLAW_GATEWAY_PASSWORD`, or `--auth password --password <password>`) as a critical finding; password-mode reuse stays startup-compatible and should be repaired by rotating one of the secrets.
- `hooks.token` must be **distinct** from `gateway.auth.token`; reusing the Gateway token is rejected.
- `hooks.path` cannot be `/`; use a dedicated subpath such as `/hooks`.
- If `hooks.allowRequestSessionKey=true`, constrain `hooks.allowedSessionKeyPrefixes` (for example `["hook:"]`).
- If a mapping or preset uses a templated `sessionKey`, set `hooks.allowedSessionKeyPrefixes` and `hooks.allowRequestSessionKey=true`. Static mapping keys do not require that opt-in.
@@ -1052,7 +1051,6 @@ Notes:
toolInputs: false,
toolOutputs: false,
systemPrompt: false,
toolDefinitions: false,
},
},
@@ -1081,8 +1079,8 @@ Notes:
- `otel.traces` / `otel.metrics` / `otel.logs`: enable trace, metrics, or log export.
- `otel.sampleRate`: trace sampling rate `0`-`1`.
- `otel.flushIntervalMs`: periodic telemetry flush interval in ms.
- `otel.captureContent`: opt-in raw content capture for OTEL span attributes. Defaults to off. Boolean `true` captures non-system message/tool content; the object form lets you enable `inputMessages`, `outputMessages`, `toolInputs`, `toolOutputs`, `systemPrompt`, and `toolDefinitions` explicitly.
- `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`: environment toggle for latest experimental GenAI inference span shape, including `{gen_ai.operation.name} {gen_ai.request.model}` span names, `CLIENT` span kind, and `gen_ai.provider.name` instead of legacy `gen_ai.system`. By default spans keep `openclaw.model.call` and `gen_ai.system` for compatibility; GenAI metrics use bounded semantic attributes.
- `otel.captureContent`: opt-in raw content capture for OTEL span attributes. Defaults to off. Boolean `true` captures non-system message/tool content; the object form lets you enable `inputMessages`, `outputMessages`, `toolInputs`, `toolOutputs`, and `systemPrompt` explicitly.
- `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`: environment toggle for latest experimental GenAI span provider attributes. By default spans keep the legacy `gen_ai.system` attribute for compatibility; GenAI metrics use bounded semantic attributes.
- `OPENCLAW_OTEL_PRELOADED=1`: environment toggle for hosts that already registered a global OpenTelemetry SDK. OpenClaw then skips plugin-owned SDK startup/shutdown while keeping diagnostic listeners active.
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, and `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`: signal-specific endpoint env vars used when the matching config key is unset.
- `cacheTrace.enabled`: log cache trace snapshots for embedded runs (default: `false`).
@@ -1241,7 +1239,7 @@ Current builds no longer include the TCP bridge. Nodes connect over the Gateway
{
cron: {
enabled: true,
maxConcurrentRuns: 8, // default; cron dispatch + isolated cron agent-turn execution
maxConcurrentRuns: 2, // cron dispatch + isolated cron agent-turn execution
webhook: "https://example.invalid/legacy", // deprecated fallback for stored notify:true jobs
webhookToken: "replace-with-dedicated-token", // optional bearer token for outbound webhook auth
sessionRetention: "24h", // duration string or false

View File

@@ -419,7 +419,7 @@ candidate contains redacted secret placeholders such as `***`.
{
cron: {
enabled: true,
maxConcurrentRuns: 8, // default; cron dispatch + isolated cron agent-turn execution
maxConcurrentRuns: 2, // cron dispatch + isolated cron agent-turn execution
sessionRetention: "24h",
runLog: {
maxBytes: "2mb",
@@ -461,7 +461,7 @@ candidate contains redacted secret placeholders such as `***`.
Security note:
- Treat all hook/webhook payload content as untrusted input.
- Use a dedicated `hooks.token`; do not reuse active Gateway auth secrets (`gateway.auth.token` / `OPENCLAW_GATEWAY_TOKEN` or `gateway.auth.password` / `OPENCLAW_GATEWAY_PASSWORD`).
- Use a dedicated `hooks.token`; do not reuse the shared Gateway token.
- Hook auth is header-only (`Authorization: Bearer ...` or `x-openclaw-token`); query-string tokens are rejected.
- `hooks.path` cannot be `/`; keep webhook ingress on a dedicated subpath such as `/hooks`.
- Keep unsafe-content bypass flags disabled (`hooks.gmail.allowUnsafeExternalContent`, `hooks.mappings[].allowUnsafeExternalContent`) unless doing tightly scoped debugging.

View File

@@ -413,7 +413,7 @@ That stages grounded durable candidates into the short-term dreaming store while
- short cooldowns (rate limits/timeouts/auth failures)
- longer disables (billing/credit failures)
Legacy Codex OAuth profiles whose tokens live in macOS Keychain (older onboarding before the file-based sidecar layout) are not picked up by the embedded runtime path — that path runs with `allowKeychainPrompt: false` and cannot trigger a Keychain prompt. Affected users will see a one-shot `log.warn` from the legacy sidecar loader naming `openclaw doctor --fix` and macOS Keychain (instead of the credential silently falling through to a downstream `No API key found for provider "openai-codex"`). Run `openclaw doctor --fix` once from an interactive terminal to migrate Keychain-backed legacy tokens inline into `auth-profiles.json`; after that, embedded turns (Telegram, cron, sub-agent dispatch) resolve them like any other inline OAuth profile.
Legacy Codex OAuth profiles whose tokens live in macOS Keychain (older onboarding before the file-based sidecar layout) are not picked up by the embedded runtime path — that path runs with `allowKeychainPrompt: false` and cannot trigger a Keychain prompt. Run `openclaw doctor --fix` once to migrate Keychain-backed legacy tokens inline into `auth-profiles.json`; after that, embedded turns (Telegram, cron, sub-agent dispatch) resolve them like any other inline OAuth profile.
</Accordion>
<Accordion title="6. Hooks model validation">

View File

@@ -219,11 +219,8 @@ Set `stream: true` to receive Server-Sent Events (SSE):
- `max_tokens`: number; legacy alias accepted for backwards compatibility. Ignored when `max_completion_tokens` is also present.
- `temperature`: number; best-effort sampling temperature forwarded to the upstream provider via the agent stream-param channel.
- `top_p`: number; best-effort nucleus sampling forwarded to the upstream provider via the agent stream-param channel.
- `frequency_penalty`: number; best-effort frequency penalty forwarded to the upstream provider via the agent stream-param channel. Validated range: -2.0 to 2.0. Returns `400 invalid_request_error` for out-of-range values.
- `presence_penalty`: number; best-effort presence penalty forwarded to the upstream provider via the agent stream-param channel. Validated range: -2.0 to 2.0. Returns `400 invalid_request_error` for out-of-range values.
- `seed`: number (integer); best-effort seed forwarded to the upstream provider via the agent stream-param channel. Returns `400 invalid_request_error` for non-integer values.
When either token-cap field is set, the value is forwarded to the upstream provider via the agent stream-param channel. The actual wire field name sent to the upstream provider is chosen by the provider transport: `max_completion_tokens` for OpenAI-family endpoints, and `max_tokens` for providers that only accept the legacy name (such as Mistral and Chutes). Sampling fields (`temperature`, `top_p`, `frequency_penalty`, `presence_penalty`, `seed`) follow the same stream-param channel; the ChatGPT-based Codex Responses backend strips them server-side since it uses fixed sampling.
When either token-cap field is set, the value is forwarded to the upstream provider via the agent stream-param channel. The actual wire field name sent to the upstream provider is chosen by the provider transport: `max_completion_tokens` for OpenAI-family endpoints, and `max_tokens` for providers that only accept the legacy name (such as Mistral and Chutes). Sampling fields (`temperature`, `top_p`) follow the same stream-param channel; the ChatGPT-based Codex Responses backend strips them server-side since it uses fixed sampling.
### Unsupported variants

View File

@@ -261,7 +261,7 @@ Defaults when omitted:
- `images.maxBytes`: 10MB
- `images.maxRedirects`: 3
- `images.timeoutMs`: 10s
- HEIC/HEIF `input_image` sources are accepted when a system converter is available and are normalized to JPEG before provider delivery. Supported converters are macOS `sips`, ImageMagick, GraphicsMagick, or ffmpeg.
- HEIC/HEIF `input_image` sources are accepted and normalized to JPEG before provider delivery.
Security note:

View File

@@ -70,15 +70,14 @@ openclaw plugins enable diagnostics-otel
## Signals exported
| Signal | What goes in it |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Metrics** | Counters and histograms for token usage, cost, run duration, failover, skill usage, message flow, Talk events, queue lanes, session state/recovery, tool execution, oversized payloads, exec, and memory pressure. |
| **Traces** | Spans for model usage, model calls, harness lifecycle, skill usage, tool execution, exec, webhook/message processing, context assembly, and tool loops. |
| **Logs** | Structured `logging.file` records exported over OTLP when `diagnostics.otel.logs` is enabled; log bodies are withheld unless content capture is explicitly enabled. |
| Signal | What goes in it |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Metrics** | Counters and histograms for token usage, cost, run duration, skill usage, message flow, Talk events, queue lanes, session state/recovery, tool execution, exec, and memory pressure. |
| **Traces** | Spans for model usage, model calls, harness lifecycle, skill usage, tool execution, exec, webhook/message processing, context assembly, and tool loops. |
| **Logs** | Structured `logging.file` records exported over OTLP when `diagnostics.otel.logs` is enabled; log bodies are withheld unless content capture is explicitly enabled. |
Toggle `traces`, `metrics`, and `logs` independently. Traces and metrics
default to on when `diagnostics.otel.enabled` is true. Logs default to off and
are exported only when `diagnostics.otel.logs` is explicitly `true`.
Toggle `traces`, `metrics`, and `logs` independently. All three default to on
when `diagnostics.otel.enabled` is true.
## Configuration reference
@@ -107,7 +106,6 @@ are exported only when `diagnostics.otel.logs` is explicitly `true`.
toolInputs: false,
toolOutputs: false,
systemPrompt: false,
toolDefinitions: false,
},
},
},
@@ -116,14 +114,14 @@ are exported only when `diagnostics.otel.logs` is explicitly `true`.
### Environment variables
| Variable | Purpose |
| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Override `diagnostics.otel.endpoint`. If the value already contains `/v1/traces`, `/v1/metrics`, or `/v1/logs`, it is used as-is. |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` / `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Signal-specific endpoint overrides used when the matching `diagnostics.otel.*Endpoint` config key is unset. Signal-specific config wins over signal-specific env, which wins over the shared endpoint. |
| `OTEL_SERVICE_NAME` | Override `diagnostics.otel.serviceName`. |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | Override the wire protocol (only `http/protobuf` is honored today). |
| `OTEL_SEMCONV_STABILITY_OPT_IN` | Set to `gen_ai_latest_experimental` to emit the latest experimental GenAI inference span shape, including `{gen_ai.operation.name} {gen_ai.request.model}` span names, `CLIENT` span kind, and `gen_ai.provider.name` instead of the legacy `gen_ai.system`. GenAI metrics always use bounded, low-cardinality semantic attributes regardless. |
| `OPENCLAW_OTEL_PRELOADED` | Set to `1` when another preload or host process already registered the global OpenTelemetry SDK. The plugin then skips its own NodeSDK lifecycle but still wires diagnostic listeners and honors `traces`/`metrics`/`logs`. |
| Variable | Purpose |
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Override `diagnostics.otel.endpoint`. If the value already contains `/v1/traces`, `/v1/metrics`, or `/v1/logs`, it is used as-is. |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` / `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Signal-specific endpoint overrides used when the matching `diagnostics.otel.*Endpoint` config key is unset. Signal-specific config wins over signal-specific env, which wins over the shared endpoint. |
| `OTEL_SERVICE_NAME` | Override `diagnostics.otel.serviceName`. |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | Override the wire protocol (only `http/protobuf` is honored today). |
| `OTEL_SEMCONV_STABILITY_OPT_IN` | Set to `gen_ai_latest_experimental` to emit the latest experimental GenAI span attribute (`gen_ai.provider.name`) instead of the legacy `gen_ai.system`. GenAI metrics always use bounded, low-cardinality semantic attributes regardless. |
| `OPENCLAW_OTEL_PRELOADED` | Set to `1` when another preload or host process already registered the global OpenTelemetry SDK. The plugin then skips its own NodeSDK lifecycle but still wires diagnostic listeners and honors `traces`/`metrics`/`logs`. |
## Privacy and content capture
@@ -154,7 +152,6 @@ text. Each subkey is opt-in independently:
- `toolInputs` - tool argument payloads.
- `toolOutputs` - tool result payloads.
- `systemPrompt` - assembled system/developer prompt.
- `toolDefinitions` - model tool names, descriptions, and schemas.
When any subkey is enabled, model and tool spans get bounded, redacted
`openclaw.content.*` attributes for that class only. Use boolean
@@ -192,7 +189,6 @@ message bodies are also approved for export.
- `openclaw.model_call.request_bytes` (histogram, UTF-8 byte size of the final model request payload; no raw payload content)
- `openclaw.model_call.response_bytes` (histogram, UTF-8 byte size of streamed model response events; no raw response content)
- `openclaw.model_call.time_to_first_byte_ms` (histogram, elapsed time before the first streamed response event)
- `openclaw.model.failover` (counter, attrs: `openclaw.provider`, `openclaw.model`, `openclaw.failover.to_provider`, `openclaw.failover.to_model`, `openclaw.failover.reason`, `openclaw.failover.suspended`, `openclaw.lane`)
- `openclaw.skill.used` (counter, attrs: `openclaw.skill.name`, `openclaw.skill.source`, `openclaw.skill.activation`, optional `openclaw.agent`, optional `openclaw.toolName`)
### Message flow
@@ -264,31 +260,16 @@ unchanged, so dashboards should alert on sustained increases rather than every
heartbeat tick. For the config knob and defaults, see
[Configuration reference](/gateway/configuration-reference#diagnostics).
Liveness warnings also emit:
- `openclaw.liveness.warning` (counter, attrs: `openclaw.liveness.reason`)
- `openclaw.liveness.event_loop_delay_p99_ms` (histogram, attrs: `openclaw.liveness.reason`)
- `openclaw.liveness.event_loop_delay_max_ms` (histogram, attrs: `openclaw.liveness.reason`)
- `openclaw.liveness.event_loop_utilization` (histogram, attrs: `openclaw.liveness.reason`)
- `openclaw.liveness.cpu_core_ratio` (histogram, attrs: `openclaw.liveness.reason`)
### Harness lifecycle
- `openclaw.harness.duration_ms` (histogram, attrs: `openclaw.harness.id`, `openclaw.harness.plugin`, `openclaw.outcome`, `openclaw.harness.phase` on errors)
### Tool execution
- `openclaw.tool.execution.duration_ms` (histogram, attrs: `gen_ai.tool.name`, `openclaw.toolName`, `openclaw.tool.source`, `openclaw.tool.owner`, `openclaw.tool.params.kind`, plus `openclaw.errorCategory` on errors)
- `openclaw.tool.execution.blocked` (counter, attrs: `gen_ai.tool.name`, `openclaw.toolName`, `openclaw.tool.source`, `openclaw.tool.owner`, `openclaw.tool.params.kind`, `openclaw.deniedReason`)
### Exec
- `openclaw.exec.duration_ms` (histogram, attrs: `openclaw.exec.target`, `openclaw.exec.mode`, `openclaw.outcome`, `openclaw.failureKind`)
### Diagnostics internals (memory and tool loop)
- `openclaw.payload.large` (counter, attrs: `openclaw.payload.surface`, `openclaw.payload.action`, `openclaw.channel`, `openclaw.plugin`, `openclaw.reason`)
- `openclaw.payload.large_bytes` (histogram, attrs: same as `openclaw.payload.large`)
- `openclaw.memory.heap_used_bytes` (histogram, attrs: `openclaw.memory.kind`)
- `openclaw.memory.rss_bytes` (histogram)
- `openclaw.memory.pressure` (counter, attrs: `openclaw.memory.level`)
@@ -310,7 +291,6 @@ Liveness warnings also emit:
- `openclaw.errorCategory` and optional `openclaw.failureKind` on errors
- `openclaw.model_call.request_bytes`, `openclaw.model_call.response_bytes`, `openclaw.model_call.time_to_first_byte_ms`
- `openclaw.provider.request_id_hash` (bounded SHA-based hash of the upstream provider request id; raw ids are not exported)
- With `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`, model-call spans use the latest GenAI inference span name `{gen_ai.operation.name} {gen_ai.request.model}` and `CLIENT` span kind instead of `openclaw.model.call`.
- `openclaw.harness.run`
- `openclaw.harness.id`, `openclaw.harness.plugin`, `openclaw.outcome`, `openclaw.provider`, `openclaw.model`, `openclaw.channel`
- On completion: `openclaw.harness.result_classification`, `openclaw.harness.yield_detected`, `openclaw.harness.items.started`, `openclaw.harness.items.completed`, `openclaw.harness.items.active`

View File

@@ -8,7 +8,7 @@ read_when:
- You want metrics without running an OpenTelemetry collector
---
OpenClaw can expose diagnostics metrics through the official `diagnostics-prometheus` plugin. It listens to trusted diagnostics plus core-emitted gateway stability events, then renders a Prometheus text endpoint at:
OpenClaw can expose diagnostics metrics through the official `diagnostics-prometheus` plugin. It listens to trusted internal diagnostics and renders a Prometheus text endpoint at:
```text
GET /api/diagnostics/prometheus
@@ -87,59 +87,44 @@ For traces, logs, OTLP push, and OpenTelemetry GenAI semantic attributes, see [O
## Metrics exported
| Metric | Type | Labels |
| ------------------------------------------------ | --------- | ----------------------------------------------------------------------------------------- |
| `openclaw_run_completed_total` | counter | `channel`, `model`, `outcome`, `provider`, `trigger` |
| `openclaw_run_duration_seconds` | histogram | `channel`, `model`, `outcome`, `provider`, `trigger` |
| `openclaw_model_call_total` | counter | `api`, `error_category`, `model`, `outcome`, `provider`, `transport` |
| `openclaw_model_call_duration_seconds` | histogram | `api`, `error_category`, `model`, `outcome`, `provider`, `transport` |
| `openclaw_model_failover_total` | counter | `from_model`, `from_provider`, `lane`, `reason`, `suspended`, `to_model`, `to_provider` |
| `openclaw_model_tokens_total` | counter | `agent`, `channel`, `model`, `provider`, `token_type` |
| `openclaw_gen_ai_client_token_usage` | histogram | `model`, `provider`, `token_type` |
| `openclaw_model_cost_usd_total` | counter | `agent`, `channel`, `model`, `provider` |
| `openclaw_skill_used_total` | counter | `activation`, `agent`, `skill`, `source` |
| `openclaw_tool_execution_total` | counter | `error_category`, `outcome`, `params_kind`, `tool`, `tool_owner`, `tool_source` |
| `openclaw_tool_execution_duration_seconds` | histogram | `error_category`, `outcome`, `params_kind`, `tool`, `tool_owner`, `tool_source` |
| `openclaw_tool_execution_blocked_total` | counter | `denied_reason`, `params_kind`, `tool`, `tool_owner`, `tool_source` |
| `openclaw_harness_run_total` | counter | `channel`, `error_category`, `harness`, `model`, `outcome`, `phase`, `plugin`, `provider` |
| `openclaw_harness_run_duration_seconds` | histogram | `channel`, `error_category`, `harness`, `model`, `outcome`, `phase`, `plugin`, `provider` |
| `openclaw_webhook_received_total` | counter | `channel`, `webhook` |
| `openclaw_webhook_error_total` | counter | `channel`, `webhook` |
| `openclaw_webhook_duration_seconds` | histogram | `channel`, `webhook` |
| `openclaw_message_received_total` | counter | `channel`, `source` |
| `openclaw_message_dispatch_started_total` | counter | `channel`, `source` |
| `openclaw_message_dispatch_completed_total` | counter | `channel`, `outcome`, `reason`, `source` |
| `openclaw_message_dispatch_duration_seconds` | histogram | `channel`, `outcome`, `reason`, `source` |
| `openclaw_message_processed_total` | counter | `channel`, `outcome`, `reason` |
| `openclaw_message_processed_duration_seconds` | histogram | `channel`, `outcome`, `reason` |
| `openclaw_message_delivery_started_total` | counter | `channel`, `delivery_kind` |
| `openclaw_message_delivery_total` | counter | `channel`, `delivery_kind`, `error_category`, `outcome` |
| `openclaw_message_delivery_duration_seconds` | histogram | `channel`, `delivery_kind`, `error_category`, `outcome` |
| `openclaw_talk_event_total` | counter | `brain`, `event_type`, `mode`, `provider`, `transport` |
| `openclaw_talk_event_duration_seconds` | histogram | `brain`, `event_type`, `mode`, `provider`, `transport` |
| `openclaw_talk_audio_bytes` | histogram | `brain`, `event_type`, `mode`, `provider`, `transport` |
| `openclaw_queue_lane_size` | gauge | `lane` |
| `openclaw_queue_lane_wait_seconds` | histogram | `lane` |
| `openclaw_session_state_total` | counter | `reason`, `state` |
| `openclaw_session_queue_depth` | gauge | `state` |
| `openclaw_session_turn_created_total` | counter | `agent`, `channel`, `trigger` |
| `openclaw_session_stuck_total` | counter | `reason`, `state` |
| `openclaw_session_stuck_age_seconds` | histogram | `reason`, `state` |
| `openclaw_session_recovery_total` | counter | `action`, `active_work_kind`, `state`, `status` |
| `openclaw_session_recovery_age_seconds` | histogram | `action`, `active_work_kind`, `state`, `status` |
| `openclaw_liveness_warning_total` | counter | `reason` |
| `openclaw_liveness_sessions` | gauge | `state` |
| `openclaw_liveness_event_loop_delay_p99_seconds` | histogram | `reason` |
| `openclaw_liveness_event_loop_delay_max_seconds` | histogram | `reason` |
| `openclaw_liveness_event_loop_utilization_ratio` | histogram | `reason` |
| `openclaw_liveness_cpu_core_ratio` | histogram | `reason` |
| `openclaw_payload_large_total` | counter | `action`, `channel`, `plugin`, `reason`, `surface` |
| `openclaw_payload_large_bytes` | histogram | `action`, `channel`, `plugin`, `reason`, `surface` |
| `openclaw_memory_bytes` | gauge | `kind` |
| `openclaw_memory_rss_bytes` | histogram | none |
| `openclaw_memory_pressure_total` | counter | `level`, `reason` |
| `openclaw_telemetry_exporter_total` | counter | `exporter`, `reason`, `signal`, `status` |
| `openclaw_prometheus_series_dropped_total` | counter | none |
| Metric | Type | Labels |
| --------------------------------------------- | --------- | ----------------------------------------------------------------------------------------- |
| `openclaw_run_completed_total` | counter | `channel`, `model`, `outcome`, `provider`, `trigger` |
| `openclaw_run_duration_seconds` | histogram | `channel`, `model`, `outcome`, `provider`, `trigger` |
| `openclaw_model_call_total` | counter | `api`, `error_category`, `model`, `outcome`, `provider`, `transport` |
| `openclaw_model_call_duration_seconds` | histogram | `api`, `error_category`, `model`, `outcome`, `provider`, `transport` |
| `openclaw_model_tokens_total` | counter | `agent`, `channel`, `model`, `provider`, `token_type` |
| `openclaw_gen_ai_client_token_usage` | histogram | `model`, `provider`, `token_type` |
| `openclaw_model_cost_usd_total` | counter | `agent`, `channel`, `model`, `provider` |
| `openclaw_skill_used_total` | counter | `activation`, `agent`, `skill`, `source` |
| `openclaw_tool_execution_total` | counter | `error_category`, `outcome`, `params_kind`, `tool`, `tool_owner`, `tool_source` |
| `openclaw_tool_execution_duration_seconds` | histogram | `error_category`, `outcome`, `params_kind`, `tool`, `tool_owner`, `tool_source` |
| `openclaw_harness_run_total` | counter | `channel`, `error_category`, `harness`, `model`, `outcome`, `phase`, `plugin`, `provider` |
| `openclaw_harness_run_duration_seconds` | histogram | `channel`, `error_category`, `harness`, `model`, `outcome`, `phase`, `plugin`, `provider` |
| `openclaw_message_received_total` | counter | `channel`, `source` |
| `openclaw_message_dispatch_started_total` | counter | `channel`, `source` |
| `openclaw_message_dispatch_completed_total` | counter | `channel`, `outcome`, `reason`, `source` |
| `openclaw_message_dispatch_duration_seconds` | histogram | `channel`, `outcome`, `reason`, `source` |
| `openclaw_message_processed_total` | counter | `channel`, `outcome`, `reason` |
| `openclaw_message_processed_duration_seconds` | histogram | `channel`, `outcome`, `reason` |
| `openclaw_message_delivery_started_total` | counter | `channel`, `delivery_kind` |
| `openclaw_message_delivery_total` | counter | `channel`, `delivery_kind`, `error_category`, `outcome` |
| `openclaw_message_delivery_duration_seconds` | histogram | `channel`, `delivery_kind`, `error_category`, `outcome` |
| `openclaw_talk_event_total` | counter | `brain`, `event_type`, `mode`, `provider`, `transport` |
| `openclaw_talk_event_duration_seconds` | histogram | `brain`, `event_type`, `mode`, `provider`, `transport` |
| `openclaw_talk_audio_bytes` | histogram | `brain`, `event_type`, `mode`, `provider`, `transport` |
| `openclaw_queue_lane_size` | gauge | `lane` |
| `openclaw_queue_lane_wait_seconds` | histogram | `lane` |
| `openclaw_session_state_total` | counter | `reason`, `state` |
| `openclaw_session_queue_depth` | gauge | `state` |
| `openclaw_session_turn_created_total` | counter | `agent`, `channel`, `trigger` |
| `openclaw_session_recovery_total` | counter | `action`, `active_work_kind`, `state`, `status` |
| `openclaw_session_recovery_age_seconds` | histogram | `action`, `active_work_kind`, `state`, `status` |
| `openclaw_memory_bytes` | gauge | `kind` |
| `openclaw_memory_rss_bytes` | histogram | none |
| `openclaw_memory_pressure_total` | counter | `level`, `reason` |
| `openclaw_telemetry_exporter_total` | counter | `exporter`, `reason`, `signal`, `status` |
| `openclaw_prometheus_series_dropped_total` | counter | none |
## Label policy

View File

@@ -64,7 +64,7 @@ exhaustive):
| `security.audit.suppressions.active` | info | Audit output has configured suppressions and may be filtered | `security.audit.suppressions` | no |
| `config.secrets.gateway_password_in_config` | warn | Gateway password is stored directly in config | `gateway.auth.password` | no |
| `config.secrets.hooks_token_in_config` | warn | Hook bearer token is stored directly in config | `hooks.token` | no |
| `hooks.token_reuse_gateway_token` | critical | Hook ingress token also unlocks Gateway auth | `hooks.token`, `gateway.auth.token`, `gateway.auth.password` | no |
| `hooks.token_reuse_gateway_token` | critical | Hook ingress token also unlocks Gateway auth | `hooks.token`, `gateway.auth.token` | no |
| `hooks.token_too_short` | warn | Easier brute force on hook ingress | `hooks.token` | no |
| `hooks.default_session_key_unset` | warn | Hook agent runs fan out into generated per-request sessions | `hooks.defaultSessionKey` | no |
| `hooks.allowed_agent_ids_unrestricted` | warn/critical | Authenticated hook callers may route to any configured agent | `hooks.allowedAgentIds` | no |

View File

@@ -289,7 +289,7 @@ troubleshooting, see the main [FAQ](/help/faq).
- `gpt-nano` → `openai/gpt-5.4-nano`
- `gemini` → `google/gemini-3.1-pro-preview`
- `gemini-flash` → `google/gemini-3-flash-preview`
- `gemini-flash-lite` → `google/gemini-3.1-flash-lite`
- `gemini-flash-lite` → `google/gemini-3.1-flash-lite-preview`
If you set your own alias with the same name, your value wins.

View File

@@ -509,8 +509,8 @@ Think of the suites as "increasing realism" (and increasing flakiness/cost):
Native dependency policy:
- Default test installs skip optional native Discord opus builds. Discord voice uses bundled `libopus-wasm`, and `@discordjs/opus` stays disabled in `allowBuilds` so local tests and Testbox lanes do not compile the native addon.
- Compare native opus performance in the `libopus-wasm` benchmark repo, not in default OpenClaw install/test loops. Do not set `@discordjs/opus` to `true` in the default `allowBuilds`; that makes unrelated install/test loops compile native code.
- Default test installs skip optional native Discord opus builds. Discord voice receive uses the pure-JS `opusscript` decoder, and `@discordjs/opus` stays disabled in `allowBuilds` so local tests and Testbox lanes do not compile the native addon.
- Use a dedicated Discord voice performance or live lane if you intentionally need to compare a native opus build. Do not set `@discordjs/opus` to `true` in the default `allowBuilds`; that makes unrelated install/test loops compile native code.
<AccordionGroup>
<Accordion title="Projects, shards, and scoped lanes">
@@ -557,10 +557,6 @@ Native dependency policy:
processes by default to reduce V8 compile churn during big local runs.
Set `OPENCLAW_VITEST_ENABLE_MAGLEV=1` to compare against stock V8
behavior.
- `scripts/run-vitest.mjs` terminates explicit non-watch Vitest runs after
5 minutes with no stdout or stderr output. Set
`OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=0` to disable the watchdog for an
intentionally silent investigation.
</Accordion>
@@ -750,7 +746,6 @@ These Docker runners split into two buckets:
- Build and release checks run `scripts/check-cli-bootstrap-imports.mjs` after tsdown. The guard walks the static built graph from `dist/entry.js` and `dist/cli/run-main.js` and fails if pre-dispatch startup imports package dependencies such as Commander, prompt UI, undici, or logging before command dispatch; it also keeps the bundled gateway run chunk under budget and rejects static imports of known cold gateway paths. Packaged CLI smoke also covers root help, onboard help, doctor help, status, config schema, and a model-list command.
- Package Acceptance legacy compatibility is capped at `2026.4.25` (`2026.4.25-beta.*` included). Through that cutoff, the harness tolerates only shipped-package metadata gaps: omitted private QA inventory entries, missing `gateway install --wrapper`, missing patch files in the tarball-derived git fixture, missing persisted `update.channel`, legacy plugin install-record locations, missing marketplace install-record persistence, and config metadata migration during `plugins update`. For packages after `2026.4.25`, those paths are strict failures.
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:release-user-journey`, `test:docker:release-typed-onboarding`, `test:docker:release-media-memory`, `test:docker:release-upgrade-user-journey`, `test:docker:release-plugin-marketplace`, `test:docker:skill-install`, `test:docker:update-channel-switch`, `test:docker:upgrade-survivor`, `test:docker:published-upgrade-survivor`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, `test:docker:plugin-lifecycle-matrix`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths.
- Docker/Bash E2E lanes that install the packed OpenClaw tarball through `scripts/lib/openclaw-e2e-instance.sh` cap `npm install` at `OPENCLAW_E2E_NPM_INSTALL_TIMEOUT` (default `600s`; set `0` to disable the wrapper for debugging).
The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -108,6 +108,15 @@ If you already manage Node yourself:
</Tab>
</Tabs>
<Accordion title="Troubleshooting: sharp build errors (npm)">
If `sharp` fails due to a globally installed libvips:
```bash
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest
```
</Accordion>
### From source
For contributors or anyone who wants to run from a local checkout:

View File

@@ -72,10 +72,9 @@ Recommended for most interactive installs on macOS/Linux/WSL.
</Step>
<Step title="Ensure Node.js 24 by default">
Checks Node version and installs Node 24 if needed (Homebrew on macOS, NodeSource setup scripts on Linux apt/dnf/yum). OpenClaw still supports Node 22 LTS, currently `22.19+`, for compatibility.
On Alpine/musl Linux, the installer uses apk packages instead of NodeSource; the configured Alpine repositories must provide Node `22.19+` (Alpine 3.21 or newer at the time of writing).
</Step>
<Step title="Ensure Git">
Installs Git if missing using the detected package manager, including apk on Alpine.
Installs Git if missing.
</Step>
<Step title="Install OpenClaw">
- `npm` method (default): global npm install
@@ -86,6 +85,7 @@ Recommended for most interactive installs on macOS/Linux/WSL.
- Refreshes a loaded gateway service best-effort (`openclaw gateway install --force`, then restart)
- Runs `openclaw doctor --non-interactive` on upgrades and git installs (best effort)
- Attempts onboarding when appropriate (TTY available, onboarding not disabled, and bootstrap/config checks pass)
- Defaults `SHARP_IGNORE_GLOBAL_LIBVIPS=1`
</Step>
</Steps>
@@ -167,6 +167,7 @@ The script exits with code `2` for invalid method selection or invalid `--instal
| `OPENCLAW_DRY_RUN=1` | Dry run mode |
| `OPENCLAW_VERBOSE=1` | Debug mode |
| `OPENCLAW_NPM_LOGLEVEL=error\|warn\|notice` | npm log level |
| `SHARP_IGNORE_GLOBAL_LIBVIPS=0\|1` | Control sharp/libvips behavior (default: `1`) |
</Accordion>
</AccordionGroup>
@@ -188,10 +189,10 @@ by default, plus git-checkout installs under the same prefix flow.
<Steps>
<Step title="Install local Node runtime">
Downloads a pinned supported Node LTS tarball (the version is embedded in the script and updated independently) to `<prefix>/tools/node-v<version>` and verifies SHA-256.
On Alpine/musl Linux, where Node does not publish compatible tarballs for the pinned runtime, installs `nodejs` and `npm` with `apk` and links that runtime into the prefix wrapper path. The Alpine repositories must provide Node `22.19+`; use Alpine 3.21 or newer if older repositories only provide Node 20 or 21.
On Alpine/musl Linux, where Node does not publish compatible tarballs for the pinned runtime, installs `nodejs` and `npm` with `apk` and links that runtime into the prefix wrapper path.
</Step>
<Step title="Ensure Git">
If Git is missing, attempts install via apt/dnf/yum/apk on Linux or Homebrew on macOS.
If Git is missing, attempts install via apt/dnf/yum on Linux or Homebrew on macOS.
</Step>
<Step title="Install OpenClaw under prefix">
- `npm` method (default): installs under the prefix with npm, then writes wrapper to `<prefix>/bin/openclaw`
@@ -268,6 +269,7 @@ by default, plus git-checkout installs under the same prefix flow.
| `OPENCLAW_GIT_UPDATE=0\|1` | Toggle git updates for existing checkouts |
| `OPENCLAW_NO_ONBOARD=1` | Skip onboarding |
| `OPENCLAW_NPM_LOGLEVEL=error\|warn\|notice` | npm log level |
| `SHARP_IGNORE_GLOBAL_LIBVIPS=0\|1` | Control sharp/libvips behavior (default: `1`) |
</Accordion>
</AccordionGroup>
@@ -415,6 +417,15 @@ Use non-interactive flags/env vars for predictable runs.
Some Linux setups point npm global prefix to root-owned paths. `install.sh` can switch prefix to `~/.npm-global` and append PATH exports to shell rc files (when those files exist).
</Accordion>
<Accordion title="sharp/libvips issues">
The scripts default `SHARP_IGNORE_GLOBAL_LIBVIPS=1` to avoid sharp building against system libvips. To override:
```bash
SHARP_IGNORE_GLOBAL_LIBVIPS=0 curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash
```
</Accordion>
<Accordion title='Windows: "npm error spawn git / ENOENT"'>
Rerun the installer so it can bootstrap user-local MinGit, or install Git for Windows and reopen PowerShell.
</Accordion>

View File

@@ -1,205 +0,0 @@
---
summary: "Per-agent Policy plugin overlays layered on top of global policy rules."
read_when:
- You are designing per-agent policy requirements
- You need to distinguish tool posture policy from workspace policy
- You are configuring stricter policy for one named agent
title: "Agent-scoped policy overlays"
---
# Agent-scoped policy overlays
OpenClaw policy supports global requirements and stricter requirements for
explicit runtime agent ids. Some deployments need one agent to use a tighter
workspace and tool posture than other agents, but deployment-wide rules should
not force every agent to use the same posture.
This page describes the agent-scoped overlay model. The field reference remains
[`openclaw policy`](/cli/policy).
## Design goals
- Keep global policy as the deployment baseline.
- Let a named agent add stricter requirements without weakening global rules.
- Reuse existing policy section shapes where the evidence can be attributed to
an agent.
- Avoid making `agents.workspace` a second tool-permission system.
- Leave global-only checks global until their evidence can be mapped to an
agent.
## Shape
Use `scopes.<scopeName>` for purpose-named agent policy scopes. Each
scope lists the runtime `agentIds` it applies to, then reuses the normal
top-level policy section grammar where the section evidence can be attributed to
those agents. The initial shipped scoped sections are `tools` and
`agents.workspace`; sandbox and ingress stay out of this PR and can join the
same container once those policy PRs land and their evidence carries agent
identity. The scoped field inventory is backed by policy rule metadata that
records each field's strictness semantics for later policy-file conformance.
```jsonc
{
"tools": {
"denyTools": ["process"],
},
"agents": {
"workspace": {
"allowedAccess": ["none", "ro"],
},
},
"scopes": {
"release-agent-lockdown": {
"agentIds": ["release-agent"],
"agents": {
"workspace": {
"allowedAccess": ["none", "ro"],
},
},
"tools": {
"profiles": { "allow": ["minimal", "messaging"] },
"fs": { "requireWorkspaceOnly": true },
"exec": {
"allowSecurity": ["deny", "allowlist"],
"requireAsk": ["always"],
"allowHosts": ["sandbox"],
},
"elevated": { "allow": false },
"alsoAllow": { "expected": ["message", "read"] },
"denyTools": ["exec", "process", "write", "edit", "apply_patch"],
},
},
},
}
```
`agents.workspace` remains the existing all-agent workspace baseline.
`scopes.<scopeName>` is a scoped overlay, not a replacement for global
policy. The scope name is descriptive only; matching uses `agentIds`, not
display names. It deliberately contains normal section names instead of a
bespoke per-agent mini-grammar.
Every scope present in `policy.jsonc` must be valid and enforceable. In this
PR, the only supported selector is `agentIds`, and it supports only `tools.*`
and `agents.workspace.*`.
## Layering semantics
Policy evaluation is additive:
1. Top-level policy applies to all matching evidence.
2. Existing `agents.workspace` applies to defaults and every listed agent.
3. `scopes.<scopeName>` applies to evidence for each normalized runtime
id in `agentIds`.
4. Multiple scope blocks may target the same agent when they govern
different fields, or when a later value for the same field is equally or
more restrictive according to policy metadata.
5. A named-agent overlay can tighten policy, but it cannot make a global
violation acceptable.
If both global and agent-scoped rules fail, findings should point at the rule
that was violated:
```text
oc://policy.jsonc/tools/denyTools
oc://policy.jsonc/scopes/release-agent-lockdown/tools/denyTools
oc://policy.jsonc/scopes/release-agent-lockdown/agents/workspace/allowedAccess
```
That keeps broad tool posture, named-agent tool posture, and workspace posture
auditable as separate requirements even when they observe the same config
fields.
Exact-list claims such as `tools.alsoAllow.expected` compare the configured list
to the expected list and report both missing expected entries and unexpected
extra entries. This is intended for additive posture such as `alsoAllow`, where
one extra entry can widen an agent beyond its reviewed role.
## Policy and config layering
The overlay model separates where policy is authored from where OpenClaw config
is observed:
| Policy scope | Observed config | Applies to | Example result |
| --------------------------------------- | ---------------------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------- |
| Top-level `tools.*` | Global `tools.*` and inherited agent tool posture | All agents using matching posture | Deny `gateway` exec host for every agent unless the global policy allows it. |
| Top-level `tools.*` | `agents.list[].tools.*` overrides | Any agent with an override | Flag one agent that overrides `tools.exec.host` to an unapproved value. |
| `scopes.<scopeName>.tools.*` | Matching `agents.list[]` entry and inherited posture | Only that named agent | Let most agents use `node` exec host while one agent must use only `sandbox`. |
| `agents.workspace` | Defaults and every listed agent workspace posture | Defaults and all listed agents | Require every agent workspace access to be `none` or `ro`. |
| `scopes.<scopeName>.agents.workspace.*` | Matching `agents.list[]` workspace posture | Only that named agent | Require one agent to be read-only without requiring the same for `main`. |
Per-agent overlays are additive. A named-agent rule can be stricter than the
top-level rule, but it cannot make a global violation acceptable. For allow-list
rules, the effective allowed set is the intersection of the global rule and the
named-agent overlay when both are present.
For example, if top-level `tools.exec.allowHosts` permits `["sandbox", "node"]`
and `scopes.release-agent-lockdown.tools.exec.allowHosts` permits only
`["sandbox"]`, `release-agent` fails when its effective exec host is `node`;
another agent can still pass
with `node`.
## Tool posture versus workspace posture
Tool posture belongs under `tools` because it describes what tool behavior a
configuration may expose. The existing `tools.*` policy observes both global
`tools.*` config and per-agent `agents.list[].tools.*` overrides.
Workspace posture belongs under `workspace` because it describes sandbox mode
and workspace access. The workspace section should not grow into a general tool
policy namespace. If one agent needs stricter tool restrictions to make its
workspace posture meaningful, put those restrictions in the same agent overlay
under `scopes.<scopeName>.tools`.
For a restricted release agent, the intended split is:
```jsonc
{
"scopes": {
"release-agent-lockdown": {
"agentIds": ["release-agent"],
"agents": {
"workspace": { "allowedAccess": ["none", "ro"] },
},
"tools": {
"denyTools": ["exec", "process", "write", "edit", "apply_patch"],
},
},
},
}
```
## Section eligibility
An agent-scoped section should be added only when policy evidence carries an
agent id or can be attributed to one without guessing.
| Section | Initial agent-scoped status | Reason |
| ----------- | --------------------------- | ------------------------------------------------------------------------ |
| `workspace` | Include | Agent sandbox/workspace evidence already has agent identity. |
| `tools` | Include | Tool posture evidence includes global and per-agent tool config. |
| `sandbox` | Pipeline follow-up | Keep out until the sandbox posture PR lands and evidence can be scoped. |
| `ingress` | Pipeline follow-up | Keep out until ingress/channel posture lands with agent attribution. |
| `models` | Include when mapped | Selected model refs can be agent-specific. |
| `mcp` | Include when mapped | Use only when MCP server evidence is attributable to an agent. |
| `auth` | Defer | Auth profile metadata is a config catalog unless agent binding is clear. |
| `channels` | Defer | Channel provider posture is deployment-level until routing is scoped. |
| `gateway` | Keep global | Gateway exposure/auth/http posture is process-level. |
| `network` | Keep global | Private-network SSRF posture is runtime-level. |
| `secrets` | Keep global first | Secret provider posture is shared unless refs are agent-attributed. |
## Compatibility
The implementation is additive:
- keep all existing top-level policy fields valid;
- keep `agents.workspace` semantics unchanged;
- validate `scopes` before evaluating scoped rules;
- reject unsupported scoped sections clearly until their evidence and policy
contracts are implemented;
- do not reinterpret top-level `tools.requireMetadata` as agent-scoped, because
tool metadata describes the declared workspace tool catalog;
- include agent-scoped evidence in the attestation hash when any scoped rule is
present.
This lets broad tool posture remain a top-level policy contract while named
agents add stricter observable claims without weakening the global baseline.

View File

@@ -42,7 +42,7 @@ Capabilities are the public **native plugin** model inside OpenClaw. Every nativ
| Realtime transcription | `api.registerRealtimeTranscriptionProvider(...)` | `openai` |
| Realtime voice | `api.registerRealtimeVoiceProvider(...)` | `openai` |
| Media understanding | `api.registerMediaUnderstandingProvider(...)` | `openai`, `google` |
| Transcripts source | `api.registerTranscriptSourceProvider(...)` | `discord` |
| Meeting notes source | `api.registerMeetingNotesSourceProvider(...)` | `discord`, `meeting-notes` |
| Image generation | `api.registerImageGenerationProvider(...)` | `openai`, `google`, `fal`, `minimax` |
| Music generation | `api.registerMusicGenerationProvider(...)` | `google`, `minimax` |
| Video generation | `api.registerVideoGenerationProvider(...)` | `qwen` |

View File

@@ -353,17 +353,17 @@ If discovery fails or times out, OpenClaw uses a bundled fallback catalog for:
- GPT-5.4 mini
- GPT-5.2
The current bundled harness is `@openai/codex` `0.133.0`. A `model/list` probe
The current bundled harness is `@openai/codex` `0.132.0`. A `model/list` probe
against that bundled app-server returned:
| Model id | Default | Hidden | Input modalities | Reasoning efforts |
| --------------------- | ------- | ------ | ---------------- | ------------------------ |
| `gpt-5.5` | Yes | No | text, image | low, medium, high, xhigh |
| `gpt-5.4` | No | No | text, image | low, medium, high, xhigh |
| `gpt-5.4-mini` | No | No | text, image | low, medium, high, xhigh |
| `gpt-5.3-codex` | No | No | text, image | low, medium, high, xhigh |
| `gpt-5.3-codex-spark` | No | No | text | low, medium, high, xhigh |
| `gpt-5.2` | No | No | text, image | low, medium, high, xhigh |
| Model id | Default | Hidden | Input modalities | Reasoning efforts |
| ------------------- | ------- | ------ | ---------------- | ------------------------ |
| `gpt-5.5` | Yes | No | text, image | low, medium, high, xhigh |
| `gpt-5.4` | No | No | text, image | low, medium, high, xhigh |
| `gpt-5.4-mini` | No | No | text, image | low, medium, high, xhigh |
| `gpt-5.3-codex` | No | No | text, image | low, medium, high, xhigh |
| `gpt-5.2` | No | No | text, image | low, medium, high, xhigh |
| `codex-auto-review` | No | Yes | text, image | low, medium, high, xhigh |
Hidden models can be returned by the app-server catalog for internal or
specialized flows, but they are not normal model-picker choices.

View File

@@ -27,10 +27,8 @@ native Codex turn receives Codex app-server developer instructions, while an
explicit PI compatibility route keeps the normal OpenClaw/PI system prompt even
when it uses Codex-flavored OpenAI auth or transport.
Native Codex keeps Codex-owned base/model instructions and project-doc behavior
according to the active Codex thread config. OpenClaw starts and resumes native
Codex threads with Codex's built-in personality disabled so workspace
personality files and OpenClaw agent identity stay authoritative. Lightweight
Native Codex keeps Codex-owned base/model/personality instructions and
project-doc behavior according to the active Codex thread config. Lightweight
OpenClaw runs still preserve their existing project-doc suppression. OpenClaw
developer instructions cover OpenClaw runtime concerns such as source-channel
delivery, OpenClaw dynamic tools, ACP delegation, adapter context, and the
@@ -117,19 +115,19 @@ They do not invoke OpenClaw plugin hooks.
Supported in Codex runtime v1:
| Surface | Support | Why |
| --------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| OpenAI model loop through Codex | Supported | Codex app-server owns the OpenAI turn, native thread resume, and native tool continuation. |
| OpenClaw channel routing and delivery | Supported | Telegram, Discord, Slack, WhatsApp, iMessage, and other channels stay outside the model runtime. |
| OpenClaw dynamic tools | Supported | Codex asks OpenClaw to execute these tools, so OpenClaw stays in the execution path. |
| Prompt and context plugins | Supported | OpenClaw projects OpenClaw-specific prompt/context into the Codex turn while leaving Codex-owned base, model, and configured project-doc prompts in the native Codex lane. OpenClaw disables Codex's built-in personality for native threads so agent workspace personality files remain authoritative. Native Codex developer instructions accept only command guidance explicitly scoped to `codex_app_server`; legacy global command hints remain for non-Codex prompt surfaces. |
| Context engine lifecycle | Supported | Assemble, ingest, and after-turn maintenance run around Codex turns. Context engines do not replace native Codex compaction. |
| Dynamic tool hooks | Supported | `before_tool_call`, `after_tool_call`, and tool-result middleware run around OpenClaw-owned dynamic tools. |
| Lifecycle hooks | Supported as adapter observations | `llm_input`, `llm_output`, `agent_end`, `before_compaction`, and `after_compaction` fire with honest Codex-mode payloads. |
| Final-answer revision gate | Supported through native hook relay | Codex `Stop` is relayed to `before_agent_finalize`; `revise` asks Codex for one more model pass before finalization. |
| Native shell, patch, and MCP block or observe | Supported through native hook relay | Codex `PreToolUse` and `PostToolUse` are relayed for committed native tool surfaces, including MCP payloads on Codex app-server `0.125.0` or newer. Blocking is supported; argument rewriting is not. |
| Native permission policy | Supported through Codex app-server approvals and compatibility native hook relay | Codex app-server approval requests route through OpenClaw after Codex review. The `PermissionRequest` native hook relay is opt-in for native approval modes because Codex emits it before guardian review. |
| App-server trajectory capture | Supported | OpenClaw records the request it sent to app-server and the app-server notifications it receives. |
| Surface | Support | Why |
| --------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| OpenAI model loop through Codex | Supported | Codex app-server owns the OpenAI turn, native thread resume, and native tool continuation. |
| OpenClaw channel routing and delivery | Supported | Telegram, Discord, Slack, WhatsApp, iMessage, and other channels stay outside the model runtime. |
| OpenClaw dynamic tools | Supported | Codex asks OpenClaw to execute these tools, so OpenClaw stays in the execution path. |
| Prompt and context plugins | Supported | OpenClaw projects OpenClaw-specific prompt/context into the Codex turn while leaving Codex-owned base, model, personality, and configured project-doc prompts in the native Codex lane. Native Codex developer instructions accept only command guidance explicitly scoped to `codex_app_server`; legacy global command hints remain for non-Codex prompt surfaces. |
| Context engine lifecycle | Supported | Assemble, ingest, and after-turn maintenance run around Codex turns. Context engines do not replace native Codex compaction. |
| Dynamic tool hooks | Supported | `before_tool_call`, `after_tool_call`, and tool-result middleware run around OpenClaw-owned dynamic tools. |
| Lifecycle hooks | Supported as adapter observations | `llm_input`, `llm_output`, `agent_end`, `before_compaction`, and `after_compaction` fire with honest Codex-mode payloads. |
| Final-answer revision gate | Supported through native hook relay | Codex `Stop` is relayed to `before_agent_finalize`; `revise` asks Codex for one more model pass before finalization. |
| Native shell, patch, and MCP block or observe | Supported through native hook relay | Codex `PreToolUse` and `PostToolUse` are relayed for committed native tool surfaces, including MCP payloads on Codex app-server `0.125.0` or newer. Blocking is supported; argument rewriting is not. |
| Native permission policy | Supported through Codex app-server approvals and compatibility native hook relay | Codex app-server approval requests route through OpenClaw after Codex review. The `PermissionRequest` native hook relay is opt-in for native approval modes because Codex emits it before guardian review. |
| App-server trajectory capture | Supported | OpenClaw records the request it sent to app-server and the app-server notifications it receives. |
Not supported in Codex runtime v1:

View File

@@ -649,7 +649,7 @@ Each list is optional:
| `realtimeVoiceProviders` | `string[]` | Realtime-voice provider ids this plugin owns. |
| `memoryEmbeddingProviders` | `string[]` | Memory embedding provider ids this plugin owns. |
| `mediaUnderstandingProviders` | `string[]` | Media-understanding provider ids this plugin owns. |
| `transcriptSourceProviders` | `string[]` | Transcript source provider ids this plugin owns. |
| `meetingNotesSourceProviders` | `string[]` | Meeting-notes source provider ids this plugin owns. |
| `imageGenerationProviders` | `string[]` | Image-generation provider ids this plugin owns. |
| `videoGenerationProviders` | `string[]` | Video-generation provider ids this plugin owns. |
| `webFetchProviders` | `string[]` | Web-fetch provider ids this plugin owns. |
@@ -865,11 +865,6 @@ Fields:
| `modelPrefixes` | `string[]` | Prefixes matched with `startsWith` against shorthand model ids. |
| `modelPatterns` | `string[]` | Regex sources matched against shorthand model ids after profile suffix removal. |
`modelPatterns` entries are compiled through `compileSafeRegex`, which rejects
patterns containing nested repetition (for example `(a+)+$`). Patterns that fail
the safety check are silently skipped, the same as syntactically invalid regex.
Keep patterns simple and avoid nested quantifiers.
## modelCatalog reference
Use `modelCatalog` when OpenClaw should know provider model metadata before

View File

@@ -0,0 +1,359 @@
---
summary: "Meeting Notes plugin: capture transcripts from Discord voice and imported meeting sources, then write summaries"
read_when:
- You want OpenClaw to take meeting notes
- You are wiring Discord voice, Google Meet, Slack huddles, or another meeting source into notes
- You need the meeting_notes tool contract
title: "Meeting Notes plugin"
---
The Meeting Notes plugin is the generic notes layer for live calls and imported
meeting transcripts. It owns transcript storage, summary rendering, and the
`meeting_notes` tool. Channel plugins own capture, authentication, and
platform-specific meeting joins.
Use this page when you want OpenClaw to capture Discord voice notes today, when
you want to import a transcript from another meeting system, or when you are
building a Google Meet, Slack huddle, Zoom, or calendar-owned source provider.
## Source model
Meeting sources register `meetingNotesSourceProviders` through the plugin SDK.
The first live provider is `discord-voice`; the built-in `manual-transcript`
provider imports post-meeting transcripts.
- `live-audio`: source joins or listens to a call and streams final utterances.
- `live-caption`: source reads captions from a browser or meeting surface.
- `posthoc-transcript`: source imports a transcript or notes artifact after the meeting.
- `recording-stt`: source transcribes a recording before importing utterances.
This keeps Discord, Google Meet, Slack huddles, and future meeting surfaces out
of the notes engine. Each source supplies speaker-labeled utterances; Meeting
Notes writes the artifacts and summary.
## Install and enable
Meeting Notes is an external source plugin in this repository. It is not part of
the core OpenClaw npm package and becomes available only when the plugin is
installed as a plugin or loaded from a source checkout that contains
`extensions/meeting-notes`.
Once the plugin is loaded, it is enabled by default unless one of these settings
blocks it:
- `plugins.enabled: false` disables all plugins.
- `plugins.deny` contains `meeting-notes`.
- `plugins.allow` is set and does not contain `meeting-notes`.
- `plugins.entries.meeting-notes.enabled: false` disables this plugin entry.
- `plugins.entries.meeting-notes.config.enabled: false` keeps the plugin loaded
but disables the `meeting_notes` tool and auto-start service.
The normal user config file is `~/.openclaw/openclaw.json`. The `plugins`
section controls plugin loading, and the nested `entries.<pluginId>.config`
object is passed to that plugin as plugin-specific config. A separate
`config: { ... }` block under `meeting-notes` is expected; it is how plugins
receive their own options without adding core config keys.
Use this shape when your config has a plugin allowlist:
```json5
{
plugins: {
allow: ["discord", "meeting-notes"],
entries: {
"meeting-notes": {
enabled: true,
config: {
enabled: true,
maxUtterances: 2000,
autoStart: [],
},
},
},
},
}
```
Run a config check after editing:
```bash
openclaw config validate
```
Gateway config hot reload applies plugin allowlist and plugin-entry changes.
Restart the Gateway if you are also changing the source plugin itself, installing
new plugin files, or changing Discord voice credentials.
## Configuration
Meeting Notes has three plugin config fields:
- `enabled`: `true` by default. Set `false` to leave the plugin installed but
disable the tool and auto-start service.
- `maxUtterances`: `2000` by default. Summary generation reads only the newest
N utterances from `transcript.jsonl`; valid values are clamped to `1` through
`10000`.
- `autoStart`: empty by default. Each entry starts a live notes source when the
Gateway starts or reloads the plugin.
An `autoStart` entry accepts:
- `providerId`: required. Use `discord-voice` for Discord voice.
- `enabled`: optional, default `true`. Set `false` to keep an entry without
starting it.
- `sessionId`: optional. If omitted, OpenClaw generates a timestamped id.
- `title`: optional human-readable title for summaries and CLI output.
- `accountId`: optional source account id when more than one account exists.
- `guildId`: provider-specific Discord guild id.
- `channelId`: provider-specific Discord voice channel id.
- `meetingUrl`: provider-specific meeting URL for browser or calendar sources.
Use `autoStart` when OpenClaw should begin notes capture automatically on
gateway startup:
```json5
{
plugins: {
entries: {
"meeting-notes": {
config: {
autoStart: [
{
providerId: "discord-voice",
guildId: "123",
channelId: "456",
title: "Weekly planning",
},
],
},
},
},
},
}
```
Auto-start retries startup failures up to 12 times with a five-second delay.
This lets the notes service wait for channel plugins such as Discord to finish
initializing. Sessions that were started by auto-start are stopped and summarized
when the plugin service stops cleanly.
Discord voice capture still needs normal Discord voice setup and permissions.
See [Discord voice](/channels/discord#voice-mode).
## Discord voice
Discord is the first live source. The Discord plugin owns the voice connection,
speaker detection, audio decoding, and transcription. Meeting Notes receives
final speaker-labeled utterances and persists them.
For Discord live capture:
- Enable and configure the Discord plugin first.
- Configure Discord voice mode so OpenClaw can join the target voice channel.
- Use `providerId: "discord-voice"`.
- Provide `guildId` and `channelId`.
- Add `accountId` only when you run more than one Discord account.
The transcription model is not chosen by Meeting Notes. In Discord `stt-tts`
voice mode, STT uses `tools.media.audio`; `voice.model` controls the agent reply
model, not transcription. In realtime voice modes, transcription follows the
configured realtime provider and model. See [Discord voice](/channels/discord#voice-mode)
for the current Discord voice model and provider knobs.
## Google Meet, Slack huddles, and other sources
Meeting Notes is intentionally source-neutral. Google Meet, Slack huddles, Zoom,
calendar recordings, or browser caption capture should be separate source
providers that register with the plugin SDK.
Recommended source choices:
- Google Meet live browser/caption support: implement a `live-caption` provider
that accepts `meetingUrl` and emits final caption utterances.
- Google Meet recordings or downloaded transcripts: implement
`posthoc-transcript` or use `manual-transcript` until a provider exists.
- Slack huddles today: import post-meeting huddle notes or transcript artifacts.
Slack does not expose a general bot-join live huddle audio API.
- Slack huddles later: keep the Slack-owned source provider responsible for
Slack auth, artifact lookup, and transcript normalization.
The notes engine should not contain platform joins, browser automation, Slack
API polling, or Discord voice logic. Those belong to the owning source plugin.
## Tool
Use `meeting_notes` with an `action`:
- `status`: list registered providers and active sessions.
- `start`: start a live notes session.
- `stop`: stop a live session and write `summary.md`.
- `import`: import a transcript and write `summary.md`.
- `summarize`: regenerate a summary for an existing session.
Discord live notes require `providerId: "discord-voice"`, plus `guildId` and
`channelId`. `accountId` is optional when only one Discord account is active.
```json
{
"action": "start",
"providerId": "discord-voice",
"guildId": "123",
"channelId": "456",
"title": "Weekly planning"
}
```
Stop by session id:
```json
{
"action": "stop",
"sessionId": "meeting-2026-05-22T10-00-00-000Z-a1b2c3d4"
}
```
Import a transcript:
```json
{
"action": "import",
"providerId": "manual-transcript",
"title": "Design review",
"transcript": "Alex: We decided to ship the Discord source first.\nSam: Action item: add Slack huddle import later."
}
```
`manual-transcript` splits plain transcript text into utterances. Use it for
copied Google Meet notes, Slack huddle summaries, calendar transcripts, or any
source that already produced text.
## Storage layout
Artifacts are stored under the OpenClaw state directory:
```text
$OPENCLAW_STATE_DIR/meeting-notes/YYYY-MM-DD/<session>/
metadata.json
transcript.jsonl
summary.json
summary.md
```
If `OPENCLAW_STATE_DIR` is unset, the default state directory is
`~/.openclaw`. A normal local install therefore writes notes under
`~/.openclaw/meeting-notes/...`.
Each file has one job:
- `metadata.json`: session id, source provider, title, start time, stop time,
and provider metadata.
- `transcript.jsonl`: append-only speaker utterances. Each line is one JSON
object with the utterance text and the session id.
- `summary.json`: structured summary data used by tooling, including the
speaker-labeled transcript window used for the generated summary.
- `summary.md`: human-readable notes for terminals, editors, and document
workflows, including a speaker-labeled transcript section.
The date directory comes from the session start time, so multiple meetings per
day stay grouped. If a human session id repeats across days, use the
date-qualified selector from `openclaw meeting-notes list`, such as
`2026-05-22/standup`.
By default, OpenClaw generates timestamped session ids:
```text
meeting-2026-05-22T10-00-00-000Z-a1b2c3d4
```
That means ten meetings on the same day become ten sibling directories:
```text
~/.openclaw/meeting-notes/2026-05-22/
meeting-2026-05-22T09-00-00-000Z-a1b2c3d4/
meeting-2026-05-22T10-30-00-000Z-b2c3d4e5/
meeting-2026-05-22T13-00-00-000Z-c3d4e5f6/
```
Configure `sessionId` only when that id is unique for the day. Human ids such as
`standup` are fine for one recurring meeting per day. If the same id appears on
multiple days, use the date-qualified selector in the CLI.
## CLI access
Use the read-only CLI to find or print stored summaries:
```bash
openclaw meeting-notes list
openclaw meeting-notes show <session>
openclaw meeting-notes path <session>
openclaw meeting-notes path <session> --transcript
```
See [Meeting Notes CLI](/cli/meeting-notes) for the full command reference.
## Long meetings
For long meetings, utterances are appended to `transcript.jsonl` as they arrive.
Summary generation reads a bounded window controlled by
`plugins.entries.meeting-notes.config.maxUtterances` (default: `2000`) so a
multi-hour call does not require unbounded summary memory.
This means the transcript can keep growing on disk, while summarization stays
bounded. Increase `maxUtterances` when you need more of a multi-hour meeting in
the generated summary and speaker-labeled transcript section. Decrease it when
summaries are too slow or too large.
Current summaries are generated when a session stops, after an import, or when
the `summarize` action runs. They are not continuously rewritten for every
utterance.
## Troubleshooting
### `meeting_notes` is missing
Check that the plugin is installed or loaded from source, and that plugin
loading does not exclude it:
```bash
openclaw config validate
openclaw meeting-notes list
```
If `plugins.allow` is set, it must include `meeting-notes`. If `plugins.deny`
contains `meeting-notes`, remove it.
### Auto-start does not join Discord
Confirm the `autoStart` entry uses `providerId: "discord-voice"` and includes
both `guildId` and `channelId`. If you run multiple Discord accounts, include
`accountId`. Also verify Discord voice works outside Meeting Notes by joining
the same voice channel through the Discord voice commands.
### Summary is missing
Live sessions write `summary.md` when stopped. Stop the session with
`meeting_notes` action `stop`, then inspect it:
```bash
openclaw meeting-notes list
openclaw meeting-notes path <session>
```
Use `meeting_notes` action `summarize` to regenerate `summary.md` for an
existing stored session.
### Selector is ambiguous
If you reused a human session id such as `standup`, use the date-qualified
selector shown by `openclaw meeting-notes list`:
```bash
openclaw meeting-notes show 2026-05-22/standup
```
## Related
- [Meeting Notes CLI](/cli/meeting-notes)
- [Discord voice](/channels/discord#voice-mode)
- [Plugin management](/tools/plugin)
- [Plugin architecture](/plugins/architecture)

View File

@@ -151,7 +151,7 @@ commands.
| [diagnostics-otel](/plugins/reference/diagnostics-otel) | OpenClaw diagnostics OpenTelemetry exporter. | `@openclaw/diagnostics-otel`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-otel` | plugin |
| [diagnostics-prometheus](/plugins/reference/diagnostics-prometheus) | OpenClaw diagnostics Prometheus exporter. | `@openclaw/diagnostics-prometheus`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-prometheus` | plugin |
| [diffs](/plugins/reference/diffs) | Read-only diff viewer and file renderer for agents. | `@openclaw/diffs`<br />npm; ClawHub | contracts: tools; skills |
| [discord](/plugins/reference/discord) | Adds the Discord channel surface for sending and receiving OpenClaw messages. | `@openclaw/discord`<br />npm; ClawHub | channels: discord; contracts: transcriptSourceProviders |
| [discord](/plugins/reference/discord) | Adds the Discord channel surface for sending and receiving OpenClaw messages. | `@openclaw/discord`<br />npm; ClawHub | channels: discord; contracts: meetingNotesSourceProviders |
| [feishu](/plugins/reference/feishu) | Adds the Feishu channel surface for sending and receiving OpenClaw messages. | `@openclaw/feishu`<br />npm; ClawHub | channels: feishu; contracts: tools; skills |
| [google-meet](/plugins/reference/google-meet) | Join Google Meet calls through Chrome or Twilio transports. | `@openclaw/google-meet`<br />npm; ClawHub | contracts: tools |
| [googlechat](/plugins/reference/googlechat) | Adds the Google Chat channel surface for sending and receiving OpenClaw messages. | `@openclaw/googlechat`<br />npm; ClawHub | channels: googlechat |
@@ -175,8 +175,9 @@ commands.
## Source checkout only
| Plugin | Description | Distribution | Surface |
| ------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------ | -------------------- |
| [qa-channel](/plugins/reference/qa-channel) | Adds the QA Channel surface for sending and receiving OpenClaw messages. | `@openclaw/qa-channel`<br />source checkout only | channels: qa-channel |
| [qa-lab](/plugins/reference/qa-lab) | OpenClaw QA lab plugin with private debugger UI and scenario runner. | `@openclaw/qa-lab`<br />source checkout only | plugin |
| [qa-matrix](/plugins/reference/qa-matrix) | Matrix QA transport runner and substrate. | `@openclaw/qa-matrix`<br />source checkout only | plugin |
| Plugin | Description | Distribution | Surface |
| ------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------- |
| [meeting-notes](/plugins/reference/meeting-notes) | Capture meeting transcripts from channel-owned sources and write summaries. | `@openclaw/meeting-notes`<br />source checkout only | contracts: meetingNotesSourceProviders, tools |
| [qa-channel](/plugins/reference/qa-channel) | Adds the QA Channel surface for sending and receiving OpenClaw messages. | `@openclaw/qa-channel`<br />source checkout only | channels: qa-channel |
| [qa-lab](/plugins/reference/qa-lab) | OpenClaw QA lab plugin with private debugger UI and scenario runner. | `@openclaw/qa-lab`<br />source checkout only | plugin |
| [qa-matrix](/plugins/reference/qa-matrix) | Matrix QA transport runner and substrate. | `@openclaw/qa-matrix`<br />source checkout only | plugin |

View File

@@ -44,7 +44,7 @@ pnpm plugins:inventory:gen
| [diagnostics-otel](/plugins/reference/diagnostics-otel) | OpenClaw diagnostics OpenTelemetry exporter. | `@openclaw/diagnostics-otel`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-otel` | plugin |
| [diagnostics-prometheus](/plugins/reference/diagnostics-prometheus) | OpenClaw diagnostics Prometheus exporter. | `@openclaw/diagnostics-prometheus`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-prometheus` | plugin |
| [diffs](/plugins/reference/diffs) | Read-only diff viewer and file renderer for agents. | `@openclaw/diffs`<br />npm; ClawHub | contracts: tools; skills |
| [discord](/plugins/reference/discord) | Adds the Discord channel surface for sending and receiving OpenClaw messages. | `@openclaw/discord`<br />npm; ClawHub | channels: discord; contracts: transcriptSourceProviders |
| [discord](/plugins/reference/discord) | Adds the Discord channel surface for sending and receiving OpenClaw messages. | `@openclaw/discord`<br />npm; ClawHub | channels: discord; contracts: meetingNotesSourceProviders |
| [document-extract](/plugins/reference/document-extract) | Extract text and fallback page images from local document attachments. | `@openclaw/document-extract-plugin`<br />included in OpenClaw | contracts: documentExtractors |
| [duckduckgo](/plugins/reference/duckduckgo) | Adds web search provider support. | `@openclaw/duckduckgo-plugin`<br />included in OpenClaw | contracts: webSearchProviders |
| [elevenlabs](/plugins/reference/elevenlabs) | Adds media understanding provider support. Adds realtime transcription provider support. Adds text-to-speech provider support. | `@openclaw/elevenlabs-speech`<br />included in OpenClaw | contracts: mediaUnderstandingProviders, realtimeTranscriptionProviders, speechProviders |
@@ -73,6 +73,7 @@ pnpm plugins:inventory:gen
| [lobster](/plugins/reference/lobster) | Typed workflow tool with resumable approvals. | `@openclaw/lobster`<br />npm; ClawHub | contracts: tools |
| [matrix](/plugins/reference/matrix) | Adds the Matrix channel surface for sending and receiving OpenClaw messages. | `@openclaw/matrix`<br />ClawHub: `clawhub:@openclaw/matrix`; npm | channels: matrix |
| [mattermost](/plugins/reference/mattermost) | Adds the Mattermost channel surface for sending and receiving OpenClaw messages. | `@openclaw/mattermost`<br />included in OpenClaw | channels: mattermost |
| [meeting-notes](/plugins/reference/meeting-notes) | Capture meeting transcripts from channel-owned sources and write summaries. | `@openclaw/meeting-notes`<br />source checkout only | contracts: meetingNotesSourceProviders, tools |
| [memory-core](/plugins/reference/memory-core) | Adds memory embedding provider support. Adds agent-callable tools. | `@openclaw/memory-core`<br />included in OpenClaw | contracts: memoryEmbeddingProviders, tools |
| [memory-lancedb](/plugins/reference/memory-lancedb) | Adds agent-callable tools. | `@openclaw/memory-lancedb`<br />npm; ClawHub | contracts: tools |
| [memory-wiki](/plugins/reference/memory-wiki) | Persistent wiki compiler and Obsidian-friendly knowledge vault for OpenClaw. | `@openclaw/memory-wiki`<br />included in OpenClaw | contracts: tools; skills |

View File

@@ -16,7 +16,7 @@ Adds the Discord channel surface for sending and receiving OpenClaw messages.
## Surface
channels: discord; contracts: transcriptSourceProviders
channels: discord; contracts: meetingNotesSourceProviders
## Related docs

Some files were not shown because too many files have changed in this diff Show More