mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-10 07:51:14 +08:00
Compare commits
259 Commits
feat/node-
...
lts-checkl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d1c4af83e | ||
|
|
36d9241cf7 | ||
|
|
d896a4c7a3 | ||
|
|
b3eba2ff38 | ||
|
|
21aa297434 | ||
|
|
4752e9a67d | ||
|
|
ec91dce0b8 | ||
|
|
fbbb88925a | ||
|
|
9235c25d33 | ||
|
|
6ce71737e5 | ||
|
|
935c80d6e1 | ||
|
|
286772e930 | ||
|
|
b19904931e | ||
|
|
415272d17e | ||
|
|
002aa1061b | ||
|
|
8a83c13389 | ||
|
|
a16b6c02ce | ||
|
|
2514980118 | ||
|
|
c85b0ee3db | ||
|
|
1e683ff245 | ||
|
|
fc0b141445 | ||
|
|
a0840cad8f | ||
|
|
03b35b53e3 | ||
|
|
797bcd5bdb | ||
|
|
5a0f9cb03c | ||
|
|
e4de53a460 | ||
|
|
d1fe0184b9 | ||
|
|
da88940c6c | ||
|
|
520992a1de | ||
|
|
00d21a4720 | ||
|
|
3d68f7e5f7 | ||
|
|
ceee4c6b01 | ||
|
|
e22e857ddd | ||
|
|
57bed6ae0c | ||
|
|
0c9ac48d2c | ||
|
|
afa04d6454 | ||
|
|
85343ea546 | ||
|
|
d6dbcb2f4b | ||
|
|
61d121f1ca | ||
|
|
21512a696f | ||
|
|
ea1ef72394 | ||
|
|
7c885528ba | ||
|
|
cec5e36a39 | ||
|
|
e404ce98f5 | ||
|
|
30160933f0 | ||
|
|
8b66003a0b | ||
|
|
12a569109b | ||
|
|
1a3ce7c2a8 | ||
|
|
560b77a4af | ||
|
|
cfd5f1ad13 | ||
|
|
d7759c6a35 | ||
|
|
e0018382eb | ||
|
|
69d1d78649 | ||
|
|
cb5bb9b936 | ||
|
|
bafe17e60b | ||
|
|
613a2835cb | ||
|
|
a59eba3ee1 | ||
|
|
9b1a01e4f9 | ||
|
|
29746cf7a9 | ||
|
|
17ab517047 | ||
|
|
697eeb8bab | ||
|
|
853f1c0d9e | ||
|
|
1447a4507a | ||
|
|
748881e0a8 | ||
|
|
ff83d4d164 | ||
|
|
13078d24ab | ||
|
|
48c19590eb | ||
|
|
72547a1ac6 | ||
|
|
26bc069308 | ||
|
|
57f8d71c50 | ||
|
|
980c91d293 | ||
|
|
6b0ffa2106 | ||
|
|
056421f4f8 | ||
|
|
fb750e6eed | ||
|
|
978fdd7d2a | ||
|
|
74f3baebb7 | ||
|
|
deff9ea180 | ||
|
|
9fd5f9ee7c | ||
|
|
4dd7bc6d88 | ||
|
|
0dbf17471b | ||
|
|
f3abe61b78 | ||
|
|
92cdcae500 | ||
|
|
3cf1bd22f9 | ||
|
|
44cd0ec13f | ||
|
|
d77bac8911 | ||
|
|
1da49dcfd0 | ||
|
|
ee74fff7ad | ||
|
|
1de46bb425 | ||
|
|
e662435067 | ||
|
|
62a6fd8139 | ||
|
|
88158525a7 | ||
|
|
c8bb7330b5 | ||
|
|
8732ef2f28 | ||
|
|
8f6e71087b | ||
|
|
9448f91e6f | ||
|
|
5613a0fb6e | ||
|
|
82710b4f1f | ||
|
|
d23558e691 | ||
|
|
2f00fbf28e | ||
|
|
86872e0880 | ||
|
|
506c2ee181 | ||
|
|
1e6fb5089b | ||
|
|
14690904f0 | ||
|
|
99bb94589b | ||
|
|
de4571da4b | ||
|
|
a4087c54b5 | ||
|
|
4756d6a42a | ||
|
|
9e22b8560c | ||
|
|
c1b49bb1d0 | ||
|
|
d6c0f9ccb8 | ||
|
|
5d350e785a | ||
|
|
de68623ffe | ||
|
|
848f39e70d | ||
|
|
b311fd607f | ||
|
|
8f85f94946 | ||
|
|
5380d11977 | ||
|
|
23716de446 | ||
|
|
efd1a9ace6 | ||
|
|
7ac1eeb122 | ||
|
|
58912f8fd8 | ||
|
|
6868cde4d4 | ||
|
|
3c7c25afd2 | ||
|
|
96e5812426 | ||
|
|
126ebfc997 | ||
|
|
4b151593e2 | ||
|
|
56f652b499 | ||
|
|
5a704d26a1 | ||
|
|
e7bcbd3e7e | ||
|
|
afcf1ddb9d | ||
|
|
85e16da2b4 | ||
|
|
38e142657b | ||
|
|
4dd00347fc | ||
|
|
0973eb61c3 | ||
|
|
e282cb2af5 | ||
|
|
6f419b3853 | ||
|
|
4fa5092cdc | ||
|
|
53a3d58d62 | ||
|
|
cef423d066 | ||
|
|
5cf63f295b | ||
|
|
86d958647f | ||
|
|
12a56d4d46 | ||
|
|
39cc11ad28 | ||
|
|
4df95d3c3f | ||
|
|
b8d08f0cfd | ||
|
|
95d51c5fe8 | ||
|
|
5c6a501269 | ||
|
|
dc4c9030fc | ||
|
|
8ede9e0e07 | ||
|
|
9739249043 | ||
|
|
dbb80f3bb7 | ||
|
|
61d9ac8c5d | ||
|
|
abc00f4c98 | ||
|
|
28737a0b09 | ||
|
|
28b63e69e9 | ||
|
|
5392cb7139 | ||
|
|
55c414ca81 | ||
|
|
74680e3484 | ||
|
|
d6e1ca997b | ||
|
|
c4ed850f9b | ||
|
|
4957e3b02f | ||
|
|
323c8aa87f | ||
|
|
442a2107b5 | ||
|
|
ed52d27d78 | ||
|
|
cb17c84410 | ||
|
|
f57adba400 | ||
|
|
9f6ed16a6d | ||
|
|
99a838fac4 | ||
|
|
064182aff8 | ||
|
|
0f9bb59b73 | ||
|
|
79b6dd049e | ||
|
|
58c663920d | ||
|
|
dd2083c7ec | ||
|
|
29f5e9d35c | ||
|
|
25211167e8 | ||
|
|
ecb6779a16 | ||
|
|
edb920b857 | ||
|
|
b2e320dfb1 | ||
|
|
1bdf210b43 | ||
|
|
d8326f13c3 | ||
|
|
9b30ff181c | ||
|
|
4f79f2419c | ||
|
|
65546f0158 | ||
|
|
6d58ff3562 | ||
|
|
47bae66415 | ||
|
|
f5b6a977d7 | ||
|
|
85e6940202 | ||
|
|
5ba4eeceac | ||
|
|
a628a66e4d | ||
|
|
ef08c83e17 | ||
|
|
b6ce59d367 | ||
|
|
c8665c66ba | ||
|
|
4c3b4f8ad8 | ||
|
|
e6f85453dc | ||
|
|
f1bdc91b64 | ||
|
|
add135d238 | ||
|
|
563dac5989 | ||
|
|
5bc300a1df | ||
|
|
1d19d7ec46 | ||
|
|
87d053c0cb | ||
|
|
5b53cddc75 | ||
|
|
6c48a12562 | ||
|
|
43cee29f70 | ||
|
|
725ddd11cc | ||
|
|
d2d14d5793 | ||
|
|
f25c246f6b | ||
|
|
6486fc1c0d | ||
|
|
81eee47045 | ||
|
|
4499b24781 | ||
|
|
b59b34f9d5 | ||
|
|
912e70acbd | ||
|
|
16147e16e3 | ||
|
|
638be00f4b | ||
|
|
695e09d360 | ||
|
|
69ddcc00e6 | ||
|
|
a18c60e141 | ||
|
|
ec048ae693 | ||
|
|
7675b10223 | ||
|
|
25a1b0c240 | ||
|
|
c006ed5e16 | ||
|
|
b4e048e60a | ||
|
|
f365568f1b | ||
|
|
e2c23d8a5e | ||
|
|
408ba4c8a0 | ||
|
|
4995907541 | ||
|
|
8cb093e7a9 | ||
|
|
3e29885c83 | ||
|
|
867d7898df | ||
|
|
fa46138047 | ||
|
|
c135624c69 | ||
|
|
048f307695 | ||
|
|
feffb6d02f | ||
|
|
a16c6ca94b | ||
|
|
7fb748462e | ||
|
|
50dcaad71a | ||
|
|
7a7ca15776 | ||
|
|
bf19d198d9 | ||
|
|
eaad487c42 | ||
|
|
12ade5c5e8 | ||
|
|
076bf2a361 | ||
|
|
0156de5c34 | ||
|
|
646eb00112 | ||
|
|
06f95f9a65 | ||
|
|
9a78886c78 | ||
|
|
66212260ef | ||
|
|
dda0a98b76 | ||
|
|
c71d3e45a1 | ||
|
|
986025afe4 | ||
|
|
0d393ba6b4 | ||
|
|
0de924b35c | ||
|
|
4a47a9db98 | ||
|
|
5fa55d93f7 | ||
|
|
64008398d1 | ||
|
|
5c362884f3 | ||
|
|
71b09b99f8 | ||
|
|
3f31b62cd4 | ||
|
|
4927388580 | ||
|
|
19da9d8832 | ||
|
|
bea27678b4 | ||
|
|
ba28f7b018 |
@@ -2085,6 +2085,7 @@ class NodeRuntime(
|
||||
id = id,
|
||||
name = obj["name"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() } ?: id,
|
||||
provider = provider,
|
||||
available = obj.optionalBoolean("available"),
|
||||
supportsVision = "image" in inputTypes,
|
||||
supportsAudio = "audio" in inputTypes,
|
||||
supportsDocuments = "document" in inputTypes,
|
||||
@@ -2701,6 +2702,7 @@ data class GatewayModelSummary(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val provider: String,
|
||||
val available: Boolean?,
|
||||
val supportsVision: Boolean,
|
||||
val supportsAudio: Boolean,
|
||||
val supportsDocuments: Boolean,
|
||||
@@ -2883,6 +2885,15 @@ private fun JsonObject?.double(key: String): Double? = (this?.get(key) as? JsonP
|
||||
|
||||
private fun JsonObject?.boolean(key: String): Boolean = (this?.get(key) as? JsonPrimitive)?.content?.trim() == "true"
|
||||
|
||||
private fun JsonObject?.optionalBoolean(key: String): Boolean? =
|
||||
(this?.get(key) as? JsonPrimitive)?.content?.trim()?.lowercase()?.let { value ->
|
||||
when (value) {
|
||||
"true" -> true
|
||||
"false" -> false
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun cronJobLastRunStatus(state: JsonObject?): String? =
|
||||
state
|
||||
.cronStatus("lastStatus")
|
||||
|
||||
@@ -297,14 +297,15 @@ private fun CommandSectionLabel(title: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds provider quick-action metadata from current gateway/catalog state. */
|
||||
private fun providerCommandSubtitle(
|
||||
internal fun providerCommandSubtitle(
|
||||
isConnected: Boolean,
|
||||
providers: List<GatewayModelProviderSummary>,
|
||||
models: List<GatewayModelSummary>,
|
||||
): String {
|
||||
if (!isConnected) return "Connect Gateway to load models"
|
||||
val readyProviderCount = providers.count { modelProviderReady(it.status) }
|
||||
val expiringProviderCount = expiringModelProviderCount(providers)
|
||||
if (expiringProviderCount > 0) return "$expiringProviderCount providers expiring"
|
||||
val readyProviderCount = readyModelProviderCount(providers, models)
|
||||
if (readyProviderCount > 0) return "$readyProviderCount providers ready"
|
||||
if (models.isNotEmpty()) return "${models.size} models available"
|
||||
return "Configure model access"
|
||||
|
||||
@@ -192,6 +192,9 @@ private data class ProviderSetupRow(
|
||||
val name: String,
|
||||
val subtitle: String,
|
||||
val ready: Boolean,
|
||||
val available: Boolean,
|
||||
val statusLabel: String,
|
||||
val warning: Boolean,
|
||||
)
|
||||
|
||||
private data class ProviderRow(
|
||||
@@ -199,37 +202,60 @@ private data class ProviderRow(
|
||||
val name: String,
|
||||
val status: String,
|
||||
val ready: Boolean,
|
||||
val available: Boolean,
|
||||
val setupRequired: Boolean,
|
||||
val warning: Boolean,
|
||||
val modelCount: Int,
|
||||
)
|
||||
|
||||
/** Combines auth-provider readiness rows with catalog-only providers. */
|
||||
/** Combines auth-provider readiness rows with catalog-only browse providers. */
|
||||
private fun providerRows(
|
||||
providers: List<GatewayModelProviderSummary>,
|
||||
models: List<GatewayModelSummary>,
|
||||
): List<ProviderRow> {
|
||||
val modelCounts = models.groupingBy { it.provider }.eachCount()
|
||||
val availableProviderIds =
|
||||
models
|
||||
.filter(::modelAvailabilityUsable)
|
||||
.map { it.provider.normalizedProviderId() }
|
||||
.toSet()
|
||||
val authRows =
|
||||
providers.map { provider ->
|
||||
val ready = modelProviderReady(provider.status)
|
||||
val providerId = provider.id.normalizedProviderId()
|
||||
val authReady = modelProviderReady(provider.status)
|
||||
val expiring = modelProviderExpiring(provider.status)
|
||||
val available = providerId in availableProviderIds
|
||||
ProviderRow(
|
||||
id = provider.id,
|
||||
name = provider.displayName,
|
||||
status = if (ready) "Ready" else "Needs setup",
|
||||
ready = ready,
|
||||
status =
|
||||
when {
|
||||
authReady -> "Ready"
|
||||
expiring -> "Expiring"
|
||||
available -> "Available"
|
||||
else -> "Needs setup"
|
||||
},
|
||||
ready = authReady,
|
||||
available = available || authReady || expiring,
|
||||
setupRequired = !authReady && !available && !expiring,
|
||||
warning = expiring,
|
||||
modelCount = modelCounts[provider.id] ?: 0,
|
||||
)
|
||||
}
|
||||
// Static/catalog-only providers may expose models without a matching auth
|
||||
// provider row; keep them visible as ready providers.
|
||||
// Catalog-only providers can be browsed but are not a readiness signal.
|
||||
val missingAuthRows =
|
||||
modelCounts.keys
|
||||
.filter { provider -> authRows.none { it.id == provider } }
|
||||
.map { provider ->
|
||||
val available = provider.normalizedProviderId() in availableProviderIds
|
||||
ProviderRow(
|
||||
id = provider,
|
||||
name = providerDisplayName(provider),
|
||||
status = "Ready",
|
||||
ready = true,
|
||||
status = if (available) "Available" else "Catalog",
|
||||
ready = available,
|
||||
available = available,
|
||||
setupRequired = false,
|
||||
warning = false,
|
||||
modelCount = modelCounts[provider] ?: 0,
|
||||
)
|
||||
}
|
||||
@@ -245,6 +271,9 @@ private fun providerSetupRows(providerRows: List<ProviderRow>): List<ProviderSet
|
||||
name = providerDisplayName(id),
|
||||
subtitle = providerSetupSubtitle(id, row),
|
||||
ready = row?.ready == true,
|
||||
available = row?.available == true,
|
||||
statusLabel = providerSetupStatusLabel(row),
|
||||
warning = row?.warning == true || row?.setupRequired == true || row == null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -254,12 +283,24 @@ private fun providerSetupSubtitle(
|
||||
row: ProviderRow?,
|
||||
): String =
|
||||
when {
|
||||
row?.warning == true -> "Credential expires soon"
|
||||
row?.ready == true -> if (row.modelCount > 0) "${row.modelCount} models available" else "Ready"
|
||||
row != null -> "Finish setup to use ${row.name}"
|
||||
row?.available == true -> if (row.modelCount > 0) "${row.modelCount} models available" else "Available"
|
||||
row?.setupRequired == true -> "Finish setup to use ${row.name}"
|
||||
row != null && row.modelCount > 0 -> "${row.modelCount} catalog models"
|
||||
id == "ollama" -> "Use models running on your network"
|
||||
else -> "Add provider credentials on your Gateway"
|
||||
}
|
||||
|
||||
private fun providerSetupStatusLabel(row: ProviderRow?): String =
|
||||
when {
|
||||
row?.ready == true -> "Ready"
|
||||
row?.warning == true -> "Expiring"
|
||||
row?.available == true -> "Available"
|
||||
row?.setupRequired == false -> "Catalog"
|
||||
else -> "Setup"
|
||||
}
|
||||
|
||||
/** Normalizes gateway provider status strings into a ready/not-ready boolean. */
|
||||
internal fun modelProviderReady(status: String): Boolean {
|
||||
val normalized = status.trim().lowercase()
|
||||
@@ -270,6 +311,30 @@ internal fun modelProviderReady(status: String): Boolean {
|
||||
normalized == "static"
|
||||
}
|
||||
|
||||
private fun modelProviderExpiring(status: String): Boolean = status.trim().lowercase() == "expiring"
|
||||
|
||||
internal fun readyModelProviderCount(
|
||||
providers: List<GatewayModelProviderSummary>,
|
||||
models: List<GatewayModelSummary>,
|
||||
): Int {
|
||||
val authReadyProviders = providers.filter { modelProviderReady(it.status) }.map { it.id.normalizedProviderId() }
|
||||
val availableModelProviders = models.filter(::modelAvailabilityUsable).map { it.provider.normalizedProviderId() }
|
||||
return (authReadyProviders + availableModelProviders).distinct().size
|
||||
}
|
||||
|
||||
// Older gateways did not emit `available`; keep those rows on the legacy
|
||||
// readiness path while still honoring explicit false from upgraded gateways.
|
||||
internal fun modelAvailabilityUsable(model: GatewayModelSummary): Boolean = model.available != false
|
||||
|
||||
internal fun expiringModelProviderCount(providers: List<GatewayModelProviderSummary>): Int =
|
||||
providers
|
||||
.filter { modelProviderExpiring(it.status) }
|
||||
.map { it.id.normalizedProviderId() }
|
||||
.distinct()
|
||||
.size
|
||||
|
||||
private fun String.normalizedProviderId(): String = trim().lowercase()
|
||||
|
||||
/** Groups models by provider using the same display priority as provider rows. */
|
||||
private fun sortedModelGroups(models: List<GatewayModelSummary>): List<Pair<String, List<GatewayModelSummary>>> =
|
||||
models
|
||||
@@ -299,7 +364,18 @@ private fun ProviderList(
|
||||
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
|
||||
Column {
|
||||
if (rows.isEmpty()) {
|
||||
ProviderListRow(ProviderRow(id = "loading", name = "Provider catalog", status = if (refreshing) "Loading" else "No providers", ready = false, modelCount = 0))
|
||||
ProviderListRow(
|
||||
ProviderRow(
|
||||
id = "loading",
|
||||
name = "Provider catalog",
|
||||
status = if (refreshing) "Loading" else "No providers",
|
||||
ready = false,
|
||||
available = false,
|
||||
setupRequired = false,
|
||||
warning = false,
|
||||
modelCount = 0,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
val visibleRows = rows.take(5)
|
||||
visibleRows.forEachIndexed { index, row ->
|
||||
@@ -322,12 +398,12 @@ private fun ProviderOverviewPanel(
|
||||
onRefresh: () -> Unit,
|
||||
onSetup: () -> Unit,
|
||||
) {
|
||||
val readyCount = providerRows.count { it.ready }
|
||||
val needsSetupCount = providerRows.count { !it.ready }
|
||||
val readyCount = providerRows.count { it.available }
|
||||
val needsSetupCount = providerRows.count { it.setupRequired }
|
||||
ClawPanel(contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
ProviderMetricTile(label = "Ready", value = readyCount.toString(), modifier = Modifier.weight(1f))
|
||||
ProviderMetricTile(label = "Available", value = readyCount.toString(), modifier = Modifier.weight(1f))
|
||||
ProviderMetricTile(label = "Models", value = modelCount.toString(), modifier = Modifier.weight(1f))
|
||||
ProviderMetricTile(label = "Setup", value = needsSetupCount.toString(), modifier = Modifier.weight(1f))
|
||||
}
|
||||
@@ -398,8 +474,14 @@ private fun ProviderSetupListRow(
|
||||
Text(text = row.subtitle, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Box(modifier = Modifier.size(5.dp).clip(CircleShape).background(if (row.ready) ClawTheme.colors.success else ClawTheme.colors.warning))
|
||||
Text(text = if (row.ready) "Ready" else "Setup", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
|
||||
val statusColor =
|
||||
when {
|
||||
row.warning -> ClawTheme.colors.warning
|
||||
row.ready || row.available -> ClawTheme.colors.success
|
||||
else -> ClawTheme.colors.textMuted
|
||||
}
|
||||
Box(modifier = Modifier.size(5.dp).clip(CircleShape).background(statusColor))
|
||||
Text(text = row.statusLabel, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
|
||||
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Open ${row.name}", modifier = Modifier.size(17.dp), tint = ClawTheme.colors.text)
|
||||
}
|
||||
}
|
||||
@@ -415,7 +497,13 @@ private fun ProviderListRow(row: ProviderRow) {
|
||||
Text(text = if (row.modelCount > 0) "${row.modelCount} models" else "Provider setup", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (row.ready) ClawTheme.colors.success else ClawTheme.colors.warning))
|
||||
val statusColor =
|
||||
when {
|
||||
row.warning || row.setupRequired -> ClawTheme.colors.warning
|
||||
row.ready || row.available -> ClawTheme.colors.success
|
||||
else -> ClawTheme.colors.textMuted
|
||||
}
|
||||
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(statusColor))
|
||||
Text(text = row.status, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
|
||||
}
|
||||
}
|
||||
@@ -491,12 +579,13 @@ private fun ModelGroup(
|
||||
|
||||
@Composable
|
||||
private fun ModelRow(model: GatewayModelSummary) {
|
||||
val available = modelAvailabilityUsable(model)
|
||||
Row(modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp).padding(horizontal = 10.dp, vertical = 5.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(text = model.name, style = ClawTheme.type.mono, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
modelCapabilityLabels(model).take(3).forEach { label ->
|
||||
ProviderMiniTag(text = label)
|
||||
}
|
||||
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(ClawTheme.colors.success))
|
||||
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (available) ClawTheme.colors.success else ClawTheme.colors.warning))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -342,7 +342,8 @@ private fun OverviewScreen(
|
||||
val cronStatus by viewModel.cronStatus.collectAsState()
|
||||
val nodesDevicesSummary by viewModel.nodesDevicesSummary.collectAsState()
|
||||
val channelsSummary by viewModel.channelsSummary.collectAsState()
|
||||
val readyProviderCount = providers.count { modelProviderReady(it.status) }
|
||||
val readyProviderCount = readyModelProviderCount(providers, models)
|
||||
val expiringProviderCount = expiringModelProviderCount(providers)
|
||||
val attentionRows =
|
||||
homeAttentionRows(
|
||||
isConnected = isConnected,
|
||||
@@ -350,6 +351,7 @@ private fun OverviewScreen(
|
||||
channelsSummary = channelsSummary,
|
||||
nodesDevicesSummary = nodesDevicesSummary,
|
||||
readyProviderCount = readyProviderCount,
|
||||
expiringProviderCount = expiringProviderCount,
|
||||
)
|
||||
|
||||
LaunchedEffect(isConnected) {
|
||||
@@ -460,6 +462,7 @@ private fun OverviewScreen(
|
||||
when {
|
||||
!isConnected -> "Offline"
|
||||
readyProviderCount > 0 -> "$readyProviderCount ready"
|
||||
expiringProviderCount > 0 -> "$expiringProviderCount expiring"
|
||||
models.isNotEmpty() -> "${models.size} models"
|
||||
else -> "Setup"
|
||||
},
|
||||
@@ -541,6 +544,7 @@ internal fun homeAttentionRows(
|
||||
channelsSummary: GatewayChannelsSummary,
|
||||
nodesDevicesSummary: GatewayNodesDevicesSummary,
|
||||
readyProviderCount: Int,
|
||||
expiringProviderCount: Int = 0,
|
||||
): List<HomeAttentionRow> =
|
||||
listOfNotNull(
|
||||
if (!isConnected) {
|
||||
@@ -563,7 +567,12 @@ internal fun homeAttentionRows(
|
||||
} else {
|
||||
null
|
||||
},
|
||||
if (isConnected && readyProviderCount == 0) {
|
||||
if (isConnected && expiringProviderCount > 0) {
|
||||
HomeAttentionRow("Providers", "Provider auth expires soon", Icons.Outlined.Inventory2, Tab.ProvidersModels)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
if (isConnected && readyProviderCount == 0 && expiringProviderCount == 0) {
|
||||
HomeAttentionRow("Providers", "No ready providers", Icons.Outlined.Inventory2, Tab.ProvidersModels)
|
||||
} else {
|
||||
null
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package ai.openclaw.app.ui
|
||||
|
||||
import ai.openclaw.app.GatewayModelProviderSummary
|
||||
import ai.openclaw.app.GatewayModelSummary
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
@@ -10,8 +13,129 @@ class ProviderModelStatusTest {
|
||||
assertTrue(modelProviderReady("static"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun expiringProviderStatusIsNotFullyReady() {
|
||||
assertFalse(modelProviderReady("expiring"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingProviderStatusIsNotReady() {
|
||||
assertFalse(modelProviderReady("missing"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readyModelProviderCountUsesAuthBackedProviderStatuses() {
|
||||
val providers =
|
||||
listOf(
|
||||
GatewayModelProviderSummary(id = "openai", displayName = "OpenAI", status = "missing", profileCount = 0),
|
||||
GatewayModelProviderSummary(id = "anthropic", displayName = "Anthropic", status = "ready", profileCount = 1),
|
||||
GatewayModelProviderSummary(id = "openai", displayName = "OpenAI", status = "expiring", profileCount = 1),
|
||||
)
|
||||
|
||||
assertEquals(1, readyModelProviderCount(providers, emptyList()))
|
||||
assertEquals(1, expiringModelProviderCount(providers))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readyModelProviderCountUsesAvailableModelsAsServingReadiness() {
|
||||
val models =
|
||||
listOf(
|
||||
model(provider = "anthropic", available = true),
|
||||
model(provider = "anthropic", available = true),
|
||||
model(provider = "openrouter", available = false),
|
||||
)
|
||||
|
||||
assertEquals(1, readyModelProviderCount(emptyList(), models))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readyModelProviderCountDoesNotTreatCatalogOnlyModelsAsReady() {
|
||||
val providers =
|
||||
listOf(
|
||||
GatewayModelProviderSummary(id = "openrouter", displayName = "OpenRouter", status = "missing", profileCount = 0),
|
||||
)
|
||||
val models =
|
||||
listOf(
|
||||
model(provider = "openrouter", available = false),
|
||||
)
|
||||
|
||||
assertEquals(0, readyModelProviderCount(providers, models))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readyModelProviderCountPreservesLegacyRowsWhenAvailabilityIsMissing() {
|
||||
val models =
|
||||
listOf(
|
||||
model(provider = "openrouter", available = null),
|
||||
)
|
||||
|
||||
assertEquals(1, readyModelProviderCount(emptyList(), models))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readyModelProviderCountTreatsExpiringAvailableModelsAsUsableButWarnable() {
|
||||
val providers =
|
||||
listOf(
|
||||
GatewayModelProviderSummary(id = "openai", displayName = "OpenAI", status = "expiring", profileCount = 1),
|
||||
)
|
||||
val models =
|
||||
listOf(
|
||||
model(provider = "openai", available = true),
|
||||
)
|
||||
|
||||
assertEquals(1, readyModelProviderCount(providers, models))
|
||||
assertEquals(1, expiringModelProviderCount(providers))
|
||||
assertFalse(modelProviderReady("expiring"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun providerCommandSubtitleSurfacesExpiringBeforeReadyModels() {
|
||||
val providers =
|
||||
listOf(
|
||||
GatewayModelProviderSummary(id = "openai", displayName = "OpenAI", status = "expiring", profileCount = 1),
|
||||
)
|
||||
val models =
|
||||
listOf(
|
||||
model(provider = "openai", available = true),
|
||||
)
|
||||
|
||||
assertEquals("1 providers expiring", providerCommandSubtitle(isConnected = true, providers = providers, models = models))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readyModelProviderCountDoesNotTreatUnavailableModelsAsReadyWhenAuthProviderNeedsSetup() {
|
||||
val providers =
|
||||
listOf(
|
||||
GatewayModelProviderSummary(id = "openai", displayName = "OpenAI", status = "missing", profileCount = 0),
|
||||
)
|
||||
val models =
|
||||
listOf(
|
||||
model(provider = "openai", available = false),
|
||||
)
|
||||
|
||||
assertEquals(0, readyModelProviderCount(providers, models))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun modelAvailabilityHonorsExplicitUnavailableRows() {
|
||||
assertTrue(modelAvailabilityUsable(model(provider = "openai", available = true)))
|
||||
assertTrue(modelAvailabilityUsable(model(provider = "openai", available = null)))
|
||||
assertFalse(modelAvailabilityUsable(model(provider = "openai", available = false)))
|
||||
}
|
||||
|
||||
private fun model(
|
||||
provider: String,
|
||||
available: Boolean?,
|
||||
): GatewayModelSummary =
|
||||
GatewayModelSummary(
|
||||
id = "$provider/test-model",
|
||||
name = "test-model",
|
||||
provider = provider,
|
||||
available = available,
|
||||
supportsVision = false,
|
||||
supportsAudio = false,
|
||||
supportsDocuments = false,
|
||||
supportsReasoning = false,
|
||||
contextTokens = null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -92,6 +92,21 @@ class ShellScreenLogicTest {
|
||||
assertEquals(emptyList<String>(), rows.map { it.title })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun homeAttentionRowsSurfaceExpiringProviderAuth() {
|
||||
val rows =
|
||||
homeAttentionRows(
|
||||
isConnected = true,
|
||||
pendingApprovals = 0,
|
||||
channelsSummary = emptyChannels(),
|
||||
nodesDevicesSummary = emptyNodesDevices(),
|
||||
readyProviderCount = 0,
|
||||
expiringProviderCount = 1,
|
||||
)
|
||||
|
||||
assertEquals(listOf("Provider auth expires soon"), rows.map { it.subtitle })
|
||||
}
|
||||
|
||||
private fun emptyChannels(): GatewayChannelsSummary = GatewayChannelsSummary(channels = emptyList())
|
||||
|
||||
private fun emptyNodesDevices(): GatewayNodesDevicesSummary = GatewayNodesDevicesSummary(nodes = emptyList(), pendingDevices = emptyList(), pairedDevices = emptyList())
|
||||
|
||||
@@ -59,6 +59,11 @@ struct SettingsProTab: View {
|
||||
@State var notificationActionText = "Request Access"
|
||||
@State var diagnosticsLastRunText = "Not run"
|
||||
@State var diagnosticsIssueCount: Int?
|
||||
@State var bottomOverlayInset: CGFloat = 0
|
||||
|
||||
var bottomScrollMargin: CGFloat {
|
||||
max(0, self.bottomOverlayInset - SettingsLayout.rowHeight - SettingsLayout.bottomContentPadding)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -71,9 +76,13 @@ struct SettingsProTab: View {
|
||||
self.gatewaySection
|
||||
self.settingsListSection
|
||||
}
|
||||
.padding(.vertical, 18)
|
||||
.padding(.top, 18)
|
||||
.padding(.bottom, SettingsLayout.bottomContentPadding)
|
||||
}
|
||||
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
|
||||
.contentMargins(.bottom, self.bottomScrollMargin, for: .scrollContent)
|
||||
SettingsBottomOverlayInsetReader(inset: self.$bottomOverlayInset)
|
||||
.frame(width: 0, height: 0)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.navigationDestination(for: SettingsRoute.self) { route in
|
||||
|
||||
@@ -37,6 +37,8 @@ extension SettingsProTab {
|
||||
NavigationLink(value: SettingsRoute.gateway) {
|
||||
self.gatewayConnectionRow
|
||||
.padding(14)
|
||||
.frame(maxWidth: .infinity, minHeight: SettingsLayout.rowHeight, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
Divider()
|
||||
@@ -201,9 +203,13 @@ extension SettingsProTab {
|
||||
self.aboutDestination
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 18)
|
||||
.padding(.top, 18)
|
||||
.padding(.bottom, SettingsLayout.bottomContentPadding)
|
||||
}
|
||||
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
|
||||
.contentMargins(.bottom, self.bottomScrollMargin, for: .scrollContent)
|
||||
SettingsBottomOverlayInsetReader(inset: self.$bottomOverlayInset)
|
||||
.frame(width: 0, height: 0)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.navigationTitle(self.title(for: route))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -239,11 +245,11 @@ extension SettingsProTab {
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
|
||||
self.manualGatewayCard
|
||||
self.deviceIdentityCard
|
||||
self.agentSelectionCard
|
||||
self.gatewaySetupCard
|
||||
self.discoveredGatewaysCard
|
||||
self.manualGatewayCard
|
||||
self.gatewayAdvancedCard
|
||||
}
|
||||
}
|
||||
@@ -292,18 +298,6 @@ extension SettingsProTab {
|
||||
value: self.diagnosticsHealthValue,
|
||||
color: self.gatewayConnected ? OpenClawBrand.ok : OpenClawBrand.warn)
|
||||
|
||||
self.diagnosticChecksCard
|
||||
|
||||
self.detailListCard {
|
||||
self.detailRow("Device", value: DeviceInfoHelper.deviceFamily())
|
||||
Divider()
|
||||
self.detailRow("Platform", value: DeviceInfoHelper.platformStringForDisplay())
|
||||
Divider()
|
||||
self.detailRow("App", value: DeviceInfoHelper.openClawVersionString())
|
||||
Divider()
|
||||
self.detailRow("Model", value: DeviceInfoHelper.modelIdentifier())
|
||||
}
|
||||
|
||||
ProCard(radius: SettingsLayout.cardRadius) {
|
||||
self.gatewayActionButton(
|
||||
title: "Run Diagnostics",
|
||||
@@ -316,6 +310,18 @@ extension SettingsProTab {
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
|
||||
self.diagnosticChecksCard
|
||||
|
||||
self.detailListCard {
|
||||
self.detailRow("Device", value: DeviceInfoHelper.deviceFamily())
|
||||
Divider()
|
||||
self.detailRow("Platform", value: DeviceInfoHelper.platformStringForDisplay())
|
||||
Divider()
|
||||
self.detailRow("App", value: DeviceInfoHelper.openClawVersionString())
|
||||
Divider()
|
||||
self.detailRow("Model", value: DeviceInfoHelper.modelIdentifier())
|
||||
}
|
||||
|
||||
self.diagnosticsAdvancedCard
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Darwin
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
enum SettingsRoute: Hashable {
|
||||
case gateway
|
||||
@@ -14,6 +15,110 @@ enum SettingsRoute: Hashable {
|
||||
enum SettingsLayout {
|
||||
static let cardRadius: CGFloat = 12
|
||||
static let rowHeight: CGFloat = 58
|
||||
static let bottomContentPadding: CGFloat = 12
|
||||
}
|
||||
|
||||
struct SettingsBottomOverlayInsetReader: UIViewRepresentable {
|
||||
@Binding var inset: CGFloat
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(inset: self.$inset)
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> SettingsBottomOverlayInsetProbeView {
|
||||
let view = SettingsBottomOverlayInsetProbeView()
|
||||
view.onInsetChange = { value in
|
||||
context.coordinator.updateInset(value)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: SettingsBottomOverlayInsetProbeView, context: Context) {
|
||||
context.coordinator.inset = self.$inset
|
||||
uiView.onInsetChange = { value in
|
||||
context.coordinator.updateInset(value)
|
||||
}
|
||||
uiView.updateInset()
|
||||
}
|
||||
|
||||
final class Coordinator {
|
||||
var inset: Binding<CGFloat>
|
||||
|
||||
init(inset: Binding<CGFloat>) {
|
||||
self.inset = inset
|
||||
}
|
||||
|
||||
func updateInset(_ value: CGFloat) {
|
||||
let rounded = max(0, ceil(value))
|
||||
guard abs(self.inset.wrappedValue - rounded) > 0.5 else { return }
|
||||
self.inset.wrappedValue = rounded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class SettingsBottomOverlayInsetProbeView: UIView {
|
||||
var onInsetChange: ((CGFloat) -> Void)?
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
self.updateInset()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
self.updateInset()
|
||||
}
|
||||
|
||||
func updateInset() {
|
||||
let value = self.visibleTabBarHeight()
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.onInsetChange?(value)
|
||||
}
|
||||
}
|
||||
|
||||
private func visibleTabBarHeight() -> CGFloat {
|
||||
let tabBarController = self.nearestViewController()?.tabBarController
|
||||
?? self.findTabBarController(in: self.window?.rootViewController)
|
||||
guard let tabBar = tabBarController?.tabBar,
|
||||
!tabBar.isHidden,
|
||||
tabBar.alpha > 0.01,
|
||||
tabBar.window != nil,
|
||||
self.window != nil
|
||||
else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let tabFrame = tabBar.convert(tabBar.bounds, to: nil)
|
||||
guard tabFrame.height.isFinite else { return 0 }
|
||||
return max(0, tabFrame.height)
|
||||
}
|
||||
|
||||
private func nearestViewController() -> UIViewController? {
|
||||
var responder: UIResponder? = self
|
||||
while let current = responder {
|
||||
if let viewController = current as? UIViewController {
|
||||
return viewController
|
||||
}
|
||||
responder = current.next
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func findTabBarController(in viewController: UIViewController?) -> UITabBarController? {
|
||||
guard let viewController else { return nil }
|
||||
if let tabBarController = viewController as? UITabBarController {
|
||||
return tabBarController
|
||||
}
|
||||
if let tabBarController = self.findTabBarController(in: viewController.presentedViewController) {
|
||||
return tabBarController
|
||||
}
|
||||
for child in viewController.children {
|
||||
if let tabBarController = self.findTabBarController(in: child) {
|
||||
return tabBarController
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
enum SettingsDiagnosticIssue: String, Equatable, CaseIterable {
|
||||
|
||||
@@ -15,6 +15,7 @@ struct TalkProTab: View {
|
||||
gatewayConnected: self.gatewayConnected,
|
||||
isEnabled: self.appModel.talkMode.isEnabled || self.talkEnabled,
|
||||
statusText: self.appModel.talkMode.statusText,
|
||||
isConfigLoaded: self.appModel.talkMode.gatewayTalkConfigLoaded,
|
||||
isListening: self.appModel.talkMode.isListening,
|
||||
isSpeaking: self.appModel.talkMode.isSpeaking,
|
||||
isUserSpeechDetected: self.appModel.talkMode.isUserSpeechDetected,
|
||||
@@ -282,6 +283,9 @@ struct TalkProTab: View {
|
||||
if self.state
|
||||
.prefersPermissionCopy { return "Gateway approval is required before this phone can capture voice." }
|
||||
if !self.gatewayConnected { return "Connect to your gateway to start a voice conversation." }
|
||||
if !self.appModel.talkMode.gatewayTalkConfigLoaded {
|
||||
return "Open Voice settings after the gateway loads Talk configuration."
|
||||
}
|
||||
let subtitle = (self.appModel.talkMode.gatewayTalkVoiceModeSubtitle ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !subtitle.isEmpty { return subtitle }
|
||||
@@ -365,6 +369,7 @@ struct TalkProState: Equatable {
|
||||
let gatewayConnected: Bool
|
||||
let isEnabled: Bool
|
||||
let statusText: String
|
||||
let isConfigLoaded: Bool
|
||||
let isListening: Bool
|
||||
let isSpeaking: Bool
|
||||
let isUserSpeechDetected: Bool
|
||||
@@ -390,6 +395,7 @@ struct TalkProState: Equatable {
|
||||
default:
|
||||
break
|
||||
}
|
||||
if !self.isConfigLoaded { return "Voice config unavailable" }
|
||||
if self.isSpeaking { return "Speaking" }
|
||||
if self.isListening { return "Listening" }
|
||||
if self.normalizedStatus.contains("connecting") { return "Connecting" }
|
||||
@@ -412,6 +418,7 @@ struct TalkProState: Equatable {
|
||||
default:
|
||||
break
|
||||
}
|
||||
if !self.isConfigLoaded { return "Config" }
|
||||
if self.isSpeaking { return "Speaking" }
|
||||
if self.isListening { return "Listening" }
|
||||
if self.isEnabled { return "Ready" }
|
||||
@@ -432,6 +439,7 @@ struct TalkProState: Equatable {
|
||||
default:
|
||||
break
|
||||
}
|
||||
if !self.isConfigLoaded { return "exclamationmark.triangle.fill" }
|
||||
if self.isSpeaking { return "speaker.wave.2.fill" }
|
||||
if self.isListening { return "mic.fill" }
|
||||
if self.normalizedStatus.contains("thinking") { return "sparkles" }
|
||||
@@ -447,6 +455,7 @@ struct TalkProState: Equatable {
|
||||
case .missingScope, .requestingUpgrade, .upgradeRequested, .apiKeyMissing:
|
||||
return OpenClawBrand.warn
|
||||
default:
|
||||
if !self.isConfigLoaded { return OpenClawBrand.warn }
|
||||
return self.isEnabled ? OpenClawBrand.ok : OpenClawBrand.accentHot
|
||||
}
|
||||
}
|
||||
@@ -518,6 +527,7 @@ struct TalkProState: Equatable {
|
||||
default:
|
||||
break
|
||||
}
|
||||
if !self.isConfigLoaded { return .still }
|
||||
if self.isSpeaking { return .speaking }
|
||||
if self.isListening, self.isUserSpeechDetected { return .inputSpeech }
|
||||
if self.isListening { return .level(micLevel) }
|
||||
|
||||
73
apps/ios/Tests/TalkProStateTests.swift
Normal file
73
apps/ios/Tests/TalkProStateTests.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@Suite struct TalkProStateTests {
|
||||
@Test func disabledTalkWithoutLoadedConfigCanStartAndRetryLoad() {
|
||||
let state = TalkProState(
|
||||
gatewayConnected: true,
|
||||
isEnabled: false,
|
||||
statusText: "Offline",
|
||||
isConfigLoaded: false,
|
||||
isListening: false,
|
||||
isSpeaking: false,
|
||||
isUserSpeechDetected: false,
|
||||
permissionState: .unknown)
|
||||
|
||||
#expect(state.title == "Voice config unavailable")
|
||||
#expect(state.chipText == "Config")
|
||||
#expect(state.primaryAction == .start)
|
||||
#expect(state.primaryButtonTitle == "Start Talk")
|
||||
#expect(state.waveformMode(micLevel: 0.8) == .still)
|
||||
}
|
||||
|
||||
@Test func enabledTalkWithoutLoadedConfigCanBeStopped() {
|
||||
let state = TalkProState(
|
||||
gatewayConnected: true,
|
||||
isEnabled: true,
|
||||
statusText: "Offline",
|
||||
isConfigLoaded: false,
|
||||
isListening: false,
|
||||
isSpeaking: false,
|
||||
isUserSpeechDetected: false,
|
||||
permissionState: .unknown)
|
||||
|
||||
#expect(state.title == "Voice config unavailable")
|
||||
#expect(state.chipText == "Config")
|
||||
#expect(state.primaryAction == .stop)
|
||||
#expect(state.primaryButtonTitle == "Stop Talk")
|
||||
#expect(state.waveformMode(micLevel: 0.8) == .still)
|
||||
}
|
||||
|
||||
@Test func enabledTalkWithLoadedConfigCanBeStopped() {
|
||||
let state = TalkProState(
|
||||
gatewayConnected: true,
|
||||
isEnabled: true,
|
||||
statusText: "Ready",
|
||||
isConfigLoaded: true,
|
||||
isListening: false,
|
||||
isSpeaking: false,
|
||||
isUserSpeechDetected: false,
|
||||
permissionState: .ready)
|
||||
|
||||
#expect(state.title == "Ready to talk")
|
||||
#expect(state.chipText == "Ready")
|
||||
#expect(state.primaryAction == .stop)
|
||||
}
|
||||
|
||||
@Test func missingScopeTakesPriorityOverUnloadedConfig() {
|
||||
let state = TalkProState(
|
||||
gatewayConnected: true,
|
||||
isEnabled: false,
|
||||
statusText: "Offline",
|
||||
isConfigLoaded: false,
|
||||
isListening: false,
|
||||
isSpeaking: false,
|
||||
isUserSpeechDetected: false,
|
||||
permissionState: .missingScope("operator.talk.secrets"))
|
||||
|
||||
#expect(state.title == "Gateway permission required")
|
||||
#expect(state.chipText == "Needs approval")
|
||||
#expect(state.primaryAction == .enablePermission)
|
||||
#expect(state.primaryButtonTitle == "Enable Talk")
|
||||
}
|
||||
}
|
||||
@@ -201,6 +201,7 @@ struct OpenClawChatComposer: View {
|
||||
Image(systemName: "paperclip")
|
||||
}
|
||||
.help("Add Image")
|
||||
.accessibilityLabel("Attachments")
|
||||
.buttonStyle(.plain)
|
||||
.controlSize(.small)
|
||||
} else {
|
||||
@@ -210,6 +211,7 @@ struct OpenClawChatComposer: View {
|
||||
Image(systemName: "paperclip")
|
||||
}
|
||||
.help("Add Image")
|
||||
.accessibilityLabel("Attachments")
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
@@ -219,6 +221,7 @@ struct OpenClawChatComposer: View {
|
||||
Image(systemName: "paperclip")
|
||||
}
|
||||
.help("Add Image")
|
||||
.accessibilityLabel("Attachments")
|
||||
.buttonStyle(.plain)
|
||||
.controlSize(.small)
|
||||
.onChange(of: self.pickerItems) { _, newItems in
|
||||
@@ -229,6 +232,7 @@ struct OpenClawChatComposer: View {
|
||||
Image(systemName: "paperclip")
|
||||
}
|
||||
.help("Add Image")
|
||||
.accessibilityLabel("Attachments")
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
.onChange(of: self.pickerItems) { _, newItems in
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Bundled A2UI runtime resource embedded by OpenClawKit.
|
||||
var __defProp$1 = Object.defineProperty;
|
||||
var __exportAll = (all, no_symbols) => {
|
||||
let target = {};
|
||||
|
||||
@@ -52,7 +52,6 @@ public struct ConnectParams: Codable, Sendable {
|
||||
public let client: [String: AnyCodable]
|
||||
public let caps: [String]?
|
||||
public let commands: [String]?
|
||||
public let nodeplugintools: [NodePluginToolDescriptor]?
|
||||
public let permissions: [String: AnyCodable]?
|
||||
public let pathenv: String?
|
||||
public let role: String?
|
||||
@@ -68,7 +67,6 @@ public struct ConnectParams: Codable, Sendable {
|
||||
client: [String: AnyCodable],
|
||||
caps: [String]?,
|
||||
commands: [String]?,
|
||||
nodeplugintools: [NodePluginToolDescriptor]?,
|
||||
permissions: [String: AnyCodable]?,
|
||||
pathenv: String?,
|
||||
role: String?,
|
||||
@@ -83,7 +81,6 @@ public struct ConnectParams: Codable, Sendable {
|
||||
self.client = client
|
||||
self.caps = caps
|
||||
self.commands = commands
|
||||
self.nodeplugintools = nodeplugintools
|
||||
self.permissions = permissions
|
||||
self.pathenv = pathenv
|
||||
self.role = role
|
||||
@@ -100,7 +97,6 @@ public struct ConnectParams: Codable, Sendable {
|
||||
case client
|
||||
case caps
|
||||
case commands
|
||||
case nodeplugintools = "nodePluginTools"
|
||||
case permissions
|
||||
case pathenv = "pathEnv"
|
||||
case role
|
||||
@@ -1132,54 +1128,6 @@ public struct NodeRenameParams: Codable, Sendable {
|
||||
|
||||
public struct NodeListParams: Codable, Sendable {}
|
||||
|
||||
public struct NodePluginToolDescriptor: Codable, Sendable {
|
||||
public let pluginid: String
|
||||
public let name: String
|
||||
public let description: String
|
||||
public let parameters: [String: AnyCodable]?
|
||||
public let command: String?
|
||||
public let mcp: [String: AnyCodable]?
|
||||
|
||||
public init(
|
||||
pluginid: String,
|
||||
name: String,
|
||||
description: String,
|
||||
parameters: [String: AnyCodable]?,
|
||||
command: String?,
|
||||
mcp: [String: AnyCodable]?)
|
||||
{
|
||||
self.pluginid = pluginid
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.parameters = parameters
|
||||
self.command = command
|
||||
self.mcp = mcp
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case pluginid = "pluginId"
|
||||
case name
|
||||
case description
|
||||
case parameters
|
||||
case command
|
||||
case mcp
|
||||
}
|
||||
}
|
||||
|
||||
public struct NodePluginToolsUpdateParams: Codable, Sendable {
|
||||
public let tools: [NodePluginToolDescriptor]
|
||||
|
||||
public init(
|
||||
tools: [NodePluginToolDescriptor])
|
||||
{
|
||||
self.tools = tools
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case tools
|
||||
}
|
||||
}
|
||||
|
||||
public struct NodePendingAckParams: Codable, Sendable {
|
||||
public let ids: [String]
|
||||
|
||||
@@ -4729,6 +4677,7 @@ public struct ModelChoice: Codable, Sendable {
|
||||
public let name: String
|
||||
public let provider: String
|
||||
public let alias: String?
|
||||
public let available: Bool?
|
||||
public let contextwindow: Int?
|
||||
public let reasoning: Bool?
|
||||
|
||||
@@ -4737,6 +4686,7 @@ public struct ModelChoice: Codable, Sendable {
|
||||
name: String,
|
||||
provider: String,
|
||||
alias: String?,
|
||||
available: Bool? = nil,
|
||||
contextwindow: Int?,
|
||||
reasoning: Bool?)
|
||||
{
|
||||
@@ -4744,6 +4694,7 @@ public struct ModelChoice: Codable, Sendable {
|
||||
self.name = name
|
||||
self.provider = provider
|
||||
self.alias = alias
|
||||
self.available = available
|
||||
self.contextwindow = contextwindow
|
||||
self.reasoning = reasoning
|
||||
}
|
||||
@@ -4753,6 +4704,7 @@ public struct ModelChoice: Codable, Sendable {
|
||||
case name
|
||||
case provider
|
||||
case alias
|
||||
case available
|
||||
case contextwindow = "contextWindow"
|
||||
case reasoning
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
e3b8988a10c61dbf0a78a70bca9ef1ab43c6a58aeaa5ef9f8699f34b6dae4c9d config-baseline.json
|
||||
a2f53abfe6bbe8b1ddfa5548f555704d8ff0cdd48bcb5780d66499bec0b7775a config-baseline.core.json
|
||||
3d0f7723873da553f25dfe6892a586d774fa36e447de487eba4dd3e0a012f877 config-baseline.channel.json
|
||||
60c0700719fd2fe3f7cec4c35da10227b681d87ed1a3876ef830eb6bd80d43f2 config-baseline.json
|
||||
2ed21fa4a416ac2cec55eb2b6d1b11859aa04b40bd78c6ed9f3eb45b7240261c config-baseline.core.json
|
||||
0637c9bdcb9517f56049dd786563366877458d35df575328a6b80a890c8bc915 config-baseline.channel.json
|
||||
e6a1d6f51f0d9c04bd92d51deebfaca8c7917dd28d7998d225c0074e0a095348 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
9ce72d763de6c95566e0167f99f5454b07c7c67940675533cb24c07058619a63 plugin-sdk-api-baseline.json
|
||||
e4dfccb85b985fe865145e24978255b729cdcbca0e26650a363a11bfcfc2e27b plugin-sdk-api-baseline.jsonl
|
||||
a3ab01b572937539e563aa320ad80135bc701e20fffc43c0351d799590b7a0e0 plugin-sdk-api-baseline.json
|
||||
9d49587923f8fc4abb16d981bcab54acbf90a3e74ab05933761049e2da0cffe1 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -162,6 +162,7 @@ Configure your tunnel's ingress rules to only route the webhook path:
|
||||
4. DM access is pairing by default. Unknown senders receive a pairing code; approve with:
|
||||
- `openclaw pairing approve googlechat <code>`
|
||||
5. Group spaces require @-mention by default. Use `botUser` if mention detection needs the app's user name.
|
||||
6. When an exec or plugin approval request starts from Google Chat and a stable `users/<id>` approver is configured, OpenClaw posts a native Google Chat approval card in the originating space or thread. The card buttons use opaque callback tokens, and the manual `/approve <id> <decision>` prompt is only shown when native approval delivery is unavailable.
|
||||
|
||||
## Targets
|
||||
|
||||
@@ -214,8 +215,9 @@ Notes:
|
||||
- Default webhook path is `/googlechat` if `webhookPath` isn't set.
|
||||
- `dangerouslyAllowNameMatching` re-enables mutable email principal matching for allowlists (break-glass compatibility mode).
|
||||
- Reactions are available via the `reactions` tool and `channels action` when `actions.reactions` is enabled.
|
||||
- Native approval cards use Google Chat `cardsV2` button clicks, not reaction events. Approvers come from `dm.allowFrom` or `defaultTo` and must be stable numeric `users/<id>` values.
|
||||
- Message actions expose `send` for text and `upload-file` for explicit attachment sends. `upload-file` accepts `media` / `filePath` / `path` plus optional `message`, `filename`, and thread targeting.
|
||||
- `typingIndicator` supports `none`, `message` (default), and `reaction` (reaction requires user OAuth).
|
||||
- `typingIndicator` supports `message` (default), `none`, and `reaction` (reaction requires user OAuth).
|
||||
- Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`).
|
||||
- Bot-authored Google Chat messages are ignored by default. If you intentionally set `allowBots: true`, accepted bot-authored messages use shared [bot loop protection](/channels/bot-loop-protection). Configure `channels.defaults.botLoopProtection`, then override with `channels.googlechat.botLoopProtection` or `channels.googlechat.groups.<space>.botLoopProtection` when one space needs a different budget.
|
||||
|
||||
|
||||
@@ -232,6 +232,20 @@ Notes:
|
||||
- Tool-progress preview updates are enabled by default when Matrix preview streaming is active. Set `streaming.preview.toolProgress: false` to keep preview edits for answer text but leave tool progress on the normal delivery path.
|
||||
- Preview edits cost extra Matrix API calls. Leave `streaming: "off"` if you want the most conservative rate-limit profile.
|
||||
|
||||
## Voice messages
|
||||
|
||||
Inbound Matrix voice notes are transcribed before the room mention gate. This lets a voice note that says the bot name trigger the agent in a `requireMention: true` room, and it gives the agent the transcript instead of only an audio attachment placeholder.
|
||||
|
||||
Matrix uses the shared audio media provider configured under `tools.media.audio`, such as OpenAI `gpt-4o-mini-transcribe`. See [Media tools overview](/tools/media-overview) for provider setup and limits.
|
||||
|
||||
Behavior details:
|
||||
|
||||
- `m.audio` events and `m.file` events with an `audio/*` MIME type are eligible.
|
||||
- In encrypted rooms, OpenClaw decrypts the attachment through the existing Matrix media path before transcription.
|
||||
- The transcript is marked as machine-generated and untrusted in the agent prompt.
|
||||
- The attachment is marked as already transcribed so downstream media tools do not transcribe the same voice note again.
|
||||
- Set `tools.media.audio.enabled: false` to disable audio transcription globally.
|
||||
|
||||
## Approval metadata
|
||||
|
||||
Matrix native approval prompts are normal `m.room.message` events with OpenClaw-specific custom event content under `com.openclaw.approval`. Matrix permits custom event-content keys, so stock clients still render the text body while OpenClaw-aware clients can read the structured approval id, kind, state, available decisions, and exec/plugin details.
|
||||
|
||||
@@ -334,7 +334,7 @@ If you pass `--url`, that explicit target is added ahead of both. Human output l
|
||||
- `Local loopback`
|
||||
|
||||
<Note>
|
||||
If multiple gateways are reachable, it prints all of them. Multiple gateways are supported when you use isolated profiles/ports (e.g., a rescue bot), but most installs still run a single gateway.
|
||||
If multiple probe targets are reachable, it prints all of them. An SSH tunnel, TLS/proxy URL, and configured remote URL can all point at the same gateway even when their transport ports differ; `multiple_gateways` is reserved for distinct or identity-ambiguous reachable gateways. Multiple gateways are supported when you use isolated profiles (e.g., a rescue bot), but most installs still run a single gateway.
|
||||
</Note>
|
||||
|
||||
```bash
|
||||
@@ -379,7 +379,7 @@ openclaw gateway probe --json
|
||||
</Accordion>
|
||||
<Accordion title="Common warning codes">
|
||||
- `ssh_tunnel_failed`: SSH tunnel setup failed; the command fell back to direct probes.
|
||||
- `multiple_gateways`: more than one target was reachable; this is unusual unless you intentionally run isolated profiles, such as a rescue bot.
|
||||
- `multiple_gateways`: distinct gateway identities were reachable, or OpenClaw could not prove reachable targets are the same gateway. An SSH tunnel, proxy URL, or configured remote URL to the same gateway does not trigger this warning.
|
||||
- `auth_secretref_unresolved`: a configured auth SecretRef could not be resolved for a failed target.
|
||||
- `probe_scope_limited`: WebSocket connect succeeded, but the read probe was limited by missing `operator.read`.
|
||||
|
||||
|
||||
@@ -25,13 +25,6 @@ Common use cases:
|
||||
Execution is still guarded by **exec approvals** and per-agent allowlists on the
|
||||
node host, so you can keep command access scoped and explicit.
|
||||
|
||||
Gateway-loaded plugins can also register node-host commands. When a registered
|
||||
command includes `agentTool` metadata, `openclaw node run` advertises that
|
||||
plugin or MCP-backed tool to the Gateway while the node is connected. The agent
|
||||
sees it as a normal plugin tool, but execution still goes through `node.invoke`
|
||||
and the node command allowlist, so disconnecting the node removes the tool from
|
||||
new agent runs.
|
||||
|
||||
## Browser proxy (zero-config)
|
||||
|
||||
Node hosts automatically advertise a browser proxy if `browser.enabled` is not
|
||||
|
||||
@@ -70,6 +70,10 @@ present.
|
||||
`vsearch` and `query`). `search` is BM25-only, so OpenClaw skips semantic
|
||||
vector readiness probes and embedding maintenance in that mode. If a mode
|
||||
fails, OpenClaw retries with `qmd query`.
|
||||
- When `searchMode` is `query`, set `memory.qmd.rerank` to `false` to use QMD's
|
||||
hybrid query path without the reranker. OpenClaw passes `--no-rerank` to the
|
||||
direct QMD CLI path and `rerank: false` to QMD's MCP query tool. This option
|
||||
requires QMD 2.1 or newer.
|
||||
- With QMD releases that advertise multi-collection filters, OpenClaw groups
|
||||
same-source collections into one QMD search invocation. Older QMD releases
|
||||
keep the compatible per-collection fallback.
|
||||
|
||||
@@ -387,7 +387,7 @@ On normal runs, `HEARTBEAT.md` is only injected when heartbeat guidance is enabl
|
||||
|
||||
On the native Codex harness, `HEARTBEAT.md` content is not injected into the turn. If the file exists and has non-whitespace content, the heartbeat collaboration-mode instructions point Codex at the file and tell it to read before proceeding.
|
||||
|
||||
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls. That skip is reported as `reason=empty-heartbeat-file`. If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||
If `HEARTBEAT.md` exists but is effectively empty (only blank lines, Markdown/HTML comments, Markdown headings like `# Heading`, fence markers, or empty checklist stubs), OpenClaw skips the heartbeat run to save API calls. That skip is reported as `reason=empty-heartbeat-file`. If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||
|
||||
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
|
||||
|
||||
|
||||
@@ -167,8 +167,10 @@ What to expect:
|
||||
|
||||
- `gateway status --deep` can report `Other gateway-like services detected (best effort)`
|
||||
and print cleanup hints when stale launchd/systemd/schtasks installs are still around.
|
||||
- `gateway probe` can warn about `multiple reachable gateways` when more than one target
|
||||
answers.
|
||||
- `gateway probe` can warn about `multiple reachable gateway identities` when distinct
|
||||
gateways answer, or when OpenClaw cannot prove reachable targets are the same gateway.
|
||||
An SSH tunnel, proxy URL, or configured remote URL to the same gateway is one
|
||||
gateway with multiple transports, even when transport ports differ.
|
||||
- If that is intentional, isolate ports, config/state, and workspace roots per gateway.
|
||||
|
||||
Checklist per instance:
|
||||
|
||||
@@ -169,7 +169,7 @@ openclaw --profile rescue browser status
|
||||
Interpretation:
|
||||
|
||||
- `gateway status --deep` helps catch stale launchd/systemd/schtasks services from older installs.
|
||||
- `gateway probe` warning text such as `multiple reachable gateways detected` is expected only when you intentionally run more than one isolated gateway.
|
||||
- `gateway probe` warning text such as `multiple reachable gateway identities detected` is expected only when you intentionally run more than one isolated gateway, or when OpenClaw cannot prove reachable probe targets are the same gateway. An SSH tunnel, proxy URL, or configured remote URL to the same gateway is one gateway with multiple transports, even when transport ports differ.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -270,13 +270,6 @@ Nodes declare capability claims at connect time:
|
||||
- `permissions`: granular toggles (e.g. `screen.record`, `camera.capture`).
|
||||
|
||||
The Gateway treats these as **claims** and enforces server-side allowlists.
|
||||
Connected nodes can publish optional agent-visible plugin or MCP tool
|
||||
descriptors with `node.pluginTools.update` after a successful connect, after
|
||||
reconnect, or after a local plugin/MCP inventory change. Each descriptor must
|
||||
use a provider-safe tool `name` and name a `command` in the node's current
|
||||
command allowlist. The Gateway filters descriptors outside the approved command
|
||||
surface, removes them when the node disconnects, and rejects operator attempts
|
||||
to mutate another node's catalog.
|
||||
|
||||
## Presence
|
||||
|
||||
@@ -468,7 +461,6 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
- `node.invoke` forwards a command to a connected node.
|
||||
- `node.invoke.result` returns the result for an invoke request.
|
||||
- `node.event` carries node-originated events back into the gateway.
|
||||
- `node.pluginTools.update` replaces the connected node's agent-visible plugin/MCP tool descriptors.
|
||||
- `node.pending.pull` and `node.pending.ack` are the connected-node queue APIs.
|
||||
- `node.pending.enqueue` and `node.pending.drain` manage durable pending work for offline/disconnected nodes.
|
||||
|
||||
|
||||
@@ -625,7 +625,7 @@ Look for:
|
||||
Common signatures:
|
||||
|
||||
- `SSH tunnel failed to start; falling back to direct probes.` → SSH setup failed, but the command still tried direct configured/loopback targets.
|
||||
- `multiple reachable gateways detected` → more than one target answered. Usually this means an intentional multi-gateway setup or stale/duplicate listeners.
|
||||
- `multiple reachable gateway identities detected` → distinct gateways answered, or OpenClaw could not prove reachable targets are the same gateway. An SSH tunnel, proxy URL, or configured remote URL to the same gateway is treated as one gateway with multiple transports, even when transport ports differ.
|
||||
- `Read-probe diagnostics are limited by gateway scopes (missing operator.read)` → connect worked, but detail RPC is scope-limited; pair device identity or use credentials with `operator.read`.
|
||||
- `Gateway accepted the WebSocket connection, but follow-up read diagnostics failed` → connect worked, but the full diagnostic RPC set timed out or failed. Treat this as a reachable Gateway with degraded diagnostics; compare `connect.ok` and `connect.rpcOk` in `--json` output.
|
||||
- `Capability: pairing-pending` or `gateway closed (1008): pairing required` → the gateway answered, but this client still needs pairing/approval before normal operator access.
|
||||
@@ -691,7 +691,7 @@ Look for:
|
||||
- `cron: scheduler disabled; jobs will not run automatically` → cron disabled.
|
||||
- `cron: timer tick failed` → scheduler tick failed; check file/log/runtime errors.
|
||||
- `heartbeat skipped` with `reason=quiet-hours` → outside active hours window.
|
||||
- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank lines / markdown headers, so OpenClaw skips the model call.
|
||||
- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank, comment, header, fence, or empty-checklist scaffolding, so OpenClaw skips the model call.
|
||||
- `heartbeat skipped` with `reason=no-tasks-due` → `HEARTBEAT.md` contains a `tasks:` block, but none of the tasks are due on this tick.
|
||||
- `heartbeat: unknown accountId` → invalid account id for heartbeat delivery target.
|
||||
- `heartbeat skipped` with `reason=dm-blocked` → heartbeat target resolved to a DM-style destination while `agents.defaults.heartbeat.directPolicy` (or per-agent override) is set to `block`.
|
||||
|
||||
@@ -67,7 +67,7 @@ and troubleshooting see the main [FAQ](/help/faq).
|
||||
Common heartbeat skip reasons:
|
||||
|
||||
- `quiet-hours`: outside the configured active-hours window
|
||||
- `empty-heartbeat-file`: `HEARTBEAT.md` exists but only contains blank/header-only scaffolding
|
||||
- `empty-heartbeat-file`: `HEARTBEAT.md` exists but only contains blank, comment, header, fence, or empty-checklist scaffolding
|
||||
- `no-tasks-due`: `HEARTBEAT.md` task mode is active but none of the task intervals are due yet
|
||||
- `alerts-disabled`: all heartbeat visibility is disabled (`showOk`, `showAlerts`, and `useIndicator` are all off)
|
||||
|
||||
|
||||
@@ -1350,8 +1350,9 @@ lives on the [First-run FAQ](/help/faq-first-run).
|
||||
}
|
||||
```
|
||||
|
||||
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
|
||||
headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls.
|
||||
If `HEARTBEAT.md` exists but is effectively empty (only blank lines,
|
||||
Markdown/HTML comments, Markdown headings like `# Heading`, fence markers,
|
||||
or empty checklist stubs), OpenClaw skips the heartbeat run to save API calls.
|
||||
If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||
|
||||
Per-agent overrides use `agents.list[].heartbeat`. Docs: [Heartbeat](/gateway/heartbeat).
|
||||
|
||||
@@ -364,7 +364,7 @@ flowchart TD
|
||||
|
||||
- `cron: scheduler disabled; jobs will not run automatically` → cron is disabled.
|
||||
- `heartbeat skipped` with `reason=quiet-hours` → outside configured active hours.
|
||||
- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank/header-only scaffolding.
|
||||
- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank, comment, header, fence, or empty-checklist scaffolding.
|
||||
- `heartbeat skipped` with `reason=no-tasks-due` → `HEARTBEAT.md` task mode is active but none of the task intervals are due yet.
|
||||
- `heartbeat skipped` with `reason=alerts-disabled` → all heartbeat visibility is disabled (`showOk`, `showAlerts`, and `useIndicator` are all off).
|
||||
- `requests-in-flight` → main lane busy; heartbeat wake was deferred.
|
||||
|
||||
254
docs/plan/lts-cli-deterministic-checklist-plan.md
Normal file
254
docs/plan/lts-cli-deterministic-checklist-plan.md
Normal file
@@ -0,0 +1,254 @@
|
||||
---
|
||||
title: "LTS Deterministic Checklist Plan"
|
||||
summary: "Plan for turning maturity scorecard claims into auditable release-support evidence packets"
|
||||
read_when:
|
||||
- Designing deterministic LTS evidence packets from scorecard inputs
|
||||
- Auditing CLI support claims before an LTS or stable release
|
||||
- Reusing the checklist workflow for another OpenClaw surface
|
||||
---
|
||||
|
||||
# LTS Deterministic Checklist Plan
|
||||
|
||||
This plan turns the current Clanker-generated maturity scorecard into an auditable release-support artifact. The goal is not to replace Clanker judgment. The goal is to force every proposed LTS claim into a repeatable evidence table that maintainers can inspect before an LTS or stable release.
|
||||
|
||||
## General Flow
|
||||
|
||||
The deterministic checklist answers one question for every proposed LTS feature:
|
||||
|
||||
> Can we prove this works today, and will CI or release validation catch it if it breaks?
|
||||
|
||||
Use this hierarchy:
|
||||
|
||||
```text
|
||||
Surface
|
||||
-> Category
|
||||
-> Feature
|
||||
-> Evidence: docs, source, tests, CI/Testbox proof, known gaps
|
||||
```
|
||||
|
||||
The maturity scorecard already defines the surfaces and categories. `LTS.md` marks which categories are proposed for the initial LTS slice. The checklist should not decide LTS policy directly. It should expose which promises are strongly backed, partially backed, missing proof, or require owner judgment.
|
||||
|
||||
## Deterministic Checklist Artifact
|
||||
|
||||
For each surface, produce one Markdown evidence packet:
|
||||
|
||||
```text
|
||||
Surface: <surface name>
|
||||
Snapshot:
|
||||
- Scorecard ref:
|
||||
- LTS ref:
|
||||
- OpenClaw source ref:
|
||||
- gitcrawl freshness:
|
||||
- discrawl freshness:
|
||||
- CI/Testbox source:
|
||||
|
||||
Summary:
|
||||
- Included LTS categories:
|
||||
- Strongly covered categories:
|
||||
- Partial categories:
|
||||
- Missing-proof categories:
|
||||
- Owner-decision categories:
|
||||
|
||||
Feature Checklist:
|
||||
| Category | Feature | Docs | Source | Test or proof | Latest CI/Testbox | Verdict | Gap | Next action |
|
||||
```
|
||||
|
||||
Use these verdicts:
|
||||
|
||||
- `covered`: docs, source, and integration/e2e/live proof exist, and latest CI/Testbox proof is known.
|
||||
- `partial`: implementation exists, but proof is unit-only, stale, platform-limited, or not tied to the user path.
|
||||
- `missing`: no credible runtime-flow proof was found.
|
||||
- `owner`: evidence exists, but whether it belongs in LTS is a product/support decision.
|
||||
|
||||
Do not let unit tests alone mark a feature as `covered`. Unit tests can support the row, but LTS coverage should be based on integration, e2e, live, or real runtime-flow proof.
|
||||
|
||||
## Agent Orchestration Model
|
||||
|
||||
Use Clankers as evidence collectors and reviewers.
|
||||
|
||||
Recommended roles:
|
||||
|
||||
- `surface-auditor`: builds the first checklist for one surface.
|
||||
- `skeptic-reviewer`: attacks overclaims and downgrades weak rows.
|
||||
- `ci-finder`: finds latest CI/Testbox proof for cited tests.
|
||||
- `normalizer`: rewrites rows into the shared verdict vocabulary.
|
||||
|
||||
For high-risk surfaces, run two independent `surface-auditor` agents and compare disagreement. The useful output is often the conflict list, not the average answer.
|
||||
|
||||
## Standard Surface Auditor Prompt
|
||||
|
||||
```text
|
||||
Audit only the <SURFACE> surface for the proposed LTS checklist.
|
||||
|
||||
Inputs:
|
||||
- LTS source: docs/kevinslin/maturity-scorecard/LTS.md
|
||||
- Scorecard source: docs/kevinslin/maturity-scorecard/maturity-scorecard.md
|
||||
- Surface report: docs/kevinslin/maturity-scorecard/inventory/<surface-id>/report.md
|
||||
- Surface score source: docs/kevinslin/maturity-scorecard/inventory/<surface-id>/scores.yaml
|
||||
- Category notes: docs/kevinslin/maturity-scorecard/inventory/<surface-id>/*.md
|
||||
|
||||
Task:
|
||||
For every category included in LTS.md for this surface:
|
||||
1. Extract the user-facing features.
|
||||
2. Cite docs that promise or explain the feature.
|
||||
3. Cite implementation source that owns the feature.
|
||||
4. Cite integration, e2e, live, or runtime-flow tests.
|
||||
5. Find latest CI/Testbox proof for the cited tests when available.
|
||||
6. Mark verdict as covered, partial, missing, or owner.
|
||||
7. Explain the gap and the next action.
|
||||
|
||||
Rules:
|
||||
- Do not change LTS policy.
|
||||
- Do not score by vibes.
|
||||
- A row without source plus runtime-flow proof is not covered.
|
||||
- Unit tests alone are supporting evidence only.
|
||||
- Prefer exact file paths and line references.
|
||||
- Keep the final output to the checklist table plus a short gaps summary.
|
||||
```
|
||||
|
||||
## Standard Skeptic Prompt
|
||||
|
||||
```text
|
||||
Review this <SURFACE> LTS checklist.
|
||||
|
||||
Find:
|
||||
- rows that overclaim coverage
|
||||
- rows where unit tests are being counted as coverage
|
||||
- rows where docs/source/test do not prove the same user-facing feature
|
||||
- stale or missing CI/Testbox proof
|
||||
- vague feature names
|
||||
- categories that should be marked owner instead of covered
|
||||
|
||||
Return only actionable corrections:
|
||||
| Row | Problem | Required correction | Severity |
|
||||
```
|
||||
|
||||
## CLI Pilot
|
||||
|
||||
The CLI is the best first pilot because it is bounded, enterprise-relevant, and easier to connect to docs, source, tests, and release proof than provider or channel surfaces.
|
||||
|
||||
The proposed initial LTS slice includes 6 of 8 CLI categories:
|
||||
|
||||
- CLI Setup
|
||||
- Onboarding and Auth Setup
|
||||
- Gateway Service Management
|
||||
- CLI Observability
|
||||
- Doctor
|
||||
- Updates and Upgrades
|
||||
|
||||
The deferred categories are:
|
||||
|
||||
- Plugin and Channel Setup
|
||||
- Windows and WSL2
|
||||
|
||||
The CLI pilot should prove whether the included categories are actually backed by deterministic evidence, and whether any deferred category is obviously stronger than an included category.
|
||||
|
||||
## CLI Inputs To Read
|
||||
|
||||
Read these first:
|
||||
|
||||
- `docs/kevinslin/maturity-scorecard/LTS.md`
|
||||
- `docs/kevinslin/maturity-scorecard/maturity-scorecard.md`
|
||||
- `docs/kevinslin/maturity-scorecard/inventory/cli-install-update-onboard-doctor/report.md`
|
||||
- `docs/kevinslin/maturity-scorecard/inventory/cli-install-update-onboard-doctor/scores.yaml`
|
||||
- `docs/kevinslin/maturity-scorecard/inventory/cli-install-update-onboard-doctor/*.md`
|
||||
|
||||
Then read the product docs that match the categories:
|
||||
|
||||
- `docs/cli/index.md`
|
||||
- `docs/cli/onboard.md`
|
||||
- `docs/cli/configure.md`
|
||||
- `docs/cli/doctor.md`
|
||||
- `docs/cli/gateway.md`
|
||||
- `docs/cli/health.md`
|
||||
- `docs/cli/logs.md`
|
||||
- `docs/cli/models.md`
|
||||
- `docs/start/wizard-cli-automation.md`
|
||||
- `docs/start/wizard-cli-reference.md`
|
||||
- `docs/reference/wizard.md`
|
||||
|
||||
Use docs only as claims. Every claim still needs source and runtime proof.
|
||||
|
||||
## CLI Evidence Search Strategy
|
||||
|
||||
For each included category, search source and tests by command name and user workflow.
|
||||
|
||||
Suggested source searches:
|
||||
|
||||
```bash
|
||||
rg -n "openclaw (onboard|configure|doctor|gateway|health|logs|models|update)" src packages ui extensions scripts test
|
||||
rg -n "doctor|onboard|configure|gateway service|service install|update channel|auth profile|model set" src packages test
|
||||
rg -n "program\\.command|subcommand|Command|commander|cac|yargs|parse" src packages
|
||||
```
|
||||
|
||||
Suggested test searches:
|
||||
|
||||
```bash
|
||||
rg -n "doctor|onboard|configure|gateway service|update|auth profile|models" --glob '*.{test,e2e.test}.ts' src packages test
|
||||
rg -n "openclaw doctor|openclaw onboard|openclaw gateway|openclaw update" test scripts docs
|
||||
```
|
||||
|
||||
Suggested proof searches:
|
||||
|
||||
```bash
|
||||
gh run list -R openclaw/openclaw --limit 30 --json databaseId,headSha,conclusion,status,displayTitle,createdAt,url
|
||||
gh pr checks <PR-or-branch> -R openclaw/openclaw
|
||||
```
|
||||
|
||||
If local CI proof is not enough, mark the row `partial` and recommend Crabbox/Testbox proof.
|
||||
|
||||
## CLI Feature Table Template
|
||||
|
||||
Use this table as the CLI pilot output:
|
||||
|
||||
```markdown
|
||||
| Category | Feature | Docs | Source | Test or proof | Latest CI/Testbox | Verdict | Gap | Next action |
|
||||
| -------------------------- | ------------------------------------------------------------ | ---------------------------------------- | ------ | ------------- | ----------------- | ------- | -------------------------------------------------------- | ------------------------------------ |
|
||||
| CLI Setup | Package install exposes `openclaw` CLI | `docs/start/getting-started.md` | TBD | TBD | TBD | partial | Need current package install smoke proof | Find release CI or add package smoke |
|
||||
| Onboarding and Auth Setup | `openclaw onboard` creates usable config/auth path | `docs/cli/onboard.md` | TBD | TBD | TBD | partial | Need non-interactive and interactive proof separated | Audit onboarding tests |
|
||||
| Gateway Service Management | CLI can install/start/stop/status Gateway service | `docs/cli/gateway.md` | TBD | TBD | TBD | partial | Need Linux service proof if LTS includes Linux host path | Link service tests and Testbox run |
|
||||
| CLI Observability | `openclaw health` and `openclaw logs` expose operator status | `docs/cli/health.md`, `docs/cli/logs.md` | TBD | TBD | TBD | partial | Need running Gateway proof | Audit RPC/CLI e2e |
|
||||
| Doctor | `openclaw doctor --fix` repairs supported config/auth drift | `docs/cli/doctor.md` | TBD | TBD | TBD | partial | Need migration fixture coverage | Audit doctor tests |
|
||||
| Updates and Upgrades | CLI supports supported update channel flow | TBD | TBD | TBD | TBD | partial | Need release/update smoke proof | Find release CI/Testbox run |
|
||||
```
|
||||
|
||||
Replace `TBD` with exact evidence. Do not leave `TBD` in the final artifact.
|
||||
|
||||
## CLI Definition Of Done
|
||||
|
||||
The CLI pilot is done when:
|
||||
|
||||
- Every included CLI LTS category has at least one feature row.
|
||||
- Every feature row has docs, source, test/proof, verdict, gap, and next action.
|
||||
- Rows without integration/e2e/live/runtime-flow proof are marked `partial` or `missing`.
|
||||
- Latest CI/Testbox evidence is linked when available.
|
||||
- A skeptic review has downgraded overclaims.
|
||||
- The final summary names the top 3 CLI gaps Kevin needs to know before LTS.
|
||||
|
||||
## Likely CLI Outcomes
|
||||
|
||||
Expected useful outputs:
|
||||
|
||||
- A short list of CLI categories that are safe to keep in LTS.
|
||||
- A short list of CLI categories that need one targeted integration or package smoke test.
|
||||
- A short list of CLI claims that should be narrowed before announcement.
|
||||
- A reusable checklist template for the next surface.
|
||||
|
||||
The most valuable result is not a high score. The most valuable result is a clear distinction between:
|
||||
|
||||
- "covered by current release gates"
|
||||
- "implemented but not release-gated"
|
||||
- "documented but weakly tested"
|
||||
- "needs owner decision before support promise"
|
||||
|
||||
## Suggested First Day Plan
|
||||
|
||||
1. Run one `surface-auditor` on CLI.
|
||||
2. Run one `ci-finder` on the cited CLI tests and release checks.
|
||||
3. Run one `skeptic-reviewer` on the completed table.
|
||||
4. Normalize the verdicts.
|
||||
5. Send Kevin a concise packet:
|
||||
- CLI checklist
|
||||
- top 3 gaps
|
||||
- recommended test/proof additions
|
||||
- whether the checklist format should be repeated for Gateway runtime next
|
||||
66
docs/plan/lts-cli-deterministic-checklist.md
Normal file
66
docs/plan/lts-cli-deterministic-checklist.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: "CLI LTS Deterministic Checklist"
|
||||
summary: "Evidence packet for proposed CLI categories in the initial OpenClaw LTS slice"
|
||||
read_when:
|
||||
- Auditing whether CLI setup, onboarding, gateway service management, observability, doctor, or updates are ready for an LTS support promise
|
||||
- Preparing release validation gaps for CLI support claims
|
||||
- Extending the deterministic LTS checklist to another surface
|
||||
---
|
||||
|
||||
## Surface
|
||||
|
||||
CLI install, update, onboard, configure, gateway, health, logs, and doctor.
|
||||
|
||||
## Snapshot
|
||||
|
||||
- Scorecard ref: planned `docs/kevinslin/maturity-scorecard/maturity-scorecard.md`; not present in this checkout.
|
||||
- LTS ref: planned `docs/kevinslin/maturity-scorecard/LTS.md`; not present in this checkout.
|
||||
- OpenClaw source ref: this worktree, branch `lts-checklist-cli`.
|
||||
- gitcrawl freshness: not checked; this packet uses local source plus live GitHub Actions run metadata.
|
||||
- discrawl freshness: not checked; no Discord/operator archive claim is needed for the CLI source proof.
|
||||
- CI/Testbox source: latest observed successful runs from `gh run list` on 2026-06-05:
|
||||
- Full Release Validation: `27022847039`, `tideclaw/alpha/2026-06-05-1410Z`, `2e307827be8a7aa6fb8b70e2d5e639a6d86c7ddc`, https://github.com/openclaw/openclaw/actions/runs/27022847039
|
||||
- OpenClaw Release Checks: `27023463705`, same ref/SHA, https://github.com/openclaw/openclaw/actions/runs/27023463705
|
||||
- main CI: `27032770269`, `d896a4c7a3ef033f93bc6bd6d392e299630c52c7`, https://github.com/openclaw/openclaw/actions/runs/27032770269
|
||||
- Package Acceptance: latest observed success `26917431094`, `main`, `308114e1486dc2a2409ab1d99a1e5f8e05d97b7e`, https://github.com/openclaw/openclaw/actions/runs/26917431094
|
||||
|
||||
## Summary
|
||||
|
||||
- Included LTS categories: CLI Setup; Onboarding and Auth Setup; Gateway Service Management; CLI Observability; Doctor; Updates and Upgrades.
|
||||
- Strongly covered categories: none without a release-run manifest tying exact rows to exact lane success.
|
||||
- Partial categories: all six included categories have docs, implementation, tests, and at least some Docker or release-gate proof.
|
||||
- Missing-proof categories: CLI Setup lacks an isolated current package-install smoke row in this packet; CLI Observability lacks a named release lane for `openclaw health` plus `openclaw logs` together.
|
||||
- Owner-decision categories: Gateway Service Management, because the support promise must name which supervisors and operating systems are in LTS scope.
|
||||
|
||||
## Feature Checklist
|
||||
|
||||
| Category | Feature | Docs | Source | Test or proof | Latest CI/Testbox | Verdict | Gap | Next action |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| CLI Setup | Package install exposes `openclaw` and the documented command tree | `docs/cli/index.md:9`, `docs/cli/index.md:21`, `docs/cli/index.md:40` | `src/cli/program/build-program.ts:10`, `src/cli/program/build-program.ts:27`, `src/cli/program.ts:4` | Package install is exercised by `scripts/e2e/npm-onboard-channel-agent-docker.sh:134` and checks `command -v openclaw` at `scripts/e2e/npm-onboard-channel-agent-docker.sh:136`; launcher/package behavior has `test/openclaw-launcher.e2e.test.ts` | Package Acceptance `26917431094` is the latest successful package workflow observed; Release Checks `27023463705` is newer but this packet did not fetch its job manifest to prove the install lane ran. | partial | The proof is embedded in larger package/onboard lanes, not a row-level install smoke tied to the latest release run. | Add or identify a release-gated `install-smoke` row that records package spec, `openclaw --version`, command tree smoke, and runner OS. |
|
||||
| Onboarding and Auth Setup | `openclaw onboard` creates usable local/remote config, auth refs, workspace, and health path | `docs/cli/onboard.md:8`, `docs/cli/onboard.md:121`, `docs/cli/onboard.md:139`, `docs/start/wizard-cli-automation.md:1`, `docs/reference/wizard.md:1` | `src/commands/onboard.ts:27`, `src/commands/onboard.ts:77`, `src/commands/onboard.ts:113`, `src/commands/onboard.ts:118` | Unit/integration: `src/commands/onboard-non-interactive.gateway.test.ts:330`, `src/commands/onboard-non-interactive.gateway.test.ts:764`, `src/commands/onboard-non-interactive.gateway-health-auth.test.ts:40`; Docker flow: `scripts/e2e/onboard-docker.sh:24`, local and remote cases at `scripts/e2e/lib/onboard/scenario.sh:244`, `scripts/e2e/lib/onboard/scenario.sh:272`; package user path at `scripts/e2e/npm-onboard-channel-agent-docker.sh:149` | Package Acceptance `26917431094` covers `npm-onboard-channel-agent` by policy (`docs/ci.md:292`); OpenClaw Release Checks `27023463705` is newer and calls package lanes by policy (`docs/ci.md:306`). | partial | Onboarding has strong runtime proof, but the row still mixes local setup, remote setup, auth SecretRefs, and package channel-agent proof. | Split into local non-interactive, remote non-interactive, interactive wizard, and SecretRef auth rows, then attach latest release job names/artifacts for each. |
|
||||
| Gateway Service Management | CLI can install, start, stop, restart, and report Gateway service state | `docs/cli/gateway.md:113`, `docs/cli/gateway.md:139`, `docs/cli/gateway.md:164`; legacy alias listed at `docs/cli/index.md:37` | `src/cli/gateway-cli/register.ts:20`, `src/cli/gateway-cli/register.ts:479`, `src/cli/daemon-cli/register-service-commands.ts:57`, `src/cli/daemon-cli/register-service-commands.ts:83`, `src/cli/daemon-cli/register-service-commands.ts:106`, `src/cli/daemon-cli/register-service-commands.ts:115`, `src/cli/daemon-cli/register-service-commands.ts:129` | Unit/integration: `src/commands/gateway-readiness.test.ts:92`, `src/commands/doctor-gateway-services.test.ts:351`, `src/cli/daemon-cli/register-service-commands.test.ts:1`; Docker service-entrypoint proof: `scripts/e2e/doctor-install-switch-docker.sh:1`, `scripts/e2e/lib/doctor-install-switch/scenario.sh:122`, `scripts/e2e/lib/doctor-install-switch/scenario.sh:160`, `scripts/e2e/lib/doctor-install-switch/scenario.sh:167` | Release Checks `27023463705` is the latest successful release-check run observed; docs state release checks include `doctor-switch` in package acceptance (`docs/ci.md:306`). | owner | The Linux systemd-user Docker shim proves important flows, but LTS support scope for launchd, systemd, Scheduled Tasks, WSL2, and unmanaged supervisors is a product decision. | Define which supervisors are LTS-backed, then require one release-gated service lifecycle lane per supported supervisor. |
|
||||
| CLI Observability | `openclaw health` and `openclaw logs` expose operator status and logs through Gateway RPC with fallbacks | `docs/cli/health.md:8`, `docs/cli/health.md:31`, `docs/cli/logs.md:9`, `docs/cli/logs.md:58` | `src/commands/health.ts:411`, `src/commands/health.ts:634`, `src/commands/health.ts:658`, `src/cli/logs-cli.ts:100`, `src/cli/logs-cli.ts:119`, `src/cli/logs-cli.ts:146`, `src/cli/logs-cli.ts:483`, `src/gateway/server-methods/logs.ts:12`, `src/gateway/server-methods/health.ts:121` | Unit/integration: `src/commands/health.test.ts:127`, `src/commands/health.test.ts:251`, `src/cli/logs-cli.test.ts:138`, `src/cli/logs-cli.test.ts:416`, `src/cli/logs-cli.test.ts:534`; package/onboard lane checks status surfaces at `scripts/e2e/npm-onboard-channel-agent-docker.sh:170` | main CI `27032770269` is current general CI proof; no latest named release lane was found for `openclaw health` plus `openclaw logs`. | partial | Health and logs are implemented and tested, but logs proof is mostly unit-level; package runtime proof checks status surfaces, not logs tail/follow against a running Gateway. | Add a Docker release-path observability lane that starts Gateway, asserts `openclaw health --json`, `openclaw logs --json`, and `openclaw logs --follow` reconnect behavior. |
|
||||
| Doctor | `openclaw doctor --fix` repairs supported config, auth, plugin, state, and service drift | `docs/cli/doctor.md:20`, `docs/cli/doctor.md:25`, `docs/cli/doctor.md:61`, `docs/cli/doctor.md:79` | `src/commands/doctor.ts:7`, `src/commands/doctor.ts:25`, `src/flows/doctor-health.ts:20`, `src/flows/doctor-health.ts:63`, `src/flows/doctor-health.ts:81` | Unit/integration: `src/commands/doctor-config-preflight.test.ts:30`, `src/commands/doctor-config-preflight.state-migration.test.ts:49`, `src/commands/doctor-auth-flat-profiles.test.ts:415`, `src/commands/doctor-gateway-daemon-flow.test.ts:503`, `src/commands/doctor-gateway-services.test.ts:480`; Docker package repair proof: `scripts/e2e/lib/doctor-install-switch/scenario.sh:152`, `scripts/e2e/lib/doctor-install-switch/scenario.sh:201`, `scripts/e2e/npm-onboard-channel-agent-docker.sh:175` | Package Acceptance `26917431094`; Release Checks `27023463705` is newer and release policy includes `doctor-switch`, `update-corrupt-plugin`, `upgrade-survivor`, `published-upgrade-survivor`, and `update-restart-auth` (`docs/ci.md:306`). | partial | Doctor is broad; no single artifact summarizes which repair families are release-gated versus unit-only. | Generate a doctor repair matrix from check IDs/repair families and map each to unit, Docker, Package Acceptance, or missing proof. |
|
||||
| Updates and Upgrades | `openclaw update` supports stable/beta/dev channel switching, package updates, post-core convergence, and update status | `docs/cli/update.md:10`, `docs/cli/update.md:34`, `docs/cli/update.md:89`, `docs/cli/update.md:121`, `docs/cli/update.md:168` | `src/cli/update-cli.ts:41`, `src/cli/update-cli.ts:45`, `src/cli/update-cli.ts:50`, `src/cli/update-cli.ts:101`, `src/cli/update-cli.ts:163`; update implementation is under `src/cli/update-cli/*` and `src/infra/update-*` | Unit/integration: `src/cli/update-cli.test.ts:3307`, `src/cli/update-cli.test.ts:3971`, `src/infra/update-runner.test.ts:2272`; Docker runtime proof: `scripts/e2e/update-channel-switch-docker.sh:1`, package-to-git and git-to-package assertions at `scripts/e2e/update-channel-switch-docker.sh:100`, `scripts/e2e/update-channel-switch-docker.sh:116`; upgrade survivor proof under `scripts/e2e/lib/upgrade-survivor/run.sh:1113` and `scripts/e2e/lib/upgrade-survivor/run.sh:1241` | Release Checks `27023463705`; Package Acceptance policy includes `update-channel-switch`, `upgrade-survivor`, `published-upgrade-survivor`, `update-restart-auth`, and plugin update lanes (`docs/ci.md:294`, `docs/ci.md:306`). | partial | This is close to covered for package/update flows, but row-level proof still lacks latest job artifact/job names and exact package spec. | Pull the latest Release Checks child Package Acceptance job summary and record lane-level outcomes for `update-channel-switch`, `upgrade-survivor`, `published-upgrade-survivor`, and `update-restart-auth`. |
|
||||
|
||||
## Deferred Category Check
|
||||
|
||||
- Plugin and Channel Setup appears at least as strongly release-gated as some included categories: Package Acceptance `smoke` includes `npm-onboard-channel-agent`, and package profile includes `skill-install`, `plugins-offline`, and `plugin-update` (`docs/ci.md:292`). If CLI LTS promises include post-onboard channel setup, this category may deserve inclusion or a narrow split.
|
||||
- Windows and WSL2 have release-check hooks through cross-OS release checks (`.github/workflows/openclaw-release-checks.yml:24`, `.github/workflows/openclaw-release-checks.yml:66`) and Windows/package notes in `docs/ci.md:306`, but this packet did not audit Windows-specific source/tests. Keep deferred until a dedicated cross-OS evidence packet exists.
|
||||
|
||||
## Skeptic Review Corrections
|
||||
|
||||
| Row | Problem | Required correction | Severity |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | -------- |
|
||||
| CLI Setup | Package install proof is only inferred from package/onboard lanes. | Keep `partial` until latest release job artifacts show an install smoke or add a standalone install smoke. | Medium |
|
||||
| Onboarding and Auth Setup | Combines too many behaviors in one row; SecretRef and interactive wizard could regress independently. | Split local non-interactive, remote non-interactive, interactive, and SecretRef rows. | Medium |
|
||||
| Gateway Service Management | Current strongest Docker proof uses systemd shims; launchd and Scheduled Tasks support are not equivalently proven here. | Mark `owner`; define OS/supervisor LTS scope before upgrade to `covered`. | High |
|
||||
| CLI Observability | Unit proof is being asked to carry logs tail/follow support. | Add runtime Gateway logs proof or keep `partial`. | High |
|
||||
| Doctor | Doctor has many repair families; a single row can hide unproven repairs. | Matrix repair families and mark each separately. | Medium |
|
||||
| Updates and Upgrades | Release policy strongly covers this area, but latest lane-level artifacts were not inspected. | Fetch latest Release Checks child job summary before calling it `covered`. | Medium |
|
||||
|
||||
## Top Gaps For LTS
|
||||
|
||||
1. **Lane-level release evidence**: the current release workflows cover many CLI user paths, but the LTS artifact needs child job IDs, lane names, package specs, and artifacts for each row.
|
||||
2. **Observability runtime proof**: add a Gateway-backed Docker lane for `health` and `logs`, especially `logs --follow` reconnect/journal behavior.
|
||||
3. **Service support boundary**: decide whether LTS means Linux systemd user only, macOS launchd, native Windows Scheduled Tasks, WSL2, or all of them, then require one real runtime proof per supported supervisor.
|
||||
@@ -120,11 +120,10 @@ Use [`defineToolPlugin`](/plugins/tool-plugins) for simple tool-only plugins
|
||||
with fixed tool names. Use `api.registerTool(...)` directly for mixed plugins
|
||||
or fully dynamic tool registration.
|
||||
|
||||
| Method | What it registers |
|
||||
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `api.registerTool(tool, opts?)` | Agent tool (required or `{ optional: true }`) |
|
||||
| `api.registerCommand(def)` | Custom command (bypasses the LLM) |
|
||||
| `api.registerNodeHostCommand(command)` | Command handled by `openclaw node run`; optional `agentTool` metadata can expose it as an agent-visible tool while the node is connected |
|
||||
| Method | What it registers |
|
||||
| ------------------------------- | --------------------------------------------- |
|
||||
| `api.registerTool(tool, opts?)` | Agent tool (required or `{ optional: true }`) |
|
||||
| `api.registerCommand(def)` | Custom command (bypasses the LLM) |
|
||||
|
||||
Plugin commands can set `agentPromptGuidance` when the agent needs a short,
|
||||
command-owned routing hint. Keep that text about the command itself; do not add
|
||||
@@ -151,19 +150,6 @@ surfaces: only guidance explicitly scoped to `codex_app_server` is promoted into
|
||||
that higher-priority lane. Legacy string guidance and unscoped structured
|
||||
guidance remain available to non-Codex prompt surfaces for compatibility.
|
||||
|
||||
Node-host commands run on the connected node host, not inside the Gateway
|
||||
process. If `agentTool` is present, the node publishes a descriptor after a
|
||||
successful Gateway connect; the Gateway exposes it to agent runs only while that
|
||||
node is connected and only if the descriptor's `command` is in the node's
|
||||
approved command surface. Set `agentTool.defaultPlatforms` to opt a
|
||||
non-dangerous command into the default node command allowlist; otherwise require
|
||||
explicit `gateway.nodes.allowCommands` or a node-invoke policy. `agentTool.name`
|
||||
must be provider-safe: start with a letter, use only letters, digits,
|
||||
underscores, or hyphens, and stay within 64 characters. MCP-backed node tools
|
||||
can set `agentTool.mcp` metadata so catalog and tool-search surfaces can show
|
||||
the remote MCP server/tool identity, but execution still goes through the
|
||||
advertised node command.
|
||||
|
||||
### Infrastructure
|
||||
|
||||
| Method | What it registers |
|
||||
|
||||
@@ -251,15 +251,9 @@ two-party event loops that do not go through the shared inbound reply runner.
|
||||
});
|
||||
```
|
||||
|
||||
`nodes.list(...)` includes each connected node's advertised
|
||||
`nodePluginTools` descriptors when that node exposes plugin or MCP-backed
|
||||
tools to the agent. Those descriptors are live connection state: the Gateway
|
||||
drops them when the node disconnects, and a node can replace them with
|
||||
`node.pluginTools.update` after local plugin/MCP inventory changes.
|
||||
|
||||
Inside the Gateway this runtime is in-process. In plugin CLI commands it calls the configured Gateway over RPC, so commands such as `openclaw googlemeet recover-tab` can inspect paired nodes from the terminal. Node commands still go through normal Gateway node pairing, command allowlists, plugin node-invoke policies, and node-local command handling.
|
||||
|
||||
Plugins that expose node-hosted agent tools can set `agentTool.defaultPlatforms` for non-dangerous commands that should be allowlisted by default. Omit it when operators must opt in with `gateway.nodes.allowCommands`. Dangerous node-host commands should register a node-invoke policy with `api.registerNodeInvokePolicy(...)`; the policy runs in the Gateway after command allowlist checks and before the command is forwarded to the node, so direct `node.invoke` calls, node-hosted plugin tools, and higher-level plugin tools share the same enforcement path.
|
||||
Plugins that expose dangerous node-host commands should register a node-invoke policy with `api.registerNodeInvokePolicy(...)`. The policy runs in the Gateway after command allowlist checks and before the command is forwarded to the node, so direct `node.invoke` calls and higher-level plugin tools share the same enforcement path.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="api.runtime.tasks.managedFlows">
|
||||
|
||||
@@ -467,6 +467,7 @@ Set `memory.backend = "qmd"` to enable. All QMD settings live under `memory.qmd`
|
||||
| ------------------------ | --------- | -------- | ------------------------------------------------------------------------------------- |
|
||||
| `command` | `string` | `qmd` | QMD executable path; set an absolute path when service `PATH` differs from your shell |
|
||||
| `searchMode` | `string` | `search` | Search command: `search`, `vsearch`, `query` |
|
||||
| `rerank` | `boolean` | -- | Set to `false` with `searchMode: "query"` and QMD 2.1+ to skip QMD reranking |
|
||||
| `includeDefaultMemory` | `boolean` | `true` | Auto-index `MEMORY.md` + `memory/**/*.md` |
|
||||
| `paths[]` | `array` | -- | Extra paths: `{ name, path, pattern? }` |
|
||||
| `sessions.enabled` | `boolean` | `false` | Index session transcripts |
|
||||
@@ -475,6 +476,8 @@ Set `memory.backend = "qmd"` to enable. All QMD settings live under `memory.qmd`
|
||||
|
||||
`searchMode: "search"` is lexical/BM25-only. OpenClaw does not run semantic vector readiness probes or QMD embedding maintenance for that mode, including during `memory status --deep`; `vsearch` and `query` continue to require QMD vector readiness and embeddings.
|
||||
|
||||
`rerank: false` only changes QMD `query` mode and requires QMD 2.1 or newer. In direct CLI mode OpenClaw passes `--no-rerank`; in mcporter-backed MCP mode it passes `rerank: false` to QMD's unified query tool. Leave it unset to use QMD's default query reranking behavior.
|
||||
|
||||
OpenClaw prefers current QMD collection and MCP query shapes, but keeps older QMD releases working by trying compatible collection pattern flags and older MCP tool names when needed. When QMD advertises support for multiple collection filters, same-source collections are searched with one QMD process; older QMD builds keep the per-collection compatibility path. Same-source means durable memory collections are grouped together, while session transcript collections remain a separate group so source diversification still has both inputs.
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -172,7 +172,7 @@ By default, OpenClaw runs a heartbeat every 30 minutes with the prompt:
|
||||
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
|
||||
Set `agents.defaults.heartbeat.every: "0m"` to disable.
|
||||
|
||||
- If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls.
|
||||
- If `HEARTBEAT.md` exists but is effectively empty (only blank lines, Markdown/HTML comments, Markdown headings like `# Heading`, fence markers, or empty checklist stubs), OpenClaw skips the heartbeat run to save API calls.
|
||||
- If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), OpenClaw suppresses outbound delivery for that heartbeat.
|
||||
- By default, heartbeat delivery to DM-style `user:<id>` targets is allowed. Set `agents.defaults.heartbeat.directPolicy: "block"` to suppress direct-target delivery while keeping heartbeat runs active.
|
||||
|
||||
@@ -287,6 +287,9 @@ Generic model:
|
||||
- Slack plugin approvals can use Slack's native approval client when the request comes from Slack
|
||||
and Slack plugin approvers resolve; `approvals.plugin` can also route plugin approvals to Slack
|
||||
sessions or targets even when Slack exec approvals are disabled
|
||||
- Google Chat native approval cards handle exec and plugin approvals that originate from Google
|
||||
Chat spaces or threads when stable `users/<id>` approvers resolve from `dm.allowFrom` or
|
||||
`defaultTo`; they do not use reaction events for decisions
|
||||
- WhatsApp and Signal reaction approval delivery are gated by `approvals.exec` and
|
||||
`approvals.plugin`; they do not have `channels.<channel>.execApprovals` blocks
|
||||
|
||||
@@ -306,6 +309,8 @@ FAQ: [Why are there two exec approval configs for chat approvals?](/help/faq-fir
|
||||
- Discord: `channels.discord.execApprovals.*`
|
||||
- Slack: `channels.slack.execApprovals.*`
|
||||
- Telegram: `channels.telegram.execApprovals.*`
|
||||
- Google Chat: configure stable approvers with `channels.googlechat.dm.allowFrom` or
|
||||
`channels.googlechat.defaultTo`; no `execApprovals` block is required
|
||||
- WhatsApp: use `approvals.exec` and `approvals.plugin` to route approval prompts to WhatsApp
|
||||
- Signal: use `approvals.exec` and `approvals.plugin` to route approval prompts to Signal
|
||||
|
||||
@@ -325,6 +330,9 @@ Shared behavior:
|
||||
routing, not Slack exec approvers
|
||||
- Slack native buttons preserve approval id kind, so `plugin:` ids can resolve plugin approvals
|
||||
without a second Slack-local fallback layer
|
||||
- Google Chat native cards preserve the manual `/approve` fallback in message text but card button
|
||||
callbacks carry only opaque action tokens; approval id and decision are recovered from server-side
|
||||
pending state
|
||||
- WhatsApp emoji approvals handle both exec and plugin prompts only when the matching top-level
|
||||
forwarding family is enabled and routes to WhatsApp; target-only WhatsApp forwarding stays on
|
||||
the shared forwarding path unless it matches the same native origin target
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ACPX tests cover index plugin behavior.
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ACPX tests cover register plugin behavior.
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { runtimeRegistry } = vi.hoisted(() => ({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ACPX tests cover claude agent acp completion plugin behavior.
|
||||
import { ClaudeAcpAgent } from "@agentclientprotocol/claude-agent-acp";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ACPX tests cover codex auth bridge plugin behavior.
|
||||
import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ACPX tests cover config plugin behavior.
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ACPX tests cover manifest plugin behavior.
|
||||
import fs from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ACPX tests cover process lease plugin behavior.
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ACPX tests cover process reaper plugin behavior.
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { OPENCLAW_ACPX_LEASE_ID_ARG, OPENCLAW_GATEWAY_INSTANCE_ID_ARG } from "./process-lease.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ACPX tests cover mcp command line plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
type SplitCommandLine = (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ACPX tests cover mcp proxy plugin behavior.
|
||||
import { spawn } from "node:child_process";
|
||||
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ACPX tests cover runtime plugin behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// ACPX tests cover service plugin behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Active Memory tests cover config plugin behavior.
|
||||
import fs from "node:fs";
|
||||
import {
|
||||
type JsonSchemaObject,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Active Memory tests cover doctor contract api plugin behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Active Memory tests cover index plugin behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Admin Http Rpc tests cover index plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import plugin from "./index.js";
|
||||
import manifest from "./openclaw.plugin.json" with { type: "json" };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Admin Http Rpc tests cover handler plugin behavior.
|
||||
import { Readable } from "node:stream";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { handleAdminHttpRpcRequest } from "./handler.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Alibaba tests cover video generation provider plugin behavior.
|
||||
import {
|
||||
getProviderHttpMocks,
|
||||
installProviderHttpMockCleanup,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Amazon Bedrock Mantle tests cover discovery plugin behavior.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Amazon Bedrock Mantle tests cover index plugin behavior.
|
||||
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import bedrockMantlePlugin from "./index.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Amazon Bedrock Mantle tests cover mantle anthropic plugin behavior.
|
||||
import type { Model } from "openclaw/plugin-sdk/llm";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Amazon Bedrock tests cover config compat plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { migrateAmazonBedrockLegacyConfig } from "./config-compat.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Amazon Bedrock tests cover discovery plugin behavior.
|
||||
import type { BedrockClient } from "@aws-sdk/client-bedrock";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Amazon Bedrock tests cover embedding provider plugin behavior.
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { testing, hasAwsCredentials } from "./embedding-provider.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Amazon Bedrock tests cover index plugin behavior.
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Amazon Bedrock tests cover lazy import plugin behavior.
|
||||
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Amazon Bedrock tests cover memory embedding adapter plugin behavior.
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const hasAwsCredentialsMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Amazon Bedrock tests cover provider policy api plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveThinkingProfile } from "./provider-policy-api.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Amazon Bedrock tests cover stream plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { testing } from "./stream.runtime.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic Vertex tests cover api plugin behavior.
|
||||
import { createAssistantMessageEventStream, type Model } from "openclaw/plugin-sdk/llm";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { AnthropicVertexStreamDeps } from "./stream-runtime.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic Vertex tests cover index plugin behavior.
|
||||
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic Vertex tests cover provider discovery.import guard plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("anthropic-vertex provider discovery entry", () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic Vertex tests cover provider policy api plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveThinkingProfile } from "./provider-policy-api.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic Vertex tests cover region.adc plugin behavior.
|
||||
import { platform } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic Vertex tests cover region plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveAnthropicVertexRegion, resolveAnthropicVertexRegionFromBaseUrl } from "./api.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic Vertex tests cover stream runtime plugin behavior.
|
||||
import { createAssistantMessageEventStream, type Model } from "openclaw/plugin-sdk/llm";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { AnthropicVertexStreamDeps } from "./stream-runtime.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic tests cover cli migration plugin behavior.
|
||||
import type {
|
||||
ProviderAuthContext,
|
||||
ProviderAuthMethodNonInteractiveContext,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic tests cover cli shared plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAnthropicCliBackend } from "./cli-backend.js";
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic tests cover index plugin behavior.
|
||||
import type {
|
||||
ProviderResolveDynamicModelContext,
|
||||
ProviderRuntimeModel,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic tests cover provider policy api plugin behavior.
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-types";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic tests cover provider runtime.contract plugin behavior.
|
||||
import { describeAnthropicProviderRuntimeContract } from "openclaw/plugin-sdk/provider-test-contracts";
|
||||
|
||||
describeAnthropicProviderRuntimeContract(() => import("./index.js"));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Anthropic tests cover stream wrappers plugin behavior.
|
||||
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Arcee tests cover index plugin behavior.
|
||||
import {
|
||||
registerSingleProviderPlugin,
|
||||
resolveProviderPluginChoice,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Azure Speech tests cover azure speech plugin behavior.
|
||||
import {
|
||||
registerProviderPlugin,
|
||||
requireRegisteredProvider,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Azure Speech tests cover speech provider plugin behavior.
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { azureSpeechTTSMock, listAzureSpeechVoicesMock } = vi.hoisted(() => ({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Azure Speech tests cover tts plugin behavior.
|
||||
import { installPinnedHostnameTestHooks } from "openclaw/plugin-sdk/test-env";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Bonjour tests cover index plugin behavior.
|
||||
import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
|
||||
import { afterAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Bonjour tests cover manifest plugin behavior.
|
||||
import fs from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Bonjour tests cover advertiser plugin behavior.
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Bonjour tests cover ciao plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const { classifyCiaoUnhandledRejection, ignoreCiaoUnhandledRejection } = await import("./ciao.js");
|
||||
|
||||
@@ -56,7 +56,11 @@ export function classifyCiaoProcessError(reason: unknown): CiaoProcessErrorClass
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Alternate export name for unhandled-rejection classification. */
|
||||
/**
|
||||
* Backward-compatible alias for unhandled-rejection classification.
|
||||
*
|
||||
* @deprecated Use classifyCiaoProcessError.
|
||||
*/
|
||||
export const classifyCiaoUnhandledRejection = classifyCiaoProcessError;
|
||||
|
||||
/** Return whether a ciao unhandled rejection is known and ignorable. */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Bonjour tests cover errors plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatBonjourError } from "./errors.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Brave tests cover brave web search provider.merge plugin behavior.
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createBraveWebSearchProvider } from "./brave-web-search-provider.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Brave tests cover brave web search provider plugin behavior.
|
||||
import fs from "node:fs";
|
||||
import { validateJsonSchemaValue } from "openclaw/plugin-sdk/json-schema-runtime";
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Browser tests cover index plugin behavior.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Browser tests cover browser tool.schema plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { BrowserToolSchema } from "./browser-tool.schema.js";
|
||||
import { ACT_MAX_VIEWPORT_DIMENSION } from "./browser/act-policy.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Browser tests cover browser tool plugin behavior.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const browserClientMocks = vi.hoisted(() => ({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Browser tests cover bridge server.auth plugin behavior.
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { startBrowserBridgeServer, stopBrowserBridgeServer } from "./bridge-server.js";
|
||||
import type { ResolvedBrowserConfig } from "./config.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Browser tests cover browser proxy mode plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
hasChromeProxyControlArg,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Browser tests cover browser utils plugin behavior.
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
appendCdpPath,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Browser tests cover cdp proxy bypass plugin behavior.
|
||||
import http from "node:http";
|
||||
import https from "node:https";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Browser tests cover cdp.helpers.fuzz plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
appendCdpPath,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Browser tests cover cdp.helpers.internal plugin behavior.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { rawDataToString } from "../infra/ws.js";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Browser tests cover cdp.helpers plugin behavior.
|
||||
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveCdpReachabilityPolicy } from "./cdp-reachability-policy.js";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user