Compare commits

..

4 Commits

Author SHA1 Message Date
Dallin Romney
4fa026e46a test: fold plugin lifecycle probe into qa e2e 2026-06-16 14:00:06 -07:00
Dallin Romney
5316b1dbad test: point qa code refs at migrated e2e 2026-06-16 14:00:06 -07:00
Dallin Romney
a61ba9e140 test: migrate script checks into qa e2e 2026-06-16 14:00:06 -07:00
Dallin Romney
3c2f21e9b6 test: fold script coverage into qa scenarios 2026-06-16 14:00:06 -07:00
841 changed files with 8223 additions and 21635 deletions

View File

@@ -91,32 +91,6 @@ attribution.
- if any compatibility `removeAfter` is on/before release date, resolve it
or explicitly record the blocker before shipping
10. Validate and ship:
- generate and verify the complete contribution ledger before committing:
```bash
node .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
--base <base-tag> \
--target <target-ref> \
--version <YYYY.M.PATCH> \
--write-ledger
```
- the command fails when any `#NNN` reference in release history or the
rendered release section is absent from the ledger, when reverted work is
presented as shipped, or when an eligible PR author, issue reporter, or
known co-author is missing from that entry's `Thanks @...` credit
- after the GitHub release or prerelease is published, verify every matching
release page against the same source section:
```bash
node .agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs \
--base <base-tag> \
--target <target-ref> \
--version <YYYY.M.PATCH> \
--release-tag v<YYYY.M.PATCH> \
--check-github
```
- add one `--release-tag` for every beta and stable page in the train; a
`### Release verification` tail is permitted, but any other body drift
fails the check; the GitHub body must begin with the complete
`## YYYY.M.PATCH` changelog section, including its heading
- `git diff --check`
- for docs/changelog-only changes, no broad tests are required
- commit with `scripts/committer "docs(changelog): refresh YYYY.M.PATCH notes" CHANGELOG.md`

View File

@@ -1,443 +0,0 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
const repo = "openclaw/openclaw";
const excludedHandles = new Set(["openclaw", "clawsweeper", "codex", "steipete"]);
function fail(message) {
throw new Error(message);
}
function parseArgs(argv) {
const options = {
releaseTags: [],
checkGithub: false,
json: false,
writeLedger: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--check-github" || arg === "--json" || arg === "--write-ledger") {
options[
arg === "--check-github"
? "checkGithub"
: arg === "--write-ledger"
? "writeLedger"
: "json"
] = true;
continue;
}
if (arg === "--base" || arg === "--target" || arg === "--version" || arg === "--release-tag") {
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
fail(`missing value for ${arg}`);
}
if (arg === "--release-tag") {
options.releaseTags.push(value);
} else {
options[arg.slice(2)] = value;
}
index += 1;
continue;
}
fail(`unknown argument: ${arg}`);
}
for (const name of ["base", "target", "version"]) {
if (!options[name]) {
fail(`--${name} is required`);
}
}
if (options.checkGithub && options.releaseTags.length === 0) {
fail("--check-github requires at least one --release-tag");
}
return options;
}
function run(command, args) {
return execFileSync(command, args, {
encoding: "utf8",
env: { ...process.env, NO_COLOR: "1" },
stdio: ["ignore", "pipe", "pipe"],
});
}
function git(args) {
return run("git", args).trimEnd();
}
function githubApi(args) {
try {
return JSON.parse(run("ghx", ["api", ...args]).replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, ""));
} catch (error) {
if (typeof error.stdout === "string" && error.stdout.trim() !== "") {
return JSON.parse(error.stdout.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, ""));
}
throw error;
}
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function isEligibleHandle(handle) {
return Boolean(handle) && !handle.endsWith("[bot]") && !excludedHandles.has(handle.toLowerCase());
}
function sectionFor(changelog, version) {
const heading = new RegExp(`^## ${escapeRegExp(version)}\\r?$`, "m").exec(changelog);
if (!heading || heading.index === undefined) {
fail(`CHANGELOG.md does not contain ## ${version}`);
}
const start = heading.index;
const bodyStart = changelog.indexOf("\n", start) + 1;
const next = /^## /gm;
next.lastIndex = bodyStart;
const nextHeading = next.exec(changelog);
const end = nextHeading?.index ?? changelog.length;
return {
start,
end,
source: changelog.slice(start, end).trimEnd(),
body: changelog.slice(bodyStart, end).trim(),
};
}
function referencesIn(text) {
return [...text.matchAll(/#(\d+)/g)].map((match) => Number(match[1]));
}
function appendReferences(references, additions) {
const seen = new Set(references);
for (const number of additions) {
if (!seen.has(number)) {
references.push(number);
seen.add(number);
}
}
}
function sourceCommits(base, target) {
const mergeBase = git(["merge-base", base, target]);
const output = git([
"log",
"--first-parent",
"--reverse",
"--format=%H%x1f%s%x1f%B%x1e",
`${mergeBase}..${target}`,
]);
const commits = new Map();
const revertsByTarget = new Map();
for (const record of output.split("\x1e")) {
if (!record) {
continue;
}
const [rawHash, subject, ...bodyParts] = record.split("\x1f");
const hash = rawHash.trim();
const body = bodyParts.join("\x1f");
const revertedHash = body.match(/This reverts commit ([0-9a-f]{7,40})\./i)?.[1];
const isRevert = subject.startsWith('Revert "') || Boolean(revertedHash);
commits.set(hash, { body, hash, isRevert, revertedHash, subject });
}
for (const commit of commits.values()) {
if (!commit.revertedHash) {
continue;
}
const targetHash = [...commits.keys()].find((candidate) => candidate.startsWith(commit.revertedHash));
if (targetHash) {
const reverts = revertsByTarget.get(targetHash) ?? [];
reverts.push(commit.hash);
revertsByTarget.set(targetHash, reverts);
}
}
const active = new Map();
function isActive(hash) {
if (active.has(hash)) {
return active.get(hash);
}
const cancellingReverts = revertsByTarget.get(hash) ?? [];
const value = !cancellingReverts.some((revertHash) => isActive(revertHash));
active.set(hash, value);
return value;
}
const references = [];
const revertedReferences = new Set();
const coauthorsByReference = new Map();
for (const commit of commits.values()) {
if (commit.isRevert) {
continue;
}
const uniqueReferences = [...new Set(referencesIn(`${commit.subject}\n${commit.body}`))];
if (!isActive(commit.hash)) {
for (const number of uniqueReferences) {
revertedReferences.add(number);
}
continue;
}
appendReferences(references, uniqueReferences);
const coauthors = [...commit.body.matchAll(/<(?:(?:\d+)\+)?([^@<>\s]+)@users\.noreply\.github\.com>/gi)]
.map((match) => match[1])
.filter(isEligibleHandle);
for (const number of uniqueReferences) {
if (coauthors.length > 0) {
const handles = coauthorsByReference.get(number) ?? new Set();
for (const handle of coauthors) {
handles.add(handle);
}
coauthorsByReference.set(number, handles);
}
}
}
return { mergeBase, references, revertedReferences, coauthorsByReference };
}
function graphql(query) {
return githubApi(["graphql", "-f", `query=${query}`]).data;
}
function resolveReferences(numbers) {
const nodes = new Map();
for (let index = 0; index < numbers.length; index += 40) {
const chunk = numbers.slice(index, index + 40);
const fields = chunk
.map(
(number) => `n${number}: repository(owner: "openclaw", name: "openclaw") {
issueOrPullRequest(number: ${number}) {
__typename
... on Issue { number title author { __typename login } }
... on PullRequest { number title author { __typename login } }
}
}`,
)
.join("\n");
const data = graphql(`query { ${fields} }`);
for (const number of chunk) {
const node = data[`n${number}`]?.issueOrPullRequest;
if (node) {
nodes.set(number, node);
}
}
}
return nodes;
}
function resolveCoauthors(handles) {
const resolved = new Map();
const uniqueHandles = [...new Set(handles)];
for (let index = 0; index < uniqueHandles.length; index += 80) {
const chunk = uniqueHandles.slice(index, index + 80);
const fields = chunk
.map(
(handle, offset) =>
`u${index + offset}: user(login: ${JSON.stringify(handle)}) { __typename login }`,
)
.join("\n");
const data = graphql(`query { ${fields} }`);
for (let offset = 0; offset < chunk.length; offset += 1) {
const user = data[`u${index + offset}`];
if (user?.__typename === "User" && isEligibleHandle(user.login)) {
resolved.set(chunk[offset].toLowerCase(), user.login);
}
}
}
return resolved;
}
function thanksFor(node, coauthorHandles) {
const handles = [];
if (node.author?.__typename === "User" && isEligibleHandle(node.author.login)) {
handles.push(node.author.login);
}
for (const handle of coauthorHandles) {
if (!handles.some((candidate) => candidate.toLowerCase() === handle.toLowerCase())) {
handles.push(handle);
}
}
return handles;
}
function ledgerFor(base, target, references, nodes, coauthorsByReference, resolvedCoauthors) {
const missing = references.filter((number) => !nodes.has(number));
if (missing.length > 0) {
fail(`GitHub could not resolve source references: ${missing.map((number) => `#${number}`).join(", ")}`);
}
const entries = references.map((number) => {
const node = nodes.get(number);
const rawCoauthors = coauthorsByReference.get(number) ?? new Set();
const coauthors = [...rawCoauthors]
.map((handle) => resolvedCoauthors.get(handle.toLowerCase()))
.filter(Boolean);
return {
number,
title: node.title.replace(/#(\d+)/g, "issue $1").replace(/\s+/g, " ").trim(),
type: node.__typename,
thanks: thanksFor(node, coauthors),
};
});
const pullRequests = entries.filter((entry) => entry.type === "PullRequest");
const issues = entries.filter((entry) => entry.type === "Issue");
const renderEntry = (entry, issue = false) => {
const attribution = entry.thanks.length > 0 ? ` Thanks ${entry.thanks.map((handle) => `@${handle}`).join(" and ")}.` : "";
return `- ${issue ? "Reported: " : ""}${entry.title} (#${entry.number}).${attribution}`;
};
const ledger = [
"### Complete contribution ledger",
"",
`This audited record covers the complete ${base}..${target} history: ${pullRequests.length} PRs and ${issues.length} linked issues. The grouped notes above prioritize user impact; this ledger preserves every contribution reference and eligible human credit.`,
"",
"#### Pull requests",
"",
...pullRequests.map((entry) => renderEntry(entry)),
"",
"#### Linked issues",
"",
...issues.map((entry) => renderEntry(entry, true)),
].join("\n");
return { entries, issues, ledger, pullRequests };
}
function replaceLedger(changelog, section, ledger) {
const beforeLedger = section.source.replace(/\n+### Complete contribution ledger[\s\S]*$/m, "").trimEnd();
const replacement = `${beforeLedger}\n\n${ledger}\n`;
return `${changelog.slice(0, section.start)}${replacement}${changelog.slice(section.end)}`;
}
function ledgerChecks(section, entries) {
const errors = [];
if (!section.source.includes("### Highlights")) {
errors.push("missing ### Highlights");
}
if (!section.source.includes("### Changes")) {
errors.push("missing ### Changes");
}
if (!section.source.includes("### Fixes")) {
errors.push("missing ### Fixes");
}
const ledgerStart = section.source.indexOf("### Complete contribution ledger");
if (ledgerStart < 0) {
errors.push("missing ### Complete contribution ledger");
return errors;
}
const ledger = section.source.slice(ledgerStart);
const entryNumbers = new Set(entries.map((entry) => entry.number));
for (const number of new Set(referencesIn(section.source))) {
if (!entryNumbers.has(number)) {
errors.push(`missing ledger entry for #${number}`);
}
}
for (const entry of entries) {
const prefix = entry.type === "Issue" ? "- Reported: " : "- ";
const line = ledger
.split("\n")
.find((candidate) => candidate.startsWith(prefix) && candidate.includes(`(#${entry.number})`));
if (!line) {
errors.push(`missing ledger entry for #${entry.number}`);
continue;
}
for (const handle of entry.thanks) {
if (!line.toLowerCase().includes(`@${handle.toLowerCase()}`)) {
errors.push(`missing Thanks @${handle} for #${entry.number}`);
}
}
}
return errors;
}
function releaseChecks(section, releaseTags) {
const expected = section.source;
const checks = [];
for (const tag of releaseTags) {
const release = githubApi([`repos/${repo}/releases/tags/${encodeURIComponent(tag)}`]);
const suffix = release.body.slice(expected.length).trimStart();
const matches =
release.body === expected ||
(release.body.startsWith(expected) && (suffix === "" || suffix.startsWith("### Release verification")));
checks.push({
tag,
releaseId: release.id,
matches,
bodyLength: release.body.length,
});
}
return checks;
}
function main() {
const options = parseArgs(process.argv.slice(2));
let changelog = readFileSync("CHANGELOG.md", "utf8");
let section = sectionFor(changelog, options.version);
const source = sourceCommits(options.base, options.target);
const preexistingNotes = section.source.replace(/\n+### Complete contribution ledger[\s\S]*$/m, "");
const noteReferences = referencesIn(preexistingNotes);
const revertedNoteReferences = noteReferences.filter((number) => source.revertedReferences.has(number));
if (revertedNoteReferences.length > 0) {
fail(
`release notes reference reverted work: ${[
...new Set(revertedNoteReferences),
]
.map((number) => `#${number}`)
.join(", ")}`,
);
}
const references = [...source.references];
appendReferences(references, noteReferences);
const nodes = resolveReferences(references);
const coauthorHandles = [...source.coauthorsByReference.values()].flatMap((handles) => [...handles]);
const resolvedCoauthors = resolveCoauthors(coauthorHandles);
const ledger = ledgerFor(
options.base,
options.target,
references,
nodes,
source.coauthorsByReference,
resolvedCoauthors,
);
if (options.writeLedger) {
changelog = replaceLedger(changelog, section, ledger.ledger);
writeFileSync("CHANGELOG.md", changelog);
section = sectionFor(changelog, options.version);
}
const errors = ledgerChecks(section, ledger.entries);
const github = options.checkGithub ? releaseChecks(section, options.releaseTags) : [];
for (const check of github) {
if (!check.matches) {
errors.push(`GitHub release ${check.tag} does not match the ${options.version} CHANGELOG section`);
}
}
const result = {
base: options.base,
target: options.target,
mergeBase: source.mergeBase,
version: options.version,
source: {
references: references.length,
pullRequests: ledger.pullRequests.length,
issues: ledger.issues.length,
},
github,
errors,
};
if (options.json) {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
} else {
process.stdout.write(
`${options.version}: ${ledger.pullRequests.length} PRs, ${ledger.issues.length} issues, ${errors.length === 0 ? "verified" : `${errors.length} errors`}\n`,
);
}
if (errors.length > 0) {
process.exitCode = 1;
}
}
main();

View File

@@ -100,26 +100,6 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
- `dev`: moving head on `main`
- When using a beta Git tag, publish npm with the matching beta version suffix so the plain version is not consumed or blocked
## Close stable releases on main
Stable publication is not complete until `main` carries the actual shipped release state.
1. Start from fresh latest `main`. Audit `release/YYYY.M.PATCH` against it and
forward-port real fixes that are absent from `main`. Do not blindly merge
release-only compatibility, test, or validation adapters into newer `main`.
2. Set `main` to the shipped stable version, not a speculative next train. Run
`pnpm release:prep` after the root version change, then
`pnpm deps:shrinkwrap:generate`.
3. Make `CHANGELOG.md`'s `## YYYY.M.PATCH` section on `main` exactly match the
tagged release branch. Include the stable `appcast.xml` update when the mac
release published one.
4. Do not add `YYYY.M.PATCH+1`, a beta version, or an empty future changelog
section to `main` until the operator explicitly starts that release train.
5. Run `pnpm release:generated:check`, `pnpm deps:shrinkwrap:check`, and
`OPENCLAW_TESTBOX=1 pnpm check:changed`. Push, then verify `origin/main`
contains the shipped version and changelog before calling the stable release
done.
## Handle versions and release files consistently
- Version locations include:
@@ -225,11 +205,6 @@ Stable publication is not complete until `main` carries the actual shipped relea
`CHANGELOG.md` version section, not highlights or an excerpt. When creating
or editing a release, extract from `## YYYY.M.PATCH` through the line before the
next level-2 heading and use that complete block as the release notes.
- Before publishing or closing a release, run
`$openclaw-changelog-update`'s `verify-release-notes.mjs` with every stable
and beta release tag in the train. Do not publish or leave a page live when
it is missing a source-history reference, eligible human credit, or the
complete matching changelog body.
- To update an existing GitHub Release body, resolve the numeric release id and
patch that resource with the notes file as the `body` field:
`gh api repos/openclaw/openclaw/releases/tags/vYYYY.M.PATCH --jq .id`, then
@@ -798,13 +773,13 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
and `.dSYM.zip` artifacts to the existing GitHub release in
`openclaw/openclaw`.
32. For stable releases, download `macos-appcast-<tag>` from the successful
private mac run, update `appcast.xml` on `main`, verify the feed, then
complete the **Close stable releases on main** gate.
private mac run, update `appcast.xml` on `main`, and verify the feed. Merge
or cherry-pick release branch changes back to `main` after stable succeeds.
33. For beta releases, publish the mac assets only when intentionally requested;
expect no shared production
`appcast.xml` artifact and do not update the shared production feed unless a
separate beta feed exists.
34. After stable main closeout, verify npm and the attached release artifacts.
34. After publish, verify npm and the attached release artifacts.
## GHSA advisory work

View File

@@ -6,10 +6,6 @@ on:
type: string
description: "Testbox session ID"
required: true
timeout_minutes:
type: number
description: "Maximum GitHub job runtime for long Testbox commands"
default: 120
pull_request:
paths:
- ".github/workflows/**"
@@ -29,7 +25,7 @@ jobs:
contents: read
name: "check"
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: ${{ fromJSON(inputs.timeout_minutes || '30') }}
timeout-minutes: 30
steps:
- name: Begin Testbox
uses: useblacksmith/begin-testbox@233448af4bfdc6fca509a7f0974411ac6d8a8043

View File

@@ -407,28 +407,12 @@ jobs:
const path = require("node:path");
const packageDir = process.env.PACKAGE_DIR;
function resolveTarballFileName(value, label) {
const fileName = typeof value === "string" ? value.trim() : "";
if (
!fileName.endsWith(".tgz") ||
fileName.includes("\0") ||
fileName !== path.basename(fileName) ||
fileName !== path.win32.basename(fileName)
) {
throw new Error(`${label} must be a local .tgz filename.`);
}
return fileName;
}
const requestedFileName = process.env.INPUT_CANDIDATE_FILE_NAME.trim();
const files = fs.readdirSync(packageDir).filter((file) => file.endsWith(".tgz"));
const selectedCandidateFileName = requestedFileName || (files.length === 1 ? files[0] : "");
if (!selectedCandidateFileName) {
const candidateFileName = requestedFileName || (files.length === 1 ? files[0] : "");
if (!candidateFileName) {
throw new Error(`Expected exactly one candidate .tgz in ${packageDir}; found ${files.length}.`);
}
const candidateFileName = resolveTarballFileName(
selectedCandidateFileName,
"candidate_file_name",
);
if (!fs.existsSync(path.join(packageDir, candidateFileName))) {
throw new Error(`Provided candidate artifact does not contain ${candidateFileName}.`);
}
@@ -490,23 +474,12 @@ jobs:
run: |
node <<'NODE' >>"$GITHUB_OUTPUT"
const fs = require("node:fs");
const path = require("node:path");
function resolveTarballFileName(value, label) {
const fileName = typeof value === "string" ? value.trim() : "";
if (
!fileName.endsWith(".tgz") ||
fileName.includes("\0") ||
fileName !== path.basename(fileName) ||
fileName !== path.win32.basename(fileName)
) {
throw new Error(`${label} must be a local .tgz filename.`);
}
return fileName;
}
const payload = JSON.parse(fs.readFileSync(process.env.BASELINE_PACK_JSON, "utf8"));
const entry = Array.isArray(payload) ? payload.at(-1) : null;
const fileName = resolveTarballFileName(entry?.filename, "Baseline npm pack filename");
process.stdout.write(`file_name=${fileName}\n`);
if (!entry?.filename) {
throw new Error("Baseline npm pack did not produce a filename.");
}
process.stdout.write(`file_name=${entry.filename}\n`);
NODE
- name: Upload candidate artifact

View File

@@ -223,25 +223,10 @@ jobs:
set -euo pipefail
PACK_OUTPUT="$RUNNER_TEMP/npm-pack-output.txt"
npm pack --json 2>&1 | tee "$PACK_OUTPUT"
PACK_NAME="$(node - "$PACK_OUTPUT" <<'NODE'
PACK_PATH="$(node - "$PACK_OUTPUT" <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const input = fs.readFileSync(process.argv[2], "utf8");
function resolveTarballFileName(value) {
const fileName = typeof value === "string" ? value.trim() : "";
if (
!fileName.endsWith(".tgz") ||
fileName.includes("\0") ||
fileName !== path.basename(fileName) ||
fileName !== path.win32.basename(fileName)
) {
console.error(`npm pack reported unsafe tarball filename ${JSON.stringify(fileName)}.`);
process.exit(1);
}
return fileName;
}
function arrayEndFrom(start) {
let depth = 0;
let inString = false;
@@ -281,8 +266,8 @@ jobs:
try {
const parsed = JSON.parse(input.slice(start, end));
const first = Array.isArray(parsed) ? parsed[0] : null;
if (first && Object.prototype.hasOwnProperty.call(first, "filename")) {
process.stdout.write(resolveTarballFileName(first.filename));
if (first && typeof first.filename === "string" && first.filename) {
process.stdout.write(first.filename);
process.exit(0);
}
} catch {
@@ -294,7 +279,6 @@ jobs:
process.exit(1);
NODE
)"
PACK_PATH="$PWD/$PACK_NAME"
if [[ -z "$PACK_PATH" || ! -f "$PACK_PATH" ]]; then
echo "npm pack did not produce a tarball file." >&2
exit 1
@@ -306,7 +290,7 @@ jobs:
else
RELEASE_TAG="${RELEASE_REF}"
fi
TARBALL_NAME="$PACK_NAME"
TARBALL_NAME="$(basename "$PACK_PATH")"
TARBALL_SHA256="$(sha256sum "$PACK_PATH" | awk '{print $1}')"
ARTIFACT_DIR="$RUNNER_TEMP/openclaw-npm-preflight"
rm -rf "$ARTIFACT_DIR"

File diff suppressed because it is too large Load Diff

View File

@@ -111,8 +111,6 @@ class MainViewModel(
val isConnected: StateFlow<Boolean> = runtimeState(initial = false) { it.isConnected }
val isNodeConnected: StateFlow<Boolean> = runtimeState(initial = false) { it.nodeConnected }
val nodeCapabilityApprovalState: StateFlow<GatewayNodeApprovalState> =
runtimeState(initial = GatewayNodeApprovalState.Loading) { it.nodeCapabilityApprovalState }
val statusText: StateFlow<String> = runtimeState(initial = "Offline") { it.statusText }
val gatewayConnectionProblem: StateFlow<GatewayConnectionProblem?> = runtimeState(initial = null) { it.gatewayConnectionProblem }
val serverName: StateFlow<String?> = runtimeState(initial = null) { it.serverName }

View File

@@ -69,7 +69,6 @@ import kotlinx.coroutines.withTimeout
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
@@ -302,8 +301,6 @@ class NodeRuntime(
val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()
private val _nodeConnected = MutableStateFlow(false)
val nodeConnected: StateFlow<Boolean> = _nodeConnected.asStateFlow()
private val _nodeCapabilityApprovalState = MutableStateFlow(GatewayNodeApprovalState.Loading)
val nodeCapabilityApprovalState: StateFlow<GatewayNodeApprovalState> = _nodeCapabilityApprovalState.asStateFlow()
private val _statusText = MutableStateFlow("Offline")
val statusText: StateFlow<String> = _statusText.asStateFlow()
@@ -398,7 +395,6 @@ class NodeRuntime(
val nodesDevicesRefreshing: StateFlow<Boolean> = _nodesDevicesRefreshing.asStateFlow()
private val _nodesDevicesErrorText = MutableStateFlow<String?>(null)
val nodesDevicesErrorText: StateFlow<String?> = _nodesDevicesErrorText.asStateFlow()
private val nodeApprovalRefreshGuard = GatewayNodeApprovalRefreshGuard()
private val _channelsSummary = MutableStateFlow(GatewayChannelsSummary(channels = emptyList()))
val channelsSummary: StateFlow<GatewayChannelsSummary> = _channelsSummary.asStateFlow()
private val _channelsRefreshing = MutableStateFlow(false)
@@ -456,7 +452,6 @@ class NodeRuntime(
},
onDisconnected = { message ->
operatorConnected = false
invalidateNodeCapabilityApprovalState()
operatorStatusText = message
_serverName.value = null
_remoteAddress.value = null
@@ -517,15 +512,12 @@ class NodeRuntime(
publishNodePresenceAliveBeacon(NodePresenceAliveBeacon.Trigger.Connect)
val endpoint = connectedEndpoint
val auth = activeGatewayAuth
if (operatorConnected) {
scope.launch { refreshNodesDevicesFromGateway() }
} else if (endpoint != null && auth != null) {
if (endpoint != null && auth != null) {
maybeStartOperatorSessionAfterNodeConnect(endpoint, auth)
}
},
onDisconnected = { message ->
_nodeConnected.value = false
invalidateNodeCapabilityApprovalState()
nodeStatusText = message
didAutoRequestCanvasRehydrate = false
_canvasA2uiHydrated.value = false
@@ -2017,42 +2009,21 @@ class NodeRuntime(
}
private suspend fun refreshNodesDevicesFromGateway() {
val refreshGeneration = nodeApprovalRefreshGuard.begin()
val refreshStarted =
nodeApprovalRefreshGuard.publishIfCurrent(refreshGeneration) {
_nodesDevicesRefreshing.value = true
_nodesDevicesErrorText.value = null
_nodeCapabilityApprovalState.value = GatewayNodeApprovalState.Loading
}
if (!refreshStarted) return
_nodesDevicesRefreshing.value = true
_nodesDevicesErrorText.value = null
if (!operatorConnected) {
nodeApprovalRefreshGuard.publishIfCurrent(refreshGeneration) {
_nodesDevicesSummary.value =
GatewayNodesDevicesSummary(
nodes = emptyList(),
pendingDevices = emptyList(),
pairedDevices = emptyList(),
)
_nodesDevicesRefreshing.value = false
}
_nodesDevicesSummary.value =
GatewayNodesDevicesSummary(
nodes = emptyList(),
pendingDevices = emptyList(),
pairedDevices = emptyList(),
)
_nodesDevicesRefreshing.value = false
return
}
try {
val nodesRes = operatorSession.request("node.list", "{}")
val nodesRoot = json.parseToJsonElement(nodesRes).asObjectOrNull()
val nodes = parseGatewayNodes(nodesRoot?.get("nodes") as? JsonArray)
val approvalState =
currentNodeCapabilityApprovalState(
nodes = nodes,
selfNodeId = identityStore.loadOrCreate().deviceId,
)
val publishedApproval =
nodeApprovalRefreshGuard.publishIfCurrent(refreshGeneration) {
_nodeCapabilityApprovalState.value = approvalState
}
if (!publishedApproval) {
return
}
val devicesRoot =
try {
val devicesRes = operatorSession.request("device.pair.list", "{}")
@@ -2060,30 +2031,16 @@ class NodeRuntime(
} catch (_: Throwable) {
null
}
nodeApprovalRefreshGuard.publishIfCurrent(refreshGeneration) {
_nodesDevicesSummary.value =
GatewayNodesDevicesSummary(
nodes = nodes,
pendingDevices = parsePendingDevices(devicesRoot?.get("pending") as? JsonArray),
pairedDevices = parsePairedDevices(devicesRoot?.get("paired") as? JsonArray),
devicePairingAvailable = devicesRoot != null,
)
}
_nodesDevicesSummary.value =
GatewayNodesDevicesSummary(
nodes = parseGatewayNodes(nodesRoot?.get("nodes") as? JsonArray),
pendingDevices = parsePendingDevices(devicesRoot?.get("pending") as? JsonArray),
pairedDevices = parsePairedDevices(devicesRoot?.get("paired") as? JsonArray),
devicePairingAvailable = devicesRoot != null,
)
} catch (_: Throwable) {
nodeApprovalRefreshGuard.publishIfCurrent(refreshGeneration) {
_nodesDevicesErrorText.value = "Could not load nodes and devices."
}
_nodesDevicesErrorText.value = "Could not load nodes and devices."
} finally {
nodeApprovalRefreshGuard.publishIfCurrent(refreshGeneration) {
_nodesDevicesRefreshing.value = false
}
}
}
private fun invalidateNodeCapabilityApprovalState() {
val refreshGeneration = nodeApprovalRefreshGuard.begin()
nodeApprovalRefreshGuard.publishIfCurrent(refreshGeneration) {
_nodeCapabilityApprovalState.value = GatewayNodeApprovalState.Loading
_nodesDevicesRefreshing.value = false
}
}
@@ -2332,8 +2289,22 @@ class NodeRuntime(
private fun parseGatewayNodes(nodes: JsonArray?): List<GatewayNodeSummary> =
nodes
?.mapNotNull(::parseGatewayNodeSummary)
.orEmpty()
?.mapNotNull { item ->
val obj = item.asObjectOrNull() ?: return@mapNotNull null
val id = obj["nodeId"].asStringOrNull()?.trim().orEmpty()
if (id.isEmpty()) return@mapNotNull null
GatewayNodeSummary(
id = id,
displayName = obj["displayName"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
remoteIp = obj["remoteIp"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
version = obj["version"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
deviceFamily = obj["deviceFamily"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
paired = obj.boolean("paired"),
connected = obj.boolean("connected"),
capabilities = parseStringArray(obj["caps"] as? JsonArray),
commands = parseStringArray(obj["commands"] as? JsonArray),
)
}.orEmpty()
private fun parsePendingDevices(devices: JsonArray?): List<GatewayPendingDeviceSummary> =
devices
@@ -2861,81 +2832,6 @@ data class GatewayNodesDevicesSummary(
val devicePairingAvailable: Boolean = true,
)
enum class GatewayNodeApprovalState {
Loading,
Unsupported,
Approved,
PendingApproval,
PendingReapproval,
Unapproved,
}
/** Prevents older node.list responses from overwriting newer approval state. */
internal class GatewayNodeApprovalRefreshGuard {
private val lock = Any()
private var generation = 0L
fun begin(): Long =
synchronized(lock) {
generation += 1
generation
}
fun publishIfCurrent(
refreshGeneration: Long,
publish: () -> Unit,
): Boolean =
synchronized(lock) {
if (refreshGeneration != generation) return@synchronized false
publish()
true
}
}
internal fun parseGatewayNodeApprovalState(raw: String?): GatewayNodeApprovalState =
when (raw?.trim()?.lowercase()) {
null, "" -> GatewayNodeApprovalState.Loading
"approved" -> GatewayNodeApprovalState.Approved
"pending-approval" -> GatewayNodeApprovalState.PendingApproval
"pending-reapproval" -> GatewayNodeApprovalState.PendingReapproval
"unapproved" -> GatewayNodeApprovalState.Unapproved
else -> GatewayNodeApprovalState.Loading
}
internal fun currentNodeCapabilityApprovalState(
nodes: List<GatewayNodeSummary>,
selfNodeId: String,
): GatewayNodeApprovalState =
nodes
.firstOrNull { it.id == selfNodeId }
?.approvalState
?: GatewayNodeApprovalState.Loading
internal fun parseGatewayNodeSummary(item: JsonElement): GatewayNodeSummary? {
val obj = item.asObjectOrNull() ?: return null
val id = obj["nodeId"].asStringOrNull()?.trim().orEmpty()
if (id.isEmpty()) return null
return GatewayNodeSummary(
id = id,
displayName = obj["displayName"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
remoteIp = obj["remoteIp"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
version = obj["version"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
deviceFamily = obj["deviceFamily"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
paired = obj.boolean("paired"),
connected = obj.boolean("connected"),
// Only an omitted field identifies a legacy gateway; malformed and future values stay fail-closed.
approvalState =
if (obj.containsKey("approvalState")) {
parseGatewayNodeApprovalState(obj["approvalState"].asStringOrNull())
} else {
GatewayNodeApprovalState.Unsupported
},
pendingRequestId = obj["pendingRequestId"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
capabilities = parseGatewayStringArray(obj["caps"] as? JsonArray),
commands = parseGatewayStringArray(obj["commands"] as? JsonArray),
)
}
data class GatewayNodeSummary(
val id: String,
val displayName: String?,
@@ -2944,8 +2840,6 @@ data class GatewayNodeSummary(
val deviceFamily: String?,
val paired: Boolean,
val connected: Boolean,
val approvalState: GatewayNodeApprovalState,
val pendingRequestId: String?,
val capabilities: List<String>,
val commands: List<String>,
)
@@ -3068,11 +2962,6 @@ private fun JsonObject?.cronStatus(key: String): String? =
?.trim()
?.takeIf { it.isNotEmpty() }
private fun parseGatewayStringArray(items: JsonArray?): List<String> =
items
?.mapNotNull { it.asStringOrNull()?.trim()?.takeIf { value -> value.isNotEmpty() } }
.orEmpty()
fun providerDisplayName(provider: String): String =
when (provider.trim().lowercase()) {
"openai" -> "OpenAI"

View File

@@ -1,7 +1,6 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayDeviceTokenSummary
import ai.openclaw.app.GatewayNodeApprovalState
import ai.openclaw.app.GatewayNodeSummary
import ai.openclaw.app.GatewayNodesDevicesSummary
import ai.openclaw.app.GatewayPairedDeviceSummary
@@ -156,8 +155,8 @@ private fun NodeRow(node: GatewayNodeSummary) {
badge = nodeBadge(node.displayName ?: node.id),
title = node.displayName ?: node.id,
subtitle = nodeSubtitle(node),
statusText = nodeStatusText(node),
status = nodeStatus(node),
statusText = if (node.connected) "Online" else "Offline",
status = if (node.connected) ClawStatus.Success else ClawStatus.Warning,
)
}
@@ -206,46 +205,14 @@ private fun nodeSubtitle(node: GatewayNodeSummary): String {
val kind = node.deviceFamily ?: "Node host"
val version = node.version?.let { "OpenClaw $it" }
val status = if (node.paired) "Paired" else "Unpaired"
val approval = nodeApprovalSubtitle(node.approvalState)
val commands =
node.commands
.take(2)
.joinToString(", ")
.takeIf { it.isNotBlank() }
return listOfNotNull(kind, version, status, approval, commands).joinToString(" · ")
return listOfNotNull(kind, version, status, commands).joinToString(" · ")
}
private fun nodeStatusText(node: GatewayNodeSummary): String =
when (node.approvalState) {
GatewayNodeApprovalState.PendingApproval -> "Needs approval"
GatewayNodeApprovalState.PendingReapproval -> "Needs reapproval"
GatewayNodeApprovalState.Unapproved -> "Unapproved"
else -> if (node.connected) "Online" else "Offline"
}
private fun nodeStatus(node: GatewayNodeSummary): ClawStatus =
when (node.approvalState) {
GatewayNodeApprovalState.Approved -> if (node.connected) ClawStatus.Success else ClawStatus.Warning
GatewayNodeApprovalState.PendingApproval,
GatewayNodeApprovalState.PendingReapproval,
GatewayNodeApprovalState.Unapproved,
-> ClawStatus.Warning
GatewayNodeApprovalState.Loading,
GatewayNodeApprovalState.Unsupported,
-> if (node.connected) ClawStatus.Neutral else ClawStatus.Warning
}
private fun nodeApprovalSubtitle(approvalState: GatewayNodeApprovalState): String? =
when (approvalState) {
GatewayNodeApprovalState.Approved -> "Approved"
GatewayNodeApprovalState.PendingApproval -> "Capability approval pending"
GatewayNodeApprovalState.PendingReapproval -> "Capability reapproval pending"
GatewayNodeApprovalState.Unapproved -> "Capability unapproved"
GatewayNodeApprovalState.Loading,
GatewayNodeApprovalState.Unsupported,
-> null
}
private fun pendingDeviceSubtitle(device: GatewayPendingDeviceSummary): String {
val roles = formatDeviceList(device.roles, "role")
val scopes = formatDeviceList(device.scopes, "scope")

View File

@@ -1,7 +1,6 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayConnectionProblem
import ai.openclaw.app.GatewayNodeApprovalState
import ai.openclaw.app.LocationMode
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.R
@@ -140,7 +139,6 @@ fun OnboardingFlow(
val gatewayConnectionProblem by viewModel.gatewayConnectionProblem.collectAsState()
val isConnected by viewModel.isConnected.collectAsState()
val isNodeConnected by viewModel.isNodeConnected.collectAsState()
val nodeCapabilityApprovalState by viewModel.nodeCapabilityApprovalState.collectAsState()
val runtimeInitialized by viewModel.runtimeInitialized.collectAsState()
val serverName by viewModel.serverName.collectAsState()
val remoteAddress by viewModel.remoteAddress.collectAsState()
@@ -149,12 +147,7 @@ fun OnboardingFlow(
val savedToken by viewModel.gatewayToken.collectAsState()
val pendingTrust by viewModel.pendingGatewayTrust.collectAsState()
val startAtGatewaySetup by viewModel.startOnboardingAtGatewaySetup.collectAsState()
val ready =
canFinishOnboarding(
isConnected = isConnected,
isNodeConnected = isNodeConnected,
nodeCapabilityApprovalState = nodeCapabilityApprovalState,
)
val ready = canFinishOnboarding(isConnected = isConnected, isNodeConnected = isNodeConnected)
var step by rememberSaveable { mutableStateOf(OnboardingStep.Welcome) }
var setupCode by rememberSaveable { mutableStateOf("") }
@@ -334,7 +327,6 @@ fun OnboardingFlow(
attemptedGatewayName = attemptedGatewayName,
remoteAddress = remoteAddress,
ready = ready,
nodeCapabilityApprovalState = nodeCapabilityApprovalState,
gatewayConnectionProblem = gatewayConnectionProblem,
connectSettling = recoveryNowMs - connectAttemptStartedAtMs < GATEWAY_CONNECT_SETTLING_MS,
onBack = { step = OnboardingStep.Gateway },
@@ -617,7 +609,6 @@ private fun GatewayRecoveryScreen(
attemptedGatewayName: String?,
remoteAddress: String?,
ready: Boolean,
nodeCapabilityApprovalState: GatewayNodeApprovalState,
gatewayConnectionProblem: GatewayConnectionProblem?,
connectSettling: Boolean,
onBack: () -> Unit,
@@ -626,14 +617,7 @@ private fun GatewayRecoveryScreen(
onContinue: () -> Unit,
modifier: Modifier = Modifier,
) {
val recoveryState =
gatewayRecoveryUiState(
ready = ready,
statusText = statusText,
connectSettling = connectSettling,
nodeCapabilityApprovalState = nodeCapabilityApprovalState,
gatewayConnectionProblem = gatewayConnectionProblem,
)
val recoveryState = gatewayRecoveryUiState(ready = ready, statusText = statusText, connectSettling = connectSettling, gatewayConnectionProblem = gatewayConnectionProblem)
val context = LocalContext.current
ClawScaffold(modifier = modifier, contentPadding = PaddingValues(horizontal = 18.dp, vertical = 16.dp)) {
@@ -645,7 +629,6 @@ private fun GatewayRecoveryScreen(
imageVector =
when (recoveryState) {
GatewayRecoveryUiState.Connected -> Icons.Default.CheckCircle
GatewayRecoveryUiState.NodeCapabilityApprovalPending -> Icons.Default.WifiTethering
GatewayRecoveryUiState.ApprovalRequired -> Icons.Default.WifiTethering
GatewayRecoveryUiState.Pairing -> Icons.Default.WifiTethering
GatewayRecoveryUiState.Finishing -> Icons.Default.WifiTethering
@@ -656,7 +639,6 @@ private fun GatewayRecoveryScreen(
tint =
when (recoveryState) {
GatewayRecoveryUiState.Connected -> ClawTheme.colors.success
GatewayRecoveryUiState.NodeCapabilityApprovalPending -> ClawTheme.colors.warning
GatewayRecoveryUiState.ApprovalRequired -> ClawTheme.colors.warning
GatewayRecoveryUiState.Pairing -> ClawTheme.colors.text
GatewayRecoveryUiState.Finishing -> ClawTheme.colors.text
@@ -676,18 +658,7 @@ private fun GatewayRecoveryScreen(
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(text = "Last gateway", style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted)
Text(text = recoveryGatewayName(serverName = serverName, attemptedGatewayName = attemptedGatewayName), style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(
text =
recoveryGatewayDetail(
ready = ready,
remoteAddress = remoteAddress,
statusText = statusText,
nodeCapabilityApprovalState = nodeCapabilityApprovalState,
gatewayConnectionProblem = gatewayConnectionProblem,
),
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
)
Text(text = recoveryGatewayDetail(ready = ready, remoteAddress = remoteAddress, statusText = statusText, gatewayConnectionProblem = gatewayConnectionProblem), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
recoveryGatewayApprovalCommand(gatewayConnectionProblem)?.let { command ->
ApprovalCommandBlock(command = command, onCopy = { copyApprovalCommand(context, command) })
}
@@ -695,7 +666,6 @@ private fun GatewayRecoveryScreen(
text =
when (recoveryState) {
GatewayRecoveryUiState.Connected -> "Healthy"
GatewayRecoveryUiState.NodeCapabilityApprovalPending -> "Node approval"
GatewayRecoveryUiState.ApprovalRequired -> "Needs approval"
GatewayRecoveryUiState.Pairing -> "Pairing"
GatewayRecoveryUiState.Finishing -> "Connecting"
@@ -704,7 +674,6 @@ private fun GatewayRecoveryScreen(
status =
when (recoveryState) {
GatewayRecoveryUiState.Connected -> ClawStatus.Success
GatewayRecoveryUiState.NodeCapabilityApprovalPending -> ClawStatus.Warning
GatewayRecoveryUiState.ApprovalRequired -> ClawStatus.Warning
GatewayRecoveryUiState.Pairing -> ClawStatus.Neutral
GatewayRecoveryUiState.Finishing -> ClawStatus.Neutral
@@ -1053,10 +1022,6 @@ internal enum class GatewayRecoveryUiState(
title = "Pairing Gateway",
message = "Approve this phone on the gateway.\nThen retry the connection.",
),
NodeCapabilityApprovalPending(
title = "Node Approval Pending",
message = "Gateway pairing worked.\nApprove this phone's node capabilities from an operator UI.",
),
Pairing(
title = "Pairing Gateway",
message = "Approval is in progress.\nOpenClaw will reconnect automatically.",
@@ -1114,19 +1079,14 @@ internal fun gatewayRecoveryUiState(
ready: Boolean,
statusText: String,
connectSettling: Boolean,
nodeCapabilityApprovalState: GatewayNodeApprovalState = GatewayNodeApprovalState.Loading,
gatewayConnectionProblem: GatewayConnectionProblem? = null,
): GatewayRecoveryUiState =
when {
ready -> GatewayRecoveryUiState.Connected
nodeCapabilityApprovalState == GatewayNodeApprovalState.PendingApproval ||
nodeCapabilityApprovalState == GatewayNodeApprovalState.PendingReapproval ||
nodeCapabilityApprovalState == GatewayNodeApprovalState.Unapproved -> GatewayRecoveryUiState.NodeCapabilityApprovalPending
gatewayConnectionProblem?.isPairingRequired == true &&
!gatewayConnectionProblem.canAutoRetry -> GatewayRecoveryUiState.ApprovalRequired
gatewayConnectionProblem?.isPairingRequired == true -> GatewayRecoveryUiState.Pairing
gatewayConnectionProblem?.pauseReconnect == true -> GatewayRecoveryUiState.Failed
nodeCapabilityApprovalState == GatewayNodeApprovalState.Loading -> GatewayRecoveryUiState.Finishing
connectSettling -> GatewayRecoveryUiState.Finishing
gatewayStatusLooksLikePairing(statusText) -> GatewayRecoveryUiState.Pairing
gatewayStatusLooksLikePartialConnect(statusText) -> GatewayRecoveryUiState.Finishing
@@ -1210,21 +1170,12 @@ private fun recoveryGatewayDetail(
ready: Boolean,
remoteAddress: String?,
statusText: String,
nodeCapabilityApprovalState: GatewayNodeApprovalState,
gatewayConnectionProblem: GatewayConnectionProblem?,
): String =
remoteAddress
?.takeIf { it.isNotBlank() }
?: if (ready) {
"Ready for chat and voice"
} else if (
nodeCapabilityApprovalState == GatewayNodeApprovalState.PendingApproval ||
nodeCapabilityApprovalState == GatewayNodeApprovalState.PendingReapproval ||
nodeCapabilityApprovalState == GatewayNodeApprovalState.Unapproved
) {
"Gateway paired. Waiting for node capability approval."
} else if (nodeCapabilityApprovalState == GatewayNodeApprovalState.Loading) {
"Gateway paired. Checking node capability approval."
} else if (gatewayConnectionProblem?.isPairingRequired == true && !gatewayConnectionProblem.canAutoRetry) {
recoveryGatewayApprovalCommand(gatewayConnectionProblem)
?.let { "Gateway approval is pending. Run this on the gateway host:" }
@@ -1297,24 +1248,11 @@ private class PermissionState(
val applyToViewModel: () -> Unit,
)
/** Onboarding finishes only after the gateway resolves node capability approval. */
/** Onboarding can finish only after gateway and node channels are both ready. */
internal fun canFinishOnboarding(
isConnected: Boolean,
isNodeConnected: Boolean,
nodeCapabilityApprovalState: GatewayNodeApprovalState,
): Boolean =
isConnected &&
isNodeConnected &&
when (nodeCapabilityApprovalState) {
GatewayNodeApprovalState.PendingApproval,
GatewayNodeApprovalState.PendingReapproval,
GatewayNodeApprovalState.Unapproved,
GatewayNodeApprovalState.Loading,
-> false
GatewayNodeApprovalState.Approved,
GatewayNodeApprovalState.Unsupported,
-> true
}
): Boolean = isConnected && isNodeConnected
/** Builds permission rows and applies granted feature toggles after onboarding. */
@Composable

View File

@@ -3,7 +3,6 @@ package ai.openclaw.app.ui
import ai.openclaw.app.BuildConfig
import ai.openclaw.app.GatewayChannelsSummary
import ai.openclaw.app.GatewayDreamingSummary
import ai.openclaw.app.GatewayNodeApprovalState
import ai.openclaw.app.GatewayNodesDevicesSummary
import ai.openclaw.app.GatewaySkillSummary
import ai.openclaw.app.HomeDestination
@@ -567,7 +566,7 @@ internal fun homeAttentionRows(
} else {
null
},
if (nodesDevicesSummary.pendingDevices.isNotEmpty() || nodesDevicesSummary.hasNodeCapabilityApprovalPending()) {
if (nodesDevicesSummary.pendingDevices.isNotEmpty()) {
HomeAttentionRow("Nodes & Devices", nodesDevicesSummaryText(nodesDevicesSummary), Icons.Default.Cloud, Tab.Settings, SettingsRoute.NodesDevices)
} else {
null
@@ -998,7 +997,6 @@ private fun nodesDevicesSummaryText(summary: GatewayNodesDevicesSummary): String
val devices = summary.pairedDevices.size
return when {
summary.pendingDevices.isNotEmpty() -> "${summary.pendingDevices.size} pending"
summary.hasNodeCapabilityApprovalPending() -> "Node approval pending"
summary.nodes.isNotEmpty() -> "$online/${summary.nodes.size} online"
devices > 0 -> "$devices paired"
else -> "No devices"
@@ -1009,19 +1007,11 @@ private fun nodesDevicesSummaryText(summary: GatewayNodesDevicesSummary): String
private fun nodesDevicesStatus(summary: GatewayNodesDevicesSummary): Boolean? =
when {
summary.pendingDevices.isNotEmpty() -> false
summary.hasNodeCapabilityApprovalPending() -> false
summary.nodes.any { it.connected } -> true
summary.pairedDevices.isNotEmpty() -> true
else -> null
}
private fun GatewayNodesDevicesSummary.hasNodeCapabilityApprovalPending(): Boolean =
nodes.any { node ->
node.approvalState == GatewayNodeApprovalState.PendingApproval ||
node.approvalState == GatewayNodeApprovalState.PendingReapproval ||
node.approvalState == GatewayNodeApprovalState.Unapproved
}
/** Summarizes channel connection state, surfacing errors before connected counts. */
private fun channelsSummaryText(summary: GatewayChannelsSummary): String {
val connected = summary.channels.count { it.connected }

View File

@@ -1,118 +0,0 @@
package ai.openclaw.app
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class GatewayNodeApprovalStateTest {
@Test
fun parsesGatewayNodeApprovalState() {
assertEquals(GatewayNodeApprovalState.Approved, parseGatewayNodeApprovalState("approved"))
assertEquals(GatewayNodeApprovalState.PendingApproval, parseGatewayNodeApprovalState("pending-approval"))
assertEquals(GatewayNodeApprovalState.PendingReapproval, parseGatewayNodeApprovalState("pending-reapproval"))
assertEquals(GatewayNodeApprovalState.Unapproved, parseGatewayNodeApprovalState("unapproved"))
assertEquals(GatewayNodeApprovalState.Loading, parseGatewayNodeApprovalState(null))
assertEquals(GatewayNodeApprovalState.Loading, parseGatewayNodeApprovalState("future-state"))
}
@Test
fun parsesNodeListApprovalFields() {
val node =
parseGatewayNodeSummary(
Json.parseToJsonElement(
"""
{
"nodeId": "android-node",
"paired": true,
"connected": true,
"approvalState": "pending-approval",
"pendingRequestId": "request-1",
"caps": ["device"],
"commands": ["device.status"]
}
""".trimIndent(),
),
)
requireNotNull(node)
assertEquals(GatewayNodeApprovalState.PendingApproval, node.approvalState)
assertEquals("request-1", node.pendingRequestId)
assertEquals(listOf("device"), node.capabilities)
assertEquals(listOf("device.status"), node.commands)
}
@Test
fun treatsMissingNodeApprovalStateAsUnsupported() {
val node =
parseGatewayNodeSummary(
Json.parseToJsonElement("""{"nodeId":"android-node","paired":true,"connected":true}"""),
)
requireNotNull(node)
assertEquals(GatewayNodeApprovalState.Unsupported, node.approvalState)
assertEquals(
GatewayNodeApprovalState.Unsupported,
currentNodeCapabilityApprovalState(nodes = listOf(node), selfNodeId = "android-node"),
)
assertNull(node.pendingRequestId)
}
@Test
fun resolvesCurrentPhoneNodeApprovalState() {
val nodes =
listOf(
GatewayNodeSummary(
id = "other",
displayName = null,
remoteIp = null,
version = null,
deviceFamily = null,
paired = true,
connected = false,
approvalState = GatewayNodeApprovalState.Approved,
pendingRequestId = null,
capabilities = emptyList(),
commands = emptyList(),
),
GatewayNodeSummary(
id = "self",
displayName = null,
remoteIp = null,
version = null,
deviceFamily = null,
paired = true,
connected = true,
approvalState = GatewayNodeApprovalState.PendingApproval,
pendingRequestId = null,
capabilities = emptyList(),
commands = emptyList(),
),
)
assertEquals(
GatewayNodeApprovalState.PendingApproval,
currentNodeCapabilityApprovalState(nodes = nodes, selfNodeId = "self"),
)
assertEquals(
GatewayNodeApprovalState.Loading,
currentNodeCapabilityApprovalState(nodes = nodes, selfNodeId = "missing"),
)
}
@Test
fun ignoresStaleNodeApprovalRefreshResults() {
val guard = GatewayNodeApprovalRefreshGuard()
var approvalState = GatewayNodeApprovalState.Loading
val staleRefresh = guard.begin()
val currentRefresh = guard.begin()
assertFalse(guard.publishIfCurrent(staleRefresh) { approvalState = GatewayNodeApprovalState.Approved })
assertTrue(
guard.publishIfCurrent(currentRefresh) { approvalState = GatewayNodeApprovalState.PendingReapproval },
)
assertEquals(GatewayNodeApprovalState.PendingReapproval, approvalState)
}
}

View File

@@ -1,10 +1,6 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayConnectionProblem
import ai.openclaw.app.GatewayNodeApprovalState
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -13,48 +9,22 @@ import org.junit.Test
class OnboardingFlowLogicTest {
@Test
fun blocksFinishWhenOnlyOperatorIsConnected() {
assertFalse(canFinishOnboarding(isConnected = true, isNodeConnected = false, nodeCapabilityApprovalState = GatewayNodeApprovalState.Approved))
assertFalse(canFinishOnboarding(isConnected = true, isNodeConnected = false))
}
@Test
fun blocksFinishWhenDisconnected() {
assertFalse(canFinishOnboarding(isConnected = false, isNodeConnected = false, nodeCapabilityApprovalState = GatewayNodeApprovalState.Approved))
assertFalse(canFinishOnboarding(isConnected = false, isNodeConnected = false))
}
@Test
fun blocksFinishWhenOnlyNodeIsConnected() {
assertFalse(canFinishOnboarding(isConnected = false, isNodeConnected = true, nodeCapabilityApprovalState = GatewayNodeApprovalState.Approved))
assertFalse(canFinishOnboarding(isConnected = false, isNodeConnected = true))
}
@Test
fun blocksFinishWhenNodeCapabilityApprovalIsPending() {
assertFalse(canFinishOnboarding(isConnected = true, isNodeConnected = true, nodeCapabilityApprovalState = GatewayNodeApprovalState.PendingApproval))
assertFalse(canFinishOnboarding(isConnected = true, isNodeConnected = true, nodeCapabilityApprovalState = GatewayNodeApprovalState.PendingReapproval))
assertFalse(canFinishOnboarding(isConnected = true, isNodeConnected = true, nodeCapabilityApprovalState = GatewayNodeApprovalState.Unapproved))
}
@Test
fun allowsFinishWhenOperatorNodeAndCapabilityApprovalAreReady() {
assertTrue(canFinishOnboarding(isConnected = true, isNodeConnected = true, nodeCapabilityApprovalState = GatewayNodeApprovalState.Approved))
}
@Test
fun blocksFinishWhileDelayedNodeListResolvesPendingApproval() =
runTest {
val delayedNodeList = CompletableDeferred<GatewayNodeApprovalState>()
var approvalState = GatewayNodeApprovalState.Loading
val refresh = launch { approvalState = delayedNodeList.await() }
assertFalse(canFinishOnboarding(isConnected = true, isNodeConnected = true, nodeCapabilityApprovalState = approvalState))
delayedNodeList.complete(GatewayNodeApprovalState.PendingApproval)
refresh.join()
assertFalse(canFinishOnboarding(isConnected = true, isNodeConnected = true, nodeCapabilityApprovalState = approvalState))
}
@Test
fun allowsFinishWhenSuccessfulLegacyNodeListOmitsApprovalState() {
assertTrue(canFinishOnboarding(isConnected = true, isNodeConnected = true, nodeCapabilityApprovalState = GatewayNodeApprovalState.Unsupported))
fun allowsFinishOnlyWhenOperatorAndNodeAreConnected() {
assertTrue(canFinishOnboarding(isConnected = true, isNodeConnected = true))
}
@Test
@@ -128,32 +98,6 @@ class OnboardingFlowLogicTest {
)
}
@Test
fun showsNodeApprovalStateWhenCapabilityApprovalIsPending() {
assertEquals(
GatewayRecoveryUiState.NodeCapabilityApprovalPending,
gatewayRecoveryUiState(
ready = false,
statusText = "Connected",
connectSettling = false,
nodeCapabilityApprovalState = GatewayNodeApprovalState.PendingApproval,
),
)
}
@Test
fun showsFinishingStateWhileNodeApprovalLoads() {
assertEquals(
GatewayRecoveryUiState.Finishing,
gatewayRecoveryUiState(
ready = false,
statusText = "Connected",
connectSettling = false,
nodeCapabilityApprovalState = GatewayNodeApprovalState.Loading,
),
)
}
@Test
fun showsApprovalRequiredForPausedPairingProblem() {
assertEquals(

View File

@@ -3,8 +3,6 @@ package ai.openclaw.app.ui
import ai.openclaw.app.AppearanceThemeMode
import ai.openclaw.app.GatewayChannelSummary
import ai.openclaw.app.GatewayChannelsSummary
import ai.openclaw.app.GatewayNodeApprovalState
import ai.openclaw.app.GatewayNodeSummary
import ai.openclaw.app.GatewayNodesDevicesSummary
import ai.openclaw.app.GatewayPendingDeviceSummary
import org.junit.Assert.assertEquals
@@ -120,41 +118,6 @@ class ShellScreenLogicTest {
assertEquals(emptyList<String>(), rows.map { it.title })
}
@Test
fun homeAttentionRowsSurfacePendingNodeCapabilityApproval() {
val rows =
homeAttentionRows(
isConnected = true,
pendingApprovals = 0,
channelsSummary = emptyChannels(),
nodesDevicesSummary =
GatewayNodesDevicesSummary(
nodes =
listOf(
GatewayNodeSummary(
id = "android-node",
displayName = "Android",
remoteIp = null,
version = null,
deviceFamily = "Android",
paired = true,
connected = true,
approvalState = GatewayNodeApprovalState.PendingApproval,
pendingRequestId = null,
capabilities = emptyList(),
commands = emptyList(),
),
),
pendingDevices = emptyList(),
pairedDevices = emptyList(),
),
readyProviderCount = 1,
)
assertEquals(listOf("Nodes & Devices"), rows.map { it.title })
assertEquals("Node approval pending", rows.single().subtitle)
}
private fun emptyChannels(): GatewayChannelsSummary = GatewayChannelsSummary(channels = emptyList())
private fun emptyNodesDevices(): GatewayNodesDevicesSummary = GatewayNodesDevicesSummary(nodes = emptyList(), pendingDevices = emptyList(), pairedDevices = emptyList())

View File

@@ -368,7 +368,7 @@ enum ExecApprovalsStore {
tempURL.path,
targetURL.path,
nil,
copyfile_flags_t(COPYFILE_DATA | COPYFILE_EXCL))
copyfile_flags_t(COPYFILE_EXCL))
if copied == -1 {
if errno == EEXIST {
try? FileManager().removeItem(at: tempURL)

View File

@@ -1,2 +1,2 @@
e2a646aa93124c089fcfed3c3ef982c88d1fdd2170fcdec274446f3d02f20d2b plugin-sdk-api-baseline.json
f1762c7b4bbaea4a3ce47ab943daaa6ca3dbc58322cc5d39688da66b3d483a2d plugin-sdk-api-baseline.jsonl
99a18e1e8e3af265e233504b6cf1ff8a227a6466dd0d515c56f823503f0b7bc7 plugin-sdk-api-baseline.json
930a414cf783baa2bedb21a85af6fcaa02a12073d9e06cc49c827e7379f85646 plugin-sdk-api-baseline.jsonl

View File

@@ -422,7 +422,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
</Accordion>
<Accordion title="Rich message formatting">
Outbound text uses standard Telegram HTML messages by default so replies remain readable across current Telegram clients. This compatibility mode supports normal bold, italic, links, code, spoilers, and quotes, but not Bot API 10.1 rich-only blocks such as native tables, details, rich media, and formulas.
Outbound text uses standard Telegram HTML messages by default so replies remain readable across current Telegram clients.
Set `channels.telegram.richMessages: true` to opt into Bot API 10.1 rich messages:
@@ -436,16 +436,13 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
}
```
When enabled:
- The agent is told that Telegram rich messages are available for this bot/account.
- Markdown text is rendered through OpenClaw's Markdown IR and sent as Telegram rich HTML.
- Explicit rich HTML payloads preserve supported Bot API 10.1 tags such as headings, tables, details, rich media, and formulas.
- Media captions still use Telegram HTML captions because rich messages do not replace captions.
This keeps model text away from Telegram Rich Markdown sigils, so currency like `$400-600K` is not parsed as math. Long rich text is split automatically across Telegram's rich text and rich block limits. Tables over Telegram's column limit are sent as code blocks.
Default: off for client compatibility. Rich messages require compatible Telegram clients; some current Desktop, Web, Android, and third-party clients display accepted rich messages as unsupported. Keep this option disabled unless every client used with the bot can render them. `/status` shows whether the current Telegram session has rich messages on or off.
Rich messages require compatible Telegram clients. Some current Desktop, Web, Android, and third-party clients display accepted rich messages as unsupported, so keep this option disabled unless every client used with the bot can render them.
Link previews are enabled by default. `channels.telegram.linkPreview: false` skips automatic entity detection for rich text.

View File

@@ -224,29 +224,6 @@ Optional members:
| `onSubagentEnded(params)` | Method | Clean up after a subagent ends. |
| `dispose()` | Method | Release resources. Called during gateway shutdown or plugin reload - not per-session. |
### Runtime settings
Lifecycle hooks that run inside OpenClaw receive an optional
`runtimeSettings` object. It is a versioned, read-only internal
producer/consumer API surface: OpenClaw produces it for the selected context
engine, and the context engine consumes it inside lifecycle hooks. It is not
rendered directly to users and does not create a dedicated reporting surface.
- `schemaVersion`: currently `1`
- `runtime`: OpenClaw host, runtime mode (`normal`, `fallback`, or
`degraded`), and optional harness/runtime ids
- `contextEngineSelection`: selected context engine id and selection source
- `executionHost`: host id and label for the surface invoking the hook
- `model`: requested model, resolved model, provider, and optional model family
- `limits`: prompt token budget and max output tokens when known
- `diagnostics`: closed fallback and degraded reason codes when known
Fields that can be unknown are represented as `null`; discriminator fields such
as runtime mode and selection source remain non-nullable. Older engines remain
compatible: if a strict legacy engine rejects `runtimeSettings` as an unknown
property, OpenClaw retries the lifecycle call without it instead of quarantining
the engine.
### Host requirements
Context engines can declare host capability requirements on `info.hostRequirements`.

View File

@@ -258,9 +258,7 @@ Gemini CLI OAuth is shipped as part of the bundled `google` plugin.
</Step>
</Steps>
Gemini CLI uses `stream-json` by default. OpenClaw reads assistant stream
messages and normalizes `stats.cached` into `cacheRead`; legacy
`--output-format json` overrides still read reply text from `response`.
Gemini CLI JSON replies are parsed from `response`; usage falls back to `stats`, with `stats.cached` normalized into OpenClaw `cacheRead`.
### Z.AI (GLM)

View File

@@ -1386,11 +1386,7 @@
"clawhub/api",
"clawhub/http-api",
"clawhub/acceptable-usage",
"clawhub/moderation",
"clawhub/security",
"clawhub/security-audits",
"clawhub/content-rights",
"clawhub/plugin-validation-fixes"
"clawhub/content-rights"
]
}
]

View File

@@ -287,10 +287,8 @@ load local files from plain paths.
## Inputs / outputs
- `output: "json"` (default) tries to parse JSON and extract text + session id.
- For Gemini CLI JSON output, OpenClaw reads reply text from `response` and usage
from `stats` when `usage` is missing or empty. The bundled Gemini CLI default
uses `stream-json`, but old `--output-format json` overrides still use the
JSON parser.
- For Gemini CLI JSON output, OpenClaw reads reply text from `response` and
usage from `stats` when `usage` is missing or empty.
- `output: "jsonl"` parses JSONL streams and extracts the final agent message plus session
identifiers when present.
- `output: "text"` treats stdout as the final response.
@@ -320,11 +318,8 @@ The bundled Anthropic plugin registers a default for `claude-cli`:
The bundled Google plugin also registers a default for `google-gemini-cli`:
- `command: "gemini"`
- `args: ["--skip-trust", "--approval-mode", "auto_edit", "--output-format", "stream-json", "--prompt", "{prompt}"]`
- `resumeArgs: ["--skip-trust", "--approval-mode", "auto_edit", "--resume", "{sessionId}", "--output-format", "stream-json", "--prompt", "{prompt}"]`
- `output: "jsonl"`
- `resumeOutput: "jsonl"`
- `jsonlDialect: "gemini-stream-json"`
- `args: ["--output-format", "json", "--prompt", "{prompt}"]`
- `resumeArgs: ["--resume", "{sessionId}", "--output-format", "json", "--prompt", "{prompt}"]`
- `imageArg: "@"`
- `imagePathScope: "workspace"`
- `modelArg: "--model"`
@@ -335,13 +330,9 @@ Prerequisite: the local Gemini CLI must be installed and available as
`gemini` on `PATH` (`brew install gemini-cli` or
`npm install -g @google/gemini-cli`).
Gemini CLI output notes:
Gemini CLI JSON notes:
- The default `stream-json` parser reads assistant `message` events, tool events,
final `result` usage, and fatal Gemini error events.
- If you override Gemini args to `--output-format json`, OpenClaw normalizes that
backend back to `output: "json"` and reads reply text from the JSON `response`
field.
- Reply text is read from the JSON `response` field.
- Usage falls back to `stats` when `usage` is absent or empty.
- `stats.cached` is normalized into OpenClaw `cacheRead`.
- If `stats.input` is missing, OpenClaw derives input tokens from
@@ -381,10 +372,8 @@ api.registerTextTransforms({
rewrites streamed assistant deltas and parsed final text before OpenClaw handles
its own control markers and channel delivery.
For CLIs that emit provider-specific JSONL events, set `jsonlDialect` on that
backend's config. Supported dialects are `claude-stream-json` for Claude
Code-compatible streams and `gemini-stream-json` for Gemini CLI `stream-json`
events.
For CLIs that emit Claude Code stream-json compatible JSONL, set
`jsonlDialect: "claude-stream-json"` on that backend's config.
## Native compaction ownership

View File

@@ -388,13 +388,13 @@ For an end-to-end authoring guide, see
### Exclusive slots
| Method | What it registers |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `api.registerContextEngine(id, factory)` | Context engine (one active at a time). Lifecycle callbacks receive `runtimeSettings` when the host can provide model/provider/mode diagnostics; older strict engines are retried without that key. |
| `api.registerMemoryCapability(capability)` | Unified memory capability |
| `api.registerMemoryPromptSection(builder)` | Memory prompt section builder |
| `api.registerMemoryFlushPlan(resolver)` | Memory flush plan resolver |
| `api.registerMemoryRuntime(runtime)` | Memory runtime adapter |
| Method | What it registers |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `api.registerContextEngine(id, factory)` | Context engine (one active at a time). The `assemble()` callback receives `availableTools` and `citationsMode` so the engine can tailor prompt additions. |
| `api.registerMemoryCapability(capability)` | Unified memory capability |
| `api.registerMemoryPromptSection(builder)` | Memory prompt section builder |
| `api.registerMemoryFlushPlan(resolver)` | Memory flush plan resolver |
| `api.registerMemoryRuntime(runtime)` | Memory runtime adapter |
### Deprecated memory embedding adapters

View File

@@ -435,14 +435,11 @@ WebSocket endpoint, sends the initial setup payload, and waits for
</Accordion>
<Accordion title="Gemini CLI usage notes">
When using the `google-gemini-cli` OAuth provider, OpenClaw uses Gemini
CLI `stream-json` output by default and normalizes usage from the final
`stats` payload. Legacy `--output-format json` overrides still use the
JSON parser.
<Accordion title="Gemini CLI JSON usage notes">
When using the `google-gemini-cli` OAuth provider, OpenClaw normalizes
the CLI JSON output as follows:
- Streamed reply text comes from assistant `message` events.
- For legacy JSON output, reply text comes from the CLI JSON `response` field.
- Reply text comes from the CLI JSON `response` field.
- Usage falls back to `stats` when the CLI leaves `usage` empty.
- `stats.cached` is normalized into OpenClaw `cacheRead`.
- If `stats.input` is missing, OpenClaw derives input tokens from

View File

@@ -155,29 +155,7 @@ the maintainer-only release runbook.
11. After publish, run the npm post-publish verifier, optional standalone
published-npm Telegram E2E when you need post-publish channel proof,
dist-tag promotion when needed, verify the generated GitHub release page,
run the release announcement steps, then complete [Stable main
closeout](#stable-main-closeout) before calling a stable release finished.
## Stable main closeout
Stable publication is not complete until `main` carries the actual shipped
release state.
1. Start from fresh latest `main`. Audit `release/YYYY.M.PATCH` against it and
forward-port real fixes that are absent from `main`. Do not blindly merge
release-only compatibility, test, or validation adapters into newer `main`.
2. Set `main` to the shipped stable version, not a speculative next train. Run
`pnpm release:prep` after the root version change, then
`pnpm deps:shrinkwrap:generate`.
3. Make `CHANGELOG.md`'s `## YYYY.M.PATCH` section on `main` exactly match the
tagged release branch. Include the stable `appcast.xml` update when the mac
release published one.
4. Do not add `YYYY.M.PATCH+1`, a beta version, or an empty future changelog
section to `main` until the operator explicitly starts that release train.
5. Run `pnpm release:generated:check`, `pnpm deps:shrinkwrap:check`, and
`OPENCLAW_TESTBOX=1 pnpm check:changed`. Push, then verify `origin/main`
contains the shipped version and changelog before calling the stable release
done.
and run the release announcement steps.
## Release preflight

View File

@@ -31,9 +31,9 @@ OpenClaw features that can generate provider usage or paid API calls.
- `/usage tokens` shows tokens only; subscription-style OAuth/token and CLI flows
still show tokens only unless that runtime supplies compatible usage metadata
and an explicit local price is configured.
- Gemini CLI note: the default `stream-json` output and legacy JSON overrides
both read usage from `stats`, normalize `stats.cached` into `cacheRead`, and
derive input tokens from `stats.input_tokens - stats.cached` when needed.
- Gemini CLI note: when the CLI returns JSON output, OpenClaw reads usage from
`stats`, normalizes `stats.cached` into `cacheRead`, and derives input tokens
from `stats.input_tokens - stats.cached` when needed.
Anthropic note: Anthropic staff told us OpenClaw-style Claude CLI usage is
allowed again, so OpenClaw treats Claude CLI reuse and `claude -p` usage as

View File

@@ -163,11 +163,10 @@ If the provider does not support this cache mode, `cacheRetention` has no effect
OpenClaw manages a provider-native `cachedContents` resource rather than
injecting cache markers into the request.
### Gemini CLI usage
### Gemini CLI JSON usage
- Gemini CLI `stream-json` output can surface cache hits through `stats.cached`;
OpenClaw maps that to `cacheRead`. Legacy `--output-format json` overrides use
the same usage normalization.
- Gemini CLI JSON output can also surface cache hits through `stats.cached`;
OpenClaw maps that to `cacheRead`.
- If the CLI omits a direct `stats.input` value, OpenClaw derives input tokens
from `stats.input_tokens - stats.cached`.
- This is usage normalization only. It does not mean OpenClaw is creating

View File

@@ -362,8 +362,8 @@ OpenClaw also enforces a safety floor for embedded runs:
Why: leave enough headroom for multi-turn "housekeeping" (like memory writes) before compaction becomes unavoidable.
Implementation: `applyAgentCompactionSettingsFromConfig()` in `src/agents/agent-settings.ts`
(called from embedded-runner turn and compaction setup paths).
Implementation: `ensureAgentCompactionReserveTokens()` in `src/agents/agent-settings.ts`
(called from `src/agents/embedded-agent-runner.ts`).
---

View File

@@ -92,11 +92,9 @@ Usage surfaces normalize common provider-native field aliases before display.
For OpenAI-family Responses traffic, that includes both `input_tokens` /
`output_tokens` and `prompt_tokens` / `completion_tokens`, so transport-specific
field names do not change `/status`, `/usage`, or session summaries.
Gemini CLI usage is normalized too: the default `stream-json` parser reads
assistant `message` events, and `stats.cached` maps to `cacheRead` with
`stats.input_tokens - stats.cached` used when the CLI omits an explicit
`stats.input` field. Legacy JSON overrides still read reply text from
`response`.
Gemini CLI JSON usage is normalized too: reply text comes from `response`, and
`stats.cached` maps to `cacheRead` with `stats.input_tokens - stats.cached`
used when the CLI omits an explicit `stats.input` field.
For native OpenAI-family Responses traffic, WebSocket/SSE usage aliases are
normalized the same way, and totals fall back to normalized input + output when
`total_tokens` is missing or `0`.

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/acpx",
"version": "2026.6.8",
"version": "2026.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/acpx",
"version": "2026.6.8",
"version": "2026.6.9",
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.39.0",
"@zed-industries/codex-acp": "0.15.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/acpx",
"version": "2026.6.8",
"version": "2026.6.9",
"description": "OpenClaw ACP runtime backend with plugin-owned session and transport management.",
"repository": {
"type": "git",
@@ -26,10 +26,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.6.8"
"pluginApi": ">=2026.6.9"
},
"build": {
"openclawVersion": "2026.6.8",
"openclawVersion": "2026.6.9",
"staticAssets": [
{
"source": "./src/runtime-internals/mcp-proxy.mjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/admin-http-rpc",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw admin HTTP RPC endpoint",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/alibaba-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw Alibaba Model Studio video provider plugin",
"type": "module",

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"dependencies": {
"@anthropic-ai/sdk": "0.100.1",
"@aws/bedrock-token-generator": "1.1.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"description": "OpenClaw Amazon Bedrock Mantle provider plugin for OpenAI-compatible model routing.",
"repository": {
"type": "git",
@@ -24,10 +24,10 @@
"minHostVersion": ">=2026.5.12-beta.1"
},
"compat": {
"pluginApi": ">=2026.6.8"
"pluginApi": ">=2026.6.9"
},
"build": {
"openclawVersion": "2026.6.8",
"openclawVersion": "2026.6.9",
"bundledDist": false
},
"release": {

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/amazon-bedrock-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/amazon-bedrock-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"dependencies": {
"@aws-sdk/client-bedrock": "3.1056.0",
"@aws-sdk/client-bedrock-runtime": "3.1056.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"description": "OpenClaw Amazon Bedrock provider plugin with model discovery, embeddings, and guardrail support.",
"repository": {
"type": "git",
@@ -28,10 +28,10 @@
"minHostVersion": ">=2026.5.12-beta.1"
},
"compat": {
"pluginApi": ">=2026.6.8"
"pluginApi": ">=2026.6.9"
},
"build": {
"openclawVersion": "2026.6.8",
"openclawVersion": "2026.6.9",
"bundledDist": false
},
"release": {

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/anthropic-vertex-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/anthropic-vertex-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"dependencies": {
"@anthropic-ai/vertex-sdk": "0.16.1"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/anthropic-vertex-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"description": "OpenClaw Anthropic Vertex provider plugin for Claude models on Google Vertex AI.",
"repository": {
"type": "git",
@@ -23,10 +23,10 @@
"minHostVersion": ">=2026.5.12-beta.1"
},
"compat": {
"pluginApi": ">=2026.6.8"
"pluginApi": ">=2026.6.9"
},
"build": {
"openclawVersion": "2026.6.8",
"openclawVersion": "2026.6.9",
"bundledDist": false
},
"release": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/anthropic-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw Anthropic provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/arcee-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw Arcee provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/azure-speech",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw Azure Speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/bonjour",
"version": "2026.6.8",
"version": "2026.6.9",
"description": "OpenClaw Bonjour/mDNS gateway discovery",
"type": "module",
"dependencies": {

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/brave-plugin",
"version": "2026.6.8",
"version": "2026.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/brave-plugin",
"version": "2026.6.8"
"version": "2026.6.9"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/brave-plugin",
"version": "2026.6.8",
"version": "2026.6.9",
"description": "OpenClaw Brave Search provider plugin for web search.",
"repository": {
"type": "git",
@@ -21,10 +21,10 @@
"allowInvalidConfigRecovery": true
},
"compat": {
"pluginApi": ">=2026.6.8"
"pluginApi": ">=2026.6.9"
},
"build": {
"openclawVersion": "2026.6.8"
"openclawVersion": "2026.6.9"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/browser-plugin",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw browser tool plugin",
"type": "module",

View File

@@ -27,9 +27,9 @@ export const CHROME_STOP_TIMEOUT_MS = 2500;
export const CHROME_STOP_PROBE_TIMEOUT_MS = 200;
export const CHROME_STDERR_HINT_MAX_CHARS = 2000;
const PROFILE_HTTP_REACHABILITY_TIMEOUT_MS = 300;
const PROFILE_WS_REACHABILITY_MIN_TIMEOUT_MS = 200;
const PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS = 2000;
export const PROFILE_HTTP_REACHABILITY_TIMEOUT_MS = 300;
export const PROFILE_WS_REACHABILITY_MIN_TIMEOUT_MS = 200;
export const PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS = 2000;
export const PROFILE_ATTACH_RETRY_TIMEOUT_MS = 1200;
export const PROFILE_POST_RESTART_WS_TIMEOUT_MS = 600;
export const CHROME_MCP_ATTACH_READY_WINDOW_MS = 8000;

View File

@@ -2,14 +2,15 @@
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveCdpReachabilityPolicy } from "./cdp-reachability-policy.js";
import { resolveCdpReachabilityTimeouts } from "./cdp-timeouts.js";
import {
PROFILE_HTTP_REACHABILITY_TIMEOUT_MS,
PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS,
PROFILE_WS_REACHABILITY_MIN_TIMEOUT_MS,
resolveCdpReachabilityTimeouts,
} from "./cdp-timeouts.js";
import type { ResolvedBrowserProfile } from "./config.js";
import { assertBrowserNavigationAllowed } from "./navigation-guard.js";
const PROFILE_HTTP_REACHABILITY_TIMEOUT_MS = 300;
const PROFILE_WS_REACHABILITY_MIN_TIMEOUT_MS = 200;
const PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS = 2000;
const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => {

View File

@@ -41,21 +41,18 @@ function profileContext(tabs: Array<{ targetId: string; url: string }>) {
};
}
function routeContextForTab(
url: string,
ensureTabAvailable = vi.fn(async () => ({
targetId: "tab-1",
title: "Tab",
url,
type: "page",
})),
): BrowserRouteContext {
function routeContextForTab(url: string): BrowserRouteContext {
const profileCtx = {
profile: {
cdpUrl: "http://127.0.0.1:9222",
name: "default",
},
ensureTabAvailable,
ensureTabAvailable: vi.fn(async () => ({
targetId: "tab-1",
title: "Tab",
url,
type: "page",
})),
} as unknown as ProfileContext;
return {
@@ -135,27 +132,6 @@ describe("browser route shared helpers", () => {
});
describe("withRouteTabContext", () => {
it("opts agent routes into Playwright target-id fallback", async () => {
const response = createBrowserRouteResponse();
const ensureTabAvailable = vi.fn(async () => ({
targetId: "tab-1",
title: "Tab",
url: "https://example.com",
type: "page",
}));
await withRouteTabContext({
req: requestWithBody({}),
res: response.res,
ctx: routeContextForTab("https://example.com", ensureTabAvailable),
run: async () => {},
});
expect(ensureTabAvailable).toHaveBeenCalledWith(undefined, {
allowPlaywrightFallback: true,
});
});
it("does not enforce current-tab URL policy unless requested", async () => {
const response = createBrowserRouteResponse();
const run = vi.fn(async () => {

View File

@@ -147,10 +147,7 @@ export async function withRouteTabContext<T>(
return undefined;
}
try {
// Agent routes can address local-managed tabs through Playwright when per-tab WS discovery lags.
const tab = await profileCtx.ensureTabAvailable(params.targetId, {
allowPlaywrightFallback: true,
});
const tab = await profileCtx.ensureTabAvailable(params.targetId);
if (params.enforceCurrentUrlAllowed) {
await assertBrowserNavigationResultAllowed({
url: tab.url,

View File

@@ -128,9 +128,6 @@ describe("local-managed browser snapshot routes", () => {
expect(response.statusCode).toBe(400);
expect(response.body).toEqual({ error: "browser navigation blocked by policy" });
expect(routeState.profileCtx.ensureTabAvailable).toHaveBeenCalledWith(undefined, {
allowPlaywrightFallback: false,
});
expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledWith({
url: "http://127.0.0.1:8080/admin",
ssrfPolicy: { dangerouslyAllowPrivateNetwork: false },

View File

@@ -594,9 +594,7 @@ export function registerBrowserAgentSnapshotRoutes(
});
try {
const tab = await profileCtx.ensureTabAvailable(targetId || undefined, {
allowPlaywrightFallback: hasPlaywright,
});
const tab = await profileCtx.ensureTabAvailable(targetId || undefined);
const usesChromeMcp = getBrowserProfileCapabilities(profileCtx.profile).usesChromeMcp;
const ssrfPolicyOpts = browserNavigationPolicyForProfile(ctx, profileCtx);
if ((plan.labels || plan.mode === "efficient") && plan.format === "aria") {

View File

@@ -3,14 +3,15 @@ import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { EventEmitter } from "node:events";
import { afterEach, describe, expect, it, vi } from "vitest";
import "./server-context.chrome-test-harness.js";
import { PROFILE_ATTACH_RETRY_TIMEOUT_MS } from "./cdp-timeouts.js";
import {
PROFILE_ATTACH_RETRY_TIMEOUT_MS,
PROFILE_HTTP_REACHABILITY_TIMEOUT_MS,
} from "./cdp-timeouts.js";
import * as chromeModule from "./chrome.js";
import { BrowserProfileUnavailableError } from "./errors.js";
import { createBrowserRouteContext } from "./server-context.js";
import { makeBrowserServerState, mockLaunchedChrome } from "./server-context.test-harness.js";
const PROFILE_HTTP_REACHABILITY_TIMEOUT_MS = 300;
function setupEnsureBrowserAvailableHarness() {
vi.useFakeTimers();

View File

@@ -1,202 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ResolvedBrowserProfile } from "./config.js";
import {
OPEN_TAB_DISCOVERY_POLL_MS,
OPEN_TAB_DISCOVERY_WINDOW_MS,
} from "./server-context.constants.js";
import { createProfileSelectionOps } from "./server-context.selection.js";
import type { BrowserTab, ProfileRuntimeState } from "./server-context.types.js";
const LOCAL_PROFILE: ResolvedBrowserProfile = {
name: "openclaw",
cdpPort: 18800,
cdpUrl: "http://127.0.0.1:18800",
cdpHost: "127.0.0.1",
cdpIsLoopback: true,
color: "#FF4500",
driver: "openclaw",
headless: true,
headlessSource: "config",
attachOnly: false,
};
function tab(targetId: string, wsUrl?: string): BrowserTab {
return {
targetId,
title: targetId,
url: `https://${targetId.toLowerCase()}.example`,
type: "page",
...(wsUrl ? { wsUrl } : {}),
};
}
function createSelectionHarness(params: {
snapshots: Array<BrowserTab[] | Error>;
openedTab?: BrowserTab;
}) {
const snapshots = [...params.snapshots];
let lastSnapshot: BrowserTab[] = [];
const listTabs = vi.fn(async () => {
const next = snapshots.shift();
if (next instanceof Error) {
throw next;
}
if (next) {
lastSnapshot = next;
}
return lastSnapshot;
});
const profileState: ProfileRuntimeState = {
profile: LOCAL_PROFILE,
running: null,
lastTargetId: null,
reconcile: null,
};
const openTab = vi.fn(async () => {
const openedTab = params.openedTab ?? tab("OPENED");
profileState.lastTargetId = openedTab.targetId;
return openedTab;
});
const selection = createProfileSelectionOps({
profile: LOCAL_PROFILE,
getProfileState: () => profileState,
getCdpControlPolicy: () => undefined,
ensureBrowserAvailable: async () => {},
listTabs,
openTab,
});
return { selection, listTabs, openTab, profileState };
}
async function advancePastDiscoveryWindow(): Promise<void> {
await vi.advanceTimersByTimeAsync(OPEN_TAB_DISCOVERY_WINDOW_MS + OPEN_TAB_DISCOVERY_POLL_MS);
}
afterEach(() => {
vi.useRealTimers();
});
describe("browser profile tab selection", () => {
it("preserves the opened tab when the immediate relist omits it", async () => {
const openedTab = tab("OPENED", "ws://127.0.0.1/devtools/page/OPENED");
const { selection, listTabs, openTab } = createSelectionHarness({
snapshots: [[], []],
openedTab,
});
await expect(selection.ensureTabAvailable()).resolves.toEqual(openedTab);
expect(openTab).toHaveBeenCalledOnce();
expect(listTabs).toHaveBeenCalledTimes(2);
});
it("preserves a target-id-only opened tab for a Playwright-backed caller", async () => {
vi.useFakeTimers();
const openedTab = tab("OPENED");
const otherWithWs = tab("OTHER", "ws://127.0.0.1/devtools/page/OTHER");
const { selection } = createSelectionHarness({
snapshots: [[], [otherWithWs]],
openedTab,
});
const selected = selection.ensureTabAvailable(undefined, {
allowPlaywrightFallback: true,
});
await advancePastDiscoveryWindow();
await expect(selected).resolves.toEqual(openedTab);
});
it("polls until delayed wsUrl discovery makes an existing tab selectable", async () => {
vi.useFakeTimers();
const withoutWs = tab("LAGGING");
const withWs = tab("LAGGING", "ws://127.0.0.1/devtools/page/LAGGING");
const { selection, listTabs, openTab } = createSelectionHarness({
snapshots: [[withoutWs], [withoutWs], [withWs]],
});
const selected = selection.ensureTabAvailable();
await vi.advanceTimersByTimeAsync(OPEN_TAB_DISCOVERY_POLL_MS);
await expect(selected).resolves.toEqual(withWs);
expect(listTabs).toHaveBeenCalledTimes(3);
expect(openTab).not.toHaveBeenCalled();
});
it("allows an existing target-id-only tab only for Playwright-backed callers", async () => {
vi.useFakeTimers();
const withoutWs = tab("PLAYWRIGHT_TARGET");
const otherWithWs = tab("OTHER", "ws://127.0.0.1/devtools/page/OTHER");
const { selection } = createSelectionHarness({
snapshots: [[withoutWs, otherWithWs]],
});
const selected = selection.ensureTabAvailable("PLAYWRIGHT_TARGET", {
allowPlaywrightFallback: true,
});
await advancePastDiscoveryWindow();
await expect(selected).resolves.toEqual(withoutWs);
});
it("preserves a sticky target-id-only tab instead of switching to another tab", async () => {
vi.useFakeTimers();
const stickyWithoutWs = tab("STICKY");
const otherWithWs = tab("OTHER", "ws://127.0.0.1/devtools/page/OTHER");
const { selection, profileState } = createSelectionHarness({
snapshots: [[stickyWithoutWs, otherWithWs]],
});
profileState.lastTargetId = stickyWithoutWs.targetId;
const selected = selection.ensureTabAvailable(undefined, {
allowPlaywrightFallback: true,
});
await advancePastDiscoveryWindow();
await expect(selected).resolves.toEqual(stickyWithoutWs);
});
it("keeps polling after a transient tab-list rejection", async () => {
vi.useFakeTimers();
const withoutWs = tab("RECOVERED");
const withWs = tab("RECOVERED", "ws://127.0.0.1/devtools/page/RECOVERED");
const { selection, listTabs } = createSelectionHarness({
snapshots: [[withoutWs], new Error("transient list failure"), [withWs]],
});
const selected = selection.ensureTabAvailable();
await vi.advanceTimersByTimeAsync(OPEN_TAB_DISCOVERY_POLL_MS);
await expect(selected).resolves.toEqual(withWs);
expect(listTabs).toHaveBeenCalledTimes(3);
});
it("falls back to the last nonempty unfiltered snapshot after empty relists", async () => {
vi.useFakeTimers();
const withoutWs = tab("LAST_NONEMPTY");
const { selection, openTab } = createSelectionHarness({
snapshots: [[withoutWs], [], new Error("transient list failure")],
});
const selected = selection.ensureTabAvailable(undefined, {
allowPlaywrightFallback: true,
});
await advancePastDiscoveryWindow();
await expect(selected).resolves.toEqual(withoutWs);
expect(openTab).not.toHaveBeenCalled();
});
it("rejects a target-id-only local tab when the caller cannot use Playwright", async () => {
vi.useFakeTimers();
const { selection } = createSelectionHarness({
snapshots: [[tab("NO_PLAYWRIGHT")]],
});
const selected = expect(selection.ensureTabAvailable("NO_PLAYWRIGHT")).rejects.toThrow(
/tab not found/i,
);
await advancePastDiscoveryWindow();
await selected;
});
});

View File

@@ -2,7 +2,6 @@
* Browser tab selection operations for default tab choice, focus, and close.
*/
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { formatErrorMessage } from "../infra/errors.js";
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import { fetchOk, normalizeCdpHttpBaseForJsonEndpoints } from "./cdp.helpers.js";
import { appendCdpPath } from "./cdp.js";
@@ -12,15 +11,7 @@ import { BrowserTabNotFoundError, BrowserTargetAmbiguousError } from "./errors.j
import { getBrowserProfileCapabilities } from "./profile-capabilities.js";
import type { PwAiModule } from "./pw-ai-module.js";
import { getPwAiModule } from "./pw-ai-module.js";
import {
OPEN_TAB_DISCOVERY_POLL_MS,
OPEN_TAB_DISCOVERY_WINDOW_MS,
} from "./server-context.constants.js";
import type {
BrowserTab,
EnsureTabAvailableOptions,
ProfileRuntimeState,
} from "./server-context.types.js";
import type { BrowserTab, ProfileRuntimeState } from "./server-context.types.js";
import { resolveTargetIdFromTabs } from "./target-id.js";
type SelectionDeps = {
@@ -33,40 +24,11 @@ type SelectionDeps = {
};
type SelectionOps = {
ensureTabAvailable: (
targetId?: string,
options?: EnsureTabAvailableOptions,
) => Promise<BrowserTab>;
ensureTabAvailable: (targetId?: string) => Promise<BrowserTab>;
focusTab: (targetId: string) => Promise<void>;
closeTab: (targetId: string) => Promise<void>;
};
function mergeOpenedTabSnapshot(
tabs: BrowserTab[],
openedTab: BrowserTab | undefined,
): BrowserTab[] {
if (!openedTab) {
return tabs;
}
const index = tabs.findIndex((tab) => tab.targetId === openedTab.targetId);
if (index < 0) {
return [...tabs, openedTab];
}
const listedTab = tabs[index];
if (!listedTab || listedTab.wsUrl || !openedTab.wsUrl) {
return tabs;
}
const merged = tabs.slice();
merged[index] = { ...listedTab, wsUrl: openedTab.wsUrl };
return merged;
}
function waitForTabDiscoveryPoll(): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, OPEN_TAB_DISCOVERY_POLL_MS);
});
}
/** Builds tab selection/focus/close operations for one resolved browser profile. */
export function createProfileSelectionOps({
profile,
@@ -79,99 +41,16 @@ export function createProfileSelectionOps({
const cdpHttpBase = normalizeCdpHttpBaseForJsonEndpoints(profile.cdpUrl);
const capabilities = getBrowserProfileCapabilities(profile);
const ensureTabAvailable = async (
targetId?: string,
options?: EnsureTabAvailableOptions,
): Promise<BrowserTab> => {
const ensureTabAvailable = async (targetId?: string): Promise<BrowserTab> => {
await ensureBrowserAvailable();
const profileState = getProfileState();
let lastNonEmptyTabs: BrowserTab[] = [];
let lastListError: unknown;
let sawSuccessfulList = false;
let openedTab: BrowserTab | undefined;
const readTabs = async (): Promise<BrowserTab[]> => {
try {
const tabs = await listTabs();
sawSuccessfulList = true;
if (tabs.length > 0) {
lastNonEmptyTabs = tabs;
}
return tabs;
} catch (err) {
lastListError = err;
return [];
}
};
const openWhenConfirmedEmpty = async (tabs: BrowserTab[]): Promise<void> => {
if (!openedTab && sawSuccessfulList && lastNonEmptyTabs.length === 0 && tabs.length === 0) {
openedTab = await openTab("about:blank");
}
};
const candidateTabs = (tabs: BrowserTab[]) =>
capabilities.supportsPerTabWs ? tabs.filter((tab) => Boolean(tab.wsUrl)) : tabs;
const canResolveSelection = (tabs: BrowserTab[]) => {
const desiredTargetId =
targetId ??
openedTab?.targetId ??
normalizeOptionalString(profileState.lastTargetId) ??
undefined;
if (!desiredTargetId) {
return tabs.length > 0;
}
const resolved = resolveTargetIdFromTabs(desiredTargetId, tabs);
return resolved.ok || resolved.reason === "ambiguous";
};
const tabs1 = await readTabs();
await openWhenConfirmedEmpty(tabs1);
let listedTabs = await readTabs();
await openWhenConfirmedEmpty(listedTabs);
let unfilteredTabs = mergeOpenedTabSnapshot(listedTabs, openedTab);
let candidates = candidateTabs(unfilteredTabs);
const preservedCanResolveSelection = () =>
canResolveSelection(mergeOpenedTabSnapshot(lastNonEmptyTabs, openedTab));
if (
capabilities.supportsPerTabWs &&
!canResolveSelection(candidates) &&
(candidates.length === 0 ||
canResolveSelection(unfilteredTabs) ||
preservedCanResolveSelection())
) {
const deadline = Date.now() + OPEN_TAB_DISCOVERY_WINDOW_MS;
while (Date.now() < deadline) {
await waitForTabDiscoveryPoll();
listedTabs = await readTabs();
await openWhenConfirmedEmpty(listedTabs);
unfilteredTabs = mergeOpenedTabSnapshot(listedTabs, openedTab);
candidates = candidateTabs(unfilteredTabs);
if (canResolveSelection(candidates)) {
break;
}
}
const tabs1 = await listTabs();
if (tabs1.length === 0) {
await openTab("about:blank");
}
if (!canResolveSelection(candidates)) {
// Keep the last useful discovery snapshot across empty or failed relists.
// Target-id-only fallback is opt-in because only Playwright-backed callers can use it safely.
const preservedTabs = mergeOpenedTabSnapshot(lastNonEmptyTabs, openedTab);
const preservedCandidates = candidateTabs(preservedTabs);
if (canResolveSelection(preservedCandidates)) {
candidates = preservedCandidates;
} else if (options?.allowPlaywrightFallback && canResolveSelection(preservedTabs)) {
candidates = preservedTabs;
}
}
if (candidates.length === 0 && !sawSuccessfulList && lastListError) {
throw lastListError instanceof Error
? lastListError
: new Error(formatErrorMessage(lastListError));
}
const tabs = await listTabs();
const candidates = capabilities.supportsPerTabWs ? tabs.filter((t) => Boolean(t.wsUrl)) : tabs;
const resolveById = (raw: string) => {
const resolved = resolveTargetIdFromTabs(raw, candidates);

View File

@@ -265,8 +265,7 @@ export function createBrowserRouteContext(opts: ContextOptions): BrowserRouteCon
listProfiles,
// Legacy methods delegate to default profile
ensureBrowserAvailable: () => getDefaultContext().ensureBrowserAvailable(),
ensureTabAvailable: (targetId, options) =>
getDefaultContext().ensureTabAvailable(targetId, options),
ensureTabAvailable: (targetId) => getDefaultContext().ensureTabAvailable(targetId),
isHttpReachable: (timeoutMs) => getDefaultContext().isHttpReachable(timeoutMs),
isTransportAvailable: (timeoutMs) => getDefaultContext().isTransportAvailable(timeoutMs),
isReachable: (timeoutMs, options) => getDefaultContext().isReachable(timeoutMs, options),

View File

@@ -43,17 +43,9 @@ export type BrowserServerState = {
stopUnhandledRejectionHandler?: () => void;
};
export type EnsureTabAvailableOptions = {
/** Allow a target-id-only tab when the caller can continue through Playwright. */
allowPlaywrightFallback?: boolean;
};
type BrowserProfileActions = {
ensureBrowserAvailable: (opts?: { headless?: boolean }) => Promise<void>;
ensureTabAvailable: (
targetId?: string,
options?: EnsureTabAvailableOptions,
) => Promise<BrowserTab>;
ensureTabAvailable: (targetId?: string) => Promise<BrowserTab>;
isHttpReachable: (timeoutMs?: number) => Promise<boolean>;
isTransportAvailable: (timeoutMs?: number) => Promise<boolean>;
isReachable: (

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/byteplus-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw BytePlus provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/canvas-plugin",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw Canvas plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/cerebras-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw Cerebras provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/chutes-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw Chutes.ai provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/clickclack",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw ClickClack channel plugin",
"type": "module",
@@ -18,7 +18,7 @@
"openclaw": "2026.5.28"
},
"peerDependencies": {
"openclaw": ">=2026.6.8"
"openclaw": ">=2026.6.9"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/cloudflare-ai-gateway-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw Cloudflare AI Gateway provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/codex-supervisor",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw Codex app-server fleet supervision plugin.",
"type": "module",

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/codex",
"version": "2026.6.8",
"version": "2026.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/codex",
"version": "2026.6.8",
"version": "2026.6.9",
"dependencies": {
"@openai/codex": "0.139.0",
"typebox": "1.1.39",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/codex",
"version": "2026.6.8",
"version": "2026.6.9",
"description": "OpenClaw Codex app-server harness and model provider plugin with a Codex-managed GPT catalog.",
"repository": {
"type": "git",
@@ -34,10 +34,10 @@
]
},
"compat": {
"pluginApi": ">=2026.6.8"
"pluginApi": ">=2026.6.9"
},
"build": {
"openclawVersion": "2026.6.8"
"openclawVersion": "2026.6.9"
},
"release": {
"publishToClawHub": true,

View File

@@ -18,7 +18,6 @@ describe("Codex app-server attempt context", () => {
it("returns a run context report without deferred Codex dynamic tool schemas", () => {
const tools = [
{
type: "function",
name: "message",
description: "Send a message.",
inputSchema: {
@@ -29,23 +28,15 @@ describe("Codex app-server attempt context", () => {
},
},
{
type: "namespace",
name: "openclaw",
description: "",
tools: [
{
type: "function",
name: "web_search",
description: "Search the web.",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
},
},
deferLoading: true,
name: "web_search",
description: "Search the web.",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
},
],
},
deferLoading: true,
},
] as CodexDynamicToolSpec[];

View File

@@ -17,8 +17,7 @@ import {
import { resolveAgentWorkspaceDir } from "openclaw/plugin-sdk/agent-runtime";
import { buildMemorySystemPromptAddition } from "openclaw/plugin-sdk/core";
import { MESSAGE_TOOL_DELIVERY_HINTS } from "openclaw/plugin-sdk/message-tool-delivery-hints";
import type { CodexDynamicToolFunctionSpec, CodexDynamicToolSpec, JsonValue } from "./protocol.js";
import { flattenCodexDynamicToolFunctions } from "./protocol.js";
import type { CodexDynamicToolSpec, JsonValue } from "./protocol.js";
import { isJsonObject } from "./protocol.js";
import type { CodexAppServerThreadBinding } from "./session-binding.js";
import { readCodexMirroredSessionHistoryMessages } from "./session-history.js";
@@ -281,7 +280,7 @@ export function buildCodexSystemPromptReport(params: {
skillsPrompt: string;
tools: CodexDynamicToolSpec[];
}): CodexSystemPromptReport {
const toolEntries = flattenCodexDynamicToolFunctions(params.tools).map(buildCodexToolReportEntry);
const toolEntries = params.tools.map(buildCodexToolReportEntry);
const schemaChars = toolEntries.reduce((sum, tool) => sum + tool.schemaChars, 0);
const skillsPrompt = params.skillsPrompt.trim();
const bootstrapMaxChars = readPositiveNumber(
@@ -345,7 +344,7 @@ function buildCodexSkillReportEntries(
.filter((entry) => entry.blockChars > 0);
}
function buildCodexToolReportEntry(tool: CodexDynamicToolFunctionSpec): CodexToolReportEntry {
function buildCodexToolReportEntry(tool: CodexDynamicToolSpec): CodexToolReportEntry {
const summary = tool.description.trim();
if (tool.deferLoading === true) {
return {
@@ -855,15 +854,13 @@ function renderCodexMemoryToolSearchBridge(toolNames: readonly string[]): string
}
/** Returns whether the current dynamic tool list can serve workspace memory. */
export function hasCodexWorkspaceMemoryTools(tools: readonly CodexDynamicToolSpec[]): boolean {
export function hasCodexWorkspaceMemoryTools(tools: readonly { name: string }[]): boolean {
return getCodexWorkspaceMemoryToolNames(tools).length > 0;
}
/** Lists available memory tool names understood by Codex workspace memory routing. */
export function getCodexWorkspaceMemoryToolNames(tools: readonly CodexDynamicToolSpec[]): string[] {
const availableToolNames = new Set(
flattenCodexDynamicToolFunctions(tools).map((tool) => normalizeCodexDynamicToolName(tool.name)),
);
export function getCodexWorkspaceMemoryToolNames(tools: readonly { name: string }[]): string[] {
const availableToolNames = new Set(tools.map((tool) => normalizeCodexDynamicToolName(tool.name)));
return Array.from(CODEX_MEMORY_TOOL_NAMES).filter((name) => availableToolNames.has(name));
}

View File

@@ -29,7 +29,6 @@ import {
shouldUseDirectCodexDynamicToolsForModel,
} from "./dynamic-tool-profile.js";
import { createCodexDynamicToolBridge } from "./dynamic-tools.js";
import { flattenCodexDynamicToolFunctions } from "./protocol.js";
import { createCodexTestModel } from "./test-support.js";
let tempDir: string;
@@ -402,9 +401,7 @@ describe("Codex app-server dynamic tool build", () => {
expect(shouldUseDirectCodexDynamicToolsForModel("gpt-5.4-nano")).toBe(true);
expect(resolveCodexDynamicToolsLoadingForModel({}, "gpt-5.4-nano")).toBe("direct");
expect(resolveCodexDynamicToolsLoadingForModel({}, "gpt-5.5")).toBe("searchable");
const webSearch = flattenCodexDynamicToolFunctions(toolBridge.specs).find(
(tool) => tool.name === "web_search",
);
const webSearch = toolBridge.specs.find((tool) => tool.name === "web_search");
expect(webSearch).not.toHaveProperty("deferLoading");
expect(webSearch).not.toHaveProperty("namespace");
});

View File

@@ -27,7 +27,7 @@ import {
CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE,
createCodexDynamicToolBridge,
} from "./dynamic-tools.js";
import type { CodexDynamicToolFunctionSpec, CodexDynamicToolSpec, JsonValue } from "./protocol.js";
import type { JsonValue } from "./protocol.js";
function createTool(overrides: Partial<AnyAgentTool>): AnyAgentTool {
return {
@@ -115,20 +115,6 @@ function expectDynamicSpec(
}
}
function flattenSpecsWithNamespace(
specs: readonly CodexDynamicToolSpec[],
): Array<CodexDynamicToolFunctionSpec & { namespace?: string }> {
return specs.flatMap((spec) =>
spec.type === "namespace"
? spec.tools.map((tool) => ({ ...tool, namespace: spec.name }))
: [spec],
);
}
function specNames(specs: readonly CodexDynamicToolSpec[]): string[] {
return flattenSpecsWithNamespace(specs).map((tool) => tool.name);
}
function expectNoNamespace(spec: unknown) {
const record = requireRecord(spec, "tool spec");
expect(record).not.toHaveProperty("namespace");
@@ -190,12 +176,11 @@ describe("createCodexDynamicToolBridge", () => {
signal: new AbortController().signal,
});
const specs = flattenSpecsWithNamespace(bridge.specs);
const webSearch = specs.find((tool) => tool.name === "web_search");
const message = specs.find((tool) => tool.name === "message");
const heartbeat = specs.find((tool) => tool.name === HEARTBEAT_RESPONSE_TOOL_NAME);
const sessionsSpawn = specs.find((tool) => tool.name === "sessions_spawn");
const sessionsYield = specs.find((tool) => tool.name === "sessions_yield");
const webSearch = bridge.specs.find((tool) => tool.name === "web_search");
const message = bridge.specs.find((tool) => tool.name === "message");
const heartbeat = bridge.specs.find((tool) => tool.name === HEARTBEAT_RESPONSE_TOOL_NAME);
const sessionsSpawn = bridge.specs.find((tool) => tool.name === "sessions_spawn");
const sessionsYield = bridge.specs.find((tool) => tool.name === "sessions_yield");
expectDynamicSpec(webSearch, {
name: "web_search",
@@ -227,21 +212,14 @@ describe("createCodexDynamicToolBridge", () => {
directToolNames: ["message"],
});
const specs = flattenSpecsWithNamespace(bridge.specs);
expect(bridge.specs).toHaveLength(2);
expectDynamicSpec(
specs.find((tool) => tool.name === "message"),
{ name: "message" },
);
expectDynamicSpec(
specs.find((tool) => tool.name === "web_search"),
{
name: "web_search",
namespace: CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE,
deferLoading: true,
},
);
expectNoNamespace(specs.find((tool) => tool.name === "message"));
expectDynamicSpec(bridge.specs[0], { name: "message" });
expectDynamicSpec(bridge.specs[1], {
name: "web_search",
namespace: CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE,
deferLoading: true,
});
expectNoNamespace(bridge.specs[0]);
});
it("can register a durable tool schema while denying execution for the current turn", async () => {
@@ -258,8 +236,11 @@ describe("createCodexDynamicToolBridge", () => {
hookContext: { runId: "run-unavailable", onToolOutcome },
});
expect(specNames(bridge.availableSpecs)).toEqual(["message"]);
expect(specNames(bridge.specs)).toEqual(["message", HEARTBEAT_RESPONSE_TOOL_NAME]);
expect(bridge.availableSpecs.map((tool) => tool.name)).toEqual(["message"]);
expect(bridge.specs.map((tool) => tool.name)).toEqual([
"message",
HEARTBEAT_RESPONSE_TOOL_NAME,
]);
const result = await bridge.handleToolCall(
{
@@ -331,11 +312,11 @@ describe("createCodexDynamicToolBridge", () => {
signal: new AbortController().signal,
});
expect(flattenSpecsWithNamespace(bridge.availableSpecs)[0]?.inputSchema).toEqual({
expect(bridge.availableSpecs[0]?.inputSchema).toEqual({
type: "object",
properties: { current: { type: "string" } },
});
expect(flattenSpecsWithNamespace(bridge.specs)[0]?.inputSchema).toEqual({
expect(bridge.specs[0]?.inputSchema).toEqual({
type: "object",
properties: { durable: { type: "string" } },
});
@@ -371,8 +352,8 @@ describe("createCodexDynamicToolBridge", () => {
unsubscribeDiagnostics();
}
expect(specNames(bridge.availableSpecs)).toEqual(["message"]);
expect(specNames(bridge.specs)).toEqual(["message"]);
expect(bridge.availableSpecs.map((tool) => tool.name)).toEqual(["message"]);
expect(bridge.specs.map((tool) => tool.name)).toEqual(["message"]);
expect(bridge.telemetry.quarantinedTools).toEqual([
{
tool: "fuzzplugin_move_angles",
@@ -469,8 +450,8 @@ describe("createCodexDynamicToolBridge", () => {
signal: new AbortController().signal,
});
expect(specNames(bridge.availableSpecs)).toEqual(["message"]);
expect(specNames(bridge.specs)).toEqual(["message"]);
expect(bridge.availableSpecs.map((tool) => tool.name)).toEqual(["message"]);
expect(bridge.specs.map((tool) => tool.name)).toEqual(["message"]);
expect(bridge.telemetry.quarantinedTools).toEqual([
{
tool: "tool[0]",
@@ -528,8 +509,8 @@ describe("createCodexDynamicToolBridge", () => {
signal: new AbortController().signal,
});
expect(specNames(registeredBridge.availableSpecs)).toEqual(["message"]);
expect(specNames(registeredBridge.specs)).toEqual(["message"]);
expect(registeredBridge.availableSpecs.map((tool) => tool.name)).toEqual(["message"]);
expect(registeredBridge.specs.map((tool) => tool.name)).toEqual(["message"]);
});
it("can expose all dynamic tools directly for compatibility", () => {

View File

@@ -48,7 +48,6 @@ import type {
CodexDynamicToolCallParams,
CodexDynamicToolCallResponse,
CodexDynamicToolDiagnosticTerminalType,
CodexDynamicToolFunctionSpec,
CodexDynamicToolSpec,
JsonValue,
} from "./protocol.js";
@@ -202,16 +201,20 @@ export function createCodexDynamicToolBridge(params: {
...(params.directToolNames ?? []),
]);
return {
availableSpecs: createCodexDynamicToolSpecs({
entries: availableTools,
loading: params.loading ?? "searchable",
directToolNames,
}),
specs: createCodexDynamicToolSpecs({
entries: registeredSpecTools,
loading: params.loading ?? "searchable",
directToolNames,
}),
availableSpecs: availableTools.map((entry) =>
createCodexDynamicToolSpec({
entry,
loading: params.loading ?? "searchable",
directToolNames,
}),
),
specs: registeredSpecTools.map((entry) =>
createCodexDynamicToolSpec({
entry,
loading: params.loading ?? "searchable",
directToolNames,
}),
),
telemetry,
handleToolCall: async (call, options) => {
const toolEntry = toolMap.get(call.tool);
@@ -499,41 +502,24 @@ function wrapProjectedCodexDynamicTools(
return { tools: wrappedTools, quarantinedTools };
}
function createCodexDynamicToolSpecs(params: {
entries: readonly ProjectedCodexDynamicTool[];
function createCodexDynamicToolSpec(params: {
entry: ProjectedCodexDynamicTool;
loading: CodexDynamicToolsLoading;
directToolNames: ReadonlySet<string>;
}): CodexDynamicToolSpec[] {
const specs: CodexDynamicToolSpec[] = [];
const namespaceTools: CodexDynamicToolFunctionSpec[] = [];
for (const entry of params.entries) {
const functionSpec = createCodexDynamicToolFunctionSpec({ entry });
if (params.loading === "direct" || params.directToolNames.has(entry.name)) {
specs.push(functionSpec);
continue;
}
namespaceTools.push({ ...functionSpec, deferLoading: true });
}
if (namespaceTools.length > 0) {
specs.push({
type: "namespace",
name: CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE,
description: "",
tools: namespaceTools,
});
}
return specs;
}
function createCodexDynamicToolFunctionSpec(params: {
entry: ProjectedCodexDynamicTool;
}): CodexDynamicToolFunctionSpec {
return {
type: "function",
}): CodexDynamicToolSpec {
const base = {
name: params.entry.name,
description: params.entry.description,
inputSchema: params.entry.inputSchema,
};
if (params.loading === "direct" || params.directToolNames.has(params.entry.name)) {
return base;
}
return {
...base,
namespace: CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE,
deferLoading: true,
};
}
function projectCodexDynamicTools(tools: readonly AnyAgentTool[]): {

View File

@@ -45,14 +45,6 @@
},
{
"properties": {
"credentialSource": {
"allOf": [
{
"$ref": "#/definitions/AmazonBedrockCredentialSource"
}
],
"default": "awsManaged"
},
"type": {
"enum": [
"amazonBedrock"
@@ -69,13 +61,6 @@
}
]
},
"AmazonBedrockCredentialSource": {
"enum": [
"codexManaged",
"awsManaged"
],
"type": "string"
},
"PlanType": {
"enum": [
"free",

View File

@@ -861,14 +861,6 @@
}
]
},
"SubAgentActivityKind": {
"enum": [
"started",
"interacted",
"interrupted"
],
"type": "string"
},
"SubAgentSource": {
"oneOf": [
{
@@ -1055,14 +1047,6 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -1633,38 +1617,6 @@
"title": "CollabAgentToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentPath": {
"type": "string"
},
"agentThreadId": {
"type": "string"
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/SubAgentActivityKind"
},
"type": {
"enum": [
"subAgentActivity"
],
"title": "SubAgentActivityThreadItemType",
"type": "string"
}
},
"required": [
"agentPath",
"agentThreadId",
"id",
"kind",
"type"
],
"title": "SubAgentActivityThreadItem",
"type": "object"
},
{
"properties": {
"action": {
@@ -1723,32 +1675,6 @@
"title": "ImageViewThreadItem",
"type": "object"
},
{
"properties": {
"durationMs": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"sleep"
],
"title": "SleepThreadItemType",
"type": "string"
}
},
"required": [
"durationMs",
"id",
"type"
],
"title": "SleepThreadItem",
"type": "object"
},
{
"properties": {
"id": {
@@ -1864,6 +1790,11 @@
]
},
"ThreadSource": {
"enum": [
"user",
"subagent",
"memory_consolidation"
],
"type": "string"
},
"ThreadStatus": {

View File

@@ -861,14 +861,6 @@
}
]
},
"SubAgentActivityKind": {
"enum": [
"started",
"interacted",
"interrupted"
],
"type": "string"
},
"SubAgentSource": {
"oneOf": [
{
@@ -1055,14 +1047,6 @@
"description": "Usually the first user message in the thread, if available.",
"type": "string"
},
"recencyAt": {
"description": "Unix timestamp (in seconds) used for thread recency ordering.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"sessionId": {
"description": "Session id shared by threads that belong to the same session tree.",
"type": "string"
@@ -1633,38 +1617,6 @@
"title": "CollabAgentToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentPath": {
"type": "string"
},
"agentThreadId": {
"type": "string"
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/SubAgentActivityKind"
},
"type": {
"enum": [
"subAgentActivity"
],
"title": "SubAgentActivityThreadItemType",
"type": "string"
}
},
"required": [
"agentPath",
"agentThreadId",
"id",
"kind",
"type"
],
"title": "SubAgentActivityThreadItem",
"type": "object"
},
{
"properties": {
"action": {
@@ -1723,32 +1675,6 @@
"title": "ImageViewThreadItem",
"type": "object"
},
{
"properties": {
"durationMs": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"sleep"
],
"title": "SleepThreadItemType",
"type": "string"
}
},
"required": [
"durationMs",
"id",
"type"
],
"title": "SleepThreadItem",
"type": "object"
},
{
"properties": {
"id": {
@@ -1864,6 +1790,11 @@
]
},
"ThreadSource": {
"enum": [
"user",
"subagent",
"memory_consolidation"
],
"type": "string"
},
"ThreadStatus": {

View File

@@ -610,14 +610,6 @@
"minLength": 1,
"type": "string"
},
"SubAgentActivityKind": {
"enum": [
"started",
"interacted",
"interrupted"
],
"type": "string"
},
"TextElement": {
"properties": {
"byteRange": {
@@ -1141,38 +1133,6 @@
"title": "CollabAgentToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentPath": {
"type": "string"
},
"agentThreadId": {
"type": "string"
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/SubAgentActivityKind"
},
"type": {
"enum": [
"subAgentActivity"
],
"title": "SubAgentActivityThreadItemType",
"type": "string"
}
},
"required": [
"agentPath",
"agentThreadId",
"id",
"kind",
"type"
],
"title": "SubAgentActivityThreadItem",
"type": "object"
},
{
"properties": {
"action": {
@@ -1231,32 +1191,6 @@
"title": "ImageViewThreadItem",
"type": "object"
},
{
"properties": {
"durationMs": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"sleep"
],
"title": "SleepThreadItemType",
"type": "string"
}
},
"required": [
"durationMs",
"id",
"type"
],
"title": "SleepThreadItem",
"type": "object"
},
{
"properties": {
"id": {

View File

@@ -610,14 +610,6 @@
"minLength": 1,
"type": "string"
},
"SubAgentActivityKind": {
"enum": [
"started",
"interacted",
"interrupted"
],
"type": "string"
},
"TextElement": {
"properties": {
"byteRange": {
@@ -1141,38 +1133,6 @@
"title": "CollabAgentToolCallThreadItem",
"type": "object"
},
{
"properties": {
"agentPath": {
"type": "string"
},
"agentThreadId": {
"type": "string"
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/SubAgentActivityKind"
},
"type": {
"enum": [
"subAgentActivity"
],
"title": "SubAgentActivityThreadItemType",
"type": "string"
}
},
"required": [
"agentPath",
"agentThreadId",
"id",
"kind",
"type"
],
"title": "SubAgentActivityThreadItem",
"type": "object"
},
{
"properties": {
"action": {
@@ -1231,32 +1191,6 @@
"title": "ImageViewThreadItem",
"type": "object"
},
{
"properties": {
"durationMs": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"sleep"
],
"title": "SleepThreadItemType",
"type": "string"
}
},
"required": [
"durationMs",
"id",
"type"
],
"title": "SleepThreadItem",
"type": "object"
},
{
"properties": {
"id": {

View File

@@ -65,43 +65,12 @@ export type CodexUserInput =
path: string;
};
export type CodexDynamicToolFunctionSpec = JsonObject & {
type: "function";
export type CodexDynamicToolSpec = JsonObject & {
name: string;
description: string;
inputSchema: JsonValue;
deferLoading?: boolean;
};
export type CodexDynamicToolNamespaceTool = CodexDynamicToolFunctionSpec;
export type CodexDynamicToolNamespaceSpec = JsonObject & {
type: "namespace";
name: string;
description: string;
tools: CodexDynamicToolNamespaceTool[];
};
export type CodexDynamicToolSpec = CodexDynamicToolFunctionSpec | CodexDynamicToolNamespaceSpec;
export type CodexLegacyDynamicToolFunctionSpec = JsonObject & {
name: string;
description: string;
inputSchema: JsonValue;
deferLoading?: boolean;
namespace?: string;
};
export type CodexThreadStartDynamicToolSpec =
| CodexDynamicToolSpec
| CodexLegacyDynamicToolFunctionSpec;
export function flattenCodexDynamicToolFunctions(
tools: readonly CodexDynamicToolSpec[] | undefined,
): CodexDynamicToolFunctionSpec[] {
return (tools ?? []).flatMap((tool) => (tool.type === "namespace" ? tool.tools : [tool]));
}
export type CodexTurnEnvironmentParams = JsonObject & {
environmentId: string;
cwd: string;
@@ -117,7 +86,7 @@ export type CodexThreadStartParams = JsonObject & {
approvalsReviewer?: string | null;
sandbox?: string;
serviceTier?: CodexServiceTier | null;
dynamicTools?: CodexThreadStartDynamicToolSpec[] | null;
dynamicTools?: CodexDynamicToolSpec[] | null;
developerInstructions?: string;
experimentalRawEvents?: boolean;
environments?: CodexTurnEnvironmentParams[] | null;

View File

@@ -14,10 +14,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { CodexAppServerClientFactory } from "./client-factory.js";
import type { CodexServerNotification } from "./protocol.js";
import { runCodexAppServerAttempt as runCodexAppServerAttemptImpl } from "./run-attempt.js";
import {
readCodexAppServerBinding,
writeCodexAppServerBinding as writeRawCodexAppServerBinding,
} from "./session-binding.js";
import { readCodexAppServerBinding, writeCodexAppServerBinding } from "./session-binding.js";
import { createCodexTestModel } from "./test-support.js";
let tempDir: string;
@@ -245,23 +242,6 @@ function createContextEngine(overrides: Partial<ContextEngine> = {}): ContextEng
return engine;
}
const DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT = JSON.stringify({
"features.standalone_web_search": false,
web_search: "disabled",
});
function writeCodexAppServerBinding(...args: Parameters<typeof writeRawCodexAppServerBinding>) {
const [sessionFile, binding, lookup] = args;
return writeRawCodexAppServerBinding(
sessionFile,
{
webSearchThreadConfigFingerprint: DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT,
...binding,
},
lookup,
);
}
type MockCallReader = { mock: { calls: unknown[][] } };
function requireRecord(value: unknown, label: string): Record<string, unknown> {
@@ -347,9 +327,6 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
const params = createParams(sessionFile, workspaceDir);
params.contextEngine = contextEngine;
params.contextTokenBudget = 321;
params.requestedModelId = "gpt-5.4-codex-primary";
params.fallbackReason = "provider_unavailable";
params.degradedReason = "context_overflow";
params.config = { memory: { citations: "on" } } as EmbeddedRunAttemptParams["config"];
const run = runCodexAppServerAttempt(params);
@@ -366,17 +343,6 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
expect(bootstrapParams.sessionId).toBe("session-1");
expect(bootstrapParams.sessionKey).toBe("agent:main:session-1");
expect(bootstrapParams.sessionFile).toBe(sessionFile);
expect(bootstrapParams.runtimeSettings).toMatchObject({
runtime: { mode: "degraded" },
model: {
requested: "gpt-5.4-codex-primary",
resolved: "gpt-5.4-codex",
},
diagnostics: {
fallbackReason: "provider_unavailable",
degradedReason: "context_overflow",
},
});
expect(contextEngine["assemble"]).toHaveBeenCalledTimes(1);
const assembleParams = requireFirstCallArg(contextEngine["assemble"], "assemble") as Parameters<
@@ -387,17 +353,6 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
expect(assembleParams.tokenBudget).toBe(321);
expect(assembleParams.citationsMode).toBe("on");
expect(assembleParams.model).toBe("gpt-5.4-codex");
expect(assembleParams.runtimeSettings).toMatchObject({
runtime: { mode: "degraded" },
model: {
requested: "gpt-5.4-codex-primary",
resolved: "gpt-5.4-codex",
},
diagnostics: {
fallbackReason: "provider_unavailable",
degradedReason: "context_overflow",
},
});
expect(assembleParams.prompt).toBe("hello");
expect(assembleParams.messages.map((message) => message.role)).toEqual(["assistant"]);
expect(assembleParams.availableTools).toEqual(new Set());
@@ -1684,9 +1639,6 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
const params = createParams(sessionFile, workspaceDir);
params.contextEngine = contextEngine;
params.contextTokenBudget = 111;
params.requestedModelId = "gpt-5.4-codex-primary";
params.fallbackReason = "provider_unavailable";
params.degradedReason = "context_overflow";
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
@@ -1701,24 +1653,9 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
expect(afterTurnCall.sessionKey).toBe("agent:main:session-1");
expect(afterTurnCall.prePromptMessageCount).toBe(0);
expect(afterTurnCall.tokenBudget).toBe(111);
expect(afterTurnCall.runtimeSettings).toMatchObject({
runtime: { mode: "degraded" },
model: {
requested: "gpt-5.4-codex-primary",
resolved: "gpt-5.4-codex",
},
diagnostics: {
fallbackReason: "provider_unavailable",
degradedReason: "context_overflow",
},
});
expect(afterTurnCall.messages.some((message) => message.role === "user")).toBe(true);
expect(afterTurnCall.messages.some((message) => message.role === "assistant")).toBe(true);
expect(maintain).toHaveBeenCalledTimes(1);
const maintainCall = requireFirstCallArg(maintain, "maintain") as Parameters<
NonNullable<ContextEngine["maintain"]>
>[0];
expect(maintainCall.runtimeSettings).toBe(afterTurnCall.runtimeSettings);
});
it("reloads mirrored history after bootstrap mutates the session transcript", async () => {

View File

@@ -23,11 +23,7 @@ import {
emitDynamicToolTerminalDiagnostic,
} from "./dynamic-tool-diagnostics.js";
import { createCodexDynamicToolBridge } from "./dynamic-tools.js";
import {
flattenCodexDynamicToolFunctions,
type CodexDynamicToolCallParams,
type CodexDynamicToolSpec,
} from "./protocol.js";
import type { CodexDynamicToolCallParams } from "./protocol.js";
import {
createParams,
createCodexRuntimePlanFixture,
@@ -43,10 +39,6 @@ function flushDiagnosticEvents() {
return waitForDiagnosticEventsDrained();
}
function specNames(specs: readonly CodexDynamicToolSpec[]): string[] {
return flattenCodexDynamicToolFunctions(specs).map((tool) => tool.name);
}
function activeDiagnosticToolKeys(events: DiagnosticEventPayload[]): Set<string> {
const active = new Set<string>();
for (const event of events) {
@@ -374,7 +366,7 @@ describe("runCodexAppServerAttempt dynamic tools", () => {
"features.code_mode_only"?: boolean;
mcp_servers?: Record<string, unknown>;
};
dynamicTools?: CodexDynamicToolSpec[];
dynamicTools?: Array<{ name: string }>;
environments?: unknown[];
}
| undefined;
@@ -390,7 +382,7 @@ describe("runCodexAppServerAttempt dynamic tools", () => {
},
});
expect(startParams?.environments).toBeUndefined();
expect(specNames(startParams?.dynamicTools ?? [])).toEqual([
expect(startParams?.dynamicTools?.map((tool) => tool.name)).toEqual([
"message",
"node_exec",
"node_process",

View File

@@ -41,12 +41,7 @@ import {
} from "./event-projector.js";
import { buildCodexPluginAppCacheKey } from "./plugin-app-cache-key.js";
import { buildCodexPluginThreadConfig } from "./plugin-thread-config.js";
import {
flattenCodexDynamicToolFunctions,
type CodexDynamicToolFunctionSpec,
type CodexDynamicToolSpec,
type CodexServerNotification,
} from "./protocol.js";
import type { CodexServerNotification } from "./protocol.js";
import {
assistantMessage,
createAppServerHarness,
@@ -154,7 +149,6 @@ function createMessageDynamicTool(
actions: string[] = ["send"],
): Parameters<typeof startOrResumeThread>[0]["dynamicTools"][number] {
return {
type: "function",
name: "message",
description,
inputSchema: {
@@ -175,7 +169,6 @@ function createNamedDynamicTool(
name: string,
): Parameters<typeof startOrResumeThread>[0]["dynamicTools"][number] {
return {
type: "function",
name,
description: `${name} test tool`,
inputSchema: {
@@ -389,20 +382,6 @@ type RuntimeDynamicToolForTest = Parameters<
typeof createCodexDynamicToolBridge
>[0]["tools"][number];
function flattenSpecsWithNamespace(
specs: readonly CodexDynamicToolSpec[],
): Array<CodexDynamicToolFunctionSpec & { namespace?: string }> {
return specs.flatMap((spec) =>
spec.type === "namespace"
? spec.tools.map((tool) => ({ ...tool, namespace: spec.name }))
: [spec],
);
}
function specNames(specs: readonly CodexDynamicToolSpec[]): string[] {
return flattenCodexDynamicToolFunctions(specs).map((tool) => tool.name);
}
function createRuntimeDynamicTool(name: string): RuntimeDynamicToolForTest {
return {
name,
@@ -527,11 +506,11 @@ describe("runCodexAppServerAttempt", () => {
const startRequest = request.mock.calls.find(([method]) => method === "thread/start");
const startParams = startRequest?.[1] as Record<string, unknown> | undefined;
const startConfig = startParams?.config as Record<string, unknown> | undefined;
const startDynamicTools = startParams?.dynamicTools as CodexDynamicToolSpec[] | undefined;
const startDynamicTools = startParams?.dynamicTools as Array<{ name: string }> | undefined;
expect(startConfig?.["features.code_mode"]).toBe(false);
expect(startConfig?.["features.code_mode_only"]).toBe(false);
expect(startParams?.environments).toEqual([]);
expect(specNames(startDynamicTools ?? [])).toEqual([
expect(startDynamicTools?.map((tool) => tool.name)).toEqual([
"message",
"sandbox_exec",
"sandbox_process",
@@ -652,7 +631,7 @@ describe("runCodexAppServerAttempt", () => {
const startParams = startRequest?.[1] as
| {
cwd?: string;
dynamicTools?: CodexDynamicToolSpec[];
dynamicTools?: Array<{ name: string }>;
environments?: Array<{ environmentId?: string; cwd?: string }>;
sandbox?: string;
config?: {
@@ -670,7 +649,7 @@ describe("runCodexAppServerAttempt", () => {
expect(startParams?.config?.["features.code_mode"]).toBe(true);
expect(startParams?.config?.["features.code_mode_only"]).toBe(false);
expect(startParams?.config?.["features.apply_patch_streaming_events"]).toBe(true);
expect(specNames(startParams?.dynamicTools ?? [])).toEqual(["message"]);
expect(startParams?.dynamicTools?.map((tool) => tool.name)).toEqual(["message"]);
expect(startParams?.environments).toEqual([
{ environmentId: environmentAddParams?.environmentId, cwd: "/workspace" },
]);
@@ -923,10 +902,10 @@ describe("runCodexAppServerAttempt", () => {
});
const startRequest = request.mock.calls.find(([method]) => method === "thread/start");
const dynamicToolNames = specNames(
(startRequest?.[1] as { dynamicTools?: CodexDynamicToolSpec[] } | undefined)?.dynamicTools ??
[],
);
const dynamicToolNames = (
(startRequest?.[1] as { dynamicTools?: Array<{ name: string }> } | undefined)?.dynamicTools ??
[]
).map((tool) => tool.name);
expect(dynamicToolNames).toContain("message");
expect(dynamicToolNames).toContain("web_search");
@@ -1593,12 +1572,11 @@ describe("runCodexAppServerAttempt", () => {
directToolNames: ["message"],
});
const specs = flattenSpecsWithNamespace(toolBridge.specs);
const message = specs.find((tool) => tool.name === "message");
const webSearch = specs.find((tool) => tool.name === "web_search");
const heartbeat = specs.find((tool) => tool.name === "heartbeat_respond");
const sessionsSpawn = specs.find((tool) => tool.name === "sessions_spawn");
const sessionsYield = specs.find((tool) => tool.name === "sessions_yield");
const message = toolBridge.specs.find((tool) => tool.name === "message");
const webSearch = toolBridge.specs.find((tool) => tool.name === "web_search");
const heartbeat = toolBridge.specs.find((tool) => tool.name === "heartbeat_respond");
const sessionsSpawn = toolBridge.specs.find((tool) => tool.name === "sessions_spawn");
const sessionsYield = toolBridge.specs.find((tool) => tool.name === "sessions_yield");
expect(message).not.toHaveProperty("namespace");
expect(message).not.toHaveProperty("deferLoading");
@@ -1646,7 +1624,7 @@ describe("runCodexAppServerAttempt", () => {
const normalInstructions = testing.buildDeveloperInstructions(createRunParams(), {
dynamicTools: normalBridge.availableSpecs,
});
const registeredToolNames = specNames(normalBridge.specs);
const registeredToolNames = normalBridge.specs.map((tool) => tool.name);
expect(registeredToolNames).toContain("message");
expect(registeredToolNames).toContain("heartbeat_respond");
@@ -1668,8 +1646,8 @@ describe("runCodexAppServerAttempt", () => {
registeredTools,
);
expect(specNames(heartbeatBridge.specs)).toEqual(registeredToolNames);
expect(specNames(nextNormalBridge.specs)).toEqual(registeredToolNames);
expect(heartbeatBridge.specs.map((tool) => tool.name)).toEqual(registeredToolNames);
expect(nextNormalBridge.specs.map((tool) => tool.name)).toEqual(registeredToolNames);
});
it("keeps the persistent dynamic schema stable across heartbeat-only turns", async () => {
@@ -1722,9 +1700,13 @@ describe("runCodexAppServerAttempt", () => {
registeredTools,
);
expect(specNames(heartbeatBridge.availableSpecs)).toEqual(["heartbeat_respond"]);
expect(specNames(heartbeatBridge.specs)).toEqual(specNames(normalBridge.specs));
expect(specNames(nextNormalBridge.specs)).toEqual(specNames(normalBridge.specs));
expect(heartbeatBridge.availableSpecs.map((tool) => tool.name)).toEqual(["heartbeat_respond"]);
expect(heartbeatBridge.specs.map((tool) => tool.name)).toEqual(
normalBridge.specs.map((tool) => tool.name),
);
expect(nextNormalBridge.specs.map((tool) => tool.name)).toEqual(
normalBridge.specs.map((tool) => tool.name),
);
});
it("disables Codex native tool surfaces when runtime toolsAllow is empty", async () => {
@@ -1763,7 +1745,7 @@ describe("runCodexAppServerAttempt", () => {
const startRequest = request.mock.calls.find(([method]) => method === "thread/start");
const startParams = startRequest?.[1] as
| {
dynamicTools?: CodexDynamicToolSpec[];
dynamicTools?: Array<{ name?: string }>;
environments?: unknown[];
developerInstructions?: string;
config?: {
@@ -5325,9 +5307,9 @@ describe("runCodexAppServerAttempt", () => {
const startRequest = requests.find((request) => request.method === "thread/start");
const startRequestParams = startRequest?.params as Record<string, unknown> | undefined;
const startConfig = startRequestParams?.config as Record<string, unknown> | undefined;
const dynamicToolNames = specNames(
(startRequestParams?.dynamicTools as CodexDynamicToolSpec[] | undefined) ?? [],
);
const dynamicToolNames = (
startRequestParams?.dynamicTools as Array<{ name?: string }> | undefined
)?.map((tool) => tool.name);
expect(startRequestParams?.model).toBe("local-model");
expect(startRequestParams?.modelProvider).toBe("lmstudio");
expect(startConfig?.web_search).toBe("disabled");

View File

@@ -206,7 +206,6 @@ import {
readCodexDynamicToolCallParams,
} from "./protocol-validators.js";
import {
flattenCodexDynamicToolFunctions,
isJsonObject,
type CodexSandboxPolicy,
type CodexTurnEnvironmentParams,
@@ -855,12 +854,6 @@ export async function runCodexAppServerAttempt(
sessionKey: contextSessionKey,
sessionFile: activeSessionFile,
runtimeContext: buildActiveContextEngineRuntimeContext(),
contextEngineHostSupport: CODEX_APP_SERVER_CONTEXT_ENGINE_HOST,
providerId: params.provider,
requestedModelId: params.requestedModelId,
modelId: params.modelId,
fallbackReason: params.fallbackReason,
degradedReason: params.degradedReason,
runMaintenance: runHarnessContextEngineMaintenance,
config: params.config,
warn: (message) => embeddedAgentLog.warn(message),
@@ -921,17 +914,10 @@ export async function runCodexAppServerAttempt(
messages: historyMessages,
tokenBudget: params.contextTokenBudget,
availableTools: new Set(
flattenCodexDynamicToolFunctions(toolBridge.availableSpecs)
.map((tool) => tool.name)
.filter(isNonEmptyString),
toolBridge.availableSpecs.map((tool) => tool.name).filter(isNonEmptyString),
),
citationsMode: params.config?.memory?.citations,
modelId: params.modelId,
contextEngineHostSupport: CODEX_APP_SERVER_CONTEXT_ENGINE_HOST,
providerId: params.provider,
requestedModelId: params.requestedModelId,
fallbackReason: params.fallbackReason,
degradedReason: params.degradedReason,
prompt: params.prompt,
});
if (!assembled) {
@@ -1358,7 +1344,7 @@ export async function runCodexAppServerAttempt(
threadId: thread.threadId,
authProfileId: startupAuthProfileId,
workspaceDir: effectiveWorkspace,
toolCount: flattenCodexDynamicToolFunctions(toolBridge.specs).length,
toolCount: toolBridge.specs.length,
});
recordCodexTrajectoryContext(trajectoryRecorder, {
attempt: params,
@@ -2807,12 +2793,6 @@ export async function runCodexAppServerAttempt(
lastCallUsage: result.attemptUsage,
promptCache: result.promptCache,
}),
contextEngineHostSupport: CODEX_APP_SERVER_CONTEXT_ENGINE_HOST,
providerId: params.provider,
requestedModelId: params.requestedModelId,
modelId: params.modelId,
fallbackReason: params.fallbackReason,
degradedReason: params.degradedReason,
runMaintenance: runHarnessContextEngineMaintenance,
config: params.config,
warn: (message) => embeddedAgentLog.warn(message),

View File

@@ -97,12 +97,11 @@ describe("Codex app-server dynamic tool schema boundary contract", () => {
vi.restoreAllMocks();
});
it("passes prepared executable dynamic tool schemas through legacy thread start specs", async () => {
it("passes prepared executable dynamic tool schemas through thread start unchanged", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const parameterFreeTool = createParameterFreeTool("message");
const dynamicTool = {
type: "function" as const,
name: parameterFreeTool.name,
description: parameterFreeTool.description,
inputSchema: normalizedParameterFreeSchema(),
@@ -128,13 +127,7 @@ describe("Codex app-server dynamic tool schema boundary contract", () => {
throw new Error(`expected thread/start request, got ${method}`);
}
const startPayload = payload as CodexThreadStartParams | undefined;
expect(startPayload?.dynamicTools).toStrictEqual([
{
name: dynamicTool.name,
description: dynamicTool.description,
inputSchema: dynamicTool.inputSchema,
},
]);
expect(startPayload?.dynamicTools).toStrictEqual([dynamicTool]);
expect(startPayload?.cwd).toBe(workspaceDir);
expect(startPayload?.model).toBe("gpt-5.4");
expect(startPayload?.modelProvider).toBeUndefined();
@@ -187,7 +180,6 @@ describe("Codex app-server dynamic tool schema boundary contract", () => {
cwd: workspaceDir,
dynamicTools: [
{
type: "function",
name: "message",
description: "Permissive test tool",
inputSchema: { type: "object" },
@@ -202,7 +194,6 @@ describe("Codex app-server dynamic tool schema boundary contract", () => {
cwd: workspaceDir,
dynamicTools: [
{
type: "function",
name: permissiveTool.name,
description: permissiveTool.description,
inputSchema: permissiveTool.parameters,

View File

@@ -1,7 +1,6 @@
// Codex tests cover thread lifecycle.binding plugin behavior.
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import type { CodexDynamicToolFunctionSpec } from "./protocol.js";
import {
createParams as createRunAttemptParams,
setupRunAttemptTestHooks,
@@ -67,9 +66,8 @@ function writeCodexAppServerBinding(...args: Parameters<typeof writeRawCodexAppS
function createMessageDynamicTool(
description: string,
actions: string[] = ["send"],
): CodexDynamicToolFunctionSpec {
): Parameters<typeof startOrResumeThread>[0]["dynamicTools"][number] {
return {
type: "function",
name: "message",
description,
inputSchema: {
@@ -86,9 +84,10 @@ function createMessageDynamicTool(
};
}
function createNamedDynamicTool(name: string): CodexDynamicToolFunctionSpec {
function createNamedDynamicTool(
name: string,
): Parameters<typeof startOrResumeThread>[0]["dynamicTools"][number] {
return {
type: "function",
name,
description: `${name} test tool`,
inputSchema: {
@@ -103,10 +102,9 @@ function createDeferredNamedDynamicTool(
name: string,
): Parameters<typeof startOrResumeThread>[0]["dynamicTools"][number] {
return {
type: "namespace",
name: "openclaw",
description: "",
tools: [{ ...createNamedDynamicTool(name), deferLoading: true }],
...createNamedDynamicTool(name),
namespace: "openclaw",
deferLoading: true,
};
}
@@ -290,47 +288,6 @@ describe("Codex app-server thread lifecycle bindings", () => {
expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start", "thread/resume"]);
});
it("sends legacy flat dynamic tools on thread start", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const params = createParams(sessionFile, workspaceDir);
const appServer = createThreadLifecycleAppServerOptions();
const request = vi.fn(async (method: string, _requestParams?: unknown) => {
if (method === "thread/start") {
return threadStartResult("thread-flat-tools");
}
throw new Error(`unexpected method: ${method}`);
});
await startOrResumeThread({
client: { request } as never,
params,
cwd: workspaceDir,
dynamicTools: [
createMessageDynamicTool("Send a message."),
createDeferredNamedDynamicTool("web_search"),
],
appServer,
});
const startParams = request.mock.calls.find(([method]) => method === "thread/start")?.[1] as
| { dynamicTools?: unknown[] }
| undefined;
expect(startParams?.dynamicTools).toEqual([
expect.objectContaining({
name: "message",
description: "Send a message.",
}),
expect.objectContaining({
name: "web_search",
namespace: "openclaw",
deferLoading: true,
}),
]);
expect(startParams?.dynamicTools?.[0]).not.toHaveProperty("type");
expect(startParams?.dynamicTools?.[1]).not.toHaveProperty("type");
});
it("keeps the bound local provider when recoverable resume failure starts a fresh thread", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");

View File

@@ -196,31 +196,23 @@ describe("Codex app-server native code mode config", () => {
const instructions = buildDeveloperInstructions(createAttemptParams({ provider: "openai" }), {
dynamicTools: [
{
type: "function",
name: "message",
description: "Send a message",
inputSchema: { type: "object" },
},
{
type: "namespace",
name: "openclaw",
description: "",
tools: [
{
type: "function",
name: "music_generate",
description: "Create music",
inputSchema: { type: "object" },
deferLoading: true,
},
{
type: "function",
name: "image_generate",
description: "Create images",
inputSchema: { type: "object" },
deferLoading: true,
},
],
name: "music_generate",
description: "Create music",
inputSchema: { type: "object" },
namespace: "openclaw",
deferLoading: true,
},
{
name: "image_generate",
description: "Create images",
inputSchema: { type: "object" },
namespace: "openclaw",
deferLoading: true,
},
],
});
@@ -236,18 +228,11 @@ describe("Codex app-server native code mode config", () => {
const instructions = buildDeveloperInstructions(createAttemptParams({ provider: "openai" }), {
dynamicTools: [
{
type: "namespace",
name: "openclaw",
description: "",
tools: [
{
type: "function",
name: "skill_workshop",
description: "Manage skill proposals",
inputSchema: { type: "object" },
deferLoading: true,
},
],
name: "skill_workshop",
description: "Manage skill proposals",
inputSchema: { type: "object" },
namespace: "openclaw",
deferLoading: true,
},
],
});
@@ -265,7 +250,6 @@ describe("Codex app-server native code mode config", () => {
const instructions = buildDeveloperInstructions(createAttemptParams({ provider: "openai" }), {
dynamicTools: [
{
type: "function",
name: "message",
description: "Send a message",
inputSchema: { type: "object" },
@@ -287,7 +271,6 @@ describe("Codex app-server native code mode config", () => {
};
const directFingerprint = codexDynamicToolsFingerprint([
{
type: "function",
name: "message",
description: "Send a visible message",
inputSchema,
@@ -295,18 +278,11 @@ describe("Codex app-server native code mode config", () => {
]);
const searchableFingerprint = codexDynamicToolsFingerprint([
{
type: "namespace",
name: "openclaw",
description: "",
tools: [
{
type: "function",
name: "message",
description: "Load and send a visible message",
inputSchema,
deferLoading: true,
},
],
name: "message",
description: "Load and send a visible message",
inputSchema,
namespace: "openclaw",
deferLoading: true,
},
]);

View File

@@ -37,11 +37,8 @@ import {
assertCodexThreadStartResponse,
} from "./protocol-validators.js";
import {
flattenCodexDynamicToolFunctions,
isJsonObject,
type CodexDynamicToolFunctionSpec,
type CodexDynamicToolSpec,
type CodexLegacyDynamicToolFunctionSpec,
type CodexSandboxPolicy,
type CodexThreadResumeParams,
type CodexThreadStartParams,
@@ -325,7 +322,7 @@ export async function startOrResumeThread(params: {
const dynamicToolsFingerprint = lifecycleTiming.measureSync("dynamic-tools-fingerprint", () =>
fingerprintDynamicTools(params.dynamicTools),
);
const dynamicToolsContainDeferred = flattenCodexDynamicToolFunctions(params.dynamicTools).some(
const dynamicToolsContainDeferred = params.dynamicTools.some(
(tool) => tool.deferLoading === true,
);
const webSearchPlan = lifecycleTiming.measureSync("web-search-plan", () =>
@@ -1068,33 +1065,12 @@ export function buildThreadStartParams(
developerInstructions:
options.developerInstructions ??
buildDeveloperInstructions(params, { dynamicTools: options.dynamicTools }),
dynamicTools: toCodexThreadStartDynamicTools(options.dynamicTools),
dynamicTools: options.dynamicTools,
experimentalRawEvents: true,
persistExtendedHistory: true,
};
}
function toCodexThreadStartDynamicTools(
dynamicTools: readonly CodexDynamicToolSpec[],
): CodexLegacyDynamicToolFunctionSpec[] {
// Managed stable Codex still accepts the legacy flat start payload. Keep
// OpenClaw namespaces internally, but omit `type` on the wire so Codex does
// not reject a mixed canonical/legacy shape before thread creation.
return dynamicTools.flatMap((tool) =>
tool.type === "namespace"
? tool.tools.map((child) => toCodexLegacyDynamicTool(child, tool.name))
: [toCodexLegacyDynamicTool(tool)],
);
}
function toCodexLegacyDynamicTool(
tool: CodexDynamicToolFunctionSpec,
namespace?: string,
): CodexLegacyDynamicToolFunctionSpec {
const { type: _type, ...legacyTool } = tool;
return namespace ? { ...legacyTool, namespace } : legacyTool;
}
export function buildThreadResumeParams(
params: EmbeddedRunAttemptParams,
options: {
@@ -1513,25 +1489,17 @@ function fingerprintEnvironmentSelection(
}
function fingerprintDynamicToolSpec(tool: JsonValue): JsonValue {
return stabilizeDynamicToolFingerprintValue(tool);
}
function stabilizeDynamicToolFingerprintValue(value: JsonValue): JsonValue {
if (Array.isArray(value)) {
return value.map(stabilizeDynamicToolFingerprintValue);
if (!isJsonObject(tool)) {
return stabilizeJsonValue(tool);
}
if (!isJsonObject(value)) {
return value;
}
const stable: JsonObject = {};
for (const [key, child] of Object.entries(value).toSorted(([left], [right]) =>
for (const [key, child] of Object.entries(tool).toSorted(([left], [right]) =>
left.localeCompare(right),
)) {
if (key === "description") {
continue;
}
stable[key] = stabilizeDynamicToolFingerprintValue(child);
stable[key] = stabilizeJsonValue(child);
}
return stable;
}
@@ -1606,7 +1574,7 @@ function buildDeferredDynamicToolManifest(
): string | undefined {
const deferredToolNames = [
...new Set(
flattenCodexDynamicToolFunctions(dynamicTools)
(dynamicTools ?? [])
.filter((tool) => tool.deferLoading === true)
.map((tool) => tool.name.trim())
.filter(Boolean),
@@ -1621,7 +1589,7 @@ function buildDeferredDynamicToolManifest(
function buildSkillWorkshopInstruction(
dynamicTools: readonly CodexDynamicToolSpec[] | undefined,
): string | undefined {
const hasSkillWorkshop = flattenCodexDynamicToolFunctions(dynamicTools).some(
const hasSkillWorkshop = (dynamicTools ?? []).some(
(tool) => tool.name.trim() === SKILL_WORKSHOP_TOOL_NAME,
);
if (!hasSkillWorkshop) {
@@ -1635,7 +1603,7 @@ function buildVisibleReplyInstruction(
dynamicTools: readonly CodexDynamicToolSpec[] | undefined,
): string {
const messageToolAvailable = dynamicTools
? flattenCodexDynamicToolFunctions(dynamicTools).some((tool) => tool.name.trim() === "message")
? dynamicTools.some((tool) => tool.name.trim() === "message")
: params.disableMessageTool !== true;
if (params.sourceReplyDeliveryMode === "message_tool_only" && messageToolAvailable) {
return "Visible source replies are not automatically delivered for this run. Use `message(action=send)` for user-visible source-channel output. Do not repeat that visible content in your final answer.";

View File

@@ -5,7 +5,6 @@ import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
createCodexTrajectoryRecorder,
recordCodexTrajectoryContext,
resolveCodexTrajectoryAppendFlags,
resolveCodexTrajectoryPointerFlags,
} from "./trajectory.js";
@@ -121,55 +120,6 @@ describe("Codex trajectory recorder", () => {
expect(parsed.modelId).toBe("gpt-5.5");
});
it("records namespace dynamic tools as callable trajectory tool definitions", async () => {
const tmpDir = makeTempDir();
const sessionFile = path.join(tmpDir, "session.jsonl");
const init = {
cwd: tmpDir,
attempt: {
sessionFile,
sessionId: "session-1",
sessionKey: "agent:main:session-1",
runId: "run-1",
provider: "codex",
modelId: "gpt-5.4",
model: { api: "responses" },
} as never,
env: {},
tools: [
{
type: "namespace",
name: "openclaw",
description: "",
tools: [
{
type: "function",
name: "web_search",
description: "Search the web.",
inputSchema: { type: "object" },
deferLoading: true,
},
],
},
],
} satisfies Parameters<typeof createCodexTrajectoryRecorder>[0];
const recorder = createCodexTrajectoryRecorder(init);
recordCodexTrajectoryContext(expectTrajectoryRecorder(recorder), init);
await recorder?.flush();
const parsed = JSON.parse(
fs.readFileSync(path.join(tmpDir, "session.trajectory.jsonl"), "utf8"),
);
expect(parsed.data?.tools).toEqual([
{
name: "web_search",
description: "Search the web.",
parameters: { type: "object" },
},
]);
});
it("sanitizes session ids when resolving an override directory", async () => {
const tmpDir = makeTempDir();
const recorder = createCodexTrajectoryRecorder({

View File

@@ -15,7 +15,6 @@ import {
resolveRegularFileAppendFlags,
} from "openclaw/plugin-sdk/security-runtime";
import { resolveCodexLocalRuntimeAttribution } from "./local-runtime-attribution.js";
import { flattenCodexDynamicToolFunctions, type CodexDynamicToolSpec } from "./protocol.js";
/** Runtime trajectory recorder used by Codex run attempts and event projectors. */
export type CodexTrajectoryRecorder = {
@@ -29,7 +28,7 @@ type CodexTrajectoryInit = {
cwd: string;
developerInstructions?: string;
prompt?: string;
tools?: CodexDynamicToolSpec[];
tools?: Array<{ name?: string; description?: string; inputSchema?: unknown }>;
env?: NodeJS.ProcessEnv;
};
@@ -299,12 +298,12 @@ function resolveContainedPath(baseDir: string, fileName: string): string {
}
function toTrajectoryToolDefinitions(
tools: readonly CodexDynamicToolSpec[] | undefined,
tools: Array<{ name?: string; description?: string; inputSchema?: unknown }> | undefined,
): Array<{ name: string; description?: string; parameters?: unknown }> | undefined {
if (!tools || tools.length === 0) {
return undefined;
}
return flattenCodexDynamicToolFunctions(tools)
return tools
.flatMap((tool) => {
const name = tool.name?.trim();
if (!name) {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/comfy-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw ComfyUI provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/copilot-proxy",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw Copilot Proxy provider plugin",
"type": "module",

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/copilot",
"version": "2026.6.8",
"version": "2026.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/copilot",
"version": "2026.6.8",
"version": "2026.6.9",
"dependencies": {
"@github/copilot-sdk": "1.0.0-beta.9"
}

View File

@@ -2,7 +2,7 @@
"id": "copilot",
"name": "GitHub Copilot agent runtime",
"description": "Registers the GitHub Copilot agent runtime.",
"version": "2026.6.2",
"version": "2026.6.9",
"activation": {
"onStartup": false,
"onAgentHarnesses": ["copilot"]

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/copilot",
"version": "2026.6.8",
"version": "2026.6.9",
"description": "OpenClaw GitHub Copilot agent runtime plugin (registers a `github-copilot` AgentHarness backed by @github/copilot-sdk over JSON-RPC to the GitHub Copilot CLI)",
"repository": {
"type": "git",
@@ -25,10 +25,10 @@
"minHostVersion": ">=2026.5.28"
},
"compat": {
"pluginApi": ">=2026.6.8"
"pluginApi": ">=2026.6.9"
},
"build": {
"openclawVersion": "2026.6.8",
"openclawVersion": "2026.6.9",
"bundledDist": false
},
"release": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepgram-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw Deepgram media-understanding provider",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepinfra-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw DeepInfra provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepseek-provider",
"version": "2026.6.8",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw DeepSeek provider plugin",
"type": "module",

View File

@@ -1,23 +1,23 @@
{
"name": "@openclaw/diagnostics-otel",
"version": "2026.6.8",
"version": "2026.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/diagnostics-otel",
"version": "2026.6.8",
"version": "2026.6.9",
"dependencies": {
"@opentelemetry/api": "1.9.1",
"@opentelemetry/api-logs": "0.219.0",
"@opentelemetry/exporter-logs-otlp-proto": "0.219.0",
"@opentelemetry/exporter-metrics-otlp-proto": "0.219.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.219.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-logs": "0.219.0",
"@opentelemetry/sdk-metrics": "2.8.0",
"@opentelemetry/sdk-node": "0.219.0",
"@opentelemetry/sdk-trace-base": "2.8.0",
"@opentelemetry/api-logs": "0.218.0",
"@opentelemetry/exporter-logs-otlp-proto": "0.218.0",
"@opentelemetry/exporter-metrics-otlp-proto": "0.218.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.218.0",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-logs": "0.218.0",
"@opentelemetry/sdk-metrics": "2.7.1",
"@opentelemetry/sdk-node": "0.218.0",
"@opentelemetry/sdk-trace-base": "2.7.1",
"@opentelemetry/semantic-conventions": "1.41.1"
}
},
@@ -72,9 +72,9 @@
}
},
"node_modules/@opentelemetry/api-logs": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz",
"integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz",
"integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "^1.3.0"
@@ -84,12 +84,12 @@
}
},
"node_modules/@opentelemetry/configuration": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.219.0.tgz",
"integrity": "sha512-wXZUYv4ngu43nA4WEhuXNacm46LW+17LRM8nKyIhBzroRA24PBYjMnakwzR/w777nFUB5xlgsYTTeuXxumZM1Q==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.218.0.tgz",
"integrity": "sha512-W8wIz7H2R1pufR5jfjb3gU2XkMpm2x/7b1RJcsuzvd70Il/rWWE+g5/Od7hQKrxRTSrTrOWlru101PWXz5I1EQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/core": "2.7.1",
"yaml": "^2.0.0"
},
"engines": {
@@ -100,9 +100,9 @@
}
},
"node_modules/@opentelemetry/context-async-hooks": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.8.0.tgz",
"integrity": "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.1.tgz",
"integrity": "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==",
"license": "Apache-2.0",
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -112,9 +112,9 @@
}
},
"node_modules/@opentelemetry/core": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz",
"integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz",
"integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -127,17 +127,17 @@
}
},
"node_modules/@opentelemetry/exporter-logs-otlp-grpc": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.219.0.tgz",
"integrity": "sha512-7SvzDCIclHWAcCwZ1MTOLcwn4BVNPGI3QxS/DJraPNe1TTL+4TvUBq5zeQV8tsnYvtDN7wKW2qocVmaCP2l7sQ==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.218.0.tgz",
"integrity": "sha512-hoxrNH1l/Xy6F9WTJ5IK+6j1r9nQFlPOmrnTlhYHTySdunfXLmUCPv3bQtKYntxag9h3wLYBZQ2HI6FOx+BT2g==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.14.3",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/otlp-exporter-base": "0.219.0",
"@opentelemetry/otlp-grpc-exporter-base": "0.219.0",
"@opentelemetry/otlp-transformer": "0.219.0",
"@opentelemetry/sdk-logs": "0.219.0"
"@opentelemetry/core": "2.7.1",
"@opentelemetry/otlp-exporter-base": "0.218.0",
"@opentelemetry/otlp-grpc-exporter-base": "0.218.0",
"@opentelemetry/otlp-transformer": "0.218.0",
"@opentelemetry/sdk-logs": "0.218.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -147,16 +147,16 @@
}
},
"node_modules/@opentelemetry/exporter-logs-otlp-http": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.219.0.tgz",
"integrity": "sha512-mhl2HL6GmZI8b8PwPfqMws/5ovJfbRTxwc9Y5agVVHiQ+e5SL1btsFr/kJDgt7YCexDtsUn5HAreHQO9szFS0A==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.218.0.tgz",
"integrity": "sha512-Qx+4rpVHzgg89dawcWRHyt+XRXeLnhFz/qBtvggmjkcgPUdr+NAB0/u/eIPA8yAeJV0J80Vz43JZCh/XFvZFGw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.219.0",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/otlp-exporter-base": "0.219.0",
"@opentelemetry/otlp-transformer": "0.219.0",
"@opentelemetry/sdk-logs": "0.219.0"
"@opentelemetry/api-logs": "0.218.0",
"@opentelemetry/core": "2.7.1",
"@opentelemetry/otlp-exporter-base": "0.218.0",
"@opentelemetry/otlp-transformer": "0.218.0",
"@opentelemetry/sdk-logs": "0.218.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -166,18 +166,18 @@
}
},
"node_modules/@opentelemetry/exporter-logs-otlp-proto": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.219.0.tgz",
"integrity": "sha512-Ayw4Gf71PS9jhBVaYywa4WsajnqfDehMkTdVH3TSAVHqPcsAv/AhH/wTNRYNt99szeYr6Gbd/D6RjZD77wAxHg==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.218.0.tgz",
"integrity": "sha512-1/noQNsp9gXD75HPzgjBrcF1+XTtry7pFAUfxVEJgg7mPv2AawKQuYkhMmJ8qjxz4Ubc3Y8bwvfxevXsKTq4cg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.219.0",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/otlp-exporter-base": "0.219.0",
"@opentelemetry/otlp-transformer": "0.219.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-logs": "0.219.0",
"@opentelemetry/sdk-trace-base": "2.8.0"
"@opentelemetry/api-logs": "0.218.0",
"@opentelemetry/core": "2.7.1",
"@opentelemetry/otlp-exporter-base": "0.218.0",
"@opentelemetry/otlp-transformer": "0.218.0",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-logs": "0.218.0",
"@opentelemetry/sdk-trace-base": "2.7.1"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -187,19 +187,19 @@
}
},
"node_modules/@opentelemetry/exporter-metrics-otlp-grpc": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.219.0.tgz",
"integrity": "sha512-6LaaSrPxK5L55bXevWajvOMxGOpNm0n12tG53TeZaUeNzXwLPg6d2KCC1zAlGsojan+xRG71mA4Qqs9K2VVrKQ==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.218.0.tgz",
"integrity": "sha512-YapQ9vNMX0NSZF6LK5pWAFfjpJleV2O9uYWfYGeb/5F1Kb9rPGK8tZDMJFa/sOksgdFuflDvYuA0B4qjDB4fjQ==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.14.3",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/exporter-metrics-otlp-http": "0.219.0",
"@opentelemetry/otlp-exporter-base": "0.219.0",
"@opentelemetry/otlp-grpc-exporter-base": "0.219.0",
"@opentelemetry/otlp-transformer": "0.219.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-metrics": "2.8.0"
"@opentelemetry/core": "2.7.1",
"@opentelemetry/exporter-metrics-otlp-http": "0.218.0",
"@opentelemetry/otlp-exporter-base": "0.218.0",
"@opentelemetry/otlp-grpc-exporter-base": "0.218.0",
"@opentelemetry/otlp-transformer": "0.218.0",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-metrics": "2.7.1"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -209,16 +209,16 @@
}
},
"node_modules/@opentelemetry/exporter-metrics-otlp-http": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.219.0.tgz",
"integrity": "sha512-6CaDRbMVHZSDWzNXwrR8y/H4B/Z1eMNnkHiPQlTx3Ojz2OHY4X/aff/UC4P/3pHUQSuTfi3oh2UsPPZppw+Vrg==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.218.0.tgz",
"integrity": "sha512-bV7d2OuMpZu2+gAaxUAhzfZ0h3WVZk8ETQUEE3DNSntbTaMpuITjtm8I0rNyHFdm7Ax57K6ty7SgFXlBmOLIvQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/otlp-exporter-base": "0.219.0",
"@opentelemetry/otlp-transformer": "0.219.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-metrics": "2.8.0"
"@opentelemetry/core": "2.7.1",
"@opentelemetry/otlp-exporter-base": "0.218.0",
"@opentelemetry/otlp-transformer": "0.218.0",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-metrics": "2.7.1"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -228,17 +228,17 @@
}
},
"node_modules/@opentelemetry/exporter-metrics-otlp-proto": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.219.0.tgz",
"integrity": "sha512-DUS7XyIiEnoeccQUvuKy0G2/YqeKhpN8FVIrGbrLNIVMj10yeIFLRzRv0tibCI2kXXvlTTABVexGAk78wHk2ug==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.218.0.tgz",
"integrity": "sha512-ubLddKjWULhla9YZRCj/rTBeppjJYE4e9w0icx5mTu3eFhWjQzbV75NYjXuIlEG+NJsBl6d+sTFw5Qu+oej4oQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/exporter-metrics-otlp-http": "0.219.0",
"@opentelemetry/otlp-exporter-base": "0.219.0",
"@opentelemetry/otlp-transformer": "0.219.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-metrics": "2.8.0"
"@opentelemetry/core": "2.7.1",
"@opentelemetry/exporter-metrics-otlp-http": "0.218.0",
"@opentelemetry/otlp-exporter-base": "0.218.0",
"@opentelemetry/otlp-transformer": "0.218.0",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-metrics": "2.7.1"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -248,14 +248,14 @@
}
},
"node_modules/@opentelemetry/exporter-prometheus": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.219.0.tgz",
"integrity": "sha512-TxOnJ85eWJY5JyOJsNMXiRTYlkDcOv0u3KbXEzWCc+tUS9sjL/BC6BcdxZ0B9r2OFVqsrZFXUzSD2sZUy42Ucw==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.218.0.tgz",
"integrity": "sha512-RT5oEyu1kddZJ1vt7/BUo5wV+P7hpNAESsR3dUd3+8deHuX7gWNoCOZn+SfDT+hJHlIJ5h/AxiCLXIrutswDJg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-metrics": "2.8.0",
"@opentelemetry/core": "2.7.1",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-metrics": "2.7.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
@@ -266,18 +266,18 @@
}
},
"node_modules/@opentelemetry/exporter-trace-otlp-grpc": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.219.0.tgz",
"integrity": "sha512-BkDNv1UD6BscW19MxbAxVmSYSSFuyeqR6buV2/HTYqA7GrR0EbTFzqG6h86T3PtXmpdbsWjMGLDdjG2rikG27Q==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.218.0.tgz",
"integrity": "sha512-3fXxVQEj9TNAFaCi79JeFKfeLd0sDtInaR3gaZDVlzNSPHtz8PZuCV34JKWjD4XXzT20IdMe8IpX6mRVNDA4Tw==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.14.3",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/otlp-exporter-base": "0.219.0",
"@opentelemetry/otlp-grpc-exporter-base": "0.219.0",
"@opentelemetry/otlp-transformer": "0.219.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-trace-base": "2.8.0"
"@opentelemetry/core": "2.7.1",
"@opentelemetry/otlp-exporter-base": "0.218.0",
"@opentelemetry/otlp-grpc-exporter-base": "0.218.0",
"@opentelemetry/otlp-transformer": "0.218.0",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-trace-base": "2.7.1"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -287,16 +287,16 @@
}
},
"node_modules/@opentelemetry/exporter-trace-otlp-http": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.219.0.tgz",
"integrity": "sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.218.0.tgz",
"integrity": "sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/otlp-exporter-base": "0.219.0",
"@opentelemetry/otlp-transformer": "0.219.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-trace-base": "2.8.0"
"@opentelemetry/core": "2.7.1",
"@opentelemetry/otlp-exporter-base": "0.218.0",
"@opentelemetry/otlp-transformer": "0.218.0",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-trace-base": "2.7.1"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -306,16 +306,16 @@
}
},
"node_modules/@opentelemetry/exporter-trace-otlp-proto": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.219.0.tgz",
"integrity": "sha512-lF/LUBfhOFmxJa+SQsLN7ziV4MHa2pyKgOM6JNehSOfU+npjM4gwm9oIKEJrzrWcexMcqydiyoFy0XCb1Ql3wQ==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.218.0.tgz",
"integrity": "sha512-r1Msf8SNLRmwh9J6XQ5uh82D7CdDWMNHnPB7LAVHjzut0TkSeKc5KcIvr4SvHvfk/xwN5gxC+VLKQ1k0o8PSPw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/otlp-exporter-base": "0.219.0",
"@opentelemetry/otlp-transformer": "0.219.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-trace-base": "2.8.0"
"@opentelemetry/core": "2.7.1",
"@opentelemetry/otlp-exporter-base": "0.218.0",
"@opentelemetry/otlp-transformer": "0.218.0",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-trace-base": "2.7.1"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -325,14 +325,14 @@
}
},
"node_modules/@opentelemetry/exporter-zipkin": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.8.0.tgz",
"integrity": "sha512-Mj84UkEa17BK2o903VTXW3wM8CrSZexGs4tRGVZVIMM9ni1T6TuGx5IrRfoWKAbshx42D5/kc7YV+axypLPYyA==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.7.1.tgz",
"integrity": "sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-trace-base": "2.8.0",
"@opentelemetry/core": "2.7.1",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-trace-base": "2.7.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
@@ -343,12 +343,12 @@
}
},
"node_modules/@opentelemetry/instrumentation": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.219.0.tgz",
"integrity": "sha512-X5t7I8GyIO9rmGHwoedZLREpQqrF1WW2nxzNNym6HOKpFiE+rvqV3ngC0xcZVO2YwIGf3KKmRdWrYwdwz3H9RQ==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz",
"integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.219.0",
"@opentelemetry/api-logs": "0.218.0",
"import-in-the-middle": "^3.0.0",
"require-in-the-middle": "^8.0.0"
},
@@ -360,13 +360,13 @@
}
},
"node_modules/@opentelemetry/otlp-exporter-base": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.219.0.tgz",
"integrity": "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz",
"integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/otlp-transformer": "0.219.0"
"@opentelemetry/core": "2.7.1",
"@opentelemetry/otlp-transformer": "0.218.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -376,15 +376,15 @@
}
},
"node_modules/@opentelemetry/otlp-grpc-exporter-base": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.219.0.tgz",
"integrity": "sha512-iIk/s8QQu39zpTrRRmsW/Eg3SE2+Hg8tLWepr2FLRgmwUpNd0IpCTLJEHJ77hpt4hgIS8MAh44UYI4xQPZwWlw==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.218.0.tgz",
"integrity": "sha512-H/lCGJ536N98VpYJOaWTQOkv4Dx6TnmStK6Rqfu1W7KkFbPAx04hjdYEMZF/YbnHzPUSIK4kM6OE2GKGBTpV9A==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.14.3",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/otlp-exporter-base": "0.219.0",
"@opentelemetry/otlp-transformer": "0.219.0"
"@opentelemetry/core": "2.7.1",
"@opentelemetry/otlp-exporter-base": "0.218.0",
"@opentelemetry/otlp-transformer": "0.218.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -394,17 +394,17 @@
}
},
"node_modules/@opentelemetry/otlp-transformer": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.219.0.tgz",
"integrity": "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz",
"integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.219.0",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-logs": "0.219.0",
"@opentelemetry/sdk-metrics": "2.8.0",
"@opentelemetry/sdk-trace-base": "2.8.0"
"@opentelemetry/api-logs": "0.218.0",
"@opentelemetry/core": "2.7.1",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-logs": "0.218.0",
"@opentelemetry/sdk-metrics": "2.7.1",
"@opentelemetry/sdk-trace-base": "2.7.1"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -414,12 +414,12 @@
}
},
"node_modules/@opentelemetry/propagator-b3": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.8.0.tgz",
"integrity": "sha512-SazlvuSKi5533rPHTW2TwBwdMakhjZST4SYs0YauuvfGDkT13KbG1gJS75hV0uWVeevhtVP9sAIlaZLTHdSbMg==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.7.1.tgz",
"integrity": "sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0"
"@opentelemetry/core": "2.7.1"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -429,12 +429,12 @@
}
},
"node_modules/@opentelemetry/propagator-jaeger": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.8.0.tgz",
"integrity": "sha512-Xnz9zZvvQzUw+9DrOn0MomR7BxFCkA2pcfXBQuHC28ndJpSbjLs7knzYb05kw5SyCjSsEWombkZMgGcJSk8JVg==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.7.1.tgz",
"integrity": "sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0"
"@opentelemetry/core": "2.7.1"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -444,12 +444,12 @@
}
},
"node_modules/@opentelemetry/resources": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz",
"integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz",
"integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/core": "2.7.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
@@ -460,14 +460,14 @@
}
},
"node_modules/@opentelemetry/sdk-logs": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz",
"integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz",
"integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.219.0",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/api-logs": "0.218.0",
"@opentelemetry/core": "2.7.1",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
@@ -478,13 +478,13 @@
}
},
"node_modules/@opentelemetry/sdk-metrics": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz",
"integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz",
"integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/resources": "2.8.0"
"@opentelemetry/core": "2.7.1",
"@opentelemetry/resources": "2.7.1"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -494,36 +494,35 @@
}
},
"node_modules/@opentelemetry/sdk-node": {
"version": "0.219.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.219.0.tgz",
"integrity": "sha512-NWLpWLEb8gV3+JBHYoIrktbM385wyHpRJoh3J/4Q52d4PR+AlPMNGJT3DzBUrDSUEVbKAXoHR+EDAPxtiNcj8g==",
"version": "0.218.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.218.0.tgz",
"integrity": "sha512-tPMjHrLV5gsfNdYqoRHjeGbCAZBXXD9c1Qo/2ut7VwnUABDNh76xNxrT0SEhkIIJuCN45bbN1vZnYL1gY0IkOg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.219.0",
"@opentelemetry/configuration": "0.219.0",
"@opentelemetry/context-async-hooks": "2.8.0",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/exporter-logs-otlp-grpc": "0.219.0",
"@opentelemetry/exporter-logs-otlp-http": "0.219.0",
"@opentelemetry/exporter-logs-otlp-proto": "0.219.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "0.219.0",
"@opentelemetry/exporter-metrics-otlp-http": "0.219.0",
"@opentelemetry/exporter-metrics-otlp-proto": "0.219.0",
"@opentelemetry/exporter-prometheus": "0.219.0",
"@opentelemetry/exporter-trace-otlp-grpc": "0.219.0",
"@opentelemetry/exporter-trace-otlp-http": "0.219.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.219.0",
"@opentelemetry/exporter-zipkin": "2.8.0",
"@opentelemetry/instrumentation": "0.219.0",
"@opentelemetry/otlp-exporter-base": "0.219.0",
"@opentelemetry/otlp-grpc-exporter-base": "0.219.0",
"@opentelemetry/propagator-b3": "2.8.0",
"@opentelemetry/propagator-jaeger": "2.8.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-logs": "0.219.0",
"@opentelemetry/sdk-metrics": "2.8.0",
"@opentelemetry/sdk-trace-base": "2.8.0",
"@opentelemetry/sdk-trace-node": "2.8.0",
"@opentelemetry/api-logs": "0.218.0",
"@opentelemetry/configuration": "0.218.0",
"@opentelemetry/context-async-hooks": "2.7.1",
"@opentelemetry/core": "2.7.1",
"@opentelemetry/exporter-logs-otlp-grpc": "0.218.0",
"@opentelemetry/exporter-logs-otlp-http": "0.218.0",
"@opentelemetry/exporter-logs-otlp-proto": "0.218.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "0.218.0",
"@opentelemetry/exporter-metrics-otlp-http": "0.218.0",
"@opentelemetry/exporter-metrics-otlp-proto": "0.218.0",
"@opentelemetry/exporter-prometheus": "0.218.0",
"@opentelemetry/exporter-trace-otlp-grpc": "0.218.0",
"@opentelemetry/exporter-trace-otlp-http": "0.218.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.218.0",
"@opentelemetry/exporter-zipkin": "2.7.1",
"@opentelemetry/instrumentation": "0.218.0",
"@opentelemetry/otlp-exporter-base": "0.218.0",
"@opentelemetry/propagator-b3": "2.7.1",
"@opentelemetry/propagator-jaeger": "2.7.1",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-logs": "0.218.0",
"@opentelemetry/sdk-metrics": "2.7.1",
"@opentelemetry/sdk-trace-base": "2.7.1",
"@opentelemetry/sdk-trace-node": "2.7.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
@@ -534,13 +533,13 @@
}
},
"node_modules/@opentelemetry/sdk-trace-base": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz",
"integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz",
"integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.8.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/core": "2.7.1",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
@@ -551,14 +550,14 @@
}
},
"node_modules/@opentelemetry/sdk-trace-node": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.8.0.tgz",
"integrity": "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.7.1.tgz",
"integrity": "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/context-async-hooks": "2.8.0",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/sdk-trace-base": "2.8.0"
"@opentelemetry/context-async-hooks": "2.7.1",
"@opentelemetry/core": "2.7.1",
"@opentelemetry/sdk-trace-base": "2.7.1"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
@@ -576,78 +575,6 @@
"node": ">=14"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
"integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
"integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
"integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz",
"integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
"integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
"license": "BSD-3-Clause"
},
"node_modules/@types/node": {
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"license": "MIT",
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -821,23 +748,12 @@
"license": "MIT"
},
"node_modules/protobufjs": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.3.tgz",
"integrity": "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==",
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.1.tgz",
"integrity": "sha512-oXf2UgIty8jnwfN4yvL1x79VLhL5uiKjZJbSGXGCIUmHmItTP4eS/UIlWDCeNx3seg+ujfn9vDlPMSrsh7wO+Q==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.5",
"@protobufjs/eventemitter": "^1.1.1",
"@protobufjs/fetch": "^1.1.1",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.2",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.1",
"@types/node": ">=13.7.0",
"long": "^5.3.2"
},
"engines": {
@@ -892,12 +808,6 @@
"node": ">=8"
}
},
"node_modules/undici-types": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"license": "MIT"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diagnostics-otel",
"version": "2026.6.8",
"version": "2026.6.9",
"description": "OpenClaw diagnostics OpenTelemetry exporter for metrics and traces.",
"repository": {
"type": "git",
@@ -9,15 +9,15 @@
"type": "module",
"dependencies": {
"@opentelemetry/api": "1.9.1",
"@opentelemetry/api-logs": "0.219.0",
"@opentelemetry/exporter-logs-otlp-proto": "0.219.0",
"@opentelemetry/exporter-metrics-otlp-proto": "0.219.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.219.0",
"@opentelemetry/resources": "2.8.0",
"@opentelemetry/sdk-logs": "0.219.0",
"@opentelemetry/sdk-metrics": "2.8.0",
"@opentelemetry/sdk-node": "0.219.0",
"@opentelemetry/sdk-trace-base": "2.8.0",
"@opentelemetry/api-logs": "0.218.0",
"@opentelemetry/exporter-logs-otlp-proto": "0.218.0",
"@opentelemetry/exporter-metrics-otlp-proto": "0.218.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.218.0",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-logs": "0.218.0",
"@opentelemetry/sdk-metrics": "2.7.1",
"@opentelemetry/sdk-node": "0.218.0",
"@opentelemetry/sdk-trace-base": "2.7.1",
"@opentelemetry/semantic-conventions": "1.41.1"
},
"devDependencies": {
@@ -34,10 +34,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.6.8"
"pluginApi": ">=2026.6.9"
},
"build": {
"openclawVersion": "2026.6.8"
"openclawVersion": "2026.6.9"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/diagnostics-prometheus",
"version": "2026.6.8",
"version": "2026.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/diagnostics-prometheus",
"version": "2026.6.8"
"version": "2026.6.9"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diagnostics-prometheus",
"version": "2026.6.8",
"version": "2026.6.9",
"description": "OpenClaw diagnostics Prometheus exporter for runtime metrics.",
"repository": {
"type": "git",
@@ -21,10 +21,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.6.8"
"pluginApi": ">=2026.6.9"
},
"build": {
"openclawVersion": "2026.6.8"
"openclawVersion": "2026.6.9"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/diffs-language-pack",
"version": "2026.6.8",
"version": "2026.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/diffs-language-pack",
"version": "2026.6.8"
"version": "2026.6.9"
}
}
}

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