Skip to content

Commit 154e8a5

Browse files
committed
feat: enhance gateway types and store with shutdown and update handling
- Added GatewayErrorInfo type for error responses. - Updated GatewayHelloOk type to require server and features fields. - Enhanced GatewaySnapshot type with additional fields for presence, health, and state management. - Introduced gatewayShutdown and gatewayUpdateAvailable states in the gateway store. - Implemented event handlers for shutdown and update events in the gateway store. - Added UpdateAgentDialog component for agent configuration updates. - Created ExecApprovalBell component for handling execution approval requests. - Implemented GatewayShutdownBanner to display shutdown notifications with countdown. - Updated unit tests to cover new functionality and ensure stability.
1 parent 23c26ae commit 154e8a5

86 files changed

Lines changed: 1234 additions & 1150 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,70 @@
66

77
### Added
88

9+
#### Exec Approval UI
10+
- **Exec Approval Bell** — new `ExecApprovalBell` component in the global status bar; real-time exec approval queue powered by `exec.approval.requested` / `exec.approval.resolved` gateway events
11+
- **Approve / Deny actions** — per-request allow-once, allow-always, and deny buttons via `exec.approval.resolve` RPC; auto-expire entries based on server-provided TTL
12+
- **Timer dedup guard** — duplicate `entry.id` events are rejected before timer creation to prevent leaked timers
13+
14+
#### Agent Update Dialog
15+
- **`UpdateAgentDialog`** — edit agent name and model via `agents.update` RPC; accessible from agent overview; validates non-empty changes; shows "No changes to save" toast on no-op
16+
17+
#### Session Compact
18+
- **Compact button** — per-session compact action in session cards via `sessions.compact` RPC; disabled state with "Compacting…" label during operation; success/info toast feedback
19+
20+
#### Dashboard Quick Actions
21+
- **Wake Agent** — "Wake Agent" button triggers `wake` RPC (`{ mode: 'now', text: 'Manual wake from ClawKernel dashboard' }`)
22+
- **Update Gateway** — "Update Gateway" button triggers `update.run` RPC with confirm dialog; shows restart countdown toast
23+
24+
#### Gateway Shutdown Banner
25+
- **`GatewayShutdownBanner`** — layout-level banner displayed when the gateway broadcasts a `shutdown` event; shows reason text + countdown timer until expected restart; auto-clears on successful reconnect (`hello-ok`); uses `useRef` for timestamp tracking to handle repeated shutdown cycles correctly
26+
27+
### Changed
28+
29+
#### OpenClaw v2026.3.11 Compatibility
30+
- **Device auth v3**`buildDeviceAuthPayloadV3` and `normalizeDeviceMetadataForAuth` added to `device-auth.ts`; client now signs with v3 payload format (includes `platform` and `deviceFamily`); server falls back to v2 if v3 verification fails
31+
- **Display name** — connect params now send `displayName` from `opts.clientName` (defaults to `ClawKernel`); previously hardcoded `WebClaw`
32+
- **Snapshot type realigned** — removed 7 phantom fields (`agents`, `sessions`, `channels`, `config`, `skills`, `cron`, `models`); added 6 upstream fields (`stateVersion`, `uptimeMs`, `configPath`, `stateDir`, `sessionDefaults`, `authMode`); `presence` and `health` now required (matching `SnapshotSchema`)
33+
- **`GatewayHelloOk` type corrected**`server`, `features`, `snapshot`, `policy` marked required; `auth` sub-fields (`deviceToken`, `role`, `scopes`) required when `auth` is present; `policy` includes `maxPayload` and `maxBufferedBytes`
34+
- **Presence type expanded** — added 8 upstream fields (`deviceFamily`, `modelIdentifier`, `reason`, `tags`, `text`, `deviceId`, `roles`, `scopes`); `ts` now required
35+
- **`ChannelAccountSnapshot` parity** — removed 5 speculative fields (`credentialSource`, `audienceType`, `audience`, `webhookPath`, `webhookUrl`); added 3 upstream fields (`busy`, `activeRuns`, `lastRunActivityAt`); matches `ChannelAccountSnapshotSchema` exactly
36+
- **`updateAvailable` type** — removed erroneous `| null` union; upstream uses `Type.Optional(Type.Object(...))` only
37+
38+
#### Event Handling
39+
- **Cron event** — replaced dead `cron.status` / `cron.jobs` handlers with single `cron` handler; re-fetches via RPC on event (matching upstream broadcast pattern at `server-cron.ts:359`)
40+
- **Device pairing events**`pairing-bell.tsx` now subscribes to live `device.pair.requested` / `device.pair.resolved` events in addition to polling
41+
- **Shutdown + update.available events** — new store fields `gatewayShutdown` and `gatewayUpdateAvailable`; `gatewayShutdown` cleared on reconnect
42+
- **Dead handler cleanup** — removed 4 event handlers for events upstream never broadcasts (`sessions`, `config`, `channels`, `skills`)
43+
- **Presence event** — correctly unwraps `{ presence: Array<...> }` envelope (matching `presence-events.ts:11`)
44+
45+
#### Gateway Client
46+
- **`isNonRecoverableAuthError`** — added 3 missing upstream error codes: `PAIRING_REQUIRED`, `CONTROL_UI_DEVICE_IDENTITY_REQUIRED`, `DEVICE_IDENTITY_REQUIRED`; prevents infinite reconnect loops on these errors
47+
- **`ConnectErrorDetailCodes`** — expanded from 4 to 7 codes to match upstream `connect-error-details.ts`
48+
- **`GatewayRequestError` class** — matches upstream `ui/src/ui/gateway.ts` (`gatewayCode` + `details`)
49+
- **Connect params** — sends `caps: ['tool-events']` capability; `instanceId` included in client info
50+
51+
#### Store & UI
52+
- **`chat.inject` params** — corrected from `{ sessionKey, role, content }` to `{ sessionKey, message }` (matching `ChatInjectParamsSchema`)
53+
- **Event handlers use injected `set`**`handleCronEvent` no longer calls `useGatewayStore.setState()` directly
54+
- **`presenceArrayToRecord`** — replaced `arr.indexOf(entry)` fallback (O(n²)) with loop counter (O(n))
55+
- **Navigator reference**`getBrowserNavigator()` called once and shared between auth payload and connect params; prevents platform divergence in v3 signature verification
56+
- **Pairing bell cleanup** — simplified `unsubs` array to single `unsub` variable with `?.()` cleanup
57+
- **Agent overview** — added `.catch()` to fire-and-forget `agents.list` refresh in `onUpdated`
58+
- **`compactingKey` ref guard**`useCallback` dependency uses `compactingRef` instead of stale closure over `compactingKey` state
59+
60+
### Fixed
61+
62+
#### Channels
63+
- **DM policy enum**`allow/deny``pairing/allowlist/open/disabled` (matching `DmPolicySchema` at `zod-schema.core.ts:313`)
64+
- **Group policy enum**`allow/mention/deny``open/disabled/allowlist` (matching `GroupPolicySchema` at `zod-schema.core.ts:315`)
65+
66+
#### Dependencies
67+
- **`@noble/ed25519`** — updated to `^2.3.0`
68+
69+
---
70+
71+
### Added
72+
973
#### Usage & Cost Analytics `/usage`
1074
- **Usage page** — new route `/usage`; nav item (ChartColumn) in sidebar; lazy-loaded with `PageErrorBoundary`
1175
- **Manual fetch workflow** — page loads idle; user selects date range (or preset: Today, 7d, 30d) and clicks Refresh; empty state with guidance message; matches OpenClaw's pattern

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/db.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
// ---------------------------------------------------------------------------
2-
// ClawKernel — Database
3-
//
4-
// SQLite via better-sqlite3 + Drizzle ORM.
5-
// DB file: ~/.clawkernel.db
6-
// Tables are created with IF NOT EXISTS — no migrations needed.
7-
// ---------------------------------------------------------------------------
1+
// SQLite database at ~/.clawkernel.db. Tables are created with IF NOT EXISTS, so no migrations are required.
82

93
import os from 'node:os'
104
import path from 'node:path'

server/index.ts

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
// ---------------------------------------------------------------------------
2-
// ClawKernel — Hono server
3-
//
4-
// Entry points:
5-
// Production: spawned by bin/clawkernel.mjs (config via CK_* env vars)
6-
// Development: `npm run dev:server` via tsx (reads ~/.clawkernel.json)
7-
//
8-
// Serves the Vite dist/ build + all /api/* routes.
9-
// ---------------------------------------------------------------------------
1+
// Hono server for the SPA and local API routes.
102

113
import { existsSync, readFileSync } from 'node:fs'
124
import { readFile, stat } from 'node:fs/promises'
@@ -60,12 +52,7 @@ const clr = COLOR
6052
? { m: '\x1b[95m', g: '\x1b[92m', dim: '\x1b[2m', b: '\x1b[1m', r: '\x1b[0m' }
6153
: { m: '', g: '', dim: '', b: '', r: '' }
6254

63-
// ---------------------------------------------------------------------------
64-
// Config injection into index.html
65-
//
66-
// Built once and cached for the server's lifetime. In dev mode (no dist/),
67-
// the server still starts — SPA fallback returns a minimal dev-mode page.
68-
// ---------------------------------------------------------------------------
55+
// Build this once per process. In dev mode without dist/, the server returns a minimal fallback page.
6956

7057
const DEV_FALLBACK_HTML = `<!DOCTYPE html><html><head><title>ClawKernel</title></head><body>
7158
<pre>dist/index.html not found.\n\nRun: npm run build\nThen restart the server.</pre></body></html>`
@@ -111,14 +98,7 @@ const MIME_TYPES = new Map([
11198
['.map', 'application/json'],
11299
])
113100

114-
// ---------------------------------------------------------------------------
115-
// Auth middleware for mutating API endpoints
116-
//
117-
// When CK_API_TOKEN is set, POST/PATCH/DELETE requests to /api/* require
118-
// a matching Authorization: Bearer <token> header. GET requests are always
119-
// public (health, version, prefs read). When CK_API_TOKEN is empty (default),
120-
// all endpoints are open — appropriate for localhost-only access.
121-
// ---------------------------------------------------------------------------
101+
// When CK_API_TOKEN is set, mutating /api requests require Authorization: Bearer <token>.
122102

123103
function requireAuth(c: { req: { header: (name: string) => string | undefined } }): Response | null {
124104
if (!API_TOKEN) return null

src/app/agents/components/agent-activity.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,7 @@ const ActivityItem = memo(function ActivityItem({ item, now }: { readonly item:
217217

218218
return (
219219
<div className="group relative flex gap-3 py-2.5">
220-
{/* Timeline line */}
221220
<div className="absolute left-[13px] top-10 bottom-0 w-px bg-border/40 group-last:hidden" />
222-
223-
{/* Dot + icon */}
224221
<div
225222
className={cn(
226223
'relative z-10 flex h-7 w-7 shrink-0 items-center justify-center rounded-full',
@@ -229,8 +226,6 @@ const ActivityItem = memo(function ActivityItem({ item, now }: { readonly item:
229226
>
230227
<Icon className={cn('h-3.5 w-3.5', CATEGORY_COLORS[item.category])} />
231228
</div>
232-
233-
{/* Content */}
234229
<div className="min-w-0 flex-1">
235230
<div className="flex items-center gap-2">
236231
<span className="text-xs font-medium text-foreground truncate">{item.title}</span>
@@ -294,7 +289,7 @@ export function AgentActivity({ agentId, client }: Props) {
294289
const [filter, setFilter] = useState<EventCategory>('all')
295290
const [cronRuns, setCronRuns] = useState<Array<CronRunLogEntry & { _jobName?: string }>>([])
296291

297-
const [cleared, setCleared] = useState<number>(0) // timestamp of last clear
292+
const [cleared, setCleared] = useState<number>(0)
298293
const [now, setNow] = useState(Date.now())
299294

300295
useEffect(() => {
@@ -339,7 +334,6 @@ export function AgentActivity({ agentId, client }: Props) {
339334

340335
return (
341336
<div className="flex h-full flex-col gap-3 p-4">
342-
{/* Header */}
343337
<div className="flex items-center justify-between">
344338
<div className="flex items-center gap-2">
345339
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-primary/10">
@@ -362,13 +356,9 @@ export function AgentActivity({ agentId, client }: Props) {
362356
</div>
363357

364358
<Separator className="opacity-50" />
365-
366-
{/* Filter bar */}
367359
<div className="rounded-2xl border border-border/50 bg-card/80 backdrop-blur-sm px-3 py-2">
368360
<ActivityFilter active={filter} onChange={setFilter} counts={counts} />
369361
</div>
370-
371-
{/* Timeline */}
372362
<div className="flex-1 min-h-0">
373363
<ScrollArea className="h-full">
374364
{filtered.length === 0 ? (

src/app/agents/components/agent-bindings.tsx

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,6 @@ function BindingFormDialog({
221221
</DialogHeader>
222222

223223
<div className="space-y-4">
224-
{/* Channel */}
225224
<div className="space-y-1.5">
226225
<Label className="text-xs">Channel *</Label>
227226
<div className="relative">
@@ -239,8 +238,6 @@ function BindingFormDialog({
239238
</select>
240239
</div>
241240
</div>
242-
243-
{/* Account ID */}
244241
<div className="space-y-1.5">
245242
<Label className="text-xs">Account ID</Label>
246243
<Input
@@ -250,8 +247,6 @@ function BindingFormDialog({
250247
className="text-sm"
251248
/>
252249
</div>
253-
254-
{/* Peer */}
255250
<div className="grid grid-cols-3 gap-2">
256251
<div className="space-y-1.5">
257252
<Label className="text-xs">Peer Kind</Label>
@@ -277,8 +272,6 @@ function BindingFormDialog({
277272
/>
278273
</div>
279274
</div>
280-
281-
{/* Guild / Team */}
282275
<div className="grid grid-cols-2 gap-2">
283276
<div className="space-y-1.5">
284277
<Label className="text-xs">Guild ID</Label>
@@ -299,8 +292,6 @@ function BindingFormDialog({
299292
/>
300293
</div>
301294
</div>
302-
303-
{/* Roles */}
304295
<div className="space-y-1.5">
305296
<Label className="text-xs">Roles</Label>
306297
<Input
@@ -421,7 +412,6 @@ export function AgentBindings({ agentId, config, isDefault, client }: Props) {
421412

422413
return (
423414
<div className="space-y-4">
424-
{/* Header */}
425415
<div className="flex flex-wrap items-center justify-between gap-3">
426416
<div className="flex flex-wrap items-center gap-2">
427417
<AgentStatPill
@@ -441,8 +431,6 @@ export function AgentBindings({ agentId, config, isDefault, client }: Props) {
441431
Add Binding
442432
</Button>
443433
</div>
444-
445-
{/* Bindings list */}
446434
{agentBindingsWithIndex.length === 0 ? (
447435
<AgentTabEmptyState
448436
icon={Unlink}
@@ -470,8 +458,6 @@ export function AgentBindings({ agentId, config, isDefault, client }: Props) {
470458
))}
471459
</div>
472460
)}
473-
474-
{/* Other agents' bindings for context */}
475461
{otherAgentBindings.length > 0 && (
476462
<div className="space-y-2">
477463
<Separator className="opacity-30" />
@@ -494,8 +480,6 @@ export function AgentBindings({ agentId, config, isDefault, client }: Props) {
494480
</div>
495481
</div>
496482
)}
497-
498-
{/* Create Dialog */}
499483
<BindingFormDialog
500484
open={createOpen}
501485
onOpenChange={setCreateOpen}
@@ -505,8 +489,6 @@ export function AgentBindings({ agentId, config, isDefault, client }: Props) {
505489
onSave={handleCreate}
506490
saving={saving}
507491
/>
508-
509-
{/* Edit Dialog */}
510492
<BindingFormDialog
511493
open={editIndex !== null}
512494
onOpenChange={(o) => {
@@ -518,8 +500,6 @@ export function AgentBindings({ agentId, config, isDefault, client }: Props) {
518500
onSave={handleEdit}
519501
saving={saving}
520502
/>
521-
522-
{/* Delete Confirm */}
523503
<ConfirmDialog
524504
open={deleteIndex !== null}
525505
onOpenChange={(o) => {

src/app/agents/components/agent-channels.tsx

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ function ChannelCard({
9898
channelBorderClass(allConnected, partial),
9999
)}
100100
>
101-
{/* Header */}
102101
<div className="flex items-start justify-between mb-3">
103102
<div
104103
className={cn('flex h-12 w-12 items-center justify-center rounded-xl', iconBoxClass(allConnected, partial))}
@@ -129,20 +128,14 @@ function ChannelCard({
129128
<span className={cn('h-3 w-3 rounded-full', dotClass(allConnected, partial))} />
130129
</div>
131130
</div>
132-
133-
{/* Name */}
134131
<p className="text-sm font-semibold text-foreground">{label}</p>
135132
<p className="font-mono text-[10px] text-muted-foreground/40 mt-0.5">{channelId}</p>
136-
137-
{/* Connection bar */}
138133
<div className="mt-3 h-1.5 rounded-full bg-muted/30 overflow-hidden">
139134
<div
140135
className={cn('h-full rounded-full transition-all duration-500', barClass(allConnected, partial))}
141136
style={{ width: `${Math.max(ratio, 2)}%` }}
142137
/>
143138
</div>
144-
145-
{/* Stats */}
146139
<div className="flex gap-3 mt-3">
147140
<div>
148141
<p className="text-xs font-bold text-foreground">{s.connected}</p>
@@ -157,8 +150,6 @@ function ChannelCard({
157150
<p className="text-[9px] text-muted-foreground/40">Enabled</p>
158151
</div>
159152
</div>
160-
161-
{/* Bindings detail */}
162153
{bindings.length > 0 && (
163154
<>
164155
<Separator className="my-3 opacity-30" />
@@ -229,7 +220,6 @@ export function AgentChannels({ agentId, channels, config, isDefault }: Props) {
229220

230221
return (
231222
<div className="space-y-4">
232-
{/* Summary bar */}
233223
<div className="flex flex-wrap items-center gap-2">
234224
<AgentStatPill icon={Radio} value={ids.length} label="channels" />
235225
<AgentStatPill
@@ -245,8 +235,6 @@ export function AgentChannels({ agentId, channels, config, isDefault }: Props) {
245235
</Badge>
246236
)}
247237
</div>
248-
249-
{/* Channel grid */}
250238
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
251239
{sortedIds.map((id) => {
252240
const accounts = channels.channelAccounts?.[id] ?? []

src/app/agents/components/agent-comparison.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,6 @@ export function AgentComparison({ agents, sessions, config, identities, activeRu
279279

280280
return (
281281
<div className="rounded-2xl border border-border/50 bg-card/80 backdrop-blur-sm p-5 sm:p-6 space-y-4">
282-
{/* Top bar */}
283282
<div className="flex flex-wrap items-center gap-3">
284283
<div className="flex items-center gap-2 mr-auto">
285284
<ArrowLeftRight className="h-5 w-5 text-primary" />
@@ -293,17 +292,13 @@ export function AgentComparison({ agents, sessions, config, identities, activeRu
293292
</div>
294293

295294
<Separator className="opacity-40" />
296-
297-
{/* Column headers */}
298295
<div className="grid grid-cols-[140px_1fr_1fr] gap-4 px-4 max-sm:hidden">
299296
<div />
300297
<AgentColumnHeader d={dataA} />
301298
<AgentColumnHeader d={dataB} />
302299
</div>
303300

304301
<Separator className="opacity-30" />
305-
306-
{/* Comparison rows */}
307302
<div className="space-y-0.5">
308303
<ComparisonRow label="Identity" valueA={agentIdentityLabel(dataA)} valueB={agentIdentityLabel(dataB)} />
309304
<ComparisonRow

0 commit comments

Comments
 (0)