mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-28 02:11:53 +08:00
Compare commits
1 Commits
macos-app-
...
codex/watc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cda85965bb |
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@@ -251,6 +251,7 @@ jobs:
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const createMatrix = (include) => ({ include });
|
||||
const outputPath = process.env.GITHUB_OUTPUT;
|
||||
const isCanonicalRepository = process.env.OPENCLAW_CI_REPOSITORY === "openclaw/openclaw";
|
||||
@@ -284,7 +285,6 @@ jobs:
|
||||
if (runNodeFull) {
|
||||
checksFastCoreTasks.push(
|
||||
{ check_name: "checks-fast-bundled-protocol", runtime: "node", task: "bundled-protocol" },
|
||||
{ check_name: "QA Smoke CI", runtime: "node", task: "qa-smoke-ci" },
|
||||
{ check_name: "checks-fast-bun-launcher", runtime: "bun", task: "bun-launcher" },
|
||||
);
|
||||
} else {
|
||||
@@ -922,26 +922,6 @@ jobs:
|
||||
pnpm test:bundled
|
||||
pnpm protocol:check
|
||||
;;
|
||||
qa-smoke-ci)
|
||||
output_dir=".artifacts/qa-e2e/smoke-ci-profile"
|
||||
export OPENCLAW_BUILD_PRIVATE_QA=1
|
||||
export OPENCLAW_ENABLE_PRIVATE_QA_CLI=1
|
||||
export OPENCLAW_DISABLE_BUNDLED_PLUGINS=0
|
||||
export OPENCLAW_QA_REDACT_PUBLIC_METADATA=1
|
||||
export OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS=180000
|
||||
NODE_OPTIONS=--max-old-space-size=8192 node scripts/build-all.mjs qaRuntime
|
||||
qa_exit_code=0
|
||||
pnpm openclaw qa run \
|
||||
--repo-root . \
|
||||
--qa-profile smoke-ci \
|
||||
--concurrency 8 \
|
||||
--output-dir "$output_dir" || qa_exit_code=$?
|
||||
echo "QA smoke profile evidence: \`${output_dir}\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ "$qa_exit_code" -ne 0 ]; then
|
||||
echo "::error title=QA smoke profile failed::smoke-ci exited ${qa_exit_code}; evidence upload will still run"
|
||||
exit "$qa_exit_code"
|
||||
fi
|
||||
;;
|
||||
contracts-plugins-ci-routing)
|
||||
pnpm test:contracts:plugins
|
||||
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/changed-lanes.test.ts test/scripts/ci-workflow-guards.test.ts test/scripts/run-vitest.test.ts test/scripts/test-projects.test.ts
|
||||
@@ -958,15 +938,6 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Upload QA smoke profile evidence
|
||||
if: always() && matrix.task == 'qa-smoke-ci'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: qa-smoke-profile-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: .artifacts/qa-e2e/smoke-ci-profile/
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
||||
checks-fast-plugin-contracts-shard:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
5
.github/workflows/sandbox-common-smoke.yml
vendored
5
.github/workflows/sandbox-common-smoke.yml
vendored
@@ -57,10 +57,11 @@ jobs:
|
||||
BASE_IMAGE="openclaw-sandbox-smoke-base:bookworm-slim" \
|
||||
TARGET_IMAGE="openclaw-sandbox-common-smoke:bookworm-slim" \
|
||||
PACKAGES="ca-certificates" \
|
||||
INSTALL_PNPM=0 \
|
||||
INSTALL_BUN=0 \
|
||||
INSTALL_BREW=0 \
|
||||
FINAL_USER=sandbox \
|
||||
scripts/sandbox-common-setup.sh
|
||||
|
||||
timeout --kill-after=30s 2m docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc \
|
||||
'set -e; test "$(id -un)" = sandbox; node --version; pnpm --version'
|
||||
u="$(timeout --kill-after=30s 2m docker run --rm openclaw-sandbox-common-smoke:bookworm-slim sh -lc 'id -un')"
|
||||
test "$u" = "sandbox"
|
||||
|
||||
@@ -118,11 +118,11 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Tests in a normal source checkout: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
|
||||
- If raw Vitest is unavoidable, use `vitest run ...`; bare `vitest ...` starts local watch mode and will not exit on its own.
|
||||
- Tests in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm test*`; use `node scripts/run-vitest.mjs <path-or-filter>` for tiny explicit-file proof, or Crabbox/Testbox for anything broader.
|
||||
- Checks/lint in a normal source checkout: `pnpm check:changed` delegates to Crabbox/Testbox; lanes: `pnpm changed:lanes --json`; staged/path-scoped: `pnpm check:changed --staged` or `pnpm check:changed -- <files...>`; full `pnpm check`/`pnpm lint` only when required.
|
||||
- Checks in a normal source checkout: `pnpm check:changed` delegates to Crabbox/Testbox; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
|
||||
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... -- env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed` so pnpm runs inside Testbox, not locally.
|
||||
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
|
||||
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); never add `tsc --noEmit`, `typecheck`, `check:types`.
|
||||
- Formatting: `oxfmt`, not Prettier. Use repo wrappers (`pnpm format:*`, `scripts/run-oxlint.mjs`; full `pnpm lint:*` only when scope requires).
|
||||
- Formatting: `oxfmt`, not Prettier. Use repo wrappers (`pnpm format:*`, `pnpm lint:*`, `scripts/run-oxlint.mjs`).
|
||||
- Build before push when build output, packaging, lazy/module boundaries, dynamic imports, or published surfaces can change.
|
||||
|
||||
## Validation
|
||||
|
||||
@@ -105,19 +105,6 @@ Reopen OpenClaw, confirm Talk is still active, then tap `Stop Talk`.
|
||||
4. Confirm at least one `agent` row is connected.
|
||||
5. Confirm the iPhone review device appears in the connected instances list.
|
||||
|
||||
## Live Activity / Dynamic Island
|
||||
|
||||
1. Tap `Settings`.
|
||||
2. Tap `Reconnect`.
|
||||
3. Immediately send OpenClaw to the background by returning to the Home Screen
|
||||
or locking the iPhone.
|
||||
4. Watch the Lock Screen or Dynamic Island while the Gateway reconnects.
|
||||
|
||||
Expected result: while reconnecting, iOS can show an `OpenClaw` Live Activity
|
||||
with connection status such as `Connecting...` or `Reconnecting...`. On a fast
|
||||
network this status may be brief because OpenClaw ends the Live Activity after
|
||||
the Gateway reconnects successfully.
|
||||
|
||||
## Push Notification
|
||||
|
||||
1. Tap the `Chat` tab.
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
|
||||
<string>OpenClaw uses your calendars to add events when you enable calendar access.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>OpenClaw uses the camera when you scan a Gateway setup QR code or ask your paired Gateway or assistant to capture a photo or short video from this iPhone, for example to connect to your Gateway or show your assistant a document, device screen, or workspace.</string>
|
||||
<string>OpenClaw can capture photos or short video clips when requested via the gateway.</string>
|
||||
<key>NSContactsUsageDescription</key>
|
||||
<string>OpenClaw uses your contacts so you can search and reference people while using the assistant.</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
|
||||
@@ -156,7 +156,7 @@ targets:
|
||||
NSAllowsLocalNetworking: true
|
||||
NSBonjourServices:
|
||||
- _openclaw-gw._tcp
|
||||
NSCameraUsageDescription: OpenClaw uses the camera when you scan a Gateway setup QR code or ask your paired Gateway or assistant to capture a photo or short video from this iPhone, for example to connect to your Gateway or show your assistant a document, device screen, or workspace.
|
||||
NSCameraUsageDescription: OpenClaw can capture photos or short video clips when requested via the gateway.
|
||||
NSCalendarsUsageDescription: OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.
|
||||
NSCalendarsFullAccessUsageDescription: OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.
|
||||
NSCalendarsWriteOnlyAccessUsageDescription: OpenClaw uses your calendars to add events when you enable calendar access.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
f5a5855ddd7aa8c23a732f257eceaa20fd163b1d5f342c909f4aef15aa8643cf config-baseline.json
|
||||
b8dffdb1a328aaf728a0707ab04d21c65f1a225a2360042e10832aa608699716 config-baseline.core.json
|
||||
9246475f5771612a5fd12de38b153783c4a4cbb8b2682a5c40115916661c90f2 config-baseline.json
|
||||
6349131baaa1828f2a071f42e4d7b17c8966c59b6588c8a4c1a32ea5ea4dcd5e config-baseline.core.json
|
||||
671979e86e4c4f59415d0a20879e838f9bbd883b3d29eeb02cb5131db8d187fe config-baseline.channel.json
|
||||
94529978588d6e3776a86780b22cf9ff46a6f9957f2f178d3829403fad451ca7 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
6620d5a6100d60f98cf13b8a13e3c46e9631400d1a1d7c0c6a22c490da810813 plugin-sdk-api-baseline.json
|
||||
961377a56fd0fb3307fb4be95dcb480610f14c717e1b82e4bf262dd5faaddcbc plugin-sdk-api-baseline.jsonl
|
||||
ebb0ae07e4d6f6ea1faccba7604c9da71a5401b3aa2bc3618963e1e44a8dbcce plugin-sdk-api-baseline.json
|
||||
9b7aee16d91c6a1b042a7d7e6f92a77b3e234337cc5fcf5a797de05fa9e9a02e plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -30,7 +30,7 @@ or an explicit manual dispatch.
|
||||
| `security-fast` | Private key detection, changed-workflow audit via `zizmor`, and production lockfile audit | Always on non-draft pushes and PRs |
|
||||
| `check-dependencies` | Production Knip dependency-only pass plus the unused-file allowlist guard | Node-relevant changes |
|
||||
| `build-artifacts` | Build `dist/`, Control UI, built-CLI smoke checks, embedded built-artifact checks, and reusable artifacts | Node-relevant changes |
|
||||
| `checks-fast-core` | Fast Linux correctness lanes such as bundled, protocol, QA Smoke CI, and CI-routing checks | Node-relevant changes |
|
||||
| `checks-fast-core` | Fast Linux correctness lanes such as bundled, protocol, and CI-routing checks | Node-relevant changes |
|
||||
| `checks-fast-contracts-plugins-*` | Two sharded plugin contract checks | Node-relevant changes |
|
||||
| `checks-fast-contracts-channels-*` | Two sharded channel contract checks | Node-relevant changes |
|
||||
| `checks-node-core-*` | Core Node test shards, excluding channel, bundled, contract, and extension lanes | Node-relevant changes |
|
||||
|
||||
@@ -120,7 +120,6 @@ openclaw sessions cleanup --json
|
||||
|
||||
- Scope note: `openclaw sessions cleanup` maintains session stores, transcripts, and trajectory sidecars. It does not prune cron run history, which is managed by `cron.runLog.keepLines` in [Cron configuration](/automation/cron-jobs#configuration) and explained in [Cron maintenance](/automation/cron-jobs#maintenance).
|
||||
- Cleanup also prunes unreferenced primary transcripts, compaction checkpoints, and trajectory sidecars older than `session.maintenance.pruneAfter`; files still referenced by `sessions.json` are preserved.
|
||||
- Cleanup reports short-lived gateway model-run probe cleanup separately as `modelRunPruned`. This only matches strict explicit keys shaped like `agent:*:explicit:model-run-<uuid>`. The fixed retention is `24h`, but it is pressure-gated: it only removes stale probe rows when session-entry maintenance/cap pressure is reached. When it runs, model-run cleanup happens before global stale cleanup and capping.
|
||||
|
||||
- `--dry-run`: preview how many entries would be pruned/capped without writing.
|
||||
- In text mode, dry-run prints a per-session action table (`Action`, `Key`, `Age`, `Model`, `Flags`) plus a summary grouped by session label so you can see what would be kept vs removed.
|
||||
|
||||
@@ -127,14 +127,6 @@ in `enforce` mode and applies cleanup during maintenance. Set
|
||||
|
||||
For production-sized `maxEntries` limits, Gateway runtime writes use a small high-water buffer and clean back down to the configured cap in batches. Session store reads do not prune or cap entries during Gateway startup. This avoids running full store cleanup on every startup or isolated cron session. `openclaw sessions cleanup --enforce` applies the cap immediately.
|
||||
|
||||
Gateway model-run probe sessions are short-lived by default. Matching rows with
|
||||
strict explicit keys like `agent:*:explicit:model-run-<uuid>` use fixed `24h`
|
||||
retention, but cleanup is pressure-gated: it only removes stale probe rows when
|
||||
session-entry maintenance/cap pressure is reached. When model-run cleanup runs,
|
||||
it runs before the broader stale-entry age cutoff and entry cap. Normal direct,
|
||||
group, thread, cron, hook, heartbeat, ACP, and sub-agent sessions do not inherit
|
||||
this 24h retention.
|
||||
|
||||
Maintenance preserves durable external conversation pointers, including group
|
||||
sessions and thread-scoped chat sessions, while still allowing synthetic cron,
|
||||
hook, heartbeat, ACP, and sub-agent entries to age out.
|
||||
|
||||
@@ -15,8 +15,7 @@ When `agents.defaults.typingMode` is **unset**, OpenClaw keeps the legacy behavi
|
||||
|
||||
- **Direct chats**: typing starts immediately once the model loop begins.
|
||||
- **Group chats with a mention**: typing starts immediately.
|
||||
- **Group chats without a mention**: typing starts when the admitted run has
|
||||
user-visible activity, such as harness execution activity or message text.
|
||||
- **Group chats without a mention**: typing starts only when message text begins streaming.
|
||||
- **Heartbeat runs**: typing starts when the heartbeat run begins if the
|
||||
resolved heartbeat target is a typing-capable chat and typing is not disabled.
|
||||
|
||||
@@ -27,14 +26,13 @@ Set `agents.defaults.typingMode` to one of:
|
||||
- `never` - no typing indicator, ever.
|
||||
- `instant` - start typing **as soon as the model loop begins**, even if the run
|
||||
later returns only the silent reply token.
|
||||
- `thinking` - start typing on the **first reasoning delta** or on active
|
||||
harness execution after the turn is accepted.
|
||||
- `message` - start typing on the **first user-visible reply activity**, such as
|
||||
active harness execution or a non-silent text delta. Silent reply tokens such
|
||||
as `NO_REPLY` do not count as text activity.
|
||||
- `thinking` - start typing on the **first reasoning delta** (requires
|
||||
`reasoningLevel: "stream"` for the run).
|
||||
- `message` - start typing on the **first non-silent text delta** (ignores
|
||||
the `NO_REPLY` silent token).
|
||||
|
||||
Order of "how early it fires":
|
||||
`never` → `message`/`thinking` → `instant`
|
||||
`never` → `message` → `thinking` → `instant`
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -64,10 +62,11 @@ Override mode or cadence per session:
|
||||
|
||||
## Notes
|
||||
|
||||
- `message` mode does not start from silent reply tokens, but active execution
|
||||
can still show typing before any assistant text is available.
|
||||
- `thinking` still reacts to streamed reasoning (`reasoningLevel: "stream"`),
|
||||
and it can also start from active execution before reasoning deltas arrive.
|
||||
- `message` mode won't show typing for silent-only replies when the whole
|
||||
payload is the exact silent token (for example `NO_REPLY` / `no_reply`,
|
||||
matched case-insensitively).
|
||||
- `thinking` only fires if the run streams reasoning (`reasoningLevel: "stream"`).
|
||||
If the model doesn't emit reasoning deltas, typing won't start.
|
||||
- Heartbeat typing is a liveness signal for the resolved delivery target. It
|
||||
starts at heartbeat run start instead of following `message` or `thinking`
|
||||
stream timing. Set `typingMode: "never"` to disable it.
|
||||
|
||||
@@ -30,68 +30,6 @@ title: "Usage tracking"
|
||||
- CLI: `openclaw channels list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
|
||||
- macOS menu bar: "Usage" section under Context (only if available).
|
||||
|
||||
## Default usage footer mode
|
||||
|
||||
`/usage off|tokens|full` sets the footer for a session and is remembered for that
|
||||
session. `messages.responseUsage` seeds that mode for sessions that have not
|
||||
chosen one, so the footer can be on by default without typing `/usage` each time.
|
||||
|
||||
Set one mode for every channel, or a per-channel map with a `default` fallback:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"messages": {
|
||||
"responseUsage": "tokens",
|
||||
// or: { "default": "off", "discord": "full" }
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Three distinct session states
|
||||
|
||||
A session's `responseUsage` field has three representable states, each with
|
||||
different semantics:
|
||||
|
||||
| State | Stored value | Effective mode |
|
||||
| ------------------- | ------------------------------- | --------------------------------------------------------------------- |
|
||||
| **Unset / inherit** | `undefined` (absent) | Falls through to `messages.responseUsage` config default, then `off`. |
|
||||
| **Explicit off** | `"off"` (stored) | Always off — a non-off config default cannot re-enable the footer. |
|
||||
| **Explicit on** | `"tokens"` or `"full"` (stored) | That mode, regardless of config default. |
|
||||
|
||||
### Precedence
|
||||
|
||||
Effective mode = session override → channel config entry → `default` → `off`.
|
||||
|
||||
An explicit `/usage off` is **persisted** as the literal value `"off"` in the
|
||||
session, not the same as "unset." This means a non-off `messages.responseUsage`
|
||||
default cannot turn the footer back on once the user has explicitly disabled it.
|
||||
|
||||
### Resetting vs. turning off
|
||||
|
||||
- `/usage off` — forces the footer off and persists that choice. A configured
|
||||
non-off default cannot override this.
|
||||
- `/usage reset` (aliases: `inherit`, `clear`, `default`) — clears the session
|
||||
override. The session then **inherits** the effective config default
|
||||
(`messages.responseUsage`). If no default is configured, the footer is off
|
||||
(unchanged from before). Use this to "go back to default" without explicitly
|
||||
turning the footer on.
|
||||
- A full session reset (`/reset` or `/new`) or a session rollover **preserves**
|
||||
the explicit usage-mode preference so the user's display choice survives
|
||||
session rollovers. Only `/usage reset` (and its aliases) actually clears the
|
||||
override.
|
||||
|
||||
### Toggle behavior
|
||||
|
||||
`/usage` with no arguments cycles: off → tokens → full → off. The starting point
|
||||
for the cycle is the **effective** current mode (session override falling through
|
||||
to the config default when unset), so the cycle is always consistent with what
|
||||
the user sees in the footer.
|
||||
|
||||
### Config
|
||||
|
||||
With no config the prior behavior holds (footer off until `/usage`). Use
|
||||
`/usage reset` to clear a session override and re-inherit the configured default.
|
||||
|
||||
## Custom `/usage full` footer
|
||||
|
||||
`/usage full` shows a built-in compact footer with model, reasoning, fast/slow,
|
||||
|
||||
@@ -1316,7 +1316,6 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
|
||||
- `mode`: `enforce` applies cleanup and is the default; `warn` emits warnings only.
|
||||
- `pruneAfter`: age cutoff for stale entries (default `30d`).
|
||||
- `maxEntries`: maximum number of entries in `sessions.json` (default `500`). Runtime writes batch cleanup with a small high-water buffer for production-sized caps; `openclaw sessions cleanup --enforce` applies the cap immediately.
|
||||
- Short-lived gateway model-run probe sessions use fixed `24h` retention, but cleanup is pressure-gated: it only removes stale strict model-run probe rows when session-entry maintenance/cap pressure is reached. Only strict explicit probe keys matching `agent:*:explicit:model-run-<uuid>` are eligible; normal direct, group, thread, cron, hook, heartbeat, ACP, and sub-agent sessions do not inherit this 24h retention. When model-run cleanup runs, it runs before the broader `pruneAfter` stale-entry cleanup and `maxEntries` cap.
|
||||
- `rotateBytes`: deprecated and ignored; `openclaw doctor --fix` removes it from older configs.
|
||||
- `resetArchiveRetention`: retention for `*.reset.<timestamp>` transcript archives. Defaults to `pruneAfter`; set `false` to disable.
|
||||
- `maxDiskBytes`: optional sessions-directory disk budget. In `warn` mode it logs warnings; in `enforce` mode it removes oldest artifacts/sessions first.
|
||||
|
||||
@@ -528,13 +528,25 @@ candidate contains redacted secret placeholders such as `***`.
|
||||
and re-checked, so a path that lexically lives in a config dir but whose
|
||||
real target escapes every allowed root is still rejected.
|
||||
- **Error handling**: clear errors for missing files, parse errors, circular includes, invalid path format, and excessive length
|
||||
- **Hot reload**: edits to regular include files successfully resolved by the
|
||||
last valid config are watched, including nested includes. Changing an
|
||||
authored `$include` target inside a watched file re-resolves the graph.
|
||||
Paths that were missing or invalid during the last successful resolution,
|
||||
and filesystem or symlink retargets that do not modify a watched file, are
|
||||
not discovered automatically; edit `openclaw.json` or restart the Gateway
|
||||
to resolve the graph again.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Config hot reload
|
||||
|
||||
The Gateway watches `~/.openclaw/openclaw.json` and applies changes automatically - no manual restart needed for most settings.
|
||||
The Gateway watches `~/.openclaw/openclaw.json` plus the canonical include files
|
||||
successfully resolved by the last valid config, and applies changes
|
||||
automatically - no manual restart needed for most settings. Invalid candidates
|
||||
keep the last valid watch set. Missing or invalid paths outside that set, plus
|
||||
filesystem or symlink retargets that do not modify a watched file, require an
|
||||
`openclaw.json` edit or a Gateway restart before they can be discovered.
|
||||
|
||||
Direct file edits are treated as untrusted until they validate. The watcher waits
|
||||
for editor temp-write/rename churn to settle, reads the final file, and rejects
|
||||
|
||||
@@ -415,7 +415,7 @@ If you installed OpenClaw via `npm install -g openclaw`, use the inline `docker
|
||||
|
||||
</Step>
|
||||
<Step title="Optional: build the common image">
|
||||
For a more functional sandbox image with common tooling (for example `curl`, `jq`, Node 24, pnpm, `python3`, and `git`):
|
||||
For a more functional sandbox image with common tooling (for example `curl`, `jq`, `nodejs`, `python3`, `git`):
|
||||
|
||||
From a source checkout:
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -308,7 +308,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Normal setup and repair paths are documented across install, CLI, and gateway docs. Platform-specific Windows paths are tracked in the Windows via WSL2 and Native Windows rows.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 4%</span><span>Quality Stable - 83%</span><span>Completeness Stable - 90%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 6</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 2%</span><span>Quality Stable - 83%</span><span>Completeness Stable - 90%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 6</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -317,7 +317,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">CLI Setup</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>17%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "17%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/install/index), [Installer](/install/installer), [Node](/install/node), [Updating](/install/updating)</div>
|
||||
@@ -327,7 +327,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Onboarding and Auth Setup</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Onboard](/cli/onboard), [Configure](/cli/configure), [Onboarding Overview](/start/onboarding-overview)</div>
|
||||
@@ -337,7 +337,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Plugin and Channel Setup</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Onboard](/cli/onboard), [Plugins](/cli/plugins), [Channels](/cli/channels)</div>
|
||||
@@ -347,7 +347,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Gateway Service Management</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>14%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "14%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>87%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "87%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Gateway](/cli/gateway), [Updating](/install/updating), [Troubleshooting](/gateway/troubleshooting)</div>
|
||||
@@ -357,7 +357,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">CLI Observability</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Status](/cli/status), [Health](/cli/health), [Logs](/cli/logs), [Diagnostics](/gateway/diagnostics)</div>
|
||||
@@ -367,7 +367,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Doctor</span>
|
||||
<span>10 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Doctor](/cli/doctor), [Doctor](/gateway/doctor), [Secrets](/gateway/secrets), [Troubleshooting](/gateway/troubleshooting)</div>
|
||||
@@ -377,7 +377,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Updates and Upgrades</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Updating](/install/updating), [Update](/cli/update), [Troubleshooting](/gateway/troubleshooting)</div>
|
||||
@@ -391,7 +391,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Core architecture, auth, pairing, protocol docs, daemon docs, and CLI runbooks are broad and current.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 6%</span><span>Quality Stable - 81%</span><span>Completeness Stable - 89%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 12</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 3%</span><span>Quality Stable - 81%</span><span>Completeness Stable - 89%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 12</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -400,7 +400,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Approvals and Remote Execution</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Index](/gateway/security/index)</div>
|
||||
@@ -410,7 +410,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">HTTP APIs</span>
|
||||
<span>4 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>25%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "25%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/gateway/index), [Openai Http Api](/gateway/openai-http-api), [Openresponses Http Api](/gateway/openresponses-http-api), [Tools Invoke Http Api](/gateway/tools-invoke-http-api), [Hooks](/automation/hooks), [Index](/web/index)</div>
|
||||
@@ -420,7 +420,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Hosted Web Surface</span>
|
||||
<span>4 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/gateway/index), [Architecture](/concepts/architecture), [Control Ui](/web/control-ui), [Webchat](/web/webchat), [Canvas](/refactor/canvas)</div>
|
||||
@@ -430,7 +430,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Gateway RPC APIs and Events</span>
|
||||
<span>20 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>9%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "9%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Index](/gateway/index), [Architecture](/concepts/architecture)</div>
|
||||
@@ -440,7 +440,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Device Auth and Pairing</span>
|
||||
<span>10 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Pairing](/gateway/pairing), [Index](/gateway/security/index)</div>
|
||||
@@ -450,7 +450,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Network Access and Discovery</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/gateway/index), [Discovery](/gateway/discovery), [Protocol](/gateway/protocol)</div>
|
||||
@@ -460,7 +460,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Nodes and Remote Capabilities</span>
|
||||
<span>8 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Architecture](/concepts/architecture), [Index](/nodes/index)</div>
|
||||
@@ -470,7 +470,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Health, Diagnostics, and Repair</span>
|
||||
<span>7 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/gateway/index), [Diagnostics](/gateway/diagnostics), [Doctor](/gateway/doctor)</div>
|
||||
@@ -480,7 +480,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Protocol Compatibility</span>
|
||||
<span>7 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Architecture](/concepts/architecture), [Typebox](/concepts/typebox), [Bridge Protocol](/gateway/bridge-protocol)</div>
|
||||
@@ -490,7 +490,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Roles and Permissions</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Index](/gateway/security/index)</div>
|
||||
@@ -500,7 +500,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Gateway Lifecycle</span>
|
||||
<span>7 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>33%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "33%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/gateway/index), [Architecture](/concepts/architecture)</div>
|
||||
@@ -510,7 +510,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Security Controls</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>75%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "75%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>89%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "89%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/gateway/security/index), [Protocol](/gateway/protocol), [Discovery](/gateway/discovery)</div>
|
||||
@@ -520,7 +520,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">WebSocket Connection</span>
|
||||
<span>8 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>13%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "13%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-stable"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-stable">Stable</span><span>90%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "90%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Protocol](/gateway/protocol), [Architecture](/concepts/architecture)</div>
|
||||
@@ -534,7 +534,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Main loop, models, provider routing, and tool streaming are first-class, but provider behavior shifts weekly and needs scenario proof per release.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 33%</span><span>Quality Beta - 78%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 6</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 2%</span><span>Quality Beta - 78%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 6</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -543,7 +543,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Agent Turn Execution</span>
|
||||
<span>3 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>29%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "29%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Agent Loop](/concepts/agent-loop), [Agent](/cli/agent), [Agent Runtimes](/concepts/agent-runtimes)</div>
|
||||
@@ -553,7 +553,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">External Runtimes and Subagents</span>
|
||||
<span>4 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>30%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "30%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Agent Runtimes](/concepts/agent-runtimes), [Anthropic](/providers/anthropic), [Google](/providers/google), [Subagents](/tools/subagents)</div>
|
||||
@@ -563,7 +563,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Hosted Provider Execution</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>20%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "20%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Openai](/providers/openai), [Anthropic](/providers/anthropic), [Google](/providers/google), [Models](/concepts/models)</div>
|
||||
@@ -573,7 +573,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Local and Self-hosted Providers</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Ollama](/providers/ollama), [Models](/concepts/models), [Agent](/cli/agent)</div>
|
||||
@@ -583,7 +583,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Model and Runtime Selection</span>
|
||||
<span>4 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>25%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "25%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Models](/concepts/models), [Models](/cli/models), [Openai](/providers/openai), [Agent Runtimes](/concepts/agent-runtimes)</div>
|
||||
@@ -593,7 +593,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Provider Auth</span>
|
||||
<span>10 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>24%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "24%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Models](/concepts/models), [Agent](/cli/agent), [Models](/cli/models), [Openai](/providers/openai), [Anthropic](/providers/anthropic), [Google](/providers/google), [Subagents](/tools/subagents)</div>
|
||||
@@ -603,7 +603,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Streaming and Progress</span>
|
||||
<span>2 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>56%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "56%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Streaming](/concepts/streaming), [Agent Loop](/concepts/agent-loop)</div>
|
||||
@@ -613,7 +613,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Tool Calls and Response Handling</span>
|
||||
<span>3 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>65%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "65%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Agent Loop](/concepts/agent-loop), [Ollama](/providers/ollama)</div>
|
||||
@@ -623,7 +623,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Tool Execution Controls</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>50%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "50%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Sandbox Vs Tool Policy Vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated), [Agent Loop](/concepts/agent-loop), [Subagents](/tools/subagents)</div>
|
||||
@@ -637,7 +637,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Strong docs and active implementation. Maturity depends on transcript durability, compaction quality, and cross-client parity.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 30%</span><span>Quality Beta - 77%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 6</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 0%</span><span>Quality Beta - 77%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 6</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -656,7 +656,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Token Management</span>
|
||||
<span>3 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>20%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "20%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Compaction](/concepts/compaction), [Context](/concepts/context), [Session Management Compaction](/reference/session-management-compaction)</div>
|
||||
@@ -666,7 +666,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Context Engine</span>
|
||||
<span>2 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>57%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "57%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Context](/concepts/context), [Context Engine](/concepts/context-engine), [Codex Context Engine Harness](/plan/codex-context-engine-harness)</div>
|
||||
@@ -676,7 +676,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Cross-client History and Session Parity</span>
|
||||
<span>2 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>40%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "40%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Webchat](/web/webchat), [Android](/platforms/android), [Channel Routing](/channels/channel-routing)</div>
|
||||
@@ -686,7 +686,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Diagnostics, Maintenance, and Recovery</span>
|
||||
<span>3 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>40%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "40%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Diagnostics](/gateway/diagnostics), [Session Management Compaction](/reference/session-management-compaction), [Flags](/diagnostics/flags)</div>
|
||||
@@ -696,7 +696,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Core Prompts and Context</span>
|
||||
<span>2 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>38%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "38%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Context](/concepts/context), [Transcript Hygiene](/reference/transcript-hygiene), [Discord](/channels/discord)</div>
|
||||
@@ -706,7 +706,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Memory</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>46%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "46%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Memory Config](/reference/memory-config), [Memory Qmd](/concepts/memory-qmd), [Memory](/concepts/memory), [Discord](/channels/discord)</div>
|
||||
@@ -716,7 +716,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Session Routing</span>
|
||||
<span>2 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>25%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "25%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Session](/concepts/session), [Channel Routing](/channels/channel-routing), [Discord](/channels/discord)</div>
|
||||
@@ -740,7 +740,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Many channels share Gateway delivery and routing contracts, but channel behavior varies by upstream API and account-policy constraints.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 13%</span><span>Quality Beta - 76%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 5</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 0%</span><span>Quality Beta - 76%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 5</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -759,7 +759,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Channel Setup</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>14%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "14%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/channels/index), [Pairing](/channels/pairing), [Troubleshooting](/channels/troubleshooting), [Sdk Channel Plugins](/plugins/sdk-channel-plugins)</div>
|
||||
@@ -769,7 +769,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Group Thread and Ambient Room Behavior</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>36%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "36%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Groups](/channels/groups), [Group Messages](/channels/group-messages), [Ambient Room Events](/channels/ambient-room-events), [Broadcast Groups](/channels/broadcast-groups), [Discord](/channels/discord)</div>
|
||||
@@ -799,7 +799,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Outbound Delivery and Reply Pipeline</span>
|
||||
<span>4 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>38%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "38%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Groups](/channels/groups), [Ambient Room Events](/channels/ambient-room-events), [Discord](/channels/discord), [Matrix](/channels/matrix), [Config Channels](/gateway/config-channels)</div>
|
||||
@@ -809,7 +809,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Conversation Routing and Delivery</span>
|
||||
<span>10 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>19%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "19%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Channel Routing](/channels/channel-routing), [Groups](/channels/groups), [Discord](/channels/discord), [Matrix](/channels/matrix), [Troubleshooting](/channels/troubleshooting), [Configuration Reference](/gateway/configuration-reference)</div>
|
||||
@@ -833,7 +833,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
OTel, Prometheus, logging, and diagnostics docs exist. Needs a public "what operators should look at first" maturity pass.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 18%</span><span>Quality Beta - 75%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 3</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 6%</span><span>Quality Beta - 75%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 3</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -842,7 +842,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Health and Repair</span>
|
||||
<span>12 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>28%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "28%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>6%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "6%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Health](/gateway/health), [Telegram](/channels/telegram), [Doctor](/cli/doctor), [Doctor](/gateway/doctor), [Sdk Subpaths](/plugins/sdk-subpaths), [Health](/cli/health), [Protocol](/gateway/protocol)</div>
|
||||
@@ -852,7 +852,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Logging</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>6%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "6%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Logging](/logging), [Logging](/gateway/logging), [Logs](/cli/logs)</div>
|
||||
@@ -862,7 +862,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Diagnostic Collection</span>
|
||||
<span>8 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>30%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "30%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>6%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "6%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Diagnostics](/gateway/diagnostics), [Health](/gateway/health), [Codex Harness](/plugins/codex-harness), [Protocol](/gateway/protocol)</div>
|
||||
@@ -872,7 +872,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Telemetry Export</span>
|
||||
<span>13 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>33%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "33%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>6%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "6%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Hooks](/plugins/hooks), [Opentelemetry](/gateway/opentelemetry), [Logging](/logging), [Sdk Subpaths](/plugins/sdk-subpaths), [Diagnostics Otel](/plugins/reference/diagnostics-otel), [Prometheus](/gateway/prometheus), [Diagnostics Prometheus](/plugins/reference/diagnostics-prometheus)</div>
|
||||
@@ -882,7 +882,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Session Diagnostics</span>
|
||||
<span>4 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>6%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "6%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Opentelemetry](/gateway/opentelemetry), [Prometheus](/gateway/prometheus), [Diagnostics](/gateway/diagnostics), [Protocol](/gateway/protocol)</div>
|
||||
@@ -896,7 +896,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Web UI is documented with pairing, chat, PWA, Talk, push, and remote Gateway flows. Promote after cross-browser and mobile-PWA scorecards.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 4%</span><span>Quality Beta - 74%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 0%</span><span>Quality Beta - 74%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -935,7 +935,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Browser UI</span>
|
||||
<span>10 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Control Ui](/web/control-ui), [Index](/web/index), [Dashboard](/web/dashboard), [Protocol](/gateway/protocol)</div>
|
||||
@@ -945,7 +945,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">WebChat Conversations</span>
|
||||
<span>15 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>10%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "10%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Control Ui](/web/control-ui), [Webchat](/web/webchat), [Getting Started](/start/getting-started), [Channel Routing](/channels/channel-routing), [Secure File Operations](/gateway/security/secure-file-operations)</div>
|
||||
@@ -955,7 +955,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Operator Console</span>
|
||||
<span>10 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Control Ui](/web/control-ui), [Health](/gateway/health), [Protocol](/gateway/protocol), [Dashboard](/web/dashboard)</div>
|
||||
@@ -969,7 +969,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Broad docs and strong internal runtime evidence exist across manifests, discovery, loading, provider/tool architecture, and approval boundaries. Keep the row at beta until public SDK API/subpaths and external distribution proof are stronger.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 12%</span><span>Quality Beta - 72%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 7</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 2%</span><span>Quality Beta - 72%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 7</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -978,7 +978,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Authoring and Packaging plugins</span>
|
||||
<span>8 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Building Plugins](/plugins/building-plugins), [Sdk Overview](/plugins/sdk-overview), [Sdk Entrypoints](/plugins/sdk-entrypoints), [Sdk Subpaths](/plugins/sdk-subpaths), [Manifest](/plugins/manifest), [Reference](/plugins/reference)</div>
|
||||
@@ -988,7 +988,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Bundled plugins</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Plugin Inventory](/plugins/plugin-inventory), [Plugins](/cli/plugins), [Architecture Internals](/plugins/architecture-internals)</div>
|
||||
@@ -998,7 +998,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Canvas plugin</span>
|
||||
<span>6 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Canvas](/plugins/reference/canvas), [Canvas](/refactor/canvas), [Configuration Reference](/gateway/configuration-reference)</div>
|
||||
@@ -1008,7 +1008,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Installing and running plugins</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>35%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "35%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Architecture](/plugins/architecture), [Architecture Internals](/plugins/architecture-internals), [Plugins](/cli/plugins)</div>
|
||||
@@ -1018,7 +1018,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Channel plugins</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Sdk Channel Plugins](/plugins/sdk-channel-plugins), [Sdk Channel Inbound](/plugins/sdk-channel-inbound), [Sdk Channel Outbound](/plugins/sdk-channel-outbound)</div>
|
||||
@@ -1028,7 +1028,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Provider and tool plugins</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>43%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "43%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Sdk Provider Plugins](/plugins/sdk-provider-plugins), [Tool Plugins](/plugins/tool-plugins), [Adding Capabilities](/plugins/adding-capabilities)</div>
|
||||
@@ -1038,7 +1038,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Plugin approvals</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Plugin Permission Requests](/plugins/plugin-permission-requests), [Exec Approvals](/tools/exec-approvals), [Sdk Channel Plugins](/plugins/sdk-channel-plugins)</div>
|
||||
@@ -1048,7 +1048,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Publishing plugins</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Plugins](/cli/plugins), [Compatibility](/plugins/compatibility), [Publishing](/clawhub/publishing)</div>
|
||||
@@ -1058,7 +1058,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Testing plugins</span>
|
||||
<span>6 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>27%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "27%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>2%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "2%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Sdk Testing](/plugins/sdk-testing), [Sdk Setup](/plugins/sdk-setup), [Codex Harness](/plugins/codex-harness)</div>
|
||||
@@ -1072,7 +1072,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Good docs and hardening surfaces exist. Promote after regular upgrade/security scenario runs prove no setup regressions.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 16%</span><span>Quality Beta - 72%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 5</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 0%</span><span>Quality Beta - 72%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 5</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -1081,7 +1081,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Approval Policy and Tool Safeguards</span>
|
||||
<span>2 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>50%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "50%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Exec Approvals](/tools/exec-approvals), [Approvals](/cli/approvals), [Plugin Permission Requests](/plugins/plugin-permission-requests), [Audit Checks](/gateway/security/audit-checks)</div>
|
||||
@@ -1131,7 +1131,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Credential and Secret Hygiene</span>
|
||||
<span>5 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>46%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "46%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Authentication](/gateway/authentication), [Models](/cli/models), [Openai](/providers/openai), [Oauth](/concepts/oauth), [Secrets](/gateway/secrets), [Secrets](/cli/secrets), [Secretref Credential Surface](/reference/secretref-credential-surface), [Audit Checks](/gateway/security/audit-checks)</div>
|
||||
@@ -1145,7 +1145,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Documented and usable, but scenario proof should cover unattended delivery, retries, and failure visibility.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 2%</span><span>Quality Beta - 72%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 0%</span><span>Quality Beta - 72%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -1194,7 +1194,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Heartbeat</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>14%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "14%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Index](/automation/index), [Heartbeat](/gateway/heartbeat), [Commitments](/concepts/commitments)</div>
|
||||
@@ -1218,7 +1218,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Broad capability surface exists, but provider variance, file limits, and node/app parity make this not stable yet.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 2%</span><span>Quality Alpha - 64%</span><span>Completeness Alpha - 68%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 1%</span><span>Quality Alpha - 64%</span><span>Completeness Alpha - 68%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -1227,7 +1227,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Media Intake and Access</span>
|
||||
<span>8 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>1%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "1%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>61%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "61%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Media Overview](/tools/media-overview), [Media Understanding](/nodes/media-understanding), [Secure File Operations](/gateway/security/secure-file-operations), [Pdf](/tools/pdf), [Image Generation](/tools/image-generation), [Qr](/cli/qr), [Line](/channels/line), [Whatsapp](/channels/whatsapp)</div>
|
||||
@@ -1237,7 +1237,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Channel Media Handling</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>1%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "1%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>61%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "61%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Images](/nodes/images), [Media Overview](/tools/media-overview), [Discord](/channels/discord)</div>
|
||||
@@ -1247,7 +1247,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Media Configuration</span>
|
||||
<span>1 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>1%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "1%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>61%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "61%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Media Overview](/tools/media-overview), [Image Generation](/tools/image-generation), [Manifest](/plugins/manifest), [Codex Harness](/plugins/codex-harness)</div>
|
||||
@@ -1257,7 +1257,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Text-to-Speech Delivery</span>
|
||||
<span>2 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>1%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "1%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>61%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "61%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Tts](/tools/tts), [Media Overview](/tools/media-overview), [Discord](/channels/discord)</div>
|
||||
@@ -1267,7 +1267,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Media Understanding</span>
|
||||
<span>12 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>7%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "7%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>1%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "1%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>69%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "69%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>69%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "69%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Audio](/nodes/audio), [Media Understanding](/nodes/media-understanding), [Media Overview](/tools/media-overview), [Whatsapp](/channels/whatsapp), [Images](/nodes/images), [Infer](/cli/infer), [Pdf](/tools/pdf)</div>
|
||||
@@ -1277,7 +1277,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Media Generation</span>
|
||||
<span>17 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>5%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "5%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>1%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "1%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>69%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "69%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>69%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "69%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Image Generation](/tools/image-generation), [Media Overview](/tools/media-overview), [Skills](/tools/skills), [Music Generation](/tools/music-generation), [Video Generation](/tools/video-generation)</div>
|
||||
@@ -1480,7 +1480,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
OpenClaw App SDK is a distinct external app contract separate from Gateway runtime and Plugin SDK. Current scoring shows a real `@openclaw/sdk` path with gaps around public packaging, auto-discovery, approvals, helpers, and compatibility.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 3%</span><span>Quality Alpha - 54%</span><span>Completeness Alpha - 53%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 0%</span><span>Quality Alpha - 54%</span><span>Completeness Alpha - 53%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -1529,7 +1529,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Resource Helpers</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>17%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "17%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>62%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "62%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>53%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "53%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Openclaw Sdk](/gateway/external-apps), [Openclaw Sdk Api Design](/gateway/external-apps)</div>
|
||||
@@ -1704,7 +1704,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Install docs exist and are common deployment paths. Promote after recurring release smoke captures upgrade and volume behavior.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 7%</span><span>Quality Beta - 71%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 5%</span><span>Quality Beta - 71%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -1713,7 +1713,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Container Setup</span>
|
||||
<span>6 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>5%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "5%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Docker](/install/docker), [Podman](/install/podman)</div>
|
||||
@@ -1723,7 +1723,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Container Operations</span>
|
||||
<span>11 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>5%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "5%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Podman](/install/podman), [Docker Vm Runtime](/install/docker-vm-runtime), [Docker](/install/docker), [Hetzner](/install/hetzner), [Hostinger](/install/hostinger)</div>
|
||||
@@ -1733,7 +1733,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Image Release and Validation</span>
|
||||
<span>5 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>29%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "29%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>5%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "5%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Docker](/install/docker), [Docker Vm Runtime](/install/docker-vm-runtime), [Full Release Validation](/reference/full-release-validation)</div>
|
||||
@@ -1743,7 +1743,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Agent Sandbox and Tooling</span>
|
||||
<span>3 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>5%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "5%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Docker](/install/docker), [Docker Vm Runtime](/install/docker-vm-runtime)</div>
|
||||
@@ -1757,7 +1757,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Recommended Windows path with systemd/user-service guidance and boot-chain docs. Promote after repeated install/update scorecards.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 6%</span><span>Quality Alpha - 69%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 5</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 3%</span><span>Quality Alpha - 69%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 5</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -1766,7 +1766,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">WSL Setup</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Windows](/platforms/windows), [Getting Started](/start/getting-started)</div>
|
||||
@@ -1776,7 +1776,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">CLI</span>
|
||||
<span>8 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Windows](/platforms/windows), [Getting Started](/start/getting-started), [Updating](/install/updating), [Onboard](/cli/onboard), [Doctor](/cli/doctor), [Status](/cli/status), [Logs](/cli/logs)</div>
|
||||
@@ -1786,7 +1786,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Gateway Service Lifecycle</span>
|
||||
<span>10 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Windows](/platforms/windows), [Index](/gateway/index), [Doctor](/gateway/doctor)</div>
|
||||
@@ -1796,7 +1796,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Gateway Access and Exposure</span>
|
||||
<span>11 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Authentication](/gateway/authentication), [Secrets](/gateway/secrets), [Remote](/gateway/remote), [Exposure Runbook](/gateway/security/exposure-runbook), [Windows](/platforms/windows)</div>
|
||||
@@ -1806,7 +1806,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Diagnostics and Repair</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>38%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "38%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Windows](/platforms/windows), [Status](/cli/status), [Logs](/cli/logs), [Doctor](/cli/doctor), [Doctor](/gateway/doctor)</div>
|
||||
@@ -1816,7 +1816,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Browser and Control UI</span>
|
||||
<span>6 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>3%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "3%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Browser Wsl2 Windows Remote Cdp Troubleshooting](/tools/browser-wsl2-windows-remote-cdp-troubleshooting), [Browser](/tools/browser), [Control Ui](/web/control-ui)</div>
|
||||
@@ -3276,7 +3276,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Core tools are documented, but host security and permission UX should stay under active scorecard review.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 21%</span><span>Quality Beta - 75%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 2</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 15%</span><span>Quality Beta - 75%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 2</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -3285,7 +3285,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Browser Automation</span>
|
||||
<span>8 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>13%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "13%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>15%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "15%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Browser Control](/tools/browser-control), [Testing](/help/testing), [Browser](/tools/browser), [Index](/gateway/security/index), [Audit Checks](/gateway/security/audit-checks)</div>
|
||||
@@ -3295,7 +3295,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Tool Invocation and Execution</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>50%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "50%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>15%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "15%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Exec](/tools/exec), [Background Process](/gateway/background-process), [Tools Invoke Http Api](/gateway/tools-invoke-http-api), [Operator Scopes](/gateway/operator-scopes), [Protocol](/gateway/protocol), [Exec Approvals](/tools/exec-approvals), [Exec Approvals Advanced](/tools/exec-approvals-advanced), [Elevated](/tools/elevated)</div>
|
||||
@@ -3305,7 +3305,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Sandbox and Tool Policy</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>15%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "15%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Sandboxing](/gateway/sandboxing), [Sandbox Vs Tool Policy Vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated), [Multi Agent Sandbox Tools](/tools/multi-agent-sandbox-tools), [Codex Harness Reference](/plugins/codex-harness-reference), [Config Tools](/gateway/config-tools)</div>
|
||||
@@ -3319,7 +3319,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Deep docs, OAuth/subscription path, realtime voice, image, and compatibility behavior. Provider churn keeps this from Stable without release-scorecard proof.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 26%</span><span>Quality Beta - 74%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 3</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 8%</span><span>Quality Beta - 74%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-partial">Partial - 3</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -3328,7 +3328,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Model and Auth</span>
|
||||
<span>6 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>44%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "44%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Openai](/providers/openai), [Codex Harness](/plugins/codex-harness), [Models](/concepts/models), [Oauth](/concepts/oauth), [Codex Harness Reference](/plugins/codex-harness-reference), [Auth Monitoring](/automation/auth-monitoring)</div>
|
||||
@@ -3338,7 +3338,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Responses and Tool Compatibility</span>
|
||||
<span>4 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>40%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "40%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Openai](/providers/openai), [Openresponses Http Api](/gateway/openresponses-http-api), [Openai Http Api](/gateway/openai-http-api), [Codex Native Plugins](/plugins/codex-native-plugins)</div>
|
||||
@@ -3348,7 +3348,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Native Codex Harness</span>
|
||||
<span>2 capabilities / LTS-supported</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>44%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "44%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Codex Harness](/plugins/codex-harness), [Codex Harness Runtime](/plugins/codex-harness-runtime), [Codex Harness Reference](/plugins/codex-harness-reference), [Codex Native Plugins](/plugins/codex-native-plugins)</div>
|
||||
@@ -3358,7 +3358,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Image and Multimodal Input</span>
|
||||
<span>2 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Openai](/providers/openai), [Image Generation](/tools/image-generation), [Images](/nodes/images)</div>
|
||||
@@ -3368,7 +3368,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Voice and Realtime Audio</span>
|
||||
<span>2 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>8%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "8%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>67%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "67%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Openai](/providers/openai), [Discord](/channels/discord), [Voice Call](/plugins/voice-call)</div>
|
||||
@@ -3382,7 +3382,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
|
||||
Multiple providers and docs exist. Needs quota/error/SSRF proof per provider family.
|
||||
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 9%</span><span>Quality Beta - 74%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
<div className="maturity-surface-rollup"><span>Coverage Experimental - 7%</span><span>Quality Beta - 74%</span><span>Completeness Beta - 79%</span><span><span className="maturity-lts maturity-lts-none">None</span></span></div>
|
||||
|
||||
<div className="maturity-category-list">
|
||||
<div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>
|
||||
@@ -3391,7 +3391,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Search Providers</span>
|
||||
<span>19 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>11%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "11%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>7%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "7%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Web](/tools/web), [Brave Search](/tools/brave-search), [Tavily](/tools/tavily), [Exa Search](/tools/exa-search), [Firecrawl](/tools/firecrawl), [Perplexity Search](/tools/perplexity-search), [Duckduckgo Search](/tools/duckduckgo-search), [Searxng Search](/tools/searxng-search), [Gemini Search](/tools/gemini-search), [Grok Search](/tools/grok-search), [Kimi Search](/tools/kimi-search), [Minimax Search](/tools/minimax-search), [Ollama Search](/tools/ollama-search), [Sdk Subpaths](/plugins/sdk-subpaths), [Sdk Overview](/plugins/sdk-overview), [Manifest](/plugins/manifest)</div>
|
||||
@@ -3401,7 +3401,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Setup and Diagnostics</span>
|
||||
<span>9 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>7%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "7%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Web](/tools/web), [Web Fetch](/tools/web-fetch), [Faq](/help/faq), [Api Usage Costs](/reference/api-usage-costs), [Brave Search](/tools/brave-search), [Perplexity Search](/tools/perplexity-search), [Tavily](/tools/tavily), [Firecrawl](/tools/firecrawl)</div>
|
||||
@@ -3411,7 +3411,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Network Safety</span>
|
||||
<span>4 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>0%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "0%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>7%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "7%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-alpha"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-alpha">Alpha</span><span>68%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "68%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Web](/tools/web), [Web Fetch](/tools/web-fetch), [Firecrawl](/tools/firecrawl), [Searxng Search](/tools/searxng-search)</div>
|
||||
@@ -3421,7 +3421,7 @@ A surface is a product area such as Gateway runtime, Discord, or the macOS app.
|
||||
<span className="maturity-category-title">Tool Availability and Fetch</span>
|
||||
<span>11 capabilities</span>
|
||||
</div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>25%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "25%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-experimental"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-experimental">Experimental</span><span>7%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "7%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div><span className="maturity-score maturity-score-beta"><span className="maturity-score-label"><span className="maturity-level-pill maturity-level-beta">Beta</span><span>79%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "79%" }} /></span></span></div>
|
||||
<div className="maturity-category-docs">[Config Tools](/gateway/config-tools), [Web Fetch](/tools/web-fetch), [Web](/tools/web), [Faq](/help/faq)</div>
|
||||
|
||||
@@ -57,34 +57,6 @@ Logging:
|
||||
The macOS app checks the gateway version against its own version. If they're
|
||||
incompatible, update the global CLI to match the app version.
|
||||
|
||||
## State directory on macOS
|
||||
|
||||
Keep OpenClaw state on a local, non-synced disk. Avoid iCloud Drive and other
|
||||
cloud-synced folders because sync latency and file locks can affect sessions,
|
||||
credentials, and Gateway state.
|
||||
|
||||
Set `OPENCLAW_STATE_DIR` to a local path only when you need an override.
|
||||
`openclaw doctor` warns about common cloud-synced state paths and recommends
|
||||
moving back to local storage. See
|
||||
[environment variables](/help/environment#path-related-env-vars) and
|
||||
[Doctor](/gateway/doctor).
|
||||
|
||||
## Debug app connectivity
|
||||
|
||||
Use the macOS debug CLI from a source checkout to exercise the same Gateway
|
||||
WebSocket handshake and discovery logic the app uses:
|
||||
|
||||
```bash
|
||||
cd apps/macos
|
||||
swift run openclaw-mac connect --json
|
||||
swift run openclaw-mac discover --timeout 3000 --json
|
||||
```
|
||||
|
||||
`connect` accepts `--url`, `--token`, `--timeout`, and `--json`. `discover`
|
||||
accepts `--timeout`, `--json`, and `--include-local`. Compare discovery output
|
||||
with `openclaw gateway discover --json` when you need to separate CLI discovery
|
||||
from app-side connection issues.
|
||||
|
||||
## Smoke check
|
||||
|
||||
```bash
|
||||
|
||||
@@ -114,18 +114,7 @@ Example (in JS):
|
||||
window.location.href = "openclaw://agent?message=Review%20this%20design";
|
||||
```
|
||||
|
||||
Supported query parameters:
|
||||
|
||||
- `message`: prefilled agent prompt.
|
||||
- `sessionKey`: stable session identifier.
|
||||
- `thinking`: optional thinking profile.
|
||||
- `deliver`, `to`, or `channel`: delivery target.
|
||||
- `timeoutSeconds`: optional run timeout.
|
||||
- `key`: app-generated safety token for trusted local callers.
|
||||
|
||||
The app prompts for confirmation unless a valid key is provided. Unkeyed links
|
||||
show the message and URL before approval, and ignore delivery routing fields;
|
||||
keyed links use the normal Gateway run path.
|
||||
The app prompts for confirmation unless a valid key is provided.
|
||||
|
||||
## Security notes
|
||||
|
||||
|
||||
@@ -24,9 +24,6 @@ In SSH tunnel mode, discovered LAN/tailnet hostnames are saved as
|
||||
`gateway.remote.sshTarget`. The app keeps `gateway.remote.url` on the local
|
||||
tunnel endpoint, for example `ws://127.0.0.1:18789`, so CLI, Web Chat, and
|
||||
the local node-host service all use the same safe loopback transport.
|
||||
When discovery returns both raw Tailnet IPs and stable hostnames, the app
|
||||
prefers Tailscale MagicDNS or LAN names so remote connections survive address
|
||||
changes better.
|
||||
If the local tunnel port differs from the remote gateway port, set
|
||||
`gateway.remote.remotePort` to the port on the remote host.
|
||||
|
||||
|
||||
@@ -21,10 +21,6 @@ title: "macOS IPC"
|
||||
|
||||
- The app runs the Gateway (local mode) and connects to it as a node.
|
||||
- Agent actions are performed via `node.invoke` (e.g. `system.run`, `system.notify`, `canvas.*`).
|
||||
- Common Mac node commands include `canvas.*`, `camera.snap`, `camera.clip`,
|
||||
`screen.snapshot`, `screen.record`, `system.run`, and `system.notify`.
|
||||
- The node reports a `permissions` map so agents can see whether screen,
|
||||
camera, microphone, speech, automation, or accessibility access is available.
|
||||
|
||||
### Node service + app IPC
|
||||
|
||||
|
||||
@@ -1,87 +1,228 @@
|
||||
---
|
||||
summary: "Install and use the OpenClaw macOS menu bar app"
|
||||
summary: "OpenClaw macOS companion app (menu bar + gateway broker)"
|
||||
read_when:
|
||||
- Installing the macOS app
|
||||
- Deciding between local and remote Gateway mode on macOS
|
||||
- Looking for macOS app release downloads
|
||||
- Implementing macOS app features
|
||||
- Changing gateway lifecycle or node bridging on macOS
|
||||
title: "macOS app"
|
||||
---
|
||||
|
||||
The macOS app is the OpenClaw **menu bar companion**. Use it when you want a
|
||||
native tray UI, macOS permission prompts, notifications, WebChat, voice input,
|
||||
Canvas, or Mac-hosted node tools such as `system.run`.
|
||||
The macOS app is the **menu-bar companion** for OpenClaw. It owns permissions,
|
||||
manages/attaches to the Gateway locally (launchd or manual), and exposes macOS
|
||||
capabilities to the agent as a node.
|
||||
|
||||
If you only need the CLI and Gateway, start with [Getting started](/start/getting-started).
|
||||
## What it does
|
||||
|
||||
## Download
|
||||
- Shows native notifications and status in the menu bar.
|
||||
- Owns TCC prompts (Notifications, Accessibility, Screen Recording, Microphone,
|
||||
Speech Recognition, Automation/AppleScript).
|
||||
- Runs or connects to the Gateway (local or remote).
|
||||
- Exposes macOS-only tools (Canvas, Camera, Screen Recording, `system.run`).
|
||||
- Starts the local node host service in **remote** mode (launchd), and stops it in **local** mode.
|
||||
- Optionally hosts **PeekabooBridge** for UI automation.
|
||||
- Installs the global CLI (`openclaw`) on request via npm, pnpm, or bun (the app prefers npm, then pnpm, then bun; Node remains the recommended Gateway runtime).
|
||||
|
||||
Download macOS app builds from the
|
||||
[OpenClaw GitHub releases](https://github.com/openclaw/openclaw/releases).
|
||||
When a release includes macOS app assets, look for:
|
||||
## Local vs remote mode
|
||||
|
||||
- `OpenClaw-<version>.dmg` (preferred)
|
||||
- `OpenClaw-<version>.zip`
|
||||
- **Local** (default): the app attaches to a running local Gateway if present;
|
||||
otherwise it enables the launchd service via `openclaw gateway install`.
|
||||
- **Remote**: the app connects to a Gateway over SSH/Tailscale and never starts
|
||||
a local process.
|
||||
The app starts the local **node host service** so the remote Gateway can reach this Mac.
|
||||
The app does not spawn the Gateway as a child process.
|
||||
Gateway discovery now prefers Tailscale MagicDNS names over raw tailnet IPs,
|
||||
so the Mac app recovers more reliably when tailnet IPs change.
|
||||
|
||||
Some releases only include CLI, evidence, or Windows assets. If the newest
|
||||
release has no macOS app asset, use the newest release that does, or build the
|
||||
app from source with [macOS dev setup](/platforms/mac/dev-setup).
|
||||
## Launchd control
|
||||
|
||||
## First run
|
||||
The app manages a per-user LaunchAgent labeled `ai.openclaw.gateway`
|
||||
(or `ai.openclaw.<profile>` when using `--profile`/`OPENCLAW_PROFILE`; legacy `com.openclaw.*` still unloads).
|
||||
|
||||
```bash
|
||||
launchctl kickstart -k gui/$UID/ai.openclaw.gateway
|
||||
launchctl bootout gui/$UID/ai.openclaw.gateway
|
||||
```
|
||||
|
||||
Replace the label with `ai.openclaw.<profile>` when running a named profile.
|
||||
|
||||
If the LaunchAgent isn't installed, enable it from the app or run
|
||||
`openclaw gateway install`.
|
||||
|
||||
If the gateway repeatedly disappears for minutes to hours and only resumes when you touch the Control UI or SSH into the host, see the troubleshooting note for macOS Maintenance Sleep / `ENETDOWN` crashes and launchd's respawn-protection gate in [Gateway troubleshooting](/gateway/troubleshooting#macos-gateway-silently-stops-responding-then-resumes-when-you-touch-the-dashboard).
|
||||
|
||||
## Node capabilities (mac)
|
||||
|
||||
The macOS app presents itself as a node. Common commands:
|
||||
|
||||
- Canvas: `canvas.present`, `canvas.navigate`, `canvas.eval`, `canvas.snapshot`, `canvas.a2ui.*`
|
||||
- Camera: `camera.snap`, `camera.clip`
|
||||
- Screen: `screen.snapshot`, `screen.record`
|
||||
- System: `system.run`, `system.notify`
|
||||
|
||||
The node reports a `permissions` map so agents can decide what's allowed.
|
||||
|
||||
Node service + app IPC:
|
||||
|
||||
- When the headless node host service is running (remote mode), it connects to the Gateway WS as a node.
|
||||
- `system.run` executes in the macOS app (UI/TCC context) over a local Unix socket; prompts + output stay in-app.
|
||||
|
||||
Diagram (SCI):
|
||||
|
||||
```
|
||||
Gateway -> Node Service (WS)
|
||||
| IPC (UDS + token + HMAC + TTL)
|
||||
v
|
||||
Mac App (UI + TCC + system.run)
|
||||
```
|
||||
|
||||
## Exec approvals (system.run)
|
||||
|
||||
`system.run` is controlled by **Exec approvals** in the macOS app (Settings → Exec approvals).
|
||||
Security + ask + allowlist are stored locally on the Mac in:
|
||||
|
||||
```
|
||||
~/.openclaw/exec-approvals.json
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"defaults": {
|
||||
"security": "deny",
|
||||
"ask": "on-miss"
|
||||
},
|
||||
"agents": {
|
||||
"main": {
|
||||
"security": "allowlist",
|
||||
"ask": "on-miss",
|
||||
"allowlist": [{ "pattern": "/opt/homebrew/bin/rg" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `allowlist` entries are glob patterns for resolved binary paths, or bare command names for PATH-invoked commands.
|
||||
- Raw shell command text that contains shell control or expansion syntax (`&&`, `||`, `;`, `|`, `` ` ``, `$`, `<`, `>`, `(`, `)`) is treated as an allowlist miss and requires explicit approval (or allowlisting the shell binary).
|
||||
- Choosing "Always Allow" in the prompt adds that command to the allowlist.
|
||||
- `system.run` environment overrides are filtered (drops `PATH`, `DYLD_*`, `LD_*`, `BASHOPTS`, `FPATH`, `KSH_ENV`, `NODE_OPTIONS`, `NODE_REDIRECT_WARNINGS`, `NODE_REPL_EXTERNAL_MODULE`, `NODE_REPL_HISTORY`, `NODE_V8_COVERAGE`, `PYTHON*`, `PERL*`, `RUBYOPT`, `SHELLOPTS`, `PS4`, `TCLLIBPATH`) and then merged with the app's environment.
|
||||
- For shell wrappers (`bash|sh|zsh ... -c/-lc`), request-scoped environment overrides are reduced to a small explicit allowlist (`TERM`, `LANG`, `LC_*`, `COLORTERM`, `NO_COLOR`, `FORCE_COLOR`).
|
||||
- For allow-always decisions in allowlist mode, known dispatch wrappers (`env`, `flock`, `nice`, `nohup`, `stdbuf`, `timeout`) persist inner executable paths instead of wrapper paths. If unwrapping is not safe, no allowlist entry is persisted automatically.
|
||||
|
||||
## Deep links
|
||||
|
||||
The app registers the `openclaw://` URL scheme for local actions.
|
||||
|
||||
### `openclaw://agent`
|
||||
|
||||
Triggers a Gateway `agent` request.
|
||||
|
||||
```bash
|
||||
open 'openclaw://agent?message=Hello%20from%20deep%20link'
|
||||
```
|
||||
|
||||
Query parameters:
|
||||
|
||||
- `message` (required)
|
||||
- `sessionKey` (optional)
|
||||
- `thinking` (optional)
|
||||
- `deliver` / `to` / `channel` (optional)
|
||||
- `timeoutSeconds` (optional)
|
||||
- `key` (optional unattended mode key)
|
||||
|
||||
Safety:
|
||||
|
||||
- Without `key`, the app prompts for confirmation.
|
||||
- Without `key`, the app enforces a short message limit for the confirmation prompt and ignores `deliver` / `to` / `channel`.
|
||||
- With a valid `key`, the run is unattended (intended for personal automations).
|
||||
|
||||
## Onboarding flow (typical)
|
||||
|
||||
1. Install and launch **OpenClaw.app**.
|
||||
2. Complete the macOS permission checklist.
|
||||
3. Pick **Local** or **Remote** mode.
|
||||
4. Install the `openclaw` CLI if the app asks for it.
|
||||
5. Open WebChat from the menu bar and send a test message.
|
||||
2. Complete the permissions checklist (TCC prompts).
|
||||
3. Ensure **Local** mode is active and the Gateway is running.
|
||||
4. Install the CLI if you want terminal access.
|
||||
|
||||
For the CLI/Gateway setup path, use [Getting started](/start/getting-started).
|
||||
For permission recovery, use [macOS permissions](/platforms/mac/permissions).
|
||||
## State dir placement (macOS)
|
||||
|
||||
## Choose a Gateway mode
|
||||
Avoid putting your OpenClaw state dir in iCloud or other cloud-synced folders.
|
||||
Sync-backed paths can add latency and occasionally cause file-lock/sync races for
|
||||
sessions and credentials.
|
||||
|
||||
| Mode | Use it when | Detail page |
|
||||
| ------ | --------------------------------------------------------------------------------------- | -------------------------------------------------- |
|
||||
| Local | This Mac should run the Gateway and keep it alive with launchd. | [Gateway on macOS](/platforms/mac/bundled-gateway) |
|
||||
| Remote | Another host runs the Gateway and this Mac should control it over SSH, LAN, or Tailnet. | [Remote control](/platforms/mac/remote) |
|
||||
Prefer a local non-synced state path such as:
|
||||
|
||||
Local mode requires an installed `openclaw` CLI. The app can install it, or you
|
||||
can follow [Gateway on macOS](/platforms/mac/bundled-gateway).
|
||||
```bash
|
||||
OPENCLAW_STATE_DIR=~/.openclaw
|
||||
```
|
||||
|
||||
## What the app owns
|
||||
If `openclaw doctor` detects state under:
|
||||
|
||||
- Menu bar status, notifications, health, and WebChat.
|
||||
- macOS permission prompts for screen, microphone, speech, automation, and accessibility.
|
||||
- Local node tools such as Canvas, camera/screen capture, notifications, and `system.run`.
|
||||
- Exec approval prompts for Mac-hosted commands.
|
||||
- Remote-mode SSH tunnels or direct Gateway connections.
|
||||
- `~/Library/Mobile Documents/com~apple~CloudDocs/...`
|
||||
- `~/Library/CloudStorage/...`
|
||||
|
||||
The app does **not** replace the OpenClaw Gateway or general CLI docs. Core
|
||||
Gateway configuration, providers, plugins, channels, tools, and security live in
|
||||
their own docs.
|
||||
it will warn and recommend moving back to a local path.
|
||||
|
||||
## macOS detail pages
|
||||
## Build and dev workflow (native)
|
||||
|
||||
| Task | Read |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| Install or debug the CLI/Gateway service | [Gateway on macOS](/platforms/mac/bundled-gateway) |
|
||||
| Keep state out of cloud-synced folders | [Gateway on macOS](/platforms/mac/bundled-gateway#state-directory-on-macos) |
|
||||
| Debug app discovery and connectivity | [Gateway on macOS](/platforms/mac/bundled-gateway#debug-app-connectivity) |
|
||||
| Understand launchd behavior | [Gateway lifecycle](/platforms/mac/child-process) |
|
||||
| Fix permissions or signing/TCC issues | [macOS permissions](/platforms/mac/permissions) |
|
||||
| Connect to a remote Gateway | [Remote control](/platforms/mac/remote) |
|
||||
| Read menu bar status and health checks | [Menu bar](/platforms/mac/menu-bar), [Health checks](/platforms/mac/health) |
|
||||
| Use the embedded chat UI | [WebChat](/platforms/mac/webchat) |
|
||||
| Use voice wake or push-to-talk | [Voice wake](/platforms/mac/voicewake) |
|
||||
| Use Canvas and Canvas deep links | [Canvas](/platforms/mac/canvas) |
|
||||
| Host PeekabooBridge for UI automation | [Peekaboo bridge](/platforms/mac/peekaboo) |
|
||||
| Configure command approvals | [Exec approvals](/tools/exec-approvals), [advanced details](/tools/exec-approvals-advanced) |
|
||||
| Inspect Mac node commands and app IPC | [macOS IPC](/platforms/mac/xpc) |
|
||||
| Capture logs | [macOS logging](/platforms/mac/logging) |
|
||||
| Build from source | [macOS dev setup](/platforms/mac/dev-setup) |
|
||||
- `cd apps/macos && swift build`
|
||||
- `swift run OpenClaw` (or Xcode)
|
||||
- Package app: `scripts/package-mac-app.sh`
|
||||
|
||||
## Related
|
||||
## Debug gateway connectivity (macOS CLI)
|
||||
|
||||
- [Platforms](/platforms)
|
||||
- [Getting started](/start/getting-started)
|
||||
- [Gateway](/gateway)
|
||||
- [Exec approvals](/tools/exec-approvals)
|
||||
Use the debug CLI to exercise the same Gateway WebSocket handshake and discovery
|
||||
logic that the macOS app uses, without launching the app.
|
||||
|
||||
```bash
|
||||
cd apps/macos
|
||||
swift run openclaw-mac connect --json
|
||||
swift run openclaw-mac discover --timeout 3000 --json
|
||||
```
|
||||
|
||||
Connect options:
|
||||
|
||||
- `--url <ws://host:port>`: override config
|
||||
- `--mode <local|remote>`: resolve from config (default: config or local)
|
||||
- `--probe`: force a fresh health probe
|
||||
- `--timeout <ms>`: request timeout (default: `15000`)
|
||||
- `--json`: structured output for diffing
|
||||
|
||||
Discovery options:
|
||||
|
||||
- `--include-local`: include gateways that would be filtered as "local"
|
||||
- `--timeout <ms>`: overall discovery window (default: `2000`)
|
||||
- `--json`: structured output for diffing
|
||||
|
||||
<Tip>
|
||||
Compare against `openclaw gateway discover --json` to see whether the macOS app's discovery pipeline (`local.` plus the configured wide-area domain, with wide-area and Tailscale Serve fallbacks) differs from the Node CLI's `dns-sd` based discovery.
|
||||
</Tip>
|
||||
|
||||
## Remote connection plumbing (SSH tunnels)
|
||||
|
||||
When the macOS app runs in **Remote** mode, it opens an SSH tunnel so local UI
|
||||
components can talk to a remote Gateway as if it were on localhost.
|
||||
|
||||
### Control tunnel (Gateway WebSocket port)
|
||||
|
||||
- **Purpose:** health checks, status, Web Chat, config, and other control-plane calls.
|
||||
- **Local port:** the Gateway port (default `18789`), always stable.
|
||||
- **Remote port:** the same Gateway port on the remote host.
|
||||
- **Behavior:** no random local port; the app reuses an existing healthy tunnel
|
||||
or restarts it if needed.
|
||||
- **SSH shape:** `ssh -N -L <local>:127.0.0.1:<remote>` with BatchMode +
|
||||
ExitOnForwardFailure + keepalive options.
|
||||
- **IP reporting:** the SSH tunnel uses loopback, so the gateway will see the node
|
||||
IP as `127.0.0.1`. Use **Direct (ws/wss)** transport if you want the real client
|
||||
IP to appear (see [macOS remote access](/platforms/mac/remote)).
|
||||
|
||||
For setup steps, see [macOS remote access](/platforms/mac/remote). For protocol
|
||||
details, see [Gateway protocol](/gateway/protocol).
|
||||
|
||||
## Related docs
|
||||
|
||||
- [Gateway runbook](/gateway)
|
||||
- [Gateway (macOS)](/platforms/mac/bundled-gateway)
|
||||
- [macOS permissions](/platforms/mac/permissions)
|
||||
- [Canvas](/platforms/mac/canvas)
|
||||
|
||||
@@ -71,11 +71,6 @@ OpenProse registers `/prose` as a user-invocable skill command:
|
||||
`/prose run <handle/slug>` resolves to `https://p.prose.md/<handle>/<slug>`.
|
||||
Direct URLs are fetched as-is using the `web_fetch` tool.
|
||||
|
||||
Top-level remote runs are explicit. Remote imports inside a `.prose` program are
|
||||
transitive code dependencies: before OpenProse fetches any remote `use` target,
|
||||
it shows the resolved import list and requires the operator to reply exactly
|
||||
`approve remote prose imports` for that run.
|
||||
|
||||
## What it can do
|
||||
|
||||
- Multi-agent research and synthesis with explicit parallelism.
|
||||
@@ -172,12 +167,9 @@ User-level persistent agents live at:
|
||||
|
||||
## Security
|
||||
|
||||
Treat `.prose` files like code. Review them before running, including remote
|
||||
`use` imports. Top-level `/prose run https://...` requests are explicit, but
|
||||
transitive remote imports require per-run approval before they are fetched or
|
||||
executed. Use OpenClaw tool allowlists and approval gates to control side
|
||||
effects. For deterministic, approval-gated workflows, compare with
|
||||
[Lobster](/tools/lobster).
|
||||
Treat `.prose` files like code. Review them before running. Use OpenClaw tool
|
||||
allowlists and approval gates to control side effects. For deterministic,
|
||||
approval-gated workflows, compare with [Lobster](/tools/lobster).
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -81,7 +81,6 @@ Session persistence has automatic maintenance controls (`session.maintenance`) f
|
||||
- `mode`: `enforce` (default) or `warn`
|
||||
- `pruneAfter`: stale-entry age cutoff (default `30d`)
|
||||
- `maxEntries`: cap entries in `sessions.json` (default `500`)
|
||||
- Short-lived gateway model-run probe retention is fixed at `24h`, but it is pressure-gated: it only removes stale strict probe rows when session-entry maintenance/cap pressure is reached. This applies only to strict explicit probe keys matching `agent:*:explicit:model-run-<uuid>` and runs before global stale-entry cleanup/capping when it runs.
|
||||
- `resetArchiveRetention`: retention for `*.reset.<timestamp>` transcript archives (default: same as `pruneAfter`; `false` disables cleanup)
|
||||
- `maxDiskBytes`: optional sessions-directory budget
|
||||
- `highWaterBytes`: optional target after cleanup (default `80%` of `maxDiskBytes`)
|
||||
@@ -91,12 +90,7 @@ Normal Gateway writes flow through a per-store session writer that serializes in
|
||||
Maintenance keeps durable external conversation pointers such as group sessions
|
||||
and thread-scoped chat sessions, but synthetic runtime entries for cron, hooks,
|
||||
heartbeat, ACP, and sub-agents can still be removed when they exceed the
|
||||
configured age, count, or disk budget. Gateway model-run probe sessions use the
|
||||
separate `24h` model-run retention only when their key exactly matches
|
||||
`agent:*:explicit:model-run-<uuid>`; other explicit sessions are not part of
|
||||
that retention. The model-run cleanup is applied only under session-entry cap
|
||||
pressure. Isolated cron runs keep their own `cron.sessionRetention` control,
|
||||
independent of model-run probe retention.
|
||||
configured age, count, or disk budget.
|
||||
|
||||
OpenClaw no longer creates automatic `sessions.json.bak.*` rotation backups during Gateway writes. The legacy `session.maintenance.rotateBytes` key is ignored and `openclaw doctor --fix` removes it from older configs.
|
||||
|
||||
|
||||
@@ -76,8 +76,6 @@ Use these in chat:
|
||||
configured for the active model.
|
||||
- `/usage off|tokens|full` → appends a **per-response usage footer** to every reply.
|
||||
- Persists per session (stored as `responseUsage`).
|
||||
- `/usage reset` (aliases: `inherit`, `clear`, `default`) — clears the session
|
||||
override so the session re-inherits the configured default.
|
||||
- `/usage full` shows estimated cost only when OpenClaw has usage metadata and
|
||||
local pricing for the active model. Otherwise it shows tokens only.
|
||||
- `/usage cost` → shows a local cost summary from OpenClaw session logs.
|
||||
|
||||
@@ -240,7 +240,7 @@ plugins.
|
||||
| `/tasks` | List active/recent background tasks for the current session |
|
||||
| `/context [list\|detail\|map\|json]` | Explain how context is assembled |
|
||||
| `/whoami` | Show your sender id. Alias: `/id` |
|
||||
| `/usage off\|tokens\|full\|reset\|cost` | Control the per-response usage footer (`reset`/`inherit`/`clear`/`default` clears the session override to re-inherit the configured default) or print a local cost summary |
|
||||
| `/usage off\|tokens\|full\|cost` | Control the per-response usage footer or print a local cost summary |
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Skills, allowlists, approvals">
|
||||
|
||||
@@ -126,7 +126,7 @@ Session controls:
|
||||
- `/verbose <on|full|off>`
|
||||
- `/trace <on|off>`
|
||||
- `/reasoning <on|off|stream>`
|
||||
- `/usage <off|tokens|full|reset>` (`reset`/`inherit`/`clear`/`default` clears the session override)
|
||||
- `/usage <off|tokens|full>`
|
||||
- `/goal [status] | /goal start <objective> | /goal pause|resume|complete|block|clear`
|
||||
- `/elevated <on|off|ask|full>` (alias: `/elev`)
|
||||
- `/activation <mention|always>`
|
||||
|
||||
@@ -11,7 +11,11 @@ import type {
|
||||
PluginHookInboundClaimEvent,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-payload";
|
||||
import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
resolveStorePath,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { resolveCodexAppServerForModelProvider } from "./app-server/app-server-policy.js";
|
||||
import { resolveCodexAppServerAuthProfileIdForAgent } from "./app-server/auth-bridge.js";
|
||||
import { CODEX_CONTROL_METHODS } from "./app-server/capabilities.js";
|
||||
@@ -877,11 +881,10 @@ function readSessionExecOverrides(params: {
|
||||
return undefined;
|
||||
}
|
||||
const storePath = resolveStorePath(params.config.session?.store, { agentId: params.agentId });
|
||||
const entry = getSessionEntry({
|
||||
storePath,
|
||||
const entry = resolveSessionStoreEntry({
|
||||
store: loadSessionStore(storePath, { skipCache: true }),
|
||||
sessionKey,
|
||||
readConsistency: "latest",
|
||||
});
|
||||
}).existing;
|
||||
if (!entry?.execSecurity && !entry?.execAsk) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -175,7 +175,6 @@ type DispatchInboundParams = {
|
||||
}) => Promise<void> | void;
|
||||
onReplyStart?: () => Promise<void> | void;
|
||||
sourceReplyDeliveryMode?: "automatic" | "message_tool_only";
|
||||
typingKeepalive?: boolean;
|
||||
disableBlockStreaming?: boolean;
|
||||
suppressDefaultToolProgressMessages?: boolean;
|
||||
queuedDeliveryCorrelations?: Array<{ begin: () => () => void }>;
|
||||
@@ -945,7 +944,6 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
expect(replyTypingFeedback.onReplyStart).toHaveBeenCalledTimes(1);
|
||||
expect(replyTypingFeedback.onIdle).toHaveBeenCalledTimes(1);
|
||||
expect(replyTypingFeedback.onCleanup).toHaveBeenCalledTimes(1);
|
||||
expect(getLastDispatchReplyOptions()?.typingKeepalive).toBe(false);
|
||||
expect(typingMocks.sendTyping).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -986,33 +984,6 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps one typing refresh loop for default message-tool replies", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onReplyStart?.();
|
||||
await vi.advanceTimersByTimeAsync(3_500);
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
const ctx = await createBaseContext({
|
||||
shouldRequireMention: false,
|
||||
effectiveWasMentioned: false,
|
||||
cfg: {
|
||||
messages: { groupChat: { visibleReplies: "message_tool" } },
|
||||
session: { store: "/tmp/openclaw-discord-process-test-sessions.json" },
|
||||
},
|
||||
route: BASE_CHANNEL_ROUTE,
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(getLastDispatchReplyOptions()?.typingKeepalive).toBe(false);
|
||||
expect(typingMocks.sendTyping).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("debounces intermediate phase reactions and jumps to done for short runs", async () => {
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onReasoningStream?.();
|
||||
@@ -1561,7 +1532,6 @@ describe("processDiscordMessage session routing", () => {
|
||||
|
||||
expectRecordFields(requireRecord(getLastDispatchReplyOptions(), "dispatch reply options"), {
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
typingKeepalive: false,
|
||||
disableBlockStreaming: true,
|
||||
});
|
||||
expect(createDiscordDraftStream).not.toHaveBeenCalled();
|
||||
|
||||
@@ -251,14 +251,6 @@ async function processDiscordMessageInner(
|
||||
},
|
||||
});
|
||||
const sourceRepliesAreToolOnly = sourceReplyDeliveryMode === "message_tool_only";
|
||||
const configuredTypingMode = cfg.session?.typingMode ?? cfg.agents?.defaults?.typingMode;
|
||||
const configuredTypingInterval =
|
||||
cfg.agents?.defaults?.typingIntervalSeconds ?? cfg.session?.typingIntervalSeconds;
|
||||
const shouldDisableCoreTypingKeepalive =
|
||||
Boolean(replyTypingFeedback) ||
|
||||
(sourceRepliesAreToolOnly &&
|
||||
configuredTypingMode === undefined &&
|
||||
configuredTypingInterval === undefined);
|
||||
const ackReaction = resolveAckReaction(cfg, route.agentId, {
|
||||
channel: "discord",
|
||||
accountId,
|
||||
@@ -468,7 +460,6 @@ async function processDiscordMessageInner(
|
||||
channelId: typingChannelId,
|
||||
rest: feedbackRest,
|
||||
log: logVerbose,
|
||||
keepaliveIntervalMs: shouldDisableCoreTypingKeepalive ? undefined : 0,
|
||||
});
|
||||
if (replyTypingFeedback) {
|
||||
// A carried prestart only covers queue wait time; dispatch needs a fresh
|
||||
@@ -964,7 +955,6 @@ async function processDiscordMessageInner(
|
||||
abortSignal,
|
||||
skillFilter: channelConfig?.skills,
|
||||
sourceReplyDeliveryMode,
|
||||
typingKeepalive: shouldDisableCoreTypingKeepalive ? false : undefined,
|
||||
queuedDeliveryCorrelations: isRoomEvent ? [{ begin: beginDeliveryCorrelation }] : undefined,
|
||||
suppressTyping: isRoomEvent ? true : undefined,
|
||||
allowProgressCallbacksWhenSourceDeliverySuppressed:
|
||||
|
||||
@@ -222,34 +222,6 @@ describe("createDiscordMessageHandler queue behavior", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the configured typing cadence for prestarted feedback", async () => {
|
||||
preflightDiscordMessageMock.mockReset();
|
||||
processDiscordMessageMock.mockReset();
|
||||
preflightDiscordMessageMock.mockImplementation(async () =>
|
||||
createAcceptedDmPreflightContext({
|
||||
cfg: {
|
||||
...createPreflightContext().cfg,
|
||||
agents: { defaults: { typingIntervalSeconds: 7 } },
|
||||
session: { typingIntervalSeconds: 5 },
|
||||
},
|
||||
}),
|
||||
);
|
||||
processDiscordMessageMock.mockResolvedValue(undefined);
|
||||
const replyTypingFeedback = createReplyTypingFeedbackMock("dm-1");
|
||||
const createReplyTypingFeedback = vi.fn(() => replyTypingFeedback);
|
||||
|
||||
const handler = createDiscordMessageHandler({
|
||||
...createDiscordHandlerParams(),
|
||||
testing: { createReplyTypingFeedback },
|
||||
});
|
||||
await handler(createMessageData("m-typing-cadence", "dm-1") as never, {} as never);
|
||||
await flushQueueWork();
|
||||
|
||||
expect(createReplyTypingFeedback).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ keepaliveIntervalMs: 7_000 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps accepted DM dispatch running when accepted typing feedback fails", async () => {
|
||||
preflightDiscordMessageMock.mockReset();
|
||||
processDiscordMessageMock.mockReset();
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
createChannelInboundDebouncer,
|
||||
shouldDebounceTextInbound,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/runtime-group-policy";
|
||||
import type { Client } from "../internal/discord.js";
|
||||
@@ -103,9 +102,6 @@ function startAcceptedTypingFeedback(params: {
|
||||
accountId: ctx.accountId,
|
||||
channelId: ctx.messageChannelId,
|
||||
log: logVerbose,
|
||||
keepaliveIntervalMs: finiteSecondsToTimerSafeMilliseconds(
|
||||
ctx.cfg.agents?.defaults?.typingIntervalSeconds ?? ctx.cfg.session?.typingIntervalSeconds,
|
||||
),
|
||||
});
|
||||
const cleanup = replyTypingFeedback.onCleanup;
|
||||
replyTypingFeedback.onCleanup = () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/command-auth-native";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
@@ -202,10 +202,11 @@ export async function resolveDiscordNativeChoiceContext(params: {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: route.agentId,
|
||||
});
|
||||
const sessionEntry = getSessionEntry({ storePath, sessionKey: route.sessionKey });
|
||||
const sessionStore = loadSessionStore(storePath);
|
||||
const sessionEntry = sessionStore[route.sessionKey];
|
||||
const override = resolveStoredModelOverride({
|
||||
sessionEntry,
|
||||
loadSessionEntry: (sessionKey) => getSessionEntry({ storePath, sessionKey }),
|
||||
sessionStore,
|
||||
sessionKey: route.sessionKey,
|
||||
defaultProvider: fallback.provider,
|
||||
});
|
||||
@@ -237,15 +238,11 @@ export function resolveDiscordModelPickerCurrentModel(params: {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: params.route.agentId,
|
||||
});
|
||||
const sessionEntry = getSessionEntry({
|
||||
storePath,
|
||||
sessionKey: params.route.sessionKey,
|
||||
readConsistency: "latest",
|
||||
});
|
||||
const sessionStore = loadSessionStore(storePath, { skipCache: true });
|
||||
const sessionEntry = sessionStore[params.route.sessionKey];
|
||||
const override = resolveStoredModelOverride({
|
||||
sessionEntry,
|
||||
loadSessionEntry: (sessionKey) =>
|
||||
getSessionEntry({ storePath, sessionKey, readConsistency: "latest" }),
|
||||
sessionStore,
|
||||
sessionKey: params.route.sessionKey,
|
||||
defaultProvider: params.data.resolvedDefault.provider,
|
||||
});
|
||||
@@ -270,12 +267,9 @@ export function resolveDiscordModelPickerCurrentRuntime(params: {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: params.route.agentId,
|
||||
});
|
||||
const sessionStore = loadSessionStore(storePath, { skipCache: true });
|
||||
const sessionRuntime = normalizeOptionalString(
|
||||
getSessionEntry({
|
||||
storePath,
|
||||
sessionKey: params.route.sessionKey,
|
||||
readConsistency: "latest",
|
||||
})?.agentRuntimeOverride,
|
||||
sessionStore[params.route.sessionKey]?.agentRuntimeOverride,
|
||||
);
|
||||
if (sessionRuntime) {
|
||||
return sessionRuntime;
|
||||
|
||||
@@ -24,7 +24,6 @@ export function createDiscordReplyTypingFeedback(params: {
|
||||
rest?: RequestClient;
|
||||
log: (message: string) => void;
|
||||
maxDurationMs?: number;
|
||||
keepaliveIntervalMs?: number;
|
||||
}): DiscordReplyTypingFeedback {
|
||||
let channelId = params.channelId;
|
||||
const rest =
|
||||
@@ -45,7 +44,6 @@ export function createDiscordReplyTypingFeedback(params: {
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
keepaliveIntervalMs: params.keepaliveIntervalMs,
|
||||
maxDurationMs: params.maxDurationMs ?? DISCORD_REPLY_TYPING_MAX_DURATION_MS,
|
||||
});
|
||||
const updateChannelId = (nextChannelId: string) => {
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
wrapWebContent,
|
||||
writeCachedSearchPayload,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
@@ -31,10 +30,6 @@ const EXA_SEARCH_TYPES = ["auto", "neural", "fast", "deep", "deep-reasoning", "i
|
||||
const EXA_FRESHNESS_VALUES = ["day", "week", "month", "year"] as const;
|
||||
const EXA_MAX_SEARCH_COUNT = 100;
|
||||
const EXA_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
||||
// Exa search responses are untrusted external bodies. Cap the success JSON the
|
||||
// same way other bundled providers do (16 MiB) so a misbehaving or hostile
|
||||
// endpoint cannot stream an unbounded body into memory before we parse it.
|
||||
const EXA_SEARCH_JSON_MAX_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
type ExaConfig = {
|
||||
apiKey?: string;
|
||||
@@ -75,17 +70,9 @@ type ExaSearchResponse = {
|
||||
results?: unknown;
|
||||
};
|
||||
|
||||
async function readExaSearchResults(
|
||||
response: Response,
|
||||
opts?: { maxBytes?: number },
|
||||
): Promise<ExaSearchResult[]> {
|
||||
const maxBytes = opts?.maxBytes ?? EXA_SEARCH_JSON_MAX_BYTES;
|
||||
const bytes = await readResponseWithLimit(response, maxBytes, {
|
||||
onOverflow: ({ maxBytes: maxBytesLocal }) =>
|
||||
new Error(`Exa API response exceeds ${maxBytesLocal} bytes`),
|
||||
});
|
||||
async function readExaSearchResults(response: Response): Promise<ExaSearchResult[]> {
|
||||
try {
|
||||
return normalizeExaResults(JSON.parse(new TextDecoder().decode(bytes)));
|
||||
return normalizeExaResults(await response.json());
|
||||
} catch (cause) {
|
||||
throw new Error("Exa API returned malformed JSON", { cause });
|
||||
}
|
||||
|
||||
@@ -26,33 +26,6 @@ function cancelTrackedResponse(
|
||||
};
|
||||
}
|
||||
|
||||
function streamingJsonResponse(params: { chunkCount: number; chunkSize: number }): {
|
||||
response: Response;
|
||||
getReadCount: () => number;
|
||||
} {
|
||||
// Streaming fixture proves an oversized success body stops being read before
|
||||
// the whole payload is buffered into memory.
|
||||
let reads = 0;
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
if (reads >= params.chunkCount) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
reads += 1;
|
||||
controller.enqueue(encoder.encode("a".repeat(params.chunkSize)));
|
||||
},
|
||||
});
|
||||
return {
|
||||
response: new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
getReadCount: () => reads,
|
||||
};
|
||||
}
|
||||
|
||||
describe("exa web search provider", () => {
|
||||
it("exposes the expected metadata and selection wiring", () => {
|
||||
const provider = createExaWebSearchProvider();
|
||||
@@ -292,27 +265,6 @@ describe("exa web search provider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("parses well-formed Exa search JSON under the byte cap", async () => {
|
||||
const response = new Response(
|
||||
JSON.stringify({ results: [{ url: "https://example.com", title: "Example" }] }),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
|
||||
await expect(testing.readExaSearchResults(response)).resolves.toEqual([
|
||||
{ url: "https://example.com", title: "Example" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("caps oversized Exa search JSON instead of buffering the whole body", async () => {
|
||||
const streamed = streamingJsonResponse({ chunkCount: 64, chunkSize: 1024 });
|
||||
|
||||
await expect(
|
||||
testing.readExaSearchResults(streamed.response, { maxBytes: 4096 }),
|
||||
).rejects.toThrow(/Exa API response exceeds 4096 bytes/);
|
||||
|
||||
expect(streamed.getReadCount()).toBeLessThan(64);
|
||||
});
|
||||
|
||||
it("bounds Exa API error bodies without using response.text()", async () => {
|
||||
const tracked = cancelTrackedResponse(`${"exa upstream unavailable ".repeat(1024)}tail`, {
|
||||
status: 503,
|
||||
|
||||
@@ -43,7 +43,10 @@ export {
|
||||
filterSupplementalContextItems,
|
||||
resolveChannelContextVisibilityMode,
|
||||
} from "openclaw/plugin-sdk/context-visibility-runtime";
|
||||
export { getSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
export {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
export { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
||||
export { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
export { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
|
||||
|
||||
@@ -10,4 +10,4 @@ export {
|
||||
filterSupplementalContextItems,
|
||||
normalizeAgentId,
|
||||
} from "../runtime-api.js";
|
||||
export { getSessionEntry } from "../runtime-api.js";
|
||||
export { loadSessionStore, resolveSessionStoreEntry } from "../runtime-api.js";
|
||||
|
||||
@@ -3,8 +3,8 @@ import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ClawdbotConfig } from "./bot-runtime-api.js";
|
||||
import { resolveFeishuReasoningPreviewEnabled } from "./reasoning-preview.js";
|
||||
|
||||
const { getSessionEntryMock } = vi.hoisted(() => ({
|
||||
getSessionEntryMock: vi.fn(),
|
||||
const { loadSessionStoreMock } = vi.hoisted(() => ({
|
||||
loadSessionStoreMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./bot-runtime-api.js", async () => {
|
||||
@@ -12,7 +12,7 @@ vi.mock("./bot-runtime-api.js", async () => {
|
||||
await vi.importActual<typeof import("./bot-runtime-api.js")>("./bot-runtime-api.js");
|
||||
return {
|
||||
...actual,
|
||||
getSessionEntry: getSessionEntryMock,
|
||||
loadSessionStore: loadSessionStoreMock,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -29,12 +29,9 @@ describe("resolveFeishuReasoningPreviewEnabled", () => {
|
||||
});
|
||||
|
||||
it("enables previews only for stream reasoning sessions", () => {
|
||||
getSessionEntryMock.mockImplementation(({ sessionKey }) => {
|
||||
const entries = {
|
||||
"agent:main:feishu:dm:ou_sender_1": { reasoningLevel: "stream" },
|
||||
"agent:main:feishu:dm:ou_sender_2": { reasoningLevel: "on" },
|
||||
};
|
||||
return entries[sessionKey as keyof typeof entries];
|
||||
loadSessionStoreMock.mockReturnValue({
|
||||
"agent:main:feishu:dm:ou_sender_1": { reasoningLevel: "stream" },
|
||||
"agent:main:feishu:dm:ou_sender_2": { reasoningLevel: "on" },
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -53,15 +50,10 @@ describe("resolveFeishuReasoningPreviewEnabled", () => {
|
||||
sessionKey: "agent:main:feishu:dm:ou_sender_2",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(getSessionEntryMock).toHaveBeenCalledWith({
|
||||
storePath: "/tmp/feishu-sessions.json",
|
||||
sessionKey: "agent:main:feishu:dm:ou_sender_1",
|
||||
readConsistency: "latest",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns false for missing sessions or load failures", () => {
|
||||
getSessionEntryMock.mockImplementationOnce(() => {
|
||||
loadSessionStoreMock.mockImplementationOnce(() => {
|
||||
throw new Error("disk unavailable");
|
||||
});
|
||||
|
||||
@@ -83,12 +75,9 @@ describe("resolveFeishuReasoningPreviewEnabled", () => {
|
||||
});
|
||||
|
||||
it("falls back to configured stream defaults", () => {
|
||||
getSessionEntryMock.mockImplementation(({ sessionKey }) => {
|
||||
const entries = {
|
||||
"agent:main:feishu:dm:ou_sender_1": {},
|
||||
"agent:main:feishu:dm:ou_sender_2": { reasoningLevel: "off" },
|
||||
};
|
||||
return entries[sessionKey as keyof typeof entries];
|
||||
loadSessionStoreMock.mockReturnValue({
|
||||
"agent:main:feishu:dm:ou_sender_1": {},
|
||||
"agent:main:feishu:dm:ou_sender_2": { reasoningLevel: "off" },
|
||||
});
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Feishu plugin module implements reasoning preview behavior.
|
||||
import { resolveFeishuConfigReasoningDefault } from "./agent-config.js";
|
||||
import { getSessionEntry } from "./bot-runtime-api.js";
|
||||
import { loadSessionStore, resolveSessionStoreEntry } from "./bot-runtime-api.js";
|
||||
import type { ClawdbotConfig } from "./bot-runtime-api.js";
|
||||
|
||||
export function resolveFeishuReasoningPreviewEnabled(params: {
|
||||
@@ -16,11 +16,9 @@ export function resolveFeishuReasoningPreviewEnabled(params: {
|
||||
}
|
||||
|
||||
try {
|
||||
const level = getSessionEntry({
|
||||
storePath: params.storePath,
|
||||
sessionKey: params.sessionKey,
|
||||
readConsistency: "latest",
|
||||
})?.reasoningLevel;
|
||||
const store = loadSessionStore(params.storePath, { skipCache: true });
|
||||
const level = resolveSessionStoreEntry({ store, sessionKey: params.sessionKey }).existing
|
||||
?.reasoningLevel;
|
||||
if (level === "on" || level === "stream" || level === "off") {
|
||||
return level === "stream";
|
||||
}
|
||||
|
||||
@@ -40,7 +40,10 @@ import {
|
||||
import type { GetReplyOptions } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { getSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type {
|
||||
CoreConfig,
|
||||
@@ -344,11 +347,12 @@ function resolveMatrixSharedDmContextNotice(params: {
|
||||
}
|
||||
|
||||
try {
|
||||
const store = loadSessionStore(params.storePath);
|
||||
const currentSession = resolveMatrixStoredSessionMeta(
|
||||
getSessionEntry({
|
||||
storePath: params.storePath,
|
||||
resolveSessionStoreEntry({
|
||||
store,
|
||||
sessionKey: params.sessionKey,
|
||||
}),
|
||||
}).existing,
|
||||
);
|
||||
if (!currentSession) {
|
||||
return null;
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
type ChannelOutboundSessionRouteParams,
|
||||
} from "openclaw/plugin-sdk/channel-core";
|
||||
import { parseThreadSessionSuffix } from "openclaw/plugin-sdk/routing";
|
||||
import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
resolveStorePath,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { resolveMatrixAccountConfig } from "./matrix/account-config.js";
|
||||
import { resolveDefaultMatrixAccountId } from "./matrix/accounts.js";
|
||||
import { resolveMatrixStoredSessionMeta } from "./matrix/session-store-metadata.js";
|
||||
@@ -47,10 +51,11 @@ function resolveMatrixCurrentDmRoomId(params: {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const existing = getSessionEntry({
|
||||
storePath,
|
||||
const store = loadSessionStore(storePath);
|
||||
const existing = resolveSessionStoreEntry({
|
||||
store,
|
||||
sessionKey,
|
||||
});
|
||||
}).existing;
|
||||
const currentSession = resolveMatrixStoredSessionMeta(existing);
|
||||
if (!currentSession) {
|
||||
return undefined;
|
||||
|
||||
@@ -46,7 +46,7 @@ export {
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
} from "openclaw/plugin-sdk/runtime-group-policy";
|
||||
export { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
||||
export { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
export { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
export { formatInboundFromLabel } from "openclaw/plugin-sdk/channel-inbound";
|
||||
export { logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
|
||||
export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
||||
|
||||
@@ -214,66 +214,4 @@ describe("Mattermost model picker", () => {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves current and parent model overrides from targeted session entries", () => {
|
||||
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "mm-model-picker-"));
|
||||
try {
|
||||
const storePath = path.join(testDir, "{agentId}.json");
|
||||
const supportStorePath = path.join(testDir, "support.json");
|
||||
const parentSessionKey = "agent:support:mattermost:default:channel-1";
|
||||
const childSessionKey = "agent:support:mattermost:default:child-with-explicit-parent";
|
||||
const directSessionKey = "agent:support:mattermost:default:direct-1";
|
||||
fs.writeFileSync(
|
||||
supportStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
[parentSessionKey]: {
|
||||
providerOverride: "anthropic",
|
||||
modelOverride: "claude-sonnet-4-5",
|
||||
sessionId: "parent-session",
|
||||
},
|
||||
[childSessionKey]: {
|
||||
parentSessionKey,
|
||||
sessionId: "child-session",
|
||||
},
|
||||
[directSessionKey]: {
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-5",
|
||||
sessionId: "direct-session",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
const cfg: OpenClawConfig = {
|
||||
session: {
|
||||
store: storePath,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveMattermostModelPickerCurrentModel({
|
||||
cfg,
|
||||
route: {
|
||||
agentId: "support",
|
||||
sessionKey: directSessionKey,
|
||||
},
|
||||
data,
|
||||
}),
|
||||
).toBe("openai/gpt-5");
|
||||
expect(
|
||||
resolveMattermostModelPickerCurrentModel({
|
||||
cfg,
|
||||
route: {
|
||||
agentId: "support",
|
||||
sessionKey: childSessionKey,
|
||||
},
|
||||
data,
|
||||
}),
|
||||
).toBe("anthropic/claude-sonnet-4-5");
|
||||
} finally {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||
import { parseStrictInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
normalizeOptionalString,
|
||||
normalizeStringifiedOptionalString,
|
||||
@@ -237,28 +237,21 @@ export function resolveMattermostModelPickerCurrentModel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
route: { agentId: string; sessionKey: string };
|
||||
data: ModelsProviderData;
|
||||
readConsistency?: "latest";
|
||||
skipCache?: boolean;
|
||||
}): string {
|
||||
const fallback = `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
|
||||
try {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: params.route.agentId,
|
||||
});
|
||||
const sessionEntry = getSessionEntry({
|
||||
storePath,
|
||||
sessionKey: params.route.sessionKey,
|
||||
...(params.readConsistency === "latest" ? { readConsistency: "latest" as const } : {}),
|
||||
});
|
||||
const sessionStore = params.skipCache
|
||||
? loadSessionStore(storePath, { skipCache: true })
|
||||
: loadSessionStore(storePath);
|
||||
const sessionEntry = sessionStore[params.route.sessionKey];
|
||||
const override = resolveStoredModelOverride({
|
||||
sessionEntry,
|
||||
loadSessionEntry: (sessionKey) =>
|
||||
getSessionEntry({
|
||||
storePath,
|
||||
sessionKey,
|
||||
...(params.readConsistency === "latest" ? { readConsistency: "latest" as const } : {}),
|
||||
}),
|
||||
sessionStore,
|
||||
sessionKey: params.route.sessionKey,
|
||||
parentSessionKey: sessionEntry?.parentSessionKey,
|
||||
defaultProvider: params.data.resolvedDefault.provider,
|
||||
});
|
||||
if (!override?.model) {
|
||||
|
||||
@@ -1256,7 +1256,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
cfg,
|
||||
route: modelSessionRoute,
|
||||
data,
|
||||
readConsistency: "latest",
|
||||
skipCache: true,
|
||||
});
|
||||
const view = renderMattermostModelsPickerView({
|
||||
ownerUserId: pickerState.ownerUserId,
|
||||
|
||||
@@ -36,6 +36,7 @@ export {
|
||||
isTrustedProxyAddress,
|
||||
listSkillCommandsForAgents,
|
||||
loadOutboundMediaFromUrl,
|
||||
loadSessionStore,
|
||||
logInboundDrop,
|
||||
logTypingFailure,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
|
||||
@@ -1987,78 +1987,6 @@ describe("memory-core dreaming phases", () => {
|
||||
expect(newOccurrences).toBe(1);
|
||||
});
|
||||
|
||||
it("skips reset/deleted archive artifacts without active transcripts during session ingestion", async () => {
|
||||
const workspaceDir = await createDreamingWorkspace();
|
||||
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", path.join(workspaceDir, ".state"));
|
||||
const sessionsDir = resolveSessionTranscriptsDirForAgent("main");
|
||||
await fs.mkdir(sessionsDir, { recursive: true });
|
||||
const archivePath = path.join(
|
||||
sessionsDir,
|
||||
"archived-only.jsonl.deleted.2026-04-06T01-00-00.000Z",
|
||||
);
|
||||
await fs.writeFile(
|
||||
archivePath,
|
||||
[
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
message: {
|
||||
role: "user",
|
||||
timestamp: "2026-04-05T18:01:00.000Z",
|
||||
content: [{ type: "text", text: "Archived session should not be dreamed." }],
|
||||
},
|
||||
}),
|
||||
].join("\n") + "\n",
|
||||
"utf-8",
|
||||
);
|
||||
const mtime = new Date("2026-04-06T01:05:00.000Z");
|
||||
await fs.utimes(archivePath, mtime, mtime);
|
||||
|
||||
const { beforeAgentReply } = createHarness(
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: workspaceDir,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-core": {
|
||||
config: {
|
||||
dreaming: {
|
||||
enabled: true,
|
||||
phases: {
|
||||
light: {
|
||||
enabled: true,
|
||||
limit: 20,
|
||||
lookbackDays: 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaceDir,
|
||||
);
|
||||
|
||||
try {
|
||||
await withDreamingTestClock(async () => {
|
||||
await triggerLightDreaming(beforeAgentReply, workspaceDir, 5);
|
||||
});
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
|
||||
await expectPathMissing(
|
||||
path.join(workspaceDir, "memory", ".dreams", "session-corpus", "2026-04-05.txt"),
|
||||
);
|
||||
|
||||
const sessionIngestion = await testing.readSessionIngestionState(workspaceDir);
|
||||
expect(Object.keys(sessionIngestion.files)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("buckets session snippets by per-message day rather than file mtime", async () => {
|
||||
const workspaceDir = await createDreamingWorkspace();
|
||||
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
|
||||
|
||||
@@ -848,12 +848,7 @@ async function collectSessionIngestionBatches(params: {
|
||||
for (const agentId of agentIds) {
|
||||
for (const entry of await listSessionTranscriptCorpusEntriesForAgent(agentId)) {
|
||||
const absolutePath = entry.sessionFile;
|
||||
if (
|
||||
// Dreaming learns only from the live corpus. Retained reset/delete
|
||||
// archives stay in the shared corpus for QMD and memory_search.
|
||||
entry.artifactKind === "archive-artifact" ||
|
||||
isCheckpointSessionTranscriptPath(absolutePath)
|
||||
) {
|
||||
if (isCheckpointSessionTranscriptPath(absolutePath)) {
|
||||
continue;
|
||||
}
|
||||
sessionFiles.push({
|
||||
|
||||
@@ -5,9 +5,7 @@ import {
|
||||
buildOllamaProvider,
|
||||
buildOllamaModelDefinition,
|
||||
enrichOllamaModelsWithContext,
|
||||
fetchOllamaModels,
|
||||
parseOllamaNumCtxParameter,
|
||||
queryOllamaModelShowInfo,
|
||||
resetOllamaModelShowInfoCacheForTest,
|
||||
resolveOllamaApiBase,
|
||||
type OllamaTagModel,
|
||||
@@ -382,57 +380,4 @@ describe("ollama provider models", () => {
|
||||
expect(parseOllamaNumCtxParameter('stop "<|eot_id|>"')).toBeUndefined();
|
||||
expect(parseOllamaNumCtxParameter({ num_ctx: 8192 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("fails soft and stops reading when discovery streams exceed the JSON byte cap", async () => {
|
||||
// Larger than the shared 16 MiB readProviderJsonResponse cap so the bounded reader cancels
|
||||
// the stream mid-flight; if the cap were removed the reader would buffer the whole payload.
|
||||
const ONE_MIB = 1024 * 1024;
|
||||
const TOTAL_CHUNKS = 32; // 32 MiB advertised body, double the cap.
|
||||
const chunk = new Uint8Array(ONE_MIB);
|
||||
|
||||
let bytesPulled = 0;
|
||||
let canceled = false;
|
||||
const makeOversizedJsonResponse = (): Response => {
|
||||
bytesPulled = 0;
|
||||
canceled = false;
|
||||
let pulled = 0;
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
if (pulled >= TOTAL_CHUNKS) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
pulled += 1;
|
||||
bytesPulled += chunk.length;
|
||||
controller.enqueue(chunk);
|
||||
},
|
||||
cancel() {
|
||||
canceled = true;
|
||||
},
|
||||
});
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => makeOversizedJsonResponse()),
|
||||
);
|
||||
const tags = await fetchOllamaModels("http://127.0.0.1:11434");
|
||||
expect(tags).toEqual({ reachable: false, models: [] });
|
||||
expect(canceled).toBe(true);
|
||||
// Only the bounded prefix is pulled, never the full advertised 32 MiB stream.
|
||||
expect(bytesPulled).toBeLessThan(TOTAL_CHUNKS * ONE_MIB);
|
||||
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => makeOversizedJsonResponse()),
|
||||
);
|
||||
const showInfo = await queryOllamaModelShowInfo("http://127.0.0.1:11434", "evil-model:latest");
|
||||
expect(showInfo).toEqual({});
|
||||
expect(canceled).toBe(true);
|
||||
expect(bytesPulled).toBeLessThan(TOTAL_CHUNKS * ONE_MIB);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-onboard";
|
||||
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import {
|
||||
OLLAMA_DEFAULT_BASE_URL,
|
||||
@@ -147,11 +146,11 @@ export async function queryOllamaModelShowInfo(
|
||||
if (!response.ok) {
|
||||
return {};
|
||||
}
|
||||
const data = await readProviderJsonResponse<{
|
||||
const data = (await response.json()) as {
|
||||
model_info?: Record<string, unknown>;
|
||||
capabilities?: unknown;
|
||||
parameters?: unknown;
|
||||
}>(response, "ollama-provider-models.show");
|
||||
};
|
||||
|
||||
let contextWindow: number | undefined;
|
||||
if (data.model_info) {
|
||||
@@ -315,10 +314,7 @@ export async function fetchOllamaModels(
|
||||
if (!response.ok) {
|
||||
return { reachable: true, models: [] };
|
||||
}
|
||||
const data = await readProviderJsonResponse<OllamaTagsResponse>(
|
||||
response,
|
||||
"ollama-provider-models.tags",
|
||||
);
|
||||
const data = (await response.json()) as OllamaTagsResponse;
|
||||
const models = (data.models ?? []).filter((m) => m.name);
|
||||
return { reachable: true, models };
|
||||
} finally {
|
||||
|
||||
@@ -89,27 +89,13 @@ prose run alice/code-review
|
||||
2. Fetch the `.prose` content
|
||||
3. Load the VM and execute as normal
|
||||
|
||||
Top-level remote runs are explicit user requests. Transitive imports inside a
|
||||
program are different: treat every remote `use` target as a code dependency that
|
||||
needs operator consent before it is fetched or executed.
|
||||
|
||||
This same resolution applies to `use` statements inside `.prose` files, but the
|
||||
VM must fail closed until the operator approves the remote dependency list:
|
||||
This same resolution applies to `use` statements inside `.prose` files:
|
||||
|
||||
```prose
|
||||
use "https://example.com/my-program.prose" # Direct URL
|
||||
use "alice/research" as research # Registry shorthand
|
||||
```
|
||||
|
||||
When a program contains any remote `use` target (`http://`, `https://`, or
|
||||
registry shorthand):
|
||||
|
||||
1. Collect and display the exact resolved remote targets.
|
||||
2. Explain that these are transitive code dependencies for this run.
|
||||
3. Ask the operator to reply exactly `approve remote prose imports` to continue.
|
||||
4. Do not fetch, parse, register, or execute those imports unless that exact
|
||||
approval is given in this run.
|
||||
|
||||
---
|
||||
|
||||
## File Locations
|
||||
|
||||
@@ -339,24 +339,21 @@ Please provide final recommendations.
|
||||
|
||||
## Use Statements (Program Composition)
|
||||
|
||||
Use statements import other OpenProse programs from registry paths or direct
|
||||
HTTP(S) URLs, enabling modular workflows.
|
||||
Use statements import other OpenProse programs from the registry at `p.prose.md`, enabling modular workflows.
|
||||
|
||||
### Syntax
|
||||
|
||||
```prose
|
||||
use "@handle/slug"
|
||||
use "@handle/slug" as alias
|
||||
use "https://example.com/program.prose" as alias
|
||||
```
|
||||
|
||||
### Path Format
|
||||
|
||||
Import paths are either registry references or direct HTTP(S) URLs:
|
||||
Import paths follow the format `@handle/slug`:
|
||||
|
||||
- `@handle/slug` identifies a program author/organization and slug.
|
||||
- `handle/slug` resolves to the same registry host used by the runtime.
|
||||
- `https://example.com/program.prose` fetches that exact URL after approval.
|
||||
- `@handle` identifies the program author/organization
|
||||
- `slug` is the program name
|
||||
|
||||
An optional alias (`as name`) allows referencing by a shorter name.
|
||||
|
||||
@@ -374,20 +371,16 @@ use "@bob/critique" as critic
|
||||
|
||||
When the OpenProse VM encounters a `use` statement:
|
||||
|
||||
1. Resolve the import target.
|
||||
2. If the target is remote (`http://`, `https://`, or registry shorthand), pause
|
||||
before fetching and require the operator to approve the full remote import
|
||||
list with `approve remote prose imports` for this run.
|
||||
3. Fetch the program only after approval.
|
||||
4. Parse the program to extract its contract (inputs/outputs).
|
||||
5. Register the program in the Import Registry.
|
||||
1. Fetch the program from `https://p.prose.md/@handle/slug`
|
||||
2. Parse the program to extract its contract (inputs/outputs)
|
||||
3. Register the program in the Import Registry
|
||||
|
||||
### Validation Rules
|
||||
|
||||
| Check | Severity | Message |
|
||||
| --------------------- | -------- | -------------------------------------- |
|
||||
| Empty path | Error | Use path cannot be empty |
|
||||
| Invalid path format | Error | Path must be registry path or URL |
|
||||
| Invalid path format | Error | Path must be @handle/slug format |
|
||||
| Duplicate import | Error | Program already imported |
|
||||
| Missing alias for dup | Error | Alias required when importing multiple |
|
||||
|
||||
@@ -395,11 +388,9 @@ When the OpenProse VM encounters a `use` statement:
|
||||
|
||||
Use statements are processed before any agent definitions or sessions. The OpenProse VM:
|
||||
|
||||
1. Resolves all imported program targets at the start of execution.
|
||||
2. Requires operator approval before fetching any remote imports.
|
||||
3. Fetches and validates approved imported programs.
|
||||
4. Extracts input/output contracts from each program.
|
||||
5. Registers programs in the Import Registry for later invocation.
|
||||
1. Fetches and validates all imported programs at the start of execution
|
||||
2. Extracts input/output contracts from each program
|
||||
3. Registers programs in the Import Registry for later invocation
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -162,10 +162,8 @@ For general programming tasks, please use a general-purpose agent instance.
|
||||
## Execution Algorithm (Simplified)
|
||||
|
||||
1. Parse program structure (use statements, inputs, agents, blocks)
|
||||
2. Resolve `use` imports. If any import is remote, require the operator to
|
||||
approve the full list with `approve remote prose imports` before fetching.
|
||||
3. Bind inputs from caller or prompt user if missing
|
||||
4. For each statement in order:
|
||||
2. Bind inputs from caller or prompt user if missing
|
||||
3. For each statement in order:
|
||||
- `session` → Task tool call, await result
|
||||
- `resume` → Load memory, Task tool call, await result
|
||||
- `let/const` → Execute RHS, bind result
|
||||
@@ -174,8 +172,8 @@ For general programming tasks, please use a general-purpose agent instance.
|
||||
- `try/catch` → Execute try, catch on error, always finally
|
||||
- `choice/if` → Evaluate conditions, execute matching branch
|
||||
- `do block` → Push frame, bind args, execute body, pop frame
|
||||
5. Collect output bindings
|
||||
6. Return outputs to caller
|
||||
4. Collect output bindings
|
||||
5. Return outputs to caller
|
||||
|
||||
## Remember
|
||||
|
||||
|
||||
@@ -63,13 +63,6 @@ use "https://example.com/my-program.prose" # Direct URL
|
||||
use "alice/research" as research # Registry shorthand
|
||||
```
|
||||
|
||||
Top-level remote runs are explicit user requests. Remote `use` statements are
|
||||
transitive code dependencies. Before fetching any remote `use` target, collect
|
||||
the exact resolved targets, show them to the operator, and require the operator
|
||||
to reply exactly `approve remote prose imports` for this run. If approval is not
|
||||
given, abort the run before fetching, parsing, registering, or executing the
|
||||
remote imports.
|
||||
|
||||
---
|
||||
|
||||
## Why This Is a VM
|
||||
@@ -120,18 +113,18 @@ When you execute a `.prose` program, you ARE the virtual machine. This is not a
|
||||
|
||||
Traditional dependency injection containers wire up components from configuration. You do the same—but with understanding:
|
||||
|
||||
| Declared Primitive | Your Responsibility |
|
||||
| --------------------------- | ----------------------------------------------------------------------- |
|
||||
| `use "handle/slug" as name` | Resolve import, require approval if remote, register in Import Registry |
|
||||
| `input topic: "..."` | Bind value from caller, make available as variable |
|
||||
| `output findings = ...` | Mark value as output, return to caller on completion |
|
||||
| `agent researcher:` | Register this agent template for later use |
|
||||
| `session: researcher` | Resolve the agent, merge properties, spawn the session |
|
||||
| `resume: captain` | Load agent memory, spawn session with memory context |
|
||||
| `context: { a, b }` | Wire the outputs of `a` and `b` into this session's input |
|
||||
| `parallel:` branches | Coordinate concurrent execution, collect results |
|
||||
| `block review(topic):` | Store this reusable component, invoke when called |
|
||||
| `name(input: value)` | Invoke imported program with inputs, receive outputs |
|
||||
| Declared Primitive | Your Responsibility |
|
||||
| --------------------------- | ---------------------------------------------------------- |
|
||||
| `use "handle/slug" as name` | Fetch program from p.prose.md, register in Import Registry |
|
||||
| `input topic: "..."` | Bind value from caller, make available as variable |
|
||||
| `output findings = ...` | Mark value as output, return to caller on completion |
|
||||
| `agent researcher:` | Register this agent template for later use |
|
||||
| `session: researcher` | Resolve the agent, merge properties, spawn the session |
|
||||
| `resume: captain` | Load agent memory, spawn session with memory context |
|
||||
| `context: { a, b }` | Wire the outputs of `a` and `b` into this session's input |
|
||||
| `parallel:` branches | Coordinate concurrent execution, collect results |
|
||||
| `block review(topic):` | Store this reusable component, invoke when called |
|
||||
| `name(input: value)` | Invoke imported program with inputs, receive outputs |
|
||||
|
||||
You are the container that holds these declarations and wires them together at runtime. The program declares _what_; you determine _how_ to connect them.
|
||||
|
||||
@@ -705,9 +698,7 @@ Query the database to access the content.
|
||||
|
||||
## Program Composition
|
||||
|
||||
Programs can import and invoke other programs, enabling modular workflows.
|
||||
Registry and direct-URL imports are remote code dependencies and require
|
||||
operator approval before fetching.
|
||||
Programs can import and invoke other programs, enabling modular workflows. Programs are fetched from the registry at `p.prose.md`.
|
||||
|
||||
### Importing Programs
|
||||
|
||||
@@ -718,20 +709,15 @@ use "alice/research"
|
||||
use "bob/critique" as critic
|
||||
```
|
||||
|
||||
The import path can be a registry reference (`handle/slug`) or a direct HTTP(S)
|
||||
URL. An optional alias (`as name`) allows referencing by a shorter name.
|
||||
The import path follows the format `handle/slug`. An optional alias (`as name`) allows referencing by a shorter name.
|
||||
|
||||
### Program URL Resolution
|
||||
|
||||
When the VM encounters a `use` statement:
|
||||
|
||||
1. Resolve the import target.
|
||||
2. If the target is remote (`http://`, `https://`, or registry shorthand), pause
|
||||
before fetching and require the operator to approve the full remote import
|
||||
list with `approve remote prose imports` for this run.
|
||||
3. Fetch the program only after approval.
|
||||
4. Parse the program to extract its contract (inputs/outputs).
|
||||
5. Register the program in the Import Registry.
|
||||
1. Fetch the program from `https://p.prose.md/handle/slug`
|
||||
2. Parse the program to extract its contract (inputs/outputs)
|
||||
3. Register the program in the Import Registry
|
||||
|
||||
### Input Declarations
|
||||
|
||||
@@ -1170,13 +1156,11 @@ Before spawning, substitute `{varname}` with variable values.
|
||||
|
||||
```
|
||||
function execute(program, inputs?):
|
||||
1. Collect all use statements, resolve import targets
|
||||
2. If remote imports are present, require operator approval before fetch
|
||||
3. Fetch approved imports and register them
|
||||
4. Collect all input declarations, bind values from caller
|
||||
5. Collect all agent definitions
|
||||
6. Collect all block definitions
|
||||
7. For each statement in order:
|
||||
1. Collect all use statements, fetch and register imports
|
||||
2. Collect all input declarations, bind values from caller
|
||||
3. Collect all agent definitions
|
||||
4. Collect all block definitions
|
||||
5. For each statement in order:
|
||||
- If session: spawn via Task, await result
|
||||
- If resume: load memory, spawn via Task, await result
|
||||
- If let/const: execute RHS, bind result
|
||||
@@ -1235,7 +1219,7 @@ When passing context to sessions:
|
||||
|
||||
The OpenProse VM:
|
||||
|
||||
1. **Imports** approved programs via `use` statements
|
||||
1. **Imports** programs from `p.prose.md` via `use` statements
|
||||
2. **Binds** inputs from caller to program variables
|
||||
3. **Parses** the program structure
|
||||
4. **Collects** definitions (agents, blocks)
|
||||
|
||||
@@ -210,8 +210,6 @@ For variable resolution across scopes:
|
||||
|
||||
```
|
||||
[Import] Importing: @alice/research
|
||||
Remote dependency requires approval: https://p.prose.md/@alice/research
|
||||
Operator approved: approve remote prose imports
|
||||
Fetching from: https://p.prose.md/@alice/research
|
||||
Inputs expected: [topic, depth]
|
||||
Outputs provided: [findings, sources]
|
||||
|
||||
@@ -8,8 +8,9 @@ import type {
|
||||
SandboxResolvedPath,
|
||||
} from "openclaw/plugin-sdk/sandbox";
|
||||
import { createWritableRenameTargetResolver } from "openclaw/plugin-sdk/sandbox";
|
||||
import { FsSafeError, isPathInside } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { isPathInside } from "openclaw/plugin-sdk/security-runtime";
|
||||
import type { OpenShellFsBridgeContext, OpenShellSandboxBackend } from "./backend.types.js";
|
||||
import { movePathWithCopyFallback } from "./mirror.js";
|
||||
|
||||
type ResolvedMountPath = SandboxResolvedPath & {
|
||||
mountHostRoot: string;
|
||||
@@ -17,9 +18,6 @@ type ResolvedMountPath = SandboxResolvedPath & {
|
||||
source: "workspace" | "agent" | "protectedSkill";
|
||||
};
|
||||
|
||||
type FsSafeRoot = Awaited<ReturnType<typeof fsRoot>>;
|
||||
type FsSafeStat = Awaited<ReturnType<FsSafeRoot["stat"]>>;
|
||||
|
||||
const MATERIALIZED_SKILLS_CONTAINER_PARTS = [".openclaw", "sandbox-skills", "skills"] as const;
|
||||
|
||||
export function createOpenShellFsBridge(params: {
|
||||
@@ -119,7 +117,7 @@ class OpenShellFsBridge implements SandboxFsBridge {
|
||||
allowFinalSymlinkForUnlink: false,
|
||||
});
|
||||
await this.backend.mkdirpRemotePath(target.containerPath, params.signal);
|
||||
await mkdirLocalRootPath({ hostPath, target });
|
||||
await fsPromises.mkdir(hostPath, { recursive: true });
|
||||
}
|
||||
|
||||
async remove(params: {
|
||||
@@ -143,11 +141,9 @@ class OpenShellFsBridge implements SandboxFsBridge {
|
||||
signal: params.signal,
|
||||
ignoreMissing: params.force !== false,
|
||||
});
|
||||
await removeLocalRootPath({
|
||||
force: params.force,
|
||||
hostPath,
|
||||
recursive: params.recursive,
|
||||
target,
|
||||
await fsPromises.rm(hostPath, {
|
||||
recursive: params.recursive ?? false,
|
||||
force: params.force !== false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,17 +168,9 @@ class OpenShellFsBridge implements SandboxFsBridge {
|
||||
allowMissingLeaf: true,
|
||||
allowFinalSymlinkForUnlink: false,
|
||||
});
|
||||
await assertRenameSourceSupported(fromHostPath);
|
||||
if (from.mountHostRoot !== to.mountHostRoot) {
|
||||
throw new Error("OpenShell cross-root mirror renames require pinned fs-safe support");
|
||||
}
|
||||
await assertSameDeviceRenameSupported({
|
||||
fromHostPath,
|
||||
root: from.mountHostRoot,
|
||||
toHostPath,
|
||||
});
|
||||
await this.backend.renameRemotePath(from.containerPath, to.containerPath, params.signal);
|
||||
await moveLocalRootPath({ from, fromHostPath, to, toHostPath });
|
||||
await fsPromises.mkdir(path.dirname(toHostPath), { recursive: true });
|
||||
await movePathWithCopyFallback({ from: fromHostPath, to: toHostPath });
|
||||
}
|
||||
|
||||
async stat(params: {
|
||||
@@ -355,162 +343,6 @@ class OpenShellFsBridge implements SandboxFsBridge {
|
||||
}
|
||||
}
|
||||
|
||||
async function mkdirLocalRootPath(params: {
|
||||
target: ResolvedMountPath;
|
||||
hostPath: string;
|
||||
}): Promise<void> {
|
||||
const relativePath = relativeToRoot(params.target, params.hostPath);
|
||||
if (!relativePath) {
|
||||
return;
|
||||
}
|
||||
const root = await fsRoot(params.target.mountHostRoot);
|
||||
await root.mkdir(relativePath);
|
||||
}
|
||||
|
||||
async function removeLocalRootPath(params: {
|
||||
target: ResolvedMountPath;
|
||||
hostPath: string;
|
||||
recursive?: boolean;
|
||||
force?: boolean;
|
||||
}): Promise<void> {
|
||||
const root = await fsRoot(params.target.mountHostRoot);
|
||||
const relativePath = relativeToRoot(params.target, params.hostPath);
|
||||
try {
|
||||
if (params.force === false) {
|
||||
await fsPromises.lstat(params.hostPath);
|
||||
}
|
||||
if (params.recursive) {
|
||||
const stats = await fsPromises.lstat(params.hostPath).catch((err: unknown) => {
|
||||
if (isNotFoundError(err)) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
if (stats?.isSymbolicLink()) {
|
||||
await root.remove(relativePath);
|
||||
return;
|
||||
}
|
||||
await removeRootTree(root, relativePath);
|
||||
return;
|
||||
}
|
||||
await root.remove(relativePath);
|
||||
} catch (err) {
|
||||
if (params.force !== false && isNotFoundError(err)) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeRootTree(
|
||||
root: FsSafeRoot,
|
||||
relativePath: string,
|
||||
knownStats?: FsSafeStat,
|
||||
): Promise<void> {
|
||||
const stats = knownStats ?? (await root.stat(relativePath));
|
||||
if (stats.isDirectory && !stats.isSymbolicLink) {
|
||||
const entries = await root.list(relativePath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
await removeRootTree(root, path.join(relativePath, entry.name), entry);
|
||||
}
|
||||
if (!relativePath) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await root.remove(relativePath);
|
||||
}
|
||||
|
||||
async function moveLocalRootPath(params: {
|
||||
from: ResolvedMountPath;
|
||||
fromHostPath: string;
|
||||
to: ResolvedMountPath;
|
||||
toHostPath: string;
|
||||
}): Promise<void> {
|
||||
const root = await fsRoot(params.from.mountHostRoot);
|
||||
const fromRelativePath = relativeToRoot(params.from, params.fromHostPath);
|
||||
const toRelativePath = relativeToRoot(params.to, params.toHostPath);
|
||||
await mkdirParentPath(root, toRelativePath);
|
||||
await root.move(fromRelativePath, toRelativePath, { overwrite: true });
|
||||
}
|
||||
|
||||
async function mkdirParentPath(root: FsSafeRoot, relativePath: string): Promise<void> {
|
||||
const parentPath = path.dirname(relativePath);
|
||||
if (parentPath === "." || parentPath === "") {
|
||||
return;
|
||||
}
|
||||
await root.mkdir(parentPath);
|
||||
}
|
||||
|
||||
function relativeToRoot(target: ResolvedMountPath, hostPath: string): string {
|
||||
const relativePath = path.relative(target.mountHostRoot, hostPath);
|
||||
return relativePath === "." ? "" : relativePath;
|
||||
}
|
||||
|
||||
async function assertRenameSourceSupported(fromHostPath: string): Promise<void> {
|
||||
const stats = await fsPromises.lstat(fromHostPath);
|
||||
if (stats.isSymbolicLink()) {
|
||||
throw new Error("Sandbox symlink rename sources are not supported by the local mirror bridge");
|
||||
}
|
||||
if (stats.isFile() && stats.nlink > 1) {
|
||||
throw new Error(
|
||||
"Sandbox hardlinked rename sources are not supported by the local mirror bridge",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertSameDeviceRenameSupported(params: {
|
||||
fromHostPath: string;
|
||||
root: string;
|
||||
toHostPath: string;
|
||||
}): Promise<void> {
|
||||
const sourceStats = await fsPromises.lstat(params.fromHostPath);
|
||||
const destinationParentStats = await nearestExistingDirectoryStats({
|
||||
root: params.root,
|
||||
targetPath: path.dirname(params.toHostPath),
|
||||
});
|
||||
if (sourceStats.dev !== destinationParentStats.dev) {
|
||||
throw new Error("OpenShell cross-device mirror renames require pinned fs-safe support");
|
||||
}
|
||||
}
|
||||
|
||||
async function nearestExistingDirectoryStats(params: {
|
||||
root: string;
|
||||
targetPath: string;
|
||||
}): Promise<Awaited<ReturnType<typeof fsPromises.lstat>>> {
|
||||
const rootPath = path.resolve(params.root);
|
||||
let cursor = path.resolve(params.targetPath);
|
||||
while (isPathInside(rootPath, cursor)) {
|
||||
const stats = await fsPromises.lstat(cursor).catch((err: unknown) => {
|
||||
if (isNotFoundError(err)) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
if (stats) {
|
||||
if (!stats.isDirectory()) {
|
||||
throw new Error(`Sandbox rename destination parent is not a directory: ${cursor}`);
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
const next = path.dirname(cursor);
|
||||
if (next === cursor) {
|
||||
break;
|
||||
}
|
||||
cursor = next;
|
||||
}
|
||||
return await fsPromises.lstat(rootPath);
|
||||
}
|
||||
|
||||
function isNotFoundError(err: unknown): boolean {
|
||||
return (
|
||||
(err instanceof FsSafeError && err.code === "not-found") ||
|
||||
(typeof err === "object" &&
|
||||
err !== null &&
|
||||
"code" in err &&
|
||||
(err as { code?: unknown }).code === "ENOENT")
|
||||
);
|
||||
}
|
||||
|
||||
function resolveProtectedSkillTarget(params: {
|
||||
input: string;
|
||||
skillsRoot: string;
|
||||
@@ -589,11 +421,7 @@ async function assertLocalPathSafety(params: {
|
||||
const canonicalRoot = await fsPromises
|
||||
.realpath(params.root)
|
||||
.catch(() => path.resolve(params.root));
|
||||
const targetStats = await fsPromises.lstat(params.target.hostPath).catch(() => null);
|
||||
const candidate =
|
||||
params.allowFinalSymlinkForUnlink && targetStats?.isSymbolicLink()
|
||||
? path.resolve(canonicalRoot, path.relative(params.root, params.target.hostPath))
|
||||
: await resolveCanonicalCandidate(params.target.hostPath);
|
||||
const candidate = await resolveCanonicalCandidate(params.target.hostPath);
|
||||
if (!isPathInside(canonicalRoot, candidate)) {
|
||||
throw new Error(
|
||||
`Sandbox path escapes allowed mounts; cannot access: ${params.target.containerPath}`,
|
||||
|
||||
@@ -733,90 +733,6 @@ describe("openshell fs bridges", () => {
|
||||
expect(backend["runRemoteShellScript"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects cross-root mirror renames before the remote backend commit", async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
const agentWorkspaceDir = await makeTempDir("openclaw-openshell-agent-fs-");
|
||||
const sourcePath = path.join(workspaceDir, "source.txt");
|
||||
await fs.writeFile(sourcePath, "payload", "utf8");
|
||||
const backend = createMirrorBackendMock();
|
||||
const sandbox = createSandboxTestContext({
|
||||
overrides: {
|
||||
backendId: "openshell",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir,
|
||||
containerWorkdir: "/sandbox",
|
||||
},
|
||||
});
|
||||
|
||||
const { createOpenShellFsBridge } = await import("./fs-bridge.js");
|
||||
const bridge = createOpenShellFsBridge({ sandbox, backend });
|
||||
|
||||
await expect(bridge.rename({ from: "source.txt", to: "/agent/source.txt" })).rejects.toThrow(
|
||||
"OpenShell cross-root mirror renames require pinned fs-safe support",
|
||||
);
|
||||
expect(backend["renameRemotePath"]).not.toHaveBeenCalled();
|
||||
await expect(fs.readFile(sourcePath, "utf8")).resolves.toBe("payload");
|
||||
await expectPathMissing(path.join(agentWorkspaceDir, "source.txt"));
|
||||
await expect(fs.readdir(agentWorkspaceDir)).resolves.toStrictEqual([]);
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"rejects local mirror symlink rename sources before the remote backend commit",
|
||||
async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
await fs.writeFile(path.join(workspaceDir, "target.txt"), "payload", "utf8");
|
||||
await fs.symlink("target.txt", path.join(workspaceDir, "link.txt"));
|
||||
const backend = createMirrorBackendMock();
|
||||
const sandbox = createSandboxTestContext({
|
||||
overrides: {
|
||||
backendId: "openshell",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
containerWorkdir: "/sandbox",
|
||||
},
|
||||
});
|
||||
|
||||
const { createOpenShellFsBridge } = await import("./fs-bridge.js");
|
||||
const bridge = createOpenShellFsBridge({ sandbox, backend });
|
||||
|
||||
await expect(bridge.rename({ from: "link.txt", to: "moved-link.txt" })).rejects.toThrow(
|
||||
"Sandbox symlink rename sources are not supported",
|
||||
);
|
||||
expect(backend["renameRemotePath"]).not.toHaveBeenCalled();
|
||||
await expect(fs.readlink(path.join(workspaceDir, "link.txt"))).resolves.toBe("target.txt");
|
||||
await expectPathMissing(path.join(workspaceDir, "moved-link.txt"));
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"rejects local mirror hardlinked rename sources before the remote backend commit",
|
||||
async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
const sourcePath = path.join(workspaceDir, "source.txt");
|
||||
await fs.writeFile(sourcePath, "payload", "utf8");
|
||||
await fs.link(sourcePath, path.join(workspaceDir, "other-link.txt"));
|
||||
const backend = createMirrorBackendMock();
|
||||
const sandbox = createSandboxTestContext({
|
||||
overrides: {
|
||||
backendId: "openshell",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
containerWorkdir: "/sandbox",
|
||||
},
|
||||
});
|
||||
|
||||
const { createOpenShellFsBridge } = await import("./fs-bridge.js");
|
||||
const bridge = createOpenShellFsBridge({ sandbox, backend });
|
||||
|
||||
await expect(bridge.rename({ from: "source.txt", to: "moved.txt" })).rejects.toThrow(
|
||||
"Sandbox hardlinked rename sources are not supported",
|
||||
);
|
||||
expect(backend["renameRemotePath"]).not.toHaveBeenCalled();
|
||||
await expect(fs.readFile(sourcePath, "utf8")).resolves.toBe("payload");
|
||||
await expectPathMissing(path.join(workspaceDir, "moved.txt"));
|
||||
},
|
||||
);
|
||||
|
||||
it("removes remote mirror paths through the pinned backend operation", async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
await fs.writeFile(path.join(workspaceDir, "target.txt"), "payload", "utf8");
|
||||
@@ -843,187 +759,6 @@ describe("openshell fs bridges", () => {
|
||||
expect(backend["runRemoteShellScript"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes recursive local mirror directories without raw path deletion", async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
await fs.mkdir(path.join(workspaceDir, "nested", "child"), { recursive: true });
|
||||
await fs.writeFile(path.join(workspaceDir, "nested", "child", "target.txt"), "payload", "utf8");
|
||||
const backend = createMirrorBackendMock();
|
||||
const sandbox = createSandboxTestContext({
|
||||
overrides: {
|
||||
backendId: "openshell",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
containerWorkdir: "/sandbox",
|
||||
},
|
||||
});
|
||||
|
||||
const { createOpenShellFsBridge } = await import("./fs-bridge.js");
|
||||
const bridge = createOpenShellFsBridge({ sandbox, backend });
|
||||
await bridge.remove({ filePath: "nested", recursive: true, force: true });
|
||||
|
||||
await expectPathMissing(path.join(workspaceDir, "nested"));
|
||||
expect(backend["removeRemotePath"]).toHaveBeenCalledWith("/sandbox/nested", {
|
||||
recursive: true,
|
||||
signal: undefined,
|
||||
ignoreMissing: true,
|
||||
});
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"removes recursive local mirror directories containing symlink leaves without following them",
|
||||
async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
const outsideDir = await makeTempDir("openclaw-openshell-outside-");
|
||||
const outsideTarget = path.join(outsideDir, "target.txt");
|
||||
await fs.mkdir(path.join(workspaceDir, "nested"), { recursive: true });
|
||||
await fs.writeFile(outsideTarget, "outside", "utf8");
|
||||
await fs.symlink(outsideTarget, path.join(workspaceDir, "nested", "link.txt"));
|
||||
const backend = createMirrorBackendMock();
|
||||
const sandbox = createSandboxTestContext({
|
||||
overrides: {
|
||||
backendId: "openshell",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
containerWorkdir: "/sandbox",
|
||||
},
|
||||
});
|
||||
|
||||
const { createOpenShellFsBridge } = await import("./fs-bridge.js");
|
||||
const bridge = createOpenShellFsBridge({ sandbox, backend });
|
||||
await bridge.remove({ filePath: "nested", recursive: true, force: true });
|
||||
|
||||
await expectPathMissing(path.join(workspaceDir, "nested"));
|
||||
await expect(fs.readFile(outsideTarget, "utf8")).resolves.toBe("outside");
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"removes local mirror symlink leaves when force is false",
|
||||
async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
const outsideDir = await makeTempDir("openclaw-openshell-outside-");
|
||||
const outsideTarget = path.join(outsideDir, "target.txt");
|
||||
await fs.writeFile(outsideTarget, "outside", "utf8");
|
||||
await fs.symlink(outsideTarget, path.join(workspaceDir, "link.txt"));
|
||||
const backend = createMirrorBackendMock();
|
||||
const sandbox = createSandboxTestContext({
|
||||
overrides: {
|
||||
backendId: "openshell",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
containerWorkdir: "/sandbox",
|
||||
},
|
||||
});
|
||||
|
||||
const { createOpenShellFsBridge } = await import("./fs-bridge.js");
|
||||
const bridge = createOpenShellFsBridge({ sandbox, backend });
|
||||
await bridge.remove({ filePath: "link.txt", force: false });
|
||||
|
||||
await expectPathMissing(path.join(workspaceDir, "link.txt"));
|
||||
await expect(fs.readFile(outsideTarget, "utf8")).resolves.toBe("outside");
|
||||
expect(backend["removeRemotePath"]).toHaveBeenCalledWith("/sandbox/link.txt", {
|
||||
recursive: false,
|
||||
signal: undefined,
|
||||
ignoreMissing: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"rejects local mirror mkdir when a validated parent is swapped to an outside symlink",
|
||||
async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
const outsideDir = await makeTempDir("openclaw-openshell-outside-");
|
||||
const slotPath = path.join(workspaceDir, "slot");
|
||||
await fs.mkdir(slotPath, { recursive: true });
|
||||
const backend = createMirrorBackendMock();
|
||||
backend["mkdirpRemotePath"] = vi.fn().mockImplementation(async () => {
|
||||
await fs.rm(slotPath, { recursive: true, force: true });
|
||||
await fs.symlink(outsideDir, slotPath);
|
||||
});
|
||||
const sandbox = createSandboxTestContext({
|
||||
overrides: {
|
||||
backendId: "openshell",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
containerWorkdir: "/sandbox",
|
||||
},
|
||||
});
|
||||
|
||||
const { createOpenShellFsBridge } = await import("./fs-bridge.js");
|
||||
const bridge = createOpenShellFsBridge({ sandbox, backend });
|
||||
|
||||
await expect(bridge.mkdirp({ filePath: "slot/escaped" })).rejects.toThrow();
|
||||
await expectPathMissing(path.join(outsideDir, "escaped"));
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"rejects local mirror remove when a validated parent is swapped to an outside symlink",
|
||||
async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
const outsideDir = await makeTempDir("openclaw-openshell-outside-");
|
||||
const slotPath = path.join(workspaceDir, "slot");
|
||||
const outsideTarget = path.join(outsideDir, "target.txt");
|
||||
await fs.mkdir(slotPath, { recursive: true });
|
||||
await fs.writeFile(path.join(slotPath, "target.txt"), "inside", "utf8");
|
||||
await fs.writeFile(outsideTarget, "outside", "utf8");
|
||||
const backend = createMirrorBackendMock();
|
||||
backend["removeRemotePath"] = vi.fn().mockImplementation(async () => {
|
||||
await fs.rm(slotPath, { recursive: true, force: true });
|
||||
await fs.symlink(outsideDir, slotPath);
|
||||
});
|
||||
const sandbox = createSandboxTestContext({
|
||||
overrides: {
|
||||
backendId: "openshell",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
containerWorkdir: "/sandbox",
|
||||
},
|
||||
});
|
||||
|
||||
const { createOpenShellFsBridge } = await import("./fs-bridge.js");
|
||||
const bridge = createOpenShellFsBridge({ sandbox, backend });
|
||||
|
||||
await expect(bridge.remove({ filePath: "slot/target.txt", force: true })).rejects.toThrow();
|
||||
await expect(fs.readFile(outsideTarget, "utf8")).resolves.toBe("outside");
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"rejects local mirror rename when a validated destination parent is swapped to an outside symlink",
|
||||
async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
const outsideDir = await makeTempDir("openclaw-openshell-outside-");
|
||||
const slotPath = path.join(workspaceDir, "slot");
|
||||
const sourcePath = path.join(workspaceDir, "source.txt");
|
||||
await fs.mkdir(slotPath, { recursive: true });
|
||||
await fs.writeFile(sourcePath, "payload", "utf8");
|
||||
const backend = createMirrorBackendMock();
|
||||
backend["renameRemotePath"] = vi.fn().mockImplementation(async () => {
|
||||
await fs.rm(slotPath, { recursive: true, force: true });
|
||||
await fs.symlink(outsideDir, slotPath);
|
||||
});
|
||||
const sandbox = createSandboxTestContext({
|
||||
overrides: {
|
||||
backendId: "openshell",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
containerWorkdir: "/sandbox",
|
||||
},
|
||||
});
|
||||
|
||||
const { createOpenShellFsBridge } = await import("./fs-bridge.js");
|
||||
const bridge = createOpenShellFsBridge({ sandbox, backend });
|
||||
|
||||
await expect(
|
||||
bridge.rename({ from: "source.txt", to: "slot/parent/moved.txt" }),
|
||||
).rejects.toThrow();
|
||||
await expect(fs.readFile(sourcePath, "utf8")).resolves.toBe("payload");
|
||||
await expectPathMissing(path.join(outsideDir, "parent", "moved.txt"));
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps local mirror state unchanged when remote pinned mkdir is rejected", async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
const backend = createMirrorBackendMock();
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { readPluginPackageVersion } from "openclaw/plugin-sdk/extension-shared";
|
||||
import {
|
||||
readProviderJsonResponse,
|
||||
readResponseTextLimited,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
DEFAULT_SEARCH_COUNT,
|
||||
mergeScopedSearchConfig,
|
||||
@@ -39,12 +36,6 @@ import {
|
||||
const PARALLEL_BASE_URL = "https://api.parallel.ai";
|
||||
const PARALLEL_SEARCH_PATHNAME = "/v1/search";
|
||||
const PARALLEL_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
||||
// Parallel's /v1/search returns a bounded result set, but the body is external
|
||||
// (web-search upstream) and untrusted. Cap the successful JSON read so a
|
||||
// hostile or malfunctioning endpoint streaming an unbounded body cannot force
|
||||
// the runtime to buffer the whole payload before parsing. 16 MiB matches the
|
||||
// shared provider JSON cap (readProviderJsonResponse default).
|
||||
const PARALLEL_SEARCH_RESPONSE_LIMIT_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const PLUGIN_VERSION = readPluginPackageVersion({ require });
|
||||
@@ -160,9 +151,11 @@ async function runParallelSearch(params: {
|
||||
);
|
||||
throw new Error(`Parallel API error (${res.status}): ${detail || res.statusText}`);
|
||||
}
|
||||
return await readProviderJsonResponse<ParallelSearchResponse>(res, "Parallel API", {
|
||||
maxBytes: PARALLEL_SEARCH_RESPONSE_LIMIT_BYTES,
|
||||
});
|
||||
try {
|
||||
return (await res.json()) as ParallelSearchResponse;
|
||||
} catch (cause) {
|
||||
throw new Error("Parallel API returned malformed JSON", { cause });
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -289,7 +282,6 @@ export const testing = {
|
||||
resolveParallelSearchCount,
|
||||
resolveParallelSearchEndpoint,
|
||||
PARALLEL_ERROR_BODY_LIMIT_BYTES,
|
||||
PARALLEL_SEARCH_RESPONSE_LIMIT_BYTES,
|
||||
USER_AGENT,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -59,40 +59,6 @@ function cancelTrackedResponse(
|
||||
};
|
||||
}
|
||||
|
||||
function streamedJsonResponse(params: { chunkCount: number; chunkSize: number }): {
|
||||
response: Response;
|
||||
getReadCount: () => number;
|
||||
wasCanceled: () => boolean;
|
||||
} {
|
||||
// Multi-chunk fixture: proves the bounded read stops pulling chunks before
|
||||
// the whole (here syntactically broken / unbounded) body is buffered, and
|
||||
// that the stream is cancelled on overflow.
|
||||
let reads = 0;
|
||||
let canceled = false;
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
if (reads >= params.chunkCount) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
reads += 1;
|
||||
controller.enqueue(encoder.encode("a".repeat(params.chunkSize)));
|
||||
},
|
||||
cancel() {
|
||||
canceled = true;
|
||||
},
|
||||
});
|
||||
return {
|
||||
response: new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
getReadCount: () => reads,
|
||||
wasCanceled: () => canceled,
|
||||
};
|
||||
}
|
||||
|
||||
import { testing } from "../test-api.js";
|
||||
import { createParallelWebSearchProvider as createContractParallelWebSearchProvider } from "../web-search-contract-api.js";
|
||||
import { createParallelWebSearchProvider } from "./parallel-web-search-provider.js";
|
||||
@@ -617,65 +583,6 @@ describe("parallel web search provider", () => {
|
||||
expect(textSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("bounds successful Parallel JSON bodies instead of buffering the whole response", async () => {
|
||||
// 200-chunk x 1 MiB body (~200 MiB) caps at 16 MiB: the bounded reader must
|
||||
// stop pulling chunks and cancel the stream well before draining it, then
|
||||
// surface a bounded error rather than buffering the whole payload.
|
||||
const streamed = streamedJsonResponse({ chunkCount: 200, chunkSize: 1024 * 1024 });
|
||||
endpointMockState.responses.push(streamed.response);
|
||||
const provider = createParallelWebSearchProvider();
|
||||
const tool = provider.createTool({
|
||||
config: {},
|
||||
searchConfig: { parallel: { apiKey: "par-secret" } },
|
||||
});
|
||||
if (!tool) {
|
||||
throw new Error("Expected tool definition");
|
||||
}
|
||||
|
||||
const error = await tool
|
||||
.execute({
|
||||
objective: `parallel-success-body-${Date.now()}-${Math.random()}`,
|
||||
search_queries: ["openclaw"],
|
||||
})
|
||||
.catch((cause: unknown) => cause);
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toMatch(
|
||||
new RegExp(
|
||||
`Parallel API: JSON response exceeds ${testing.PARALLEL_SEARCH_RESPONSE_LIMIT_BYTES} bytes`,
|
||||
),
|
||||
);
|
||||
// Stopped well before draining all 200 chunks, and cancelled the stream.
|
||||
expect(streamed.getReadCount()).toBeLessThan(200);
|
||||
expect(streamed.wasCanceled()).toBe(true);
|
||||
});
|
||||
|
||||
it("parses a well-formed Parallel JSON body under the byte cap", async () => {
|
||||
endpointMockState.responses.push(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
search_id: "ok",
|
||||
session_id: "ok-session",
|
||||
results: [{ url: "https://example.com/a", title: "A", excerpts: ["alpha"] }],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
),
|
||||
);
|
||||
const provider = createParallelWebSearchProvider();
|
||||
const tool = provider.createTool({
|
||||
config: {},
|
||||
searchConfig: { parallel: { apiKey: "par-secret" } },
|
||||
});
|
||||
if (!tool) {
|
||||
throw new Error("Expected tool definition");
|
||||
}
|
||||
const result = (await tool.execute({
|
||||
objective: `parallel-success-ok-${Date.now()}-${Math.random()}`,
|
||||
search_queries: ["openclaw"],
|
||||
})) as { provider?: string; searchId?: string; count?: number };
|
||||
expect(result).toMatchObject({ provider: "parallel", searchId: "ok", count: 1 });
|
||||
});
|
||||
|
||||
it("does not surface a Parallel-generated sessionId on a cache hit", async () => {
|
||||
// Unique objective so this test does not collide with the SDK's
|
||||
// module-level web-search cache across other cases.
|
||||
|
||||
@@ -462,10 +462,10 @@ describe("qa cli runtime", () => {
|
||||
profile?: unknown;
|
||||
scorecard?: {
|
||||
run?: { evidenceEntryCount?: unknown };
|
||||
coverageIds?: { fulfilled?: unknown };
|
||||
features?: { fulfilled?: unknown };
|
||||
categoryReports?: Array<{
|
||||
id?: unknown;
|
||||
coverageIds?: { fulfilled?: unknown };
|
||||
features?: { fulfilled?: unknown };
|
||||
missingCoverageIds?: unknown;
|
||||
}>;
|
||||
};
|
||||
@@ -480,11 +480,11 @@ describe("qa cli runtime", () => {
|
||||
expect(evidence.scorecard).not.toHaveProperty("kind");
|
||||
expect(evidence.scorecard).not.toHaveProperty("taxonomy");
|
||||
expect(evidence.scorecard).not.toHaveProperty("profile");
|
||||
expect(evidence.scorecard?.coverageIds?.fulfilled).toBe(1);
|
||||
expect(evidence.scorecard?.features?.fulfilled).toBe(0);
|
||||
expect(evidence.scorecard?.categoryReports?.[0]).toMatchObject({
|
||||
id: "channel-framework.conversation-routing-and-delivery",
|
||||
coverageIds: {
|
||||
fulfilled: 1,
|
||||
features: {
|
||||
fulfilled: 0,
|
||||
},
|
||||
});
|
||||
expect(evidence.entries?.[0]).not.toHaveProperty("execution");
|
||||
@@ -558,8 +558,6 @@ describe("qa cli runtime", () => {
|
||||
"qa-channel-reconnect-dedupe",
|
||||
"reaction-edit-delete",
|
||||
"thread-follow-up",
|
||||
"claude-cli-provider-capabilities",
|
||||
"claude-cli-provider-capabilities-subscription",
|
||||
"image-generation-roundtrip",
|
||||
"image-understanding-attachment",
|
||||
"native-image-generation",
|
||||
|
||||
@@ -182,9 +182,9 @@ describe("qa coverage report", () => {
|
||||
expect(inventory.scorecardTaxonomy.requiredCategoryCount).toBeLessThanOrEqual(
|
||||
inventory.scorecardTaxonomy.categoryCount,
|
||||
);
|
||||
expect(inventory.scorecardTaxonomy.requiredCoverageIdCount).toBeGreaterThan(0);
|
||||
expect(inventory.scorecardTaxonomy.fulfilledCoverageIdCount).toBeGreaterThan(0);
|
||||
expect(inventory.scorecardTaxonomy.coverageIdFulfillmentPercent).toBeGreaterThan(0);
|
||||
expect(inventory.scorecardTaxonomy.requiredFeatureCount).toBeGreaterThan(0);
|
||||
expect(inventory.scorecardTaxonomy.fulfilledFeatureCount).toBeGreaterThan(0);
|
||||
expect(inventory.scorecardTaxonomy.taxonomyFulfillmentPercent).toBeGreaterThan(0);
|
||||
expect(inventory.scorecardTaxonomy.evidenceRefCount).toBeGreaterThan(0);
|
||||
expect(inventory.scorecardTaxonomy.scenarioCoverageIdCount).toBeGreaterThan(0);
|
||||
expect(inventory.scorecardTaxonomy.unknownCoverageIdCount).toBe(0);
|
||||
@@ -259,7 +259,7 @@ describe("qa coverage report", () => {
|
||||
expect(report).toContain("## Scorecard Taxonomy");
|
||||
expect(report).toContain("- Taxonomy: taxonomy.yaml");
|
||||
expect(report).toContain("- Fulfilled taxonomy categories:");
|
||||
expect(report).toContain("- Fulfilled taxonomy coverage IDs:");
|
||||
expect(report).toContain("- Fulfilled taxonomy features:");
|
||||
expect(report).toContain("- Evidence refs:");
|
||||
expect(report).toContain("- Scenario coverage IDs:");
|
||||
expect(report).toContain(
|
||||
@@ -347,7 +347,7 @@ describe("qa coverage report", () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(report.fulfilledCoverageIdCount).toBe(0);
|
||||
expect(report.fulfilledFeatureCount).toBe(0);
|
||||
expect(report.categories[0]?.coverageStatus).toBe("missing");
|
||||
expect(report.validationIssues.map((issue) => issue.code)).toEqual([
|
||||
"coverage-id-not-found",
|
||||
@@ -375,7 +375,7 @@ describe("qa coverage report", () => {
|
||||
|
||||
expect(report.validationIssues).toStrictEqual([]);
|
||||
expect(report.fulfilledCategoryCount).toBe(1);
|
||||
expect(report.fulfilledCoverageIdCount).toBe(1);
|
||||
expect(report.fulfilledFeatureCount).toBe(1);
|
||||
expect(report.categories[0]?.coverageStatus).toBe("covered");
|
||||
expect(report.categories[0]?.scenarioRefs).toStrictEqual([
|
||||
"qa/scenarios/ui/control-ui-chat-flow-playwright.yaml",
|
||||
@@ -391,7 +391,7 @@ describe("qa coverage report", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("counts partial coverage IDs proportionately for taxonomy fulfillment", () => {
|
||||
it("requires every coverage ID on a taxonomy feature to have primary evidence", () => {
|
||||
const report = buildQaScorecardTaxonomyReport({
|
||||
taxonomy: testMaturityTaxonomy({
|
||||
featureCoverageIds: [[TEST_EXECUTABLE_COVERAGE_ID, TEST_WEBCHAT_COVERAGE_ID]],
|
||||
@@ -407,9 +407,7 @@ describe("qa coverage report", () => {
|
||||
});
|
||||
|
||||
expect(report.fulfilledCategoryCount).toBe(0);
|
||||
expect(report.requiredCoverageIdCount).toBe(2);
|
||||
expect(report.fulfilledCoverageIdCount).toBe(1);
|
||||
expect(report.coverageIdFulfillmentPercent).toBe(50);
|
||||
expect(report.fulfilledFeatureCount).toBe(0);
|
||||
expect(report.categories[0]?.coverageStatus).toBe("partial");
|
||||
expect(report.categories[0]?.fulfilledCoverageIds).toStrictEqual([TEST_EXECUTABLE_COVERAGE_ID]);
|
||||
expect(report.validationIssues).toContainEqual(
|
||||
@@ -420,75 +418,6 @@ describe("qa coverage report", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("counts each required taxonomy coverage ID once across categories", () => {
|
||||
const taxonomy: QaMaturityTaxonomy = {
|
||||
...testMaturityTaxonomy(),
|
||||
profiles: [
|
||||
{
|
||||
id: "release",
|
||||
description: "Test release profile.",
|
||||
includeAllCategories: false,
|
||||
channelDriver: "qa-channel",
|
||||
categoryIds: [
|
||||
"agent-runtime-and-provider-execution.agent-turn-execution",
|
||||
"agent-runtime-and-provider-execution.tool-execution-controls",
|
||||
],
|
||||
},
|
||||
],
|
||||
surfaces: [
|
||||
{
|
||||
id: "agent-runtime-and-provider-execution",
|
||||
name: "Agent Runtime",
|
||||
family: "test",
|
||||
level: "experimental",
|
||||
categories: [
|
||||
{
|
||||
id: "agent-turn-execution",
|
||||
name: "Agent Turn Execution",
|
||||
category_note: "agent-turn-execution.md",
|
||||
docs: [],
|
||||
search_anchors: [],
|
||||
features: [
|
||||
{
|
||||
name: "shared plus unique",
|
||||
coverageIds: [TEST_EXECUTABLE_COVERAGE_ID, TEST_WEBCHAT_COVERAGE_ID],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "tool-execution-controls",
|
||||
name: "Tool Execution Controls",
|
||||
category_note: "tool-execution-controls.md",
|
||||
docs: [],
|
||||
search_anchors: [],
|
||||
features: [
|
||||
{
|
||||
name: "shared",
|
||||
coverageIds: [TEST_EXECUTABLE_COVERAGE_ID],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const report = buildQaScorecardTaxonomyReport({
|
||||
taxonomy,
|
||||
repoRoot: process.cwd(),
|
||||
scenarios: [
|
||||
scenarioWithCoverage({
|
||||
primary: [TEST_EXECUTABLE_COVERAGE_ID],
|
||||
secondary: [TEST_WEBCHAT_COVERAGE_ID],
|
||||
sourcePath: "qa/scenarios/channels/dm-chat-baseline.yaml",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(report.requiredCoverageIdCount).toBe(2);
|
||||
expect(report.fulfilledCoverageIdCount).toBe(1);
|
||||
expect(report.coverageIdFulfillmentPercent).toBe(50);
|
||||
});
|
||||
|
||||
it("uses script producer evidence as coverage fulfillment", () => {
|
||||
const report = buildQaScorecardTaxonomyReport({
|
||||
taxonomy: testMaturityTaxonomy({
|
||||
@@ -508,7 +437,7 @@ describe("qa coverage report", () => {
|
||||
|
||||
expect(report.validationIssues).toStrictEqual([]);
|
||||
expect(report.fulfilledCategoryCount).toBe(1);
|
||||
expect(report.fulfilledCoverageIdCount).toBe(1);
|
||||
expect(report.fulfilledFeatureCount).toBe(1);
|
||||
expect(report.categories[0]?.evidence).toStrictEqual([
|
||||
{
|
||||
coverageId: TEST_BROWSER_COVERAGE_ID,
|
||||
@@ -626,7 +555,7 @@ describe("qa coverage report", () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(report.fulfilledCoverageIdCount).toBe(0);
|
||||
expect(report.fulfilledFeatureCount).toBe(0);
|
||||
expect(report.categories[0]?.coverageStatus).toBe("partial");
|
||||
expect(report.validationIssues.map((issue) => issue.code)).toEqual([
|
||||
"coverage-id-not-found",
|
||||
|
||||
@@ -331,7 +331,7 @@ function pushScorecardTaxonomyLines(lines: string[], report: QaScorecardTaxonomy
|
||||
`- Fulfilled taxonomy categories: ${report.fulfilledCategoryCount}/${report.requiredCategoryCount} (${report.categoryFulfillmentPercent}%)`,
|
||||
);
|
||||
lines.push(
|
||||
`- Fulfilled taxonomy coverage IDs: ${report.fulfilledCoverageIdCount}/${report.requiredCoverageIdCount} (${report.coverageIdFulfillmentPercent}%)`,
|
||||
`- Fulfilled taxonomy features: ${report.fulfilledFeatureCount}/${report.requiredFeatureCount} (${report.taxonomyFulfillmentPercent}%)`,
|
||||
);
|
||||
lines.push(`- Evidence refs: ${report.evidenceRefCount}`);
|
||||
lines.push(`- Scenario coverage IDs: ${report.scenarioCoverageIdCount}`);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Qa Lab tests cover QA evidence summary behavior.
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
QA_EVIDENCE_SUMMARY_KIND,
|
||||
@@ -124,29 +123,6 @@ describe("evidence summary", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the checked-out ref over an inherited GitHub event SHA", () => {
|
||||
const repoRoot = process.cwd();
|
||||
const checkedOutRef = execFileSync("git", ["rev-parse", "--verify", "HEAD"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
const evidence = buildQaSuiteEvidenceSummary({
|
||||
artifactPaths: [],
|
||||
channelId: "qa-channel",
|
||||
env: {
|
||||
GITHUB_SHA: "bd479958c04a1eadbda8b6105e0722588d71e9ad",
|
||||
} as NodeJS.ProcessEnv,
|
||||
generatedAt: "2026-06-24T12:00:00.000Z",
|
||||
primaryModel: "mock-openai/gpt-5.5",
|
||||
providerMode: "mock-openai",
|
||||
repoRoot,
|
||||
scenarioDefinitions: [{ id: "ref-probe", title: "Ref probe" }],
|
||||
scenarioResults: [{ name: "Ref probe", status: "pass" }],
|
||||
});
|
||||
|
||||
expect(evidence.entries[0]?.execution?.environment.ref).toBe(checkedOutRef);
|
||||
});
|
||||
|
||||
it("builds Telegram live transport evidence entries", () => {
|
||||
const evidence = buildLiveTransportEvidenceSummary({
|
||||
artifactPaths: [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Qa Lab plugin module implements QA evidence summary behavior.
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { z } from "zod";
|
||||
import { splitQaModelRef } from "./model-selection.js";
|
||||
import { getQaProvider, type QaProviderMode } from "./providers/index.js";
|
||||
@@ -117,18 +116,15 @@ const qaEvidenceScorecardCountSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
const qaEvidenceScorecardCoverageCountSchema = qaEvidenceScorecardCountSchema.extend({
|
||||
secondaryOnly: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
const qaEvidenceScorecardCategorySchema = z
|
||||
.object({
|
||||
id: nonEmptyStringSchema,
|
||||
surfaceId: nonEmptyStringSchema,
|
||||
name: nonEmptyStringSchema,
|
||||
status: z.enum(["fulfilled", "partial", "missing"]),
|
||||
features: qaEvidenceScorecardCountSchema,
|
||||
coverageIds: qaEvidenceScorecardCoverageCountSchema,
|
||||
features: qaEvidenceScorecardCountSchema.extend({
|
||||
secondaryOnly: z.number().int().nonnegative(),
|
||||
}),
|
||||
missingCoverageIds: z.array(nonEmptyStringSchema),
|
||||
})
|
||||
.strict();
|
||||
@@ -148,7 +144,6 @@ const qaEvidenceScorecardSchema = z
|
||||
.strict(),
|
||||
categories: qaEvidenceScorecardCountSchema,
|
||||
features: qaEvidenceScorecardCountSchema,
|
||||
coverageIds: qaEvidenceScorecardCountSchema,
|
||||
categoryReports: z.array(qaEvidenceScorecardCategorySchema),
|
||||
})
|
||||
.strict();
|
||||
@@ -293,7 +288,6 @@ type QaEvidenceBuildBase = {
|
||||
channelDriver?: string;
|
||||
packageSource?: QaEvidencePackageSource;
|
||||
profile?: QaEvidenceProfile;
|
||||
repoRoot?: string;
|
||||
runner?: string;
|
||||
};
|
||||
|
||||
@@ -394,31 +388,9 @@ function resolveQaEvidenceChannelDriver(params: { env?: NodeJS.ProcessEnv; fallb
|
||||
return id ? { id } : undefined;
|
||||
}
|
||||
|
||||
function resolveQaEvidenceCheckoutRef(repoRoot?: string) {
|
||||
try {
|
||||
const ref = execFileSync("git", ["rev-parse", "--verify", "HEAD"], {
|
||||
cwd: repoRoot ?? process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
return ref || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveQaEvidenceEnvironment(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
repoRoot?: string;
|
||||
}) {
|
||||
function resolveQaEvidenceEnvironment(env: NodeJS.ProcessEnv | undefined) {
|
||||
return {
|
||||
// GitHub's GITHUB_SHA describes the workflow event, not necessarily the
|
||||
// checked-out ref selected by a manual or remote QA run.
|
||||
ref:
|
||||
params.env?.OPENCLAW_QA_REF?.trim() ||
|
||||
resolveQaEvidenceCheckoutRef(params.repoRoot) ||
|
||||
params.env?.GITHUB_SHA?.trim() ||
|
||||
null,
|
||||
ref: env?.OPENCLAW_QA_REF?.trim() || env?.GITHUB_SHA?.trim() || null,
|
||||
os: process.platform,
|
||||
nodeVersion: process.version,
|
||||
};
|
||||
@@ -578,10 +550,7 @@ export function buildQaSuiteEvidenceSummary(
|
||||
},
|
||||
): QaEvidenceSummaryJson {
|
||||
const provider = buildQaEvidenceProvider(params);
|
||||
const environment = resolveQaEvidenceEnvironment({
|
||||
env: params.env,
|
||||
repoRoot: params.repoRoot,
|
||||
});
|
||||
const environment = resolveQaEvidenceEnvironment(params.env);
|
||||
const packageSource = resolveQaEvidenceBuildPackageSource(params);
|
||||
const runner = resolveQaEvidenceRunner({ env: params.env, fallback: params.runner });
|
||||
const profile = resolveQaEvidenceProfile({
|
||||
@@ -653,10 +622,7 @@ function buildTestRunnerEvidenceSummary(
|
||||
},
|
||||
): QaEvidenceSummaryJson {
|
||||
const provider = buildQaEvidenceProvider(params);
|
||||
const environment = resolveQaEvidenceEnvironment({
|
||||
env: params.env,
|
||||
repoRoot: params.repoRoot,
|
||||
});
|
||||
const environment = resolveQaEvidenceEnvironment(params.env);
|
||||
const packageSource = resolveQaEvidenceBuildPackageSource(params);
|
||||
const runner = resolveQaEvidenceRunner({
|
||||
env: params.env,
|
||||
@@ -760,10 +726,7 @@ export function buildLiveTransportEvidenceSummary(
|
||||
},
|
||||
): QaEvidenceSummaryJson {
|
||||
const provider = buildQaEvidenceProvider(params);
|
||||
const environment = resolveQaEvidenceEnvironment({
|
||||
env: params.env,
|
||||
repoRoot: params.repoRoot,
|
||||
});
|
||||
const environment = resolveQaEvidenceEnvironment(params.env);
|
||||
const packageSource = resolveQaEvidenceBuildPackageSource(params);
|
||||
const runner = resolveQaEvidenceRunner({ env: params.env, fallback: params.runner });
|
||||
const profile = resolveQaEvidenceProfile({
|
||||
|
||||
@@ -1863,7 +1863,6 @@ export async function runDiscordQaLive(params: {
|
||||
generatedAt: finishedAt,
|
||||
primaryModel,
|
||||
providerMode,
|
||||
repoRoot,
|
||||
transportId: "discord",
|
||||
});
|
||||
await fs.writeFile(
|
||||
|
||||
@@ -2037,7 +2037,6 @@ export async function runSlackQaLive(params: {
|
||||
generatedAt: finishedAt,
|
||||
primaryModel,
|
||||
providerMode,
|
||||
repoRoot,
|
||||
transportId: "slack",
|
||||
});
|
||||
await fs.writeFile(
|
||||
|
||||
@@ -2188,7 +2188,6 @@ export async function runTelegramQaLive(params: {
|
||||
generatedAt: finishedAt,
|
||||
primaryModel,
|
||||
providerMode,
|
||||
repoRoot,
|
||||
checks: scenarioResults,
|
||||
transportId: "telegram",
|
||||
});
|
||||
|
||||
@@ -3282,7 +3282,6 @@ export async function runWhatsAppQaLive(params: {
|
||||
generatedAt: finishedAt,
|
||||
primaryModel,
|
||||
providerMode,
|
||||
repoRoot,
|
||||
transportId: "whatsapp",
|
||||
});
|
||||
await fs.writeFile(
|
||||
|
||||
@@ -1846,52 +1846,6 @@ describe("qa mock openai server", () => {
|
||||
expect(memorySearch.status).toBe(200);
|
||||
expect(await memorySearch.text()).toContain('"name":"memory_search"');
|
||||
|
||||
const memoryGetFromPathOnlySearchResult = await fetch(`${server.baseUrl}/v1/responses`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
stream: true,
|
||||
input: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: "Memory tools check: what is the hidden project codename stored only in memory? Use memory tools first.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "function_call_output",
|
||||
output: JSON.stringify({
|
||||
results: [
|
||||
{
|
||||
path: "MEMORY.md",
|
||||
snippet: "Hidden QA fact: the project codename is ORBIT-9.",
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: "Protocol note: acknowledged. Continue with the QA scenario plan.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(memoryGetFromPathOnlySearchResult.status).toBe(200);
|
||||
const memoryGetText = await memoryGetFromPathOnlySearchResult.text();
|
||||
expect(memoryGetText).toContain('"name":"memory_get"');
|
||||
expect(memoryGetText).toContain('\\"path\\":\\"MEMORY.md\\"');
|
||||
expect(memoryGetText).toContain('\\"from\\":1');
|
||||
|
||||
const image = await fetch(`${server.baseUrl}/v1/images/generations`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@@ -2612,8 +2612,8 @@ async function buildResponsesPayload(
|
||||
});
|
||||
}
|
||||
}
|
||||
if (/memory tools check/i.test(allInputText)) {
|
||||
if (!scenarioToolOutput) {
|
||||
if (/memory tools check/i.test(prompt)) {
|
||||
if (!toolOutput) {
|
||||
return buildToolCallEventsWithArgs("memory_search", {
|
||||
query: "project codename ORBIT-9",
|
||||
maxResults: 3,
|
||||
@@ -2623,7 +2623,10 @@ async function buildResponsesPayload(
|
||||
? (toolJson.results as Array<Record<string, unknown>>)
|
||||
: [];
|
||||
const first = results[0];
|
||||
if (typeof first?.path === "string") {
|
||||
if (
|
||||
typeof first?.path === "string" &&
|
||||
(typeof first.startLine === "number" || typeof first.endLine === "number")
|
||||
) {
|
||||
const from =
|
||||
typeof first.startLine === "number"
|
||||
? Math.max(1, first.startLine)
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
// Qa Lab tests cover profile scorecard evidence math.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { QaEvidenceSummaryJson, QaEvidenceSummaryEntry } from "./evidence-summary.js";
|
||||
import { buildQaProfileScorecardEvidence } from "./scorecard-evidence.js";
|
||||
import type { QaScorecardCategoryCoverageReport } from "./scorecard-taxonomy.js";
|
||||
|
||||
function evidenceEntry(coverage: QaEvidenceSummaryEntry["coverage"]): QaEvidenceSummaryEntry {
|
||||
return {
|
||||
test: {
|
||||
kind: "flow",
|
||||
id: "partial-coverage",
|
||||
title: "Partial coverage",
|
||||
},
|
||||
coverage,
|
||||
refs: [],
|
||||
result: {
|
||||
status: "pass",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function evidenceSummary(entries: QaEvidenceSummaryEntry[]): QaEvidenceSummaryJson {
|
||||
return {
|
||||
kind: "openclaw.qa.evidence-summary",
|
||||
schemaVersion: 2,
|
||||
generatedAt: "2026-06-24T00:00:00.000Z",
|
||||
evidenceMode: "full",
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
describe("profile scorecard evidence", () => {
|
||||
it("scores partial multi-id feature coverage by covered coverage IDs", () => {
|
||||
const category: QaScorecardCategoryCoverageReport = {
|
||||
id: "surface.category",
|
||||
taxonomySurfaceId: "surface",
|
||||
taxonomyCategoryName: "Category",
|
||||
coverageStatus: "partial",
|
||||
profiles: ["release"],
|
||||
features: [{ name: "Multi-id feature", coverageIds: ["coverage.one", "coverage.two"] }],
|
||||
coverageIds: ["coverage.one", "coverage.two"],
|
||||
fulfilledCoverageIds: ["coverage.one"],
|
||||
evidence: [],
|
||||
scenarioRefs: [],
|
||||
missingCoverageIds: ["coverage.two"],
|
||||
missingEvidenceRefs: [],
|
||||
};
|
||||
|
||||
const scorecard = buildQaProfileScorecardEvidence({
|
||||
evidence: evidenceSummary([
|
||||
evidenceEntry([
|
||||
{
|
||||
id: "coverage.one",
|
||||
role: "primary",
|
||||
},
|
||||
{
|
||||
id: "coverage.two",
|
||||
role: "secondary",
|
||||
},
|
||||
]),
|
||||
]),
|
||||
filters: {},
|
||||
categories: [category],
|
||||
});
|
||||
|
||||
expect(scorecard.categoryReports[0]?.status).toBe("partial");
|
||||
expect(scorecard.categoryReports[0]?.features).toMatchObject({
|
||||
total: 1,
|
||||
fulfilled: 0,
|
||||
partial: 1,
|
||||
missing: 0,
|
||||
fulfillmentPercent: 0,
|
||||
});
|
||||
expect(scorecard.categoryReports[0]?.coverageIds).toMatchObject({
|
||||
total: 2,
|
||||
fulfilled: 1,
|
||||
secondaryOnly: 1,
|
||||
missing: 1,
|
||||
fulfillmentPercent: 50,
|
||||
});
|
||||
expect(scorecard.coverageIds).toMatchObject({
|
||||
total: 2,
|
||||
fulfilled: 1,
|
||||
missing: 1,
|
||||
fulfillmentPercent: 50,
|
||||
});
|
||||
expect(scorecard.features).toMatchObject({
|
||||
total: 1,
|
||||
fulfilled: 0,
|
||||
partial: 1,
|
||||
missing: 0,
|
||||
fulfillmentPercent: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("counts each profile coverage ID once in global totals", () => {
|
||||
const firstCategory: QaScorecardCategoryCoverageReport = {
|
||||
id: "surface.first",
|
||||
taxonomySurfaceId: "surface",
|
||||
taxonomyCategoryName: "First",
|
||||
coverageStatus: "partial",
|
||||
profiles: ["release"],
|
||||
features: [
|
||||
{ name: "Shared", coverageIds: ["coverage.shared"] },
|
||||
{ name: "Unique", coverageIds: ["coverage.unique"] },
|
||||
],
|
||||
coverageIds: ["coverage.shared", "coverage.unique"],
|
||||
fulfilledCoverageIds: ["coverage.shared"],
|
||||
evidence: [],
|
||||
scenarioRefs: [],
|
||||
missingCoverageIds: ["coverage.unique"],
|
||||
missingEvidenceRefs: [],
|
||||
};
|
||||
const secondCategory: QaScorecardCategoryCoverageReport = {
|
||||
...firstCategory,
|
||||
id: "surface.second",
|
||||
taxonomyCategoryName: "Second",
|
||||
features: [{ name: "Shared again", coverageIds: ["coverage.shared"] }],
|
||||
coverageIds: ["coverage.shared"],
|
||||
missingCoverageIds: [],
|
||||
};
|
||||
|
||||
const scorecard = buildQaProfileScorecardEvidence({
|
||||
evidence: evidenceSummary([
|
||||
evidenceEntry([
|
||||
{
|
||||
id: "coverage.shared",
|
||||
role: "primary",
|
||||
},
|
||||
]),
|
||||
]),
|
||||
filters: {},
|
||||
categories: [firstCategory, secondCategory],
|
||||
});
|
||||
|
||||
expect(scorecard.categoryReports.map((category) => category.coverageIds.total)).toStrictEqual([
|
||||
2, 1,
|
||||
]);
|
||||
expect(scorecard.coverageIds).toMatchObject({
|
||||
total: 2,
|
||||
fulfilled: 1,
|
||||
missing: 1,
|
||||
fulfillmentPercent: 50,
|
||||
});
|
||||
expect(scorecard.features).toMatchObject({
|
||||
total: 3,
|
||||
fulfilled: 2,
|
||||
partial: 0,
|
||||
missing: 1,
|
||||
fulfillmentPercent: 66.7,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
QaScorecardCategoryCoverageReport,
|
||||
QaScorecardEvidenceMode,
|
||||
} from "./scorecard-taxonomy.js";
|
||||
import { readQaScorecardFeatureCoverageByCategory } from "./scorecard-taxonomy.js";
|
||||
|
||||
type QaProfileScorecardFilters = {
|
||||
surface?: string;
|
||||
@@ -45,95 +46,85 @@ function coverageIdsForRole(
|
||||
);
|
||||
}
|
||||
|
||||
function statusForCategory(params: { coverageIdCount: number; fulfilledCoverageIdCount: number }) {
|
||||
if (params.fulfilledCoverageIdCount === 0) {
|
||||
function statusForCategory(params: { featureCount: number; fulfilledFeatureCount: number }) {
|
||||
if (params.fulfilledFeatureCount === 0) {
|
||||
return "missing" as const;
|
||||
}
|
||||
if (params.fulfilledCoverageIdCount === params.coverageIdCount) {
|
||||
if (params.fulfilledFeatureCount === params.featureCount) {
|
||||
return "fulfilled" as const;
|
||||
}
|
||||
return "partial" as const;
|
||||
}
|
||||
|
||||
function featureCounts(
|
||||
features: readonly { coverageIds: readonly string[] }[],
|
||||
primaryCoverageIds: ReadonlySet<string>,
|
||||
) {
|
||||
let fulfilled = 0;
|
||||
let partial = 0;
|
||||
let missing = 0;
|
||||
for (const feature of features) {
|
||||
const coverageIds = uniqueSortedStrings(feature.coverageIds);
|
||||
const fulfilledCoverageIds = coverageIds.filter((coverageId) =>
|
||||
primaryCoverageIds.has(coverageId),
|
||||
).length;
|
||||
if (coverageIds.length > 0 && fulfilledCoverageIds === coverageIds.length) {
|
||||
fulfilled += 1;
|
||||
} else if (fulfilledCoverageIds > 0) {
|
||||
partial += 1;
|
||||
} else {
|
||||
missing += 1;
|
||||
}
|
||||
}
|
||||
return {
|
||||
total: features.length,
|
||||
fulfilled,
|
||||
partial,
|
||||
missing,
|
||||
fulfillmentPercent: percent(fulfilled, features.length),
|
||||
};
|
||||
function categoryFeatureCoverageIds(params: {
|
||||
category: QaScorecardCategoryCoverageReport;
|
||||
featureCoverageByCategoryId?: ReadonlyMap<string, readonly (readonly string[])[]>;
|
||||
}) {
|
||||
const features = params.featureCoverageByCategoryId?.get(params.category.id);
|
||||
return features && features.length > 0
|
||||
? features
|
||||
: params.category.coverageIds.map((coverageId) => [coverageId]);
|
||||
}
|
||||
|
||||
export function buildQaProfileScorecardEvidence(params: {
|
||||
evidence: QaEvidenceSummaryJson;
|
||||
filters: QaProfileScorecardFilters;
|
||||
categories: readonly QaScorecardCategoryCoverageReport[];
|
||||
featureCoverageByCategoryId?: ReadonlyMap<string, readonly (readonly string[])[]>;
|
||||
}): QaEvidenceScorecardJson {
|
||||
const primaryCoverageIds = coverageIdsForRole(params.evidence.entries, "primary");
|
||||
const secondaryCoverageIds = coverageIdsForRole(params.evidence.entries, "secondary");
|
||||
const categoryInputs = params.categories.map((category) => ({
|
||||
category,
|
||||
features: category.features,
|
||||
coverageIds: uniqueSortedStrings(category.coverageIds),
|
||||
}));
|
||||
const categoryReports = categoryInputs.map(({ category, features, coverageIds }) => {
|
||||
const fulfilledCoverageIdCount = coverageIds.filter((coverageId) =>
|
||||
primaryCoverageIds.has(coverageId),
|
||||
const categoryReports = params.categories.map((category) => {
|
||||
const featureCoverageIds = categoryFeatureCoverageIds({
|
||||
category,
|
||||
featureCoverageByCategoryId: params.featureCoverageByCategoryId,
|
||||
});
|
||||
const fulfilledFeatureCount = featureCoverageIds.filter(
|
||||
(coverageIds) =>
|
||||
coverageIds.length > 0 &&
|
||||
coverageIds.every((coverageId) => primaryCoverageIds.has(coverageId)),
|
||||
).length;
|
||||
const secondaryOnlyCoverageIdCount = coverageIds.filter(
|
||||
(coverageId) => !primaryCoverageIds.has(coverageId) && secondaryCoverageIds.has(coverageId),
|
||||
const secondaryOnlyFeatureCount = featureCoverageIds.filter(
|
||||
(coverageIds) =>
|
||||
coverageIds.some((coverageId) => !primaryCoverageIds.has(coverageId)) &&
|
||||
coverageIds.some(
|
||||
(coverageId) =>
|
||||
!primaryCoverageIds.has(coverageId) && secondaryCoverageIds.has(coverageId),
|
||||
),
|
||||
).length;
|
||||
const missingCoverageIds = uniqueSortedStrings(
|
||||
coverageIds.filter((coverageId) => !primaryCoverageIds.has(coverageId)),
|
||||
featureCoverageIds.flatMap((coverageIds) =>
|
||||
coverageIds.filter((coverageId) => !primaryCoverageIds.has(coverageId)),
|
||||
),
|
||||
);
|
||||
const missingCoverageIdCount = coverageIds.length - fulfilledCoverageIdCount;
|
||||
const missingFeatureCount = featureCoverageIds.length - fulfilledFeatureCount;
|
||||
return {
|
||||
id: category.id,
|
||||
surfaceId: category.taxonomySurfaceId,
|
||||
name: category.taxonomyCategoryName,
|
||||
status: statusForCategory({
|
||||
coverageIdCount: coverageIds.length,
|
||||
fulfilledCoverageIdCount,
|
||||
featureCount: featureCoverageIds.length,
|
||||
fulfilledFeatureCount,
|
||||
}),
|
||||
features: featureCounts(features, primaryCoverageIds),
|
||||
coverageIds: {
|
||||
total: coverageIds.length,
|
||||
fulfilled: fulfilledCoverageIdCount,
|
||||
secondaryOnly: secondaryOnlyCoverageIdCount,
|
||||
missing: missingCoverageIdCount,
|
||||
fulfillmentPercent: percent(fulfilledCoverageIdCount, coverageIds.length),
|
||||
features: {
|
||||
total: featureCoverageIds.length,
|
||||
fulfilled: fulfilledFeatureCount,
|
||||
secondaryOnly: secondaryOnlyFeatureCount,
|
||||
missing: missingFeatureCount,
|
||||
fulfillmentPercent: percent(fulfilledFeatureCount, featureCoverageIds.length),
|
||||
},
|
||||
missingCoverageIds,
|
||||
};
|
||||
});
|
||||
const profileCoverageIds = uniqueSortedStrings(
|
||||
categoryInputs.flatMap((input) => input.coverageIds),
|
||||
const featureCount = categoryReports.reduce((sum, category) => sum + category.features.total, 0);
|
||||
const fulfilledFeatureCount = categoryReports.reduce(
|
||||
(sum, category) => sum + category.features.fulfilled,
|
||||
0,
|
||||
);
|
||||
const missingFeatureCount = categoryReports.reduce(
|
||||
(sum, category) => sum + category.features.missing,
|
||||
0,
|
||||
);
|
||||
const coverageIdCount = profileCoverageIds.length;
|
||||
const fulfilledCoverageIdCount = profileCoverageIds.filter((coverageId) =>
|
||||
primaryCoverageIds.has(coverageId),
|
||||
).length;
|
||||
const missingCoverageIdCount = coverageIdCount - fulfilledCoverageIdCount;
|
||||
const fulfilledCategoryCount = categoryReports.filter(
|
||||
(category) => category.status === "fulfilled",
|
||||
).length;
|
||||
@@ -143,7 +134,6 @@ export function buildQaProfileScorecardEvidence(params: {
|
||||
const missingCategoryCount = categoryReports.filter(
|
||||
(category) => category.status === "missing",
|
||||
).length;
|
||||
const profileFeatures = categoryInputs.flatMap((input) => input.features);
|
||||
return {
|
||||
filters: {
|
||||
surface: nullableFilter(params.filters.surface),
|
||||
@@ -159,12 +149,11 @@ export function buildQaProfileScorecardEvidence(params: {
|
||||
missing: missingCategoryCount,
|
||||
fulfillmentPercent: percent(fulfilledCategoryCount, categoryReports.length),
|
||||
},
|
||||
features: featureCounts(profileFeatures, primaryCoverageIds),
|
||||
coverageIds: {
|
||||
total: coverageIdCount,
|
||||
fulfilled: fulfilledCoverageIdCount,
|
||||
missing: missingCoverageIdCount,
|
||||
fulfillmentPercent: percent(fulfilledCoverageIdCount, coverageIdCount),
|
||||
features: {
|
||||
total: featureCount,
|
||||
fulfilled: fulfilledFeatureCount,
|
||||
missing: missingFeatureCount,
|
||||
fulfillmentPercent: percent(fulfilledFeatureCount, featureCount),
|
||||
},
|
||||
categoryReports,
|
||||
};
|
||||
@@ -184,6 +173,7 @@ export async function attachQaProfileScorecardEvidenceToFile(params: {
|
||||
evidence,
|
||||
filters: params.filters,
|
||||
categories: params.categories,
|
||||
featureCoverageByCategoryId: readQaScorecardFeatureCoverageByCategory(),
|
||||
});
|
||||
const nextEvidence = attachQaEvidenceScorecard({
|
||||
summary: evidence,
|
||||
|
||||
@@ -376,7 +376,6 @@ export type QaScorecardCategoryCoverageReport = {
|
||||
taxonomyCategoryName: string;
|
||||
coverageStatus: "covered" | "partial" | "missing";
|
||||
profiles: string[];
|
||||
features: QaScorecardCategoryFeatureCoverageReport[];
|
||||
coverageIds: string[];
|
||||
fulfilledCoverageIds: string[];
|
||||
evidence: QaScorecardEvidenceReport[];
|
||||
@@ -385,11 +384,6 @@ export type QaScorecardCategoryCoverageReport = {
|
||||
missingEvidenceRefs: string[];
|
||||
};
|
||||
|
||||
export type QaScorecardCategoryFeatureCoverageReport = {
|
||||
name: string;
|
||||
coverageIds: string[];
|
||||
};
|
||||
|
||||
export type QaScorecardProfileReport = {
|
||||
id: string;
|
||||
evidenceMode: QaScorecardEvidenceMode;
|
||||
@@ -409,9 +403,9 @@ export type QaScorecardTaxonomyReport = {
|
||||
requiredCategoryCount: number;
|
||||
fulfilledCategoryCount: number;
|
||||
categoryFulfillmentPercent: number;
|
||||
requiredCoverageIdCount: number;
|
||||
fulfilledCoverageIdCount: number;
|
||||
coverageIdFulfillmentPercent: number;
|
||||
requiredFeatureCount: number;
|
||||
fulfilledFeatureCount: number;
|
||||
taxonomyFulfillmentPercent: number;
|
||||
evidenceRefCount: number;
|
||||
scenarioCoverageIdCount: number;
|
||||
unknownCoverageIdCount: number;
|
||||
@@ -837,6 +831,16 @@ function buildMaturityRefs(taxonomy: QaMaturityTaxonomy | null) {
|
||||
return { categories, coverageIds };
|
||||
}
|
||||
|
||||
export function readQaScorecardFeatureCoverageByCategory(repoRoot?: string) {
|
||||
const maturityRefs = buildMaturityRefs(readQaMaturityTaxonomy(repoRoot));
|
||||
return new Map(
|
||||
[...maturityRefs.categories.entries()].map(([categoryId, category]) => [
|
||||
categoryId,
|
||||
category.features.map((feature) => feature.coverageIds),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export function readQaScorecardProfileOptions(profileId: string | undefined, repoRoot?: string) {
|
||||
const profile = profileId?.trim();
|
||||
if (!profile) {
|
||||
@@ -1007,8 +1011,8 @@ export function buildQaScorecardTaxonomyReport(params: {
|
||||
...categoryIdsWithEvidence,
|
||||
]);
|
||||
|
||||
const requiredCoverageIds = new Set<string>();
|
||||
const fulfilledRequiredCoverageIds = new Set<string>();
|
||||
let requiredFeatureCount = 0;
|
||||
let fulfilledFeatureCount = 0;
|
||||
for (const categoryId of relevantCategoryIds) {
|
||||
const category = maturityRefs.categories.get(categoryId);
|
||||
if (!category) {
|
||||
@@ -1074,23 +1078,21 @@ export function buildQaScorecardTaxonomyReport(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const fulfilledCoverageIdCountForCategory = category.coverageIds.filter((coverageId) =>
|
||||
fulfilledCoverageIds.has(coverageId),
|
||||
const fulfilledFeatureCountForCategory = category.features.filter(
|
||||
(feature) =>
|
||||
feature.coverageIds.length > 0 &&
|
||||
feature.coverageIds.every((coverageId) => fulfilledCoverageIds.has(coverageId)),
|
||||
).length;
|
||||
if (required) {
|
||||
for (const coverageId of category.coverageIds) {
|
||||
requiredCoverageIds.add(coverageId);
|
||||
if (fulfilledCoverageIds.has(coverageId)) {
|
||||
fulfilledRequiredCoverageIds.add(coverageId);
|
||||
}
|
||||
}
|
||||
requiredFeatureCount += category.features.length;
|
||||
fulfilledFeatureCount += fulfilledFeatureCountForCategory;
|
||||
pushMissingPrimaryIssues({
|
||||
issues,
|
||||
category,
|
||||
coverageIdsWithPrimaryEvidence: fulfilledCoverageIds,
|
||||
coverageIdsWithSecondaryEvidence: secondaryOnlyCoverageIds,
|
||||
});
|
||||
if (fulfilledCoverageIdCountForCategory === 0) {
|
||||
if (fulfilledFeatureCountForCategory === 0) {
|
||||
issues.push({
|
||||
code: "profile-category-missing-evidence",
|
||||
severity: "warning",
|
||||
@@ -1105,8 +1107,8 @@ export function buildQaScorecardTaxonomyReport(params: {
|
||||
: [];
|
||||
const coverageStatus =
|
||||
required &&
|
||||
category.coverageIds.length > 0 &&
|
||||
fulfilledCoverageIdCountForCategory === category.coverageIds.length
|
||||
category.features.length > 0 &&
|
||||
fulfilledFeatureCountForCategory === category.features.length
|
||||
? "covered"
|
||||
: evidenceReports.length > 0
|
||||
? "partial"
|
||||
@@ -1118,7 +1120,6 @@ export function buildQaScorecardTaxonomyReport(params: {
|
||||
taxonomyCategoryName: category.categoryName,
|
||||
coverageStatus,
|
||||
profiles: profileIds,
|
||||
features: category.features,
|
||||
coverageIds: category.coverageIds,
|
||||
fulfilledCoverageIds: uniqueSorted(fulfilledCoverageIds),
|
||||
evidence: evidenceReports.toSorted((left, right) =>
|
||||
@@ -1155,12 +1156,9 @@ export function buildQaScorecardTaxonomyReport(params: {
|
||||
requiredCategoryCount: requiredCategories.length,
|
||||
fulfilledCategoryCount,
|
||||
categoryFulfillmentPercent: percent(fulfilledCategoryCount, requiredCategories.length),
|
||||
requiredCoverageIdCount: requiredCoverageIds.size,
|
||||
fulfilledCoverageIdCount: fulfilledRequiredCoverageIds.size,
|
||||
coverageIdFulfillmentPercent: percent(
|
||||
fulfilledRequiredCoverageIds.size,
|
||||
requiredCoverageIds.size,
|
||||
),
|
||||
requiredFeatureCount,
|
||||
fulfilledFeatureCount,
|
||||
taxonomyFulfillmentPercent: percent(fulfilledFeatureCount, requiredFeatureCount),
|
||||
evidenceRefCount: categories.reduce((count, category) => count + category.evidence.length, 0),
|
||||
scenarioCoverageIdCount: allScenarioCoverageIds.length,
|
||||
unknownCoverageIdCount: unknownCoverageIds.length,
|
||||
|
||||
@@ -469,94 +469,6 @@ describe("qa suite runtime launcher", () => {
|
||||
expect(runQaTestFileScenarios).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("starts native suite proof before isolated flow work fills the weighted queue", async () => {
|
||||
const repoRoot = await makeTempRepo("qa-suite-native-before-isolated-");
|
||||
let releaseShared!: () => void;
|
||||
let markSharedStarted!: () => void;
|
||||
const sharedStarted = new Promise<void>((resolve) => {
|
||||
markSharedStarted = resolve;
|
||||
});
|
||||
const sharedBlocked = new Promise<void>((resolve) => {
|
||||
releaseShared = resolve;
|
||||
});
|
||||
let releaseTestFile!: () => void;
|
||||
let markTestFileStarted!: () => void;
|
||||
const testFileStarted = new Promise<void>((resolve) => {
|
||||
markTestFileStarted = resolve;
|
||||
});
|
||||
const testFileBlocked = new Promise<void>((resolve) => {
|
||||
releaseTestFile = resolve;
|
||||
});
|
||||
runQaFlowSuite.mockImplementationOnce(
|
||||
async (params: { outputDir?: string; scenarioIds?: string[] } | undefined) => {
|
||||
markSharedStarted();
|
||||
await sharedBlocked;
|
||||
const outputDir = params?.outputDir ?? "/tmp/qa-flow";
|
||||
const evidencePath = path.join(outputDir, "qa-evidence.json");
|
||||
await writeEvidence(evidencePath);
|
||||
const scenarioIds = params?.scenarioIds ?? ["channel-chat-baseline"];
|
||||
return {
|
||||
outputDir,
|
||||
evidencePath,
|
||||
reportPath: path.join(outputDir, "qa-suite-report.md"),
|
||||
summaryPath: path.join(outputDir, "qa-suite-summary.json"),
|
||||
report: "# QA Suite Report\n",
|
||||
scenarios: scenarioIds.map((scenarioId) => ({
|
||||
name: scenarioId,
|
||||
status: "pass",
|
||||
steps: [],
|
||||
})),
|
||||
watchUrl: "http://127.0.0.1:43124",
|
||||
};
|
||||
},
|
||||
);
|
||||
runQaTestFileScenarios.mockImplementationOnce(
|
||||
async (params: {
|
||||
outputDir: string;
|
||||
scenarios: Array<{ id: string; execution: { kind: "script" | "vitest" | "playwright" } }>;
|
||||
}) => {
|
||||
markTestFileStarted();
|
||||
await testFileBlocked;
|
||||
const evidencePath = path.join(params.outputDir, "qa-evidence.json");
|
||||
await writeEvidence(evidencePath);
|
||||
return {
|
||||
outputDir: params.outputDir,
|
||||
executionKind: params.scenarios[0]?.execution.kind ?? "playwright",
|
||||
evidencePath,
|
||||
results: params.scenarios.map((scenarioItem) => ({
|
||||
durationMs: 1,
|
||||
logPath: path.join(params.outputDir, `${scenarioItem.id}.log`),
|
||||
scenario: scenarioItem,
|
||||
status: "pass",
|
||||
})),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const runPromise = runQaSuite({
|
||||
repoRoot,
|
||||
outputDir: ".artifacts/qa-e2e/native-before-isolated",
|
||||
concurrency: 2,
|
||||
scenarioIds: [
|
||||
"channel-chat-baseline",
|
||||
"group-visible-reply-tool",
|
||||
"control-ui-chat-flow-playwright",
|
||||
],
|
||||
});
|
||||
await sharedStarted;
|
||||
await testFileStarted;
|
||||
await Promise.resolve();
|
||||
|
||||
expect(runQaFlowSuite).toHaveBeenCalledTimes(1);
|
||||
expect(runQaTestFileScenarios).toHaveBeenCalledTimes(1);
|
||||
|
||||
releaseTestFile();
|
||||
releaseShared();
|
||||
await runPromise;
|
||||
|
||||
expect(runQaFlowSuite).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("waits for already-started partitions before rejecting a unified suite", async () => {
|
||||
const repoRoot = await makeTempRepo("qa-suite-reject-settle-");
|
||||
let releaseTestFile!: () => void;
|
||||
|
||||
@@ -448,9 +448,7 @@ async function runUnifiedQaSuite(params: {
|
||||
);
|
||||
const evidenceSummaries: QaEvidenceSummaryJson[] = [];
|
||||
const scenarioResultsById = new Map<string, QaSuiteScenarioResult>();
|
||||
const sharedFlowPartitionTasks: QaUnifiedPartitionTask[] = [];
|
||||
const isolatedFlowPartitionTasks: QaUnifiedPartitionTask[] = [];
|
||||
const testFilePartitionTasks: QaUnifiedPartitionTask[] = [];
|
||||
const partitionTasks: QaUnifiedPartitionTask[] = [];
|
||||
if (params.plan.flowScenarios.length > 0) {
|
||||
const sharedFlowScenarios = params.plan.flowScenarios.filter(
|
||||
(scenario) => !scenarioRequiresIsolatedQaSuiteWorker(scenario),
|
||||
@@ -490,7 +488,7 @@ async function runUnifiedQaSuite(params: {
|
||||
for (const partition of flowPartitions) {
|
||||
const isolatedPartition =
|
||||
partition.kind === "isolated" || partition.kind.startsWith("isolated-");
|
||||
const task = {
|
||||
partitionTasks.push({
|
||||
weight: partition.concurrency,
|
||||
run: async () => {
|
||||
const result = await runFlowSuite({
|
||||
@@ -527,16 +525,11 @@ async function runUnifiedQaSuite(params: {
|
||||
scenarioResults,
|
||||
};
|
||||
},
|
||||
} satisfies QaUnifiedPartitionTask;
|
||||
if (isolatedPartition) {
|
||||
isolatedFlowPartitionTasks.push(task);
|
||||
} else {
|
||||
sharedFlowPartitionTasks.push(task);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (params.plan.testFileScenariosByKind.size > 0) {
|
||||
testFilePartitionTasks.push({
|
||||
partitionTasks.push({
|
||||
weight: 1,
|
||||
run: async () => {
|
||||
const testFileEvidenceSummaries: QaEvidenceSummaryJson[] = [];
|
||||
@@ -568,11 +561,6 @@ async function runUnifiedQaSuite(params: {
|
||||
},
|
||||
});
|
||||
}
|
||||
const partitionTasks = [
|
||||
...sharedFlowPartitionTasks,
|
||||
...testFilePartitionTasks,
|
||||
...isolatedFlowPartitionTasks,
|
||||
];
|
||||
const partitionResults = await runWeightedUnifiedPartitionTasks(partitionTasks, concurrency);
|
||||
for (const partitionResult of partitionResults) {
|
||||
for (const scenarioResult of partitionResult.scenarioResults) {
|
||||
|
||||
@@ -848,7 +848,6 @@ async function runQaRuntimeParitySuite(params: {
|
||||
const finishedAt = new Date();
|
||||
const { evidence, evidencePath, report, reportPath, summaryPath } = await writeQaSuiteArtifacts(
|
||||
{
|
||||
repoRoot: params.repoRoot,
|
||||
outputDir: params.outputDir,
|
||||
startedAt: params.startedAt,
|
||||
finishedAt,
|
||||
@@ -901,7 +900,6 @@ async function runQaRuntimeParitySuite(params: {
|
||||
}
|
||||
|
||||
async function writeQaSuiteArtifacts(params: {
|
||||
repoRoot?: string;
|
||||
outputDir: string;
|
||||
startedAt: Date;
|
||||
finishedAt: Date;
|
||||
@@ -976,7 +974,6 @@ async function writeQaSuiteArtifacts(params: {
|
||||
generatedAt: params.finishedAt.toISOString(),
|
||||
primaryModel: params.primaryModel,
|
||||
providerMode: params.providerMode,
|
||||
repoRoot: params.repoRoot,
|
||||
scenarioDefinitions: params.scenarioDefinitions,
|
||||
scenarioResults: params.scenarios,
|
||||
})
|
||||
@@ -1299,7 +1296,6 @@ export async function runQaFlowSuite(params?: QaSuiteRunParams): Promise<QaSuite
|
||||
.then(async () => {
|
||||
const partialFinishedAt = new Date();
|
||||
const { report, reportPath } = await writeQaSuiteArtifacts({
|
||||
repoRoot,
|
||||
outputDir,
|
||||
startedAt,
|
||||
finishedAt: partialFinishedAt,
|
||||
@@ -1452,7 +1448,6 @@ export async function runQaFlowSuite(params?: QaSuiteRunParams): Promise<QaSuite
|
||||
});
|
||||
const { evidence, evidencePath, report, reportPath, summaryPath } =
|
||||
await writeQaSuiteArtifacts({
|
||||
repoRoot,
|
||||
outputDir,
|
||||
startedAt,
|
||||
finishedAt,
|
||||
@@ -1725,7 +1720,6 @@ export async function runQaFlowSuite(params?: QaSuiteRunParams): Promise<QaSuite
|
||||
});
|
||||
const { evidence, evidencePath, report, reportPath, summaryPath } = await writeQaSuiteArtifacts(
|
||||
{
|
||||
repoRoot,
|
||||
outputDir,
|
||||
startedAt,
|
||||
finishedAt,
|
||||
|
||||
@@ -555,7 +555,6 @@ function buildTestFileEvidence(params: {
|
||||
kind: QaTestFileExecutionKind;
|
||||
primaryModel: string;
|
||||
providerMode: QaProviderMode;
|
||||
repoRoot: string;
|
||||
results: readonly QaTestFileScenarioResult[];
|
||||
evidenceMode?: QaScorecardEvidenceMode;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -582,7 +581,6 @@ function buildTestFileEvidence(params: {
|
||||
generatedAt: params.generatedAt,
|
||||
primaryModel: params.primaryModel,
|
||||
providerMode: params.providerMode,
|
||||
repoRoot: params.repoRoot,
|
||||
targets: fallbackResults.map((result) => buildScenarioEvidenceTarget(result.scenario)),
|
||||
results: fallbackResults.map((result) => ({
|
||||
id: result.scenario.id,
|
||||
@@ -618,7 +616,6 @@ function buildTestFileEvidence(params: {
|
||||
generatedAt: params.generatedAt,
|
||||
primaryModel: params.primaryModel,
|
||||
providerMode: params.providerMode,
|
||||
repoRoot: params.repoRoot,
|
||||
targets: params.results.map((result) => buildScenarioEvidenceTarget(result.scenario)),
|
||||
results: params.results.map((result) => ({
|
||||
id: result.scenario.id,
|
||||
@@ -805,7 +802,6 @@ export async function runQaTestFileScenarios(
|
||||
kind,
|
||||
primaryModel: params.primaryModel,
|
||||
providerMode: params.providerMode,
|
||||
repoRoot: params.repoRoot,
|
||||
results,
|
||||
});
|
||||
const paths = await writeTestFileEvidenceFile({
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||
import { danger, logVerbose, warn } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
@@ -112,13 +112,14 @@ function resolveSlackCommandMenuModelContext(params: {
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.agentId });
|
||||
const entry = getSessionEntry({ storePath, sessionKey: params.sessionKey });
|
||||
const store = loadSessionStore(storePath);
|
||||
const entry = store[params.sessionKey];
|
||||
if (entry?.modelOverrideSource === "auto" && normalizeOptionalString(entry.modelOverride)) {
|
||||
return { provider: defaultModel.provider, model: defaultModel.model };
|
||||
}
|
||||
const override = resolveStoredModelOverride({
|
||||
sessionEntry: entry,
|
||||
loadSessionEntry: (sessionKey) => getSessionEntry({ storePath, sessionKey }),
|
||||
sessionStore: store,
|
||||
sessionKey: params.sessionKey,
|
||||
defaultProvider: defaultModel.provider,
|
||||
});
|
||||
|
||||
@@ -90,10 +90,6 @@ export function mergeTelegramAccountConfig(
|
||||
baseAllowFrom: base.allowFrom,
|
||||
accountAllowFrom: account.allowFrom,
|
||||
});
|
||||
const capabilities =
|
||||
Array.isArray(account.capabilities) && account.capabilities.length === 0
|
||||
? base.capabilities
|
||||
: (account.capabilities ?? base.capabilities);
|
||||
|
||||
return { ...base, ...account, allowFrom, capabilities, groups };
|
||||
return { ...base, ...account, allowFrom, groups };
|
||||
}
|
||||
|
||||
@@ -1703,25 +1703,6 @@ describe("handleTelegramAction", () => {
|
||||
expect(sendMessageTelegram).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows inline buttons when legacy capabilities are empty", async () => {
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "@testchannel",
|
||||
content: "Choose",
|
||||
presentation: {
|
||||
blocks: [{ type: "buttons", buttons: [{ label: "Ok", value: "cmd:ok" }] }],
|
||||
},
|
||||
},
|
||||
telegramConfig({ capabilities: [] }),
|
||||
);
|
||||
const call = mockCall(sendMessageTelegram, 0, "empty legacy capabilities");
|
||||
expect(call[0]).toBe("@testchannel");
|
||||
expect(requireRecord(call[2], "empty legacy capabilities options").buttons).toEqual([
|
||||
[{ text: "Ok", callback_data: "cmd:ok" }],
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses interactive button labels as fallback text when message text is omitted", async () => {
|
||||
await handleTelegramAction(
|
||||
{
|
||||
|
||||
@@ -310,11 +310,12 @@ export function createTelegramBotCore(
|
||||
`agent:${agentId}:telegram:group:${buildTelegramGroupPeerId(params.chatId, params.messageThreadId)}`;
|
||||
const storePath = telegramDeps.resolveStorePath(cfg.session?.store, { agentId });
|
||||
try {
|
||||
const getSessionEntry = telegramDeps.getSessionEntry;
|
||||
if (!getSessionEntry) {
|
||||
const loadSessionStore = telegramDeps.loadSessionStore;
|
||||
if (!loadSessionStore) {
|
||||
return undefined;
|
||||
}
|
||||
const entry = getSessionEntry({ storePath, sessionKey });
|
||||
const store = loadSessionStore(storePath);
|
||||
const entry = store[sessionKey];
|
||||
if (entry?.groupActivation === "always") {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Telegram plugin module implements bot message dispatch behavior.
|
||||
export {
|
||||
getSessionEntry,
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
resolveStorePath,
|
||||
type SessionEntry,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
export { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
||||
export { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
|
||||
|
||||
@@ -83,7 +83,6 @@ const appendAssistantMirrorMessageByIdentity = vi.hoisted(() =>
|
||||
messageId: "m1",
|
||||
})),
|
||||
);
|
||||
const getSessionEntry = vi.hoisted(() => vi.fn());
|
||||
const loadSessionStore = vi.hoisted(() => vi.fn());
|
||||
const readLatestAssistantTextByIdentity = vi.hoisted(() =>
|
||||
vi.fn<() => Promise<{ text: string; timestamp?: number } | undefined>>(async () => undefined),
|
||||
@@ -103,6 +102,11 @@ const getAgentScopedMediaLocalRoots = vi.hoisted(() =>
|
||||
);
|
||||
const resolveChunkMode = vi.hoisted(() => vi.fn(() => undefined));
|
||||
const resolveMarkdownTableMode = vi.hoisted(() => vi.fn(() => "preserve"));
|
||||
const resolveSessionStoreEntry = vi.hoisted(() =>
|
||||
vi.fn(({ store, sessionKey }: { store: Record<string, unknown>; sessionKey: string }) => ({
|
||||
existing: store[sessionKey],
|
||||
})),
|
||||
);
|
||||
|
||||
vi.mock("./draft-stream.js", () => ({
|
||||
createTelegramDraftStream,
|
||||
@@ -149,11 +153,12 @@ vi.mock("./send.js", () => ({
|
||||
|
||||
vi.mock("./bot-message-dispatch.runtime.js", () => ({
|
||||
generateTopicLabel,
|
||||
getSessionEntry,
|
||||
getAgentScopedMediaLocalRoots,
|
||||
loadSessionStore,
|
||||
resolveAutoTopicLabelConfig: resolveAutoTopicLabelConfigRuntime,
|
||||
resolveChunkMode,
|
||||
resolveMarkdownTableMode,
|
||||
resolveSessionStoreEntry,
|
||||
resolveStorePath,
|
||||
}));
|
||||
|
||||
@@ -198,7 +203,6 @@ function installTelegramStateRuntimeForTest(): void {
|
||||
const telegramDepsForTest: TelegramBotDeps = {
|
||||
getRuntimeConfig: loadConfig as TelegramBotDeps["getRuntimeConfig"],
|
||||
resolveStorePath: resolveStorePath as TelegramBotDeps["resolveStorePath"],
|
||||
getSessionEntry: getSessionEntry as TelegramBotDeps["getSessionEntry"],
|
||||
loadSessionStore: loadSessionStore as TelegramBotDeps["loadSessionStore"],
|
||||
readChannelAllowFromStore:
|
||||
readChannelAllowFromStore as TelegramBotDeps["readChannelAllowFromStore"],
|
||||
@@ -262,13 +266,13 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
wasSentByBot.mockReset();
|
||||
appendAssistantMirrorMessageByIdentity.mockReset();
|
||||
readLatestAssistantTextByIdentity.mockReset();
|
||||
getSessionEntry.mockReset();
|
||||
loadSessionStore.mockReset();
|
||||
resolveStorePath.mockReset();
|
||||
generateTopicLabel.mockReset();
|
||||
getAgentScopedMediaLocalRoots.mockClear();
|
||||
resolveChunkMode.mockClear();
|
||||
resolveMarkdownTableMode.mockClear();
|
||||
resolveSessionStoreEntry.mockClear();
|
||||
describeStickerImage.mockReset();
|
||||
loadModelCatalog.mockReset();
|
||||
findModelInCatalog.mockReset();
|
||||
@@ -321,10 +325,6 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
messageId: "m1",
|
||||
});
|
||||
loadSessionStore.mockReturnValue({});
|
||||
getSessionEntry.mockImplementation(
|
||||
({ sessionKey }: { sessionKey: string }) =>
|
||||
(loadSessionStore() as Record<string, unknown>)[sessionKey],
|
||||
);
|
||||
generateTopicLabel.mockResolvedValue("Topic label");
|
||||
describeStickerImage.mockResolvedValue(null);
|
||||
loadModelCatalog.mockResolvedValue({});
|
||||
|
||||
@@ -72,11 +72,11 @@ import { deduplicateBlockSentMedia } from "./bot-message-dispatch.media-dedup.js
|
||||
import {
|
||||
generateTopicLabel,
|
||||
getAgentScopedMediaLocalRoots,
|
||||
getSessionEntry,
|
||||
loadSessionStore,
|
||||
resolveAutoTopicLabelConfig,
|
||||
resolveChunkMode,
|
||||
resolveMarkdownTableMode,
|
||||
type SessionEntry,
|
||||
resolveSessionStoreEntry,
|
||||
} from "./bot-message-dispatch.runtime.js";
|
||||
import type { TelegramBotOptions } from "./bot.types.js";
|
||||
import { deliverReplies, emitInternalMessageSentHook } from "./bot/delivery.js";
|
||||
@@ -143,7 +143,6 @@ import {
|
||||
shouldSupersedeTelegramReplyFence,
|
||||
supersedeTelegramReplyFence,
|
||||
} from "./telegram-reply-fence.js";
|
||||
import { clipTelegramProgressText } from "./truncate.js";
|
||||
|
||||
export { resetTelegramReplyFenceForTests };
|
||||
|
||||
@@ -244,37 +243,33 @@ export type TelegramDispatchResult =
|
||||
type TelegramReasoningLevel = "off" | "on" | "stream";
|
||||
|
||||
type TelegramTranscriptMirrorPayload = { text?: string; mediaUrls?: string[] };
|
||||
type TelegramSessionStore = ReturnType<typeof loadSessionStore>;
|
||||
type TelegramScopedTranscriptSession = { sessionId: string; storePath: string };
|
||||
type FreshTelegramSessionEntryLoader = ((
|
||||
agentId: string,
|
||||
sessionKey: string,
|
||||
) => {
|
||||
type FreshTelegramSessionStoreLoader = ((agentId: string) => {
|
||||
storePath: string;
|
||||
entry?: SessionEntry;
|
||||
store: TelegramSessionStore;
|
||||
}) & {
|
||||
clear: () => void;
|
||||
};
|
||||
|
||||
function createFreshTelegramSessionEntryLoader(params: {
|
||||
function createFreshTelegramSessionStoreLoader(params: {
|
||||
cfg: OpenClawConfig;
|
||||
telegramDeps: TelegramBotDeps;
|
||||
}): FreshTelegramSessionEntryLoader {
|
||||
const entriesByPathAndKey = new Map<string, SessionEntry | undefined>();
|
||||
const load = ((agentId: string, sessionKey: string) => {
|
||||
}): FreshTelegramSessionStoreLoader {
|
||||
const storesByPath = new Map<string, TelegramSessionStore>();
|
||||
const load = ((agentId: string) => {
|
||||
const storePath = params.telegramDeps.resolveStorePath(params.cfg.session?.store, { agentId });
|
||||
const cacheKey = `${storePath}\0${sessionKey}`;
|
||||
if (entriesByPathAndKey.has(cacheKey)) {
|
||||
return { storePath, entry: entriesByPathAndKey.get(cacheKey) };
|
||||
const cachedStore = storesByPath.get(storePath);
|
||||
if (cachedStore) {
|
||||
return { storePath, store: cachedStore };
|
||||
}
|
||||
const entry = (params.telegramDeps.getSessionEntry ?? getSessionEntry)({
|
||||
storePath,
|
||||
sessionKey,
|
||||
readConsistency: "latest",
|
||||
const store = (params.telegramDeps.loadSessionStore ?? loadSessionStore)(storePath, {
|
||||
skipCache: true,
|
||||
});
|
||||
entriesByPathAndKey.set(cacheKey, entry);
|
||||
return { storePath, entry };
|
||||
}) as FreshTelegramSessionEntryLoader;
|
||||
load.clear = () => entriesByPathAndKey.clear();
|
||||
storesByPath.set(storePath, store);
|
||||
return { storePath, store };
|
||||
}) as FreshTelegramSessionStoreLoader;
|
||||
load.clear = () => storesByPath.clear();
|
||||
return load;
|
||||
}
|
||||
|
||||
@@ -282,7 +277,7 @@ function resolveTelegramReasoningLevel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
sessionKey?: string;
|
||||
agentId: string;
|
||||
loadFreshSessionEntry: FreshTelegramSessionEntryLoader;
|
||||
loadFreshSessionStore: FreshTelegramSessionStoreLoader;
|
||||
}): TelegramReasoningLevel {
|
||||
const { cfg, sessionKey, agentId } = params;
|
||||
const configDefault = resolveTelegramConfigReasoningDefault(cfg, agentId);
|
||||
@@ -290,7 +285,8 @@ function resolveTelegramReasoningLevel(params: {
|
||||
return configDefault;
|
||||
}
|
||||
try {
|
||||
const { entry } = params.loadFreshSessionEntry(agentId, sessionKey);
|
||||
const { store } = params.loadFreshSessionStore(agentId);
|
||||
const entry = resolveSessionStoreEntry({ store, sessionKey }).existing;
|
||||
const level = entry?.reasoningLevel;
|
||||
if (level === "on" || level === "stream" || level === "off") {
|
||||
return level;
|
||||
@@ -321,10 +317,11 @@ function resolveTelegramMirroredTranscriptText(
|
||||
|
||||
function resolveTelegramScopedTranscriptSession(params: {
|
||||
agentId: string;
|
||||
loadFreshSessionEntry: FreshTelegramSessionEntryLoader;
|
||||
loadFreshSessionStore: FreshTelegramSessionStoreLoader;
|
||||
sessionKey: string;
|
||||
}): TelegramScopedTranscriptSession | undefined {
|
||||
const { entry, storePath } = params.loadFreshSessionEntry(params.agentId, params.sessionKey);
|
||||
const { store, storePath } = params.loadFreshSessionStore(params.agentId);
|
||||
const entry = resolveSessionStoreEntry({ store, sessionKey: params.sessionKey }).existing;
|
||||
const sessionId = entry?.sessionId?.trim();
|
||||
return sessionId ? { sessionId, storePath } : undefined;
|
||||
}
|
||||
@@ -332,7 +329,7 @@ function resolveTelegramScopedTranscriptSession(params: {
|
||||
async function mirrorTelegramAssistantReplyToTranscript(params: {
|
||||
cfg: OpenClawConfig;
|
||||
idempotencyKey: string;
|
||||
loadFreshSessionEntry: FreshTelegramSessionEntryLoader;
|
||||
loadFreshSessionStore: FreshTelegramSessionStoreLoader;
|
||||
route: TelegramMessageContext["route"];
|
||||
sessionKey: string;
|
||||
payload: TelegramTranscriptMirrorPayload;
|
||||
@@ -343,7 +340,7 @@ async function mirrorTelegramAssistantReplyToTranscript(params: {
|
||||
}
|
||||
const session = resolveTelegramScopedTranscriptSession({
|
||||
agentId: params.route.agentId,
|
||||
loadFreshSessionEntry: params.loadFreshSessionEntry,
|
||||
loadFreshSessionStore: params.loadFreshSessionStore,
|
||||
sessionKey: params.sessionKey,
|
||||
});
|
||||
if (!session) {
|
||||
@@ -367,14 +364,22 @@ async function mirrorTelegramAssistantReplyToTranscript(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_PROGRESS_MARKDOWN_TEXT_CHARS = 300;
|
||||
const TELEGRAM_GENERAL_TOPIC_ID = 1;
|
||||
|
||||
function clipProgressMarkdownText(text: string): string {
|
||||
if (text.length <= MAX_PROGRESS_MARKDOWN_TEXT_CHARS) {
|
||||
return text;
|
||||
}
|
||||
return `${text.slice(0, MAX_PROGRESS_MARKDOWN_TEXT_CHARS - 1).trimEnd()}…`;
|
||||
}
|
||||
|
||||
function sanitizeProgressMarkdownText(text: string): string {
|
||||
return text.replaceAll("`", "'");
|
||||
}
|
||||
|
||||
function formatProgressAsMarkdownCode(text: string): string {
|
||||
const clipped = clipTelegramProgressText(text);
|
||||
const clipped = clipProgressMarkdownText(text);
|
||||
return `\`${sanitizeProgressMarkdownText(clipped)}\``;
|
||||
}
|
||||
|
||||
@@ -394,7 +399,7 @@ function escapeTelegramProgressHtml(text: string): string {
|
||||
}
|
||||
|
||||
function renderTelegramProgressStringLine(text: string): string {
|
||||
const clipped = clipTelegramProgressText(text.trim());
|
||||
const clipped = clipProgressMarkdownText(text.trim());
|
||||
const italic = clipped.match(/^_(.*)_$/u);
|
||||
if (italic) {
|
||||
return `<i>${escapeTelegramProgressHtml(italic[1] ?? "")}</i>`;
|
||||
@@ -413,7 +418,7 @@ function renderTelegramProgressLine(line: ChannelProgressDraftCompositorLine): s
|
||||
const parts = [`<b>${escapeTelegramProgressHtml(label)}</b>`];
|
||||
const detail = line.detail && line.detail !== line.label ? line.detail : undefined;
|
||||
if (detail) {
|
||||
parts.push(`<code>${escapeTelegramProgressHtml(clipTelegramProgressText(detail))}</code>`);
|
||||
parts.push(`<code>${escapeTelegramProgressHtml(clipProgressMarkdownText(detail))}</code>`);
|
||||
} else {
|
||||
const text = line.text.trim();
|
||||
if (text && text !== label) {
|
||||
@@ -758,7 +763,7 @@ export const dispatchTelegramMessage = async ({
|
||||
const dispatchContext = resolveDispatchTelegramContext({ cfg, context });
|
||||
const telegramDeps =
|
||||
injectedTelegramDeps ?? (await import("./bot-deps.js")).defaultTelegramBotDeps;
|
||||
const loadFreshSessionEntry = createFreshTelegramSessionEntryLoader({ cfg, telegramDeps });
|
||||
const loadFreshSessionStore = createFreshTelegramSessionStoreLoader({ cfg, telegramDeps });
|
||||
const {
|
||||
ctxPayload,
|
||||
msg,
|
||||
@@ -894,7 +899,7 @@ export const dispatchTelegramMessage = async ({
|
||||
cfg,
|
||||
sessionKey: ctxPayload.SessionKey,
|
||||
agentId: route.agentId,
|
||||
loadFreshSessionEntry,
|
||||
loadFreshSessionStore,
|
||||
});
|
||||
const forceBlockStreamingForReasoning = resolvedReasoningLevel === "on";
|
||||
const streamReasoningDraft = resolvedReasoningLevel === "stream";
|
||||
@@ -1461,7 +1466,8 @@ export const dispatchTelegramMessage = async ({
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const { entry: sessionEntry, storePath } = loadFreshSessionEntry(route.agentId, sessionKey);
|
||||
const { store, storePath } = loadFreshSessionStore(route.agentId);
|
||||
const sessionEntry = resolveSessionStoreEntry({ store, sessionKey }).existing;
|
||||
if (!sessionEntry?.sessionId) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -1509,7 +1515,7 @@ export const dispatchTelegramMessage = async ({
|
||||
await mirrorTelegramAssistantReplyToTranscript({
|
||||
cfg,
|
||||
idempotencyKey,
|
||||
loadFreshSessionEntry,
|
||||
loadFreshSessionStore,
|
||||
route,
|
||||
sessionKey,
|
||||
payload,
|
||||
@@ -1867,9 +1873,10 @@ export const dispatchTelegramMessage = async ({
|
||||
|
||||
if (isDmTopic) {
|
||||
try {
|
||||
const { store } = loadFreshSessionStore(route.agentId);
|
||||
const sessionKeyLocal = ctxPayload.SessionKey;
|
||||
if (sessionKeyLocal) {
|
||||
const { entry } = loadFreshSessionEntry(route.agentId, sessionKeyLocal);
|
||||
const entry = resolveSessionStoreEntry({ store, sessionKey: sessionKeyLocal }).existing;
|
||||
isFirstTurnInSession = !entry?.systemSent;
|
||||
} else {
|
||||
logVerbose("auto-topic-label: SessionKey is absent, skipping first-turn detection");
|
||||
@@ -1878,7 +1885,7 @@ export const dispatchTelegramMessage = async ({
|
||||
logVerbose(`auto-topic-label: session store error: ${formatErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
loadFreshSessionEntry.clear();
|
||||
loadFreshSessionStore.clear();
|
||||
|
||||
if (statusReactionController && !isRoomEvent) {
|
||||
void statusReactionController.setThinking();
|
||||
|
||||
@@ -572,10 +572,6 @@ describe("registerTelegramNativeCommands — session metadata", () => {
|
||||
]);
|
||||
sessionMocks.getSessionEntry.mockClear().mockReturnValue(undefined);
|
||||
sessionMocks.loadSessionStore.mockClear().mockReturnValue({});
|
||||
sessionMocks.getSessionEntry.mockImplementation(
|
||||
({ storePath, sessionKey }: { storePath: string; sessionKey: string }) =>
|
||||
sessionMocks.loadSessionStore(storePath)[sessionKey],
|
||||
);
|
||||
sessionMocks.recordSessionMetaFromInbound.mockClear().mockResolvedValue(undefined);
|
||||
sessionMocks.resolveSessionTranscriptLegacyFileTarget.mockClear().mockResolvedValue({
|
||||
agentId: "main",
|
||||
@@ -655,10 +651,7 @@ describe("registerTelegramNativeCommands — session metadata", () => {
|
||||
{ provider: "anthropic", model: "claude-opus-4-7" },
|
||||
"thinking menu call",
|
||||
);
|
||||
expect(sessionMocks.getSessionEntry).toHaveBeenCalledWith({
|
||||
storePath: "/tmp/openclaw-sessions.json",
|
||||
sessionKey: "agent:main:main",
|
||||
});
|
||||
expect(sessionMocks.loadSessionStore).toHaveBeenCalledWith("/tmp/openclaw-sessions.json");
|
||||
expectSendMessageCall({
|
||||
sendMessage,
|
||||
chatId: 100,
|
||||
|
||||
@@ -40,6 +40,8 @@ import { getChildLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import {
|
||||
getSessionEntry,
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
resolveStorePath,
|
||||
type SessionEntry,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
@@ -253,7 +255,8 @@ function resolveTelegramCommandMenuModelContext(params: {
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const entry = getSessionEntry({ storePath, sessionKey: params.sessionKey });
|
||||
const store = loadSessionStore(storePath);
|
||||
const entry = resolveSessionStoreEntry({ store, sessionKey: params.sessionKey }).existing;
|
||||
const thinkingLevel = normalizeOptionalString(entry?.thinkingLevel);
|
||||
const fastMode = entry?.fastMode;
|
||||
if (entry?.modelOverrideSource === "auto" && normalizeOptionalString(entry.modelOverride)) {
|
||||
@@ -266,7 +269,7 @@ function resolveTelegramCommandMenuModelContext(params: {
|
||||
}
|
||||
const override = resolveStoredModelOverride({
|
||||
sessionEntry: entry,
|
||||
loadSessionEntry: (sessionKey) => getSessionEntry({ storePath, sessionKey }),
|
||||
sessionStore: store,
|
||||
sessionKey: params.sessionKey,
|
||||
defaultProvider: defaultModel.provider,
|
||||
});
|
||||
@@ -315,13 +318,14 @@ function resolveTelegramFastCommandModelContext(params: {
|
||||
}
|
||||
try {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.agentId });
|
||||
const entry = getSessionEntry({ storePath, sessionKey: params.sessionKey });
|
||||
const store = loadSessionStore(storePath);
|
||||
const entry = resolveSessionStoreEntry({ store, sessionKey: params.sessionKey }).existing;
|
||||
if (entry?.modelOverrideSource === "auto" && normalizeOptionalString(entry.modelOverride)) {
|
||||
return fallback();
|
||||
}
|
||||
const override = resolveStoredModelOverride({
|
||||
sessionEntry: entry,
|
||||
loadSessionEntry: (sessionKey) => getSessionEntry({ storePath, sessionKey }),
|
||||
sessionStore: store,
|
||||
sessionKey: params.sessionKey,
|
||||
defaultProvider: defaultModel.provider,
|
||||
});
|
||||
@@ -355,7 +359,8 @@ function resolveTelegramFastCommandState(params: {
|
||||
}
|
||||
try {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.agentId });
|
||||
const entry = getSessionEntry({ storePath, sessionKey: params.sessionKey });
|
||||
const store = loadSessionStore(storePath);
|
||||
const entry = resolveSessionStoreEntry({ store, sessionKey: params.sessionKey }).existing;
|
||||
const modelContext = resolveTelegramFastCommandModelContext(params);
|
||||
return resolveFastModeState({
|
||||
cfg: params.cfg,
|
||||
|
||||
@@ -12,9 +12,6 @@ type AnyMock = ReturnType<typeof vi.fn>;
|
||||
type AnyAsyncMock = ReturnType<typeof vi.fn<(...args: unknown[]) => Promise<unknown>>>;
|
||||
type GetRuntimeConfigFn =
|
||||
typeof import("openclaw/plugin-sdk/runtime-config-snapshot").getRuntimeConfig;
|
||||
type GetSessionEntryFn = typeof import("openclaw/plugin-sdk/session-store-runtime").getSessionEntry;
|
||||
type ListSessionEntriesFn =
|
||||
typeof import("openclaw/plugin-sdk/session-store-runtime").listSessionEntries;
|
||||
type LoadSessionStoreFn =
|
||||
typeof import("openclaw/plugin-sdk/session-store-runtime").loadSessionStore;
|
||||
type ResolveStorePathFn =
|
||||
@@ -64,9 +61,7 @@ vi.mock("openclaw/plugin-sdk/web-media", () => ({
|
||||
}));
|
||||
|
||||
const {
|
||||
getSessionEntryMock,
|
||||
getRuntimeConfig,
|
||||
listSessionEntriesMock,
|
||||
loadSessionStoreMock,
|
||||
readSessionUpdatedAtMock,
|
||||
recordInboundSessionMock,
|
||||
@@ -74,9 +69,7 @@ const {
|
||||
sessionStoreEntries,
|
||||
} = vi.hoisted(
|
||||
(): {
|
||||
getSessionEntryMock: MockFn<GetSessionEntryFn>;
|
||||
getRuntimeConfig: MockFn<GetRuntimeConfigFn>;
|
||||
listSessionEntriesMock: MockFn<ListSessionEntriesFn>;
|
||||
loadSessionStoreMock: MockFn<LoadSessionStoreFn>;
|
||||
readSessionUpdatedAtMock: MockFn<ReadSessionUpdatedAtFn>;
|
||||
recordInboundSessionMock: MockFn<NonNullable<TelegramBotDeps["recordInboundSession"]>>;
|
||||
@@ -84,23 +77,12 @@ const {
|
||||
sessionStoreEntries: { value: SessionStore };
|
||||
} => ({
|
||||
getRuntimeConfig: vi.fn<GetRuntimeConfigFn>(() => ({})),
|
||||
resolveStorePathMock: vi.fn<ResolveStorePathFn>(
|
||||
(storePath?: string) => storePath ?? sessionStorePath,
|
||||
),
|
||||
loadSessionStoreMock: vi.fn<LoadSessionStoreFn>(
|
||||
(_storePath, _opts) => sessionStoreEntries.value,
|
||||
),
|
||||
getSessionEntryMock: vi.fn<GetSessionEntryFn>(({ storePath, sessionKey, agentId }) => {
|
||||
const resolvedStorePath = storePath ?? resolveStorePathMock(undefined, { agentId });
|
||||
return loadSessionStoreMock(resolvedStorePath)[sessionKey];
|
||||
}),
|
||||
listSessionEntriesMock: vi.fn<ListSessionEntriesFn>(({ storePath, agentId } = {}) => {
|
||||
const resolvedStorePath = storePath ?? resolveStorePathMock(undefined, { agentId });
|
||||
return Object.entries(loadSessionStoreMock(resolvedStorePath)).map(([sessionKey, entry]) => ({
|
||||
sessionKey,
|
||||
entry,
|
||||
}));
|
||||
}),
|
||||
resolveStorePathMock: vi.fn<ResolveStorePathFn>(
|
||||
(storePath?: string) => storePath ?? sessionStorePath,
|
||||
),
|
||||
readSessionUpdatedAtMock: vi.fn<ReadSessionUpdatedAtFn>(() => undefined),
|
||||
recordInboundSessionMock: vi.fn(async () => undefined),
|
||||
sessionStoreEntries: { value: {} as SessionStore },
|
||||
@@ -462,8 +444,6 @@ export const telegramBotRuntimeForTest: TelegramBotRuntimeForTest = {
|
||||
};
|
||||
export const telegramBotDepsForTest: TelegramBotDeps = {
|
||||
getRuntimeConfig,
|
||||
getSessionEntry: getSessionEntryMock,
|
||||
listSessionEntries: listSessionEntriesMock,
|
||||
loadSessionStore: loadSessionStoreMock as TelegramBotDeps["loadSessionStore"],
|
||||
resolveStorePath: resolveStorePathMock,
|
||||
readSessionUpdatedAt: readSessionUpdatedAtMock,
|
||||
@@ -584,19 +564,6 @@ beforeEach(() => {
|
||||
loadSessionStoreMock.mockImplementation(() => sessionStoreEntries.value);
|
||||
resolveStorePathMock.mockReset();
|
||||
resolveStorePathMock.mockImplementation((storePath?: string) => storePath ?? sessionStorePath);
|
||||
getSessionEntryMock.mockReset();
|
||||
getSessionEntryMock.mockImplementation(({ storePath, sessionKey, agentId }) => {
|
||||
const resolvedStorePath = storePath ?? resolveStorePathMock(undefined, { agentId });
|
||||
return loadSessionStoreMock(resolvedStorePath)[sessionKey];
|
||||
});
|
||||
listSessionEntriesMock.mockReset();
|
||||
listSessionEntriesMock.mockImplementation(({ storePath, agentId } = {}) => {
|
||||
const resolvedStorePath = storePath ?? resolveStorePathMock(undefined, { agentId });
|
||||
return Object.entries(loadSessionStoreMock(resolvedStorePath)).map(([sessionKey, entry]) => ({
|
||||
sessionKey,
|
||||
entry,
|
||||
}));
|
||||
});
|
||||
readSessionUpdatedAtMock.mockReset();
|
||||
readSessionUpdatedAtMock.mockReturnValue(undefined);
|
||||
recordInboundSessionMock.mockReset();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Telegram plugin module implements bot behavior.
|
||||
import { getSessionEntry, listSessionEntries } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
createTelegramBotCore,
|
||||
getTelegramSequentialKey,
|
||||
setTelegramBotRuntimeForTest,
|
||||
} from "./bot-core.js";
|
||||
import { defaultTelegramBotDeps } from "./bot-deps.js";
|
||||
import { defaultTelegramBotDeps, type TelegramBotDeps } from "./bot-deps.js";
|
||||
import type { TelegramBotOptions } from "./bot.types.js";
|
||||
|
||||
export type { TelegramBotOptions } from "./bot.types.js";
|
||||
@@ -16,6 +17,39 @@ export function createTelegramBot(
|
||||
): ReturnType<typeof createTelegramBotCore> {
|
||||
return createTelegramBotCore({
|
||||
...opts,
|
||||
telegramDeps: opts.telegramDeps ?? defaultTelegramBotDeps,
|
||||
telegramDeps: withTelegramSessionAccessorDeps(opts.telegramDeps ?? defaultTelegramBotDeps),
|
||||
});
|
||||
}
|
||||
|
||||
function withTelegramSessionAccessorDeps(deps: TelegramBotDeps): TelegramBotDeps {
|
||||
if (!deps.loadSessionStore) {
|
||||
return {
|
||||
...deps,
|
||||
getSessionEntry: deps.getSessionEntry ?? getSessionEntry,
|
||||
listSessionEntries: deps.listSessionEntries ?? listSessionEntries,
|
||||
};
|
||||
}
|
||||
|
||||
const listInjectedEntries = (
|
||||
scope: Parameters<NonNullable<TelegramBotDeps["listSessionEntries"]>>[0] = {},
|
||||
) => {
|
||||
const storePath =
|
||||
scope.storePath ?? deps.resolveStorePath(undefined, { agentId: scope.agentId });
|
||||
return Object.entries(deps.loadSessionStore?.(storePath) ?? {}).map(([sessionKey, entry]) => ({
|
||||
sessionKey,
|
||||
entry,
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
...deps,
|
||||
// Existing Telegram tests and custom deps inject loadSessionStore; expose
|
||||
// the same data through the accessor seam consumed by migrated handlers.
|
||||
getSessionEntry:
|
||||
deps.getSessionEntry ??
|
||||
((scope) =>
|
||||
listInjectedEntries(scope).find(({ sessionKey }) => sessionKey === scope.sessionKey)
|
||||
?.entry),
|
||||
listSessionEntries: deps.listSessionEntries ?? listInjectedEntries,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,36 +43,6 @@ describe("telegram actions contract", () => {
|
||||
expect(capabilities?.includes("richText")).toBe(expected);
|
||||
});
|
||||
|
||||
it("advertises inline buttons when legacy Telegram capabilities are empty", () => {
|
||||
const capabilities = telegramPlugin.agentPrompt?.messageToolCapabilities?.({
|
||||
cfg: {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "123:telegram-test-token",
|
||||
capabilities: [],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(capabilities).toContain("inlineButtons");
|
||||
});
|
||||
|
||||
it("does not advertise inline buttons for non-empty legacy Telegram capabilities without inlineButtons", () => {
|
||||
const capabilities = telegramPlugin.agentPrompt?.messageToolCapabilities?.({
|
||||
cfg: {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "123:telegram-test-token",
|
||||
capabilities: ["vision"],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(capabilities).not.toContain("inlineButtons");
|
||||
});
|
||||
|
||||
it("uses the selected Telegram account's rich text setting", () => {
|
||||
const capabilities = telegramPlugin.agentPrompt?.messageToolCapabilities?.({
|
||||
cfg: {
|
||||
|
||||
@@ -109,39 +109,6 @@ describe("resolveTelegramInlineButtonsScope (#75433 SecretRef tolerance)", () =>
|
||||
expect(isTelegramInlineButtonsEnabled({ cfg })).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves the default inline-buttons scope when legacy capabilities are empty", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: { source: "exec", provider: "default", id: "telegram-token" },
|
||||
capabilities: [],
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(resolveTelegramInlineButtonsScope({ cfg })).toBe("allowlist");
|
||||
expect(isTelegramInlineButtonsEnabled({ cfg })).toBe(true);
|
||||
});
|
||||
|
||||
it("inherits the channel scope when an account legacy capabilities array is empty", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
capabilities: { inlineButtons: "off" },
|
||||
accounts: {
|
||||
ops: {
|
||||
botToken: "123:telegram-ops-token",
|
||||
capabilities: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(resolveTelegramInlineButtonsScope({ cfg, accountId: "ops" })).toBe("off");
|
||||
expect(isTelegramInlineButtonsEnabled({ cfg, accountId: "ops" })).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves configured "off" when botToken is an unresolved SecretRef', () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
|
||||
@@ -47,9 +47,6 @@ export function resolveTelegramInlineButtonsScopeFromCapabilities(
|
||||
return DEFAULT_INLINE_BUTTONS_SCOPE;
|
||||
}
|
||||
if (Array.isArray(capabilities)) {
|
||||
if (capabilities.length === 0) {
|
||||
return DEFAULT_INLINE_BUTTONS_SCOPE;
|
||||
}
|
||||
const enabled = capabilities.some(
|
||||
(entry) => normalizeLowercaseStringOrEmpty(String(entry)) === "inlinebuttons",
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { OutboundDeliveryFormattingOptions } from "openclaw/plugin-sdk/channel-outbound";
|
||||
import {
|
||||
resolveOutboundSendDep,
|
||||
sanitizeForPlainText,
|
||||
type OutboundSendDeps,
|
||||
} from "openclaw/plugin-sdk/channel-outbound";
|
||||
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/channel-send-result";
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
sendPayloadMediaSequenceOrFallback,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
||||
import type { TelegramInlineButtons } from "./button-types.js";
|
||||
import { resolveTelegramInlineButtons } from "./button-types.js";
|
||||
import { splitTelegramHtmlChunks } from "./format.js";
|
||||
@@ -200,7 +198,6 @@ export function createTelegramOutboundAdapter(
|
||||
chunkerMode: "markdown",
|
||||
extractMarkdownImages: true,
|
||||
textChunkLimit: TELEGRAM_TEXT_CHUNK_LIMIT,
|
||||
sanitizeText: ({ text }) => sanitizeForPlainText(sanitizeAssistantVisibleText(text)),
|
||||
shouldSuppressLocalPayloadPrompt: options.shouldSuppressLocalPayloadPrompt,
|
||||
beforeDeliverPayload: options.beforeDeliverPayload,
|
||||
shouldTreatDeliveredTextAsVisible: options.shouldTreatDeliveredTextAsVisible,
|
||||
|
||||
@@ -29,23 +29,10 @@ describe("telegramPlugin outbound", () => {
|
||||
expect(telegramOutbound.presentationCapabilities?.limits?.text?.markdownDialect).toBe(
|
||||
"markdown",
|
||||
);
|
||||
expect(telegramOutbound.sanitizeText).toBeUndefined();
|
||||
expect(telegramOutbound.pollMaxOptions).toBe(10);
|
||||
});
|
||||
|
||||
it("strips assistant-visible tool traces before outbound delivery", () => {
|
||||
clearTelegramRuntime();
|
||||
const text = 'Done.\n⚠️ 🛠️ `search "Pipeline" in ~/.openclaw/workspace-* (agent)` failed';
|
||||
|
||||
expect(telegramOutbound.sanitizeText?.({ text, payload: { text } })).toBe("Done.");
|
||||
});
|
||||
|
||||
it("preserves ordinary outbound text while sanitizing", () => {
|
||||
clearTelegramRuntime();
|
||||
const text = "The pipeline has 3 deals.";
|
||||
|
||||
expect(telegramOutbound.sanitizeText?.({ text, payload: { text } })).toBe(text);
|
||||
});
|
||||
|
||||
it("preserves explicit HTML parse mode before chunking", () => {
|
||||
clearTelegramRuntime();
|
||||
const text = "<b>hi</b>";
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// Telegram tests cover progress text clipping behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { clipTelegramProgressText, TELEGRAM_PROGRESS_MAX_CHARS } from "./truncate.js";
|
||||
|
||||
describe("clipTelegramProgressText", () => {
|
||||
it("drops a surrogate-pair emoji whole when it straddles the limit", () => {
|
||||
// 😀 is U+1F600, encoded as two UTF-16 code units (high \uD83D + low \uDE00).
|
||||
// Placing the emoji at positions [MAX-2, MAX-1] (0-indexed) puts its high
|
||||
// surrogate right on the .slice(0, MAX-1) cut edge. A raw .slice keeps only
|
||||
// \uD83D — an unpaired high surrogate — which is invalid in a Telegram payload.
|
||||
const base = "a".repeat(TELEGRAM_PROGRESS_MAX_CHARS - 2); // 298 'a's
|
||||
const out = clipTelegramProgressText(`${base}😀tail`);
|
||||
expect(out).toBe(`${base}…`);
|
||||
// No dangling high surrogate (high not followed by a low surrogate).
|
||||
expect(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(out)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps an emoji that fits entirely before the cut", () => {
|
||||
// 296 'a's + '😀' (2 units) + 'xyz' (3 units) = 301 total > 300.
|
||||
// The emoji sits at [296, 297] — entirely before the cut at 299 — so it stays.
|
||||
const base = "a".repeat(TELEGRAM_PROGRESS_MAX_CHARS - 4); // 296 'a's
|
||||
const out = clipTelegramProgressText(`${base}😀xyz`);
|
||||
expect(out).toBe(`${base}😀x…`);
|
||||
expect(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(out)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns text unchanged when it is within the limit", () => {
|
||||
const short = "hello 😀 world";
|
||||
expect(clipTelegramProgressText(short)).toBe(short);
|
||||
});
|
||||
|
||||
it("trims trailing whitespace before the ellipsis", () => {
|
||||
// The sliced portion may end in spaces when trailing spaces straddle the cut.
|
||||
const text = `${"a".repeat(TELEGRAM_PROGRESS_MAX_CHARS - 2)} rest`;
|
||||
const out = clipTelegramProgressText(text);
|
||||
expect(out).not.toContain(" …");
|
||||
expect(out.endsWith("…")).toBe(true);
|
||||
});
|
||||
|
||||
it("handles plain ASCII that fills exactly to the limit", () => {
|
||||
const exact = "x".repeat(TELEGRAM_PROGRESS_MAX_CHARS);
|
||||
expect(clipTelegramProgressText(exact)).toBe(exact);
|
||||
const oneOver = `${"x".repeat(TELEGRAM_PROGRESS_MAX_CHARS)}y`;
|
||||
const out = clipTelegramProgressText(oneOver);
|
||||
expect(out.length).toBeLessThanOrEqual(TELEGRAM_PROGRESS_MAX_CHARS);
|
||||
expect(out.endsWith("…")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
// Telegram tests cover progress text clipping behavior.
|
||||
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
|
||||
export const TELEGRAM_PROGRESS_MAX_CHARS = 300;
|
||||
|
||||
/**
|
||||
* Clips Telegram progress text to at most {@link TELEGRAM_PROGRESS_MAX_CHARS} UTF-16 code units,
|
||||
* slicing on a code-point boundary so a surrogate pair straddling the limit is
|
||||
* dropped whole rather than leaving a lone high surrogate in the payload.
|
||||
*/
|
||||
export function clipTelegramProgressText(text: string): string {
|
||||
if (text.length <= TELEGRAM_PROGRESS_MAX_CHARS) {
|
||||
return text;
|
||||
}
|
||||
// Slice on a code-point boundary so an emoji (or any astral character) that
|
||||
// straddles the limit is dropped whole instead of leaving a lone \uD83D-style
|
||||
// high surrogate before the ellipsis, which serializes to an invalid character
|
||||
// in the Telegram Bot API payload.
|
||||
return `${sliceUtf16Safe(text, 0, TELEGRAM_PROGRESS_MAX_CHARS - 1).trimEnd()}…`;
|
||||
}
|
||||
@@ -21,12 +21,6 @@ type EmbeddedAgentArgs = {
|
||||
provider?: string;
|
||||
model?: string;
|
||||
sessionKey?: string;
|
||||
sessionTarget?: {
|
||||
agentId?: string;
|
||||
sessionId?: string;
|
||||
sessionKey?: string;
|
||||
storePath?: string;
|
||||
};
|
||||
sandboxSessionKey?: string;
|
||||
agentDir?: string;
|
||||
agentId?: string;
|
||||
@@ -319,6 +313,7 @@ describe("generateVoiceResponse", () => {
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveAgentIdentity,
|
||||
resolveStorePath,
|
||||
resolveSessionFilePath,
|
||||
sessionStore,
|
||||
} = createAgentRuntime([{ text: '{"spoken":"Default agent."}' }]);
|
||||
const coreConfig = {} as CoreConfig;
|
||||
@@ -341,18 +336,19 @@ describe("generateVoiceResponse", () => {
|
||||
if (!defaultSessionEntry) {
|
||||
throw new Error("Expected default voice session entry");
|
||||
}
|
||||
expect(resolveSessionFilePath).toHaveBeenCalledWith(
|
||||
defaultSessionEntry.sessionId,
|
||||
defaultSessionEntry,
|
||||
{
|
||||
agentId: "main",
|
||||
},
|
||||
);
|
||||
const args = requireEmbeddedAgentArgs(runEmbeddedAgent);
|
||||
expect(args.agentDir).toBe("/tmp/openclaw/agents/main");
|
||||
expect(args.agentId).toBe("main");
|
||||
expect(args.sessionTarget).toStrictEqual({
|
||||
agentId: "main",
|
||||
sessionId: defaultSessionEntry.sessionId,
|
||||
sessionKey: "voice:15550001111",
|
||||
storePath: "/tmp/openclaw/main/sessions.json",
|
||||
});
|
||||
expect(args.sandboxSessionKey).toBe("agent:main:voice:15550001111");
|
||||
expect(args.workspaceDir).toBe("/tmp/openclaw/workspace/main");
|
||||
expect(args.sessionFile).toBeUndefined();
|
||||
expect(args.sessionFile).toBe("/tmp/openclaw/main/sessions/session.jsonl");
|
||||
});
|
||||
|
||||
it("uses the configured voice response agent workspace", async () => {
|
||||
@@ -363,6 +359,7 @@ describe("generateVoiceResponse", () => {
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveAgentIdentity,
|
||||
resolveStorePath,
|
||||
resolveSessionFilePath,
|
||||
sessionStore,
|
||||
} = createAgentRuntime([{ text: '{"spoken":"Voice agent."}' }]);
|
||||
const coreConfig = {} as CoreConfig;
|
||||
@@ -389,18 +386,19 @@ describe("generateVoiceResponse", () => {
|
||||
if (!voiceSessionEntry) {
|
||||
throw new Error("Expected routed voice session entry");
|
||||
}
|
||||
expect(resolveSessionFilePath).toHaveBeenCalledWith(
|
||||
voiceSessionEntry.sessionId,
|
||||
voiceSessionEntry,
|
||||
{
|
||||
agentId: "voice",
|
||||
},
|
||||
);
|
||||
const args = requireEmbeddedAgentArgs(runEmbeddedAgent);
|
||||
expect(args.agentDir).toBe("/tmp/openclaw/agents/voice");
|
||||
expect(args.agentId).toBe("voice");
|
||||
expect(args.sessionTarget).toStrictEqual({
|
||||
agentId: "voice",
|
||||
sessionId: voiceSessionEntry.sessionId,
|
||||
sessionKey: "voice:15550001111",
|
||||
storePath: "/tmp/openclaw/voice/sessions.json",
|
||||
});
|
||||
expect(args.sandboxSessionKey).toBe("agent:voice:voice:15550001111");
|
||||
expect(args.workspaceDir).toBe("/tmp/openclaw/workspace/voice");
|
||||
expect(args.sessionFile).toBeUndefined();
|
||||
expect(args.sessionFile).toBe("/tmp/openclaw/voice/sessions/session.jsonl");
|
||||
});
|
||||
|
||||
it("passes the routed voice agent explicit tool allowlist to the embedded run", async () => {
|
||||
|
||||
@@ -291,6 +291,10 @@ export async function generateVoiceResponse(
|
||||
}
|
||||
const sessionId = sessionEntry.sessionId;
|
||||
|
||||
const sessionFile = agentRuntime.session.resolveSessionFilePath(sessionId, sessionEntry, {
|
||||
agentId,
|
||||
});
|
||||
|
||||
// Resolve thinking level
|
||||
const thinkLevel = agentRuntime.resolveThinkingDefault({ cfg, provider, model });
|
||||
|
||||
@@ -320,15 +324,10 @@ export async function generateVoiceResponse(
|
||||
const result = await agentRuntime.runEmbeddedAgent({
|
||||
sessionId,
|
||||
sessionKey: resolvedSessionKey,
|
||||
sessionTarget: {
|
||||
agentId,
|
||||
sessionId,
|
||||
sessionKey: resolvedSessionKey,
|
||||
storePath,
|
||||
},
|
||||
sandboxSessionKey: resolveVoiceSandboxSessionKey(agentId, resolvedSessionKey),
|
||||
agentId,
|
||||
messageProvider: "voice",
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
prompt: userMessage,
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
// Whatsapp plugin module implements group activation behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
getSessionEntry,
|
||||
patchSessionEntry,
|
||||
resolveStorePath,
|
||||
type SessionEntry,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { updateSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { resolveWhatsAppLegacyGroupSessionKey } from "../../group-session-key.js";
|
||||
import { resolveWhatsAppInboundPolicy } from "../../inbound-policy.js";
|
||||
import { loadSessionStore, resolveStorePath } from "../config.runtime.js";
|
||||
import { normalizeGroupActivation } from "./group-activation.runtime.js";
|
||||
|
||||
function hasNamedWhatsAppAccounts(cfg: OpenClawConfig) {
|
||||
@@ -32,7 +28,6 @@ function isActivationOnlyEntry(
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolves group activation for a WhatsApp conversation and backfills scoped session metadata. */
|
||||
export async function resolveGroupActivationFor(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
@@ -43,15 +38,13 @@ export async function resolveGroupActivationFor(params: {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const sessionScope = { storePath, agentId: params.agentId };
|
||||
const store = loadSessionStore(storePath);
|
||||
const legacySessionKey = resolveWhatsAppLegacyGroupSessionKey({
|
||||
sessionKey: params.sessionKey,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
const legacyEntry = legacySessionKey
|
||||
? getSessionEntry({ ...sessionScope, sessionKey: legacySessionKey })
|
||||
: undefined;
|
||||
const scopedEntry = getSessionEntry({ ...sessionScope, sessionKey: params.sessionKey });
|
||||
const legacyEntry = legacySessionKey ? store[legacySessionKey] : undefined;
|
||||
const scopedEntry = store[params.sessionKey];
|
||||
const normalizedAccountId = normalizeAccountId(params.accountId);
|
||||
const ignoreScopedActivation =
|
||||
normalizedAccountId === DEFAULT_ACCOUNT_ID &&
|
||||
@@ -61,22 +54,15 @@ export async function resolveGroupActivationFor(params: {
|
||||
(ignoreScopedActivation ? undefined : scopedEntry?.groupActivation) ??
|
||||
legacyEntry?.groupActivation;
|
||||
if (activation !== undefined && scopedEntry?.groupActivation === undefined) {
|
||||
// Activation-only backfills must not synthesize session ids or activity.
|
||||
// replaceEntry preserves existing scoped metadata while keeping fallback writes sparse.
|
||||
await patchSessionEntry({
|
||||
...sessionScope,
|
||||
sessionKey: params.sessionKey,
|
||||
fallbackEntry: {} as SessionEntry,
|
||||
replaceEntry: true,
|
||||
update: (entry) => {
|
||||
if (entry.groupActivation !== undefined) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...entry,
|
||||
groupActivation: activation,
|
||||
};
|
||||
},
|
||||
await updateSessionStore(storePath, (nextStore) => {
|
||||
const nextScopedEntry = nextStore[params.sessionKey];
|
||||
if (nextScopedEntry?.groupActivation !== undefined) {
|
||||
return;
|
||||
}
|
||||
nextStore[params.sessionKey] = {
|
||||
...nextScopedEntry,
|
||||
groupActivation: activation,
|
||||
};
|
||||
});
|
||||
}
|
||||
const requireMention = resolveWhatsAppInboundPolicy({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user