@omnixal/openclaw-nats-plugin 0.2.10 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PLUGIN.md CHANGED
@@ -53,6 +53,7 @@ Plugin hooks make lightweight HTTP calls to a NATS sidecar service for event pub
53
53
  | Subject | Trigger |
54
54
  |---|---|
55
55
  | `agent.events.gateway.startup` | Gateway starts |
56
+ | `agent.events.gateway.stopped` | Gateway stops |
56
57
  | `agent.events.session.new` | `/new` command |
57
58
  | `agent.events.session.reset` | `/reset` command |
58
59
  | `agent.events.session.stop` | `/stop` command |
@@ -60,10 +61,13 @@ Plugin hooks make lightweight HTTP calls to a NATS sidecar service for event pub
60
61
  | `agent.events.session.ended` | Session ends |
61
62
  | `agent.events.tool.{name}.completed` | Tool succeeds |
62
63
  | `agent.events.tool.{name}.failed` | Tool fails |
64
+ | `agent.events.message.received` | Inbound message received |
65
+ | `agent.events.message.sent` | Message delivered |
66
+ | `agent.events.llm.output` | LLM response received |
67
+ | `agent.events.subagent.spawning` | Subagent about to spawn |
63
68
  | `agent.events.subagent.spawned` | Subagent created |
64
69
  | `agent.events.subagent.ended` | Subagent finished |
65
70
  | `agent.events.agent.run_ended` | Agent run completes |
66
- | `agent.events.message.sent` | Message delivered |
67
71
  | `agent.events.context.compacted` | Context history compressed |
68
72
 
69
73
  ## Dashboard
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ import { Modal } from '$lib/components/ui/modal';
3
+ import * as Table from '$lib/components/ui/table';
4
+
5
+ interface Props {
6
+ open: boolean;
7
+ onClose: () => void;
8
+ }
9
+
10
+ let { open, onClose }: Props = $props();
11
+
12
+ const events = [
13
+ { subject: 'agent.events.gateway.startup', trigger: 'Gateway started', payload: '{}' },
14
+ { subject: 'agent.events.gateway.stopped', trigger: 'Gateway stopped', payload: '{ reason }' },
15
+ { subject: 'agent.events.session.new', trigger: '/new command', payload: '{ sessionKey, command }' },
16
+ { subject: 'agent.events.session.reset', trigger: '/reset command', payload: '{ sessionKey, command }' },
17
+ { subject: 'agent.events.session.stop', trigger: '/stop command', payload: '{ sessionKey, command }' },
18
+ { subject: 'agent.events.session.started', trigger: 'Session began', payload: '{ sessionKey, sessionId, channel }' },
19
+ { subject: 'agent.events.session.ended', trigger: 'Session ended', payload: '{ sessionKey, sessionId, channel }' },
20
+ { subject: 'agent.events.agent.run_ended', trigger: 'Agent run completed', payload: '{ sessionKey, runId, messageCount }' },
21
+ { subject: 'agent.events.tool.{name}.completed', trigger: 'Tool succeeded', payload: '{ sessionKey, toolName, durationMs }' },
22
+ { subject: 'agent.events.tool.{name}.failed', trigger: 'Tool failed', payload: '{ sessionKey, toolName, durationMs }' },
23
+ { subject: 'agent.events.message.received', trigger: 'Inbound message', payload: '{ from, content, metadata }' },
24
+ { subject: 'agent.events.message.sent', trigger: 'Message delivered', payload: '{ sessionKey, to, success, error }' },
25
+ { subject: 'agent.events.llm.output', trigger: 'LLM response', payload: '{ provider, model, usage }' },
26
+ { subject: 'agent.events.subagent.spawning', trigger: 'Subagent creating', payload: '{ childSessionKey, agentId, label, mode }' },
27
+ { subject: 'agent.events.subagent.spawned', trigger: 'Subagent created', payload: '{ sessionKey, subagentId, task }' },
28
+ { subject: 'agent.events.subagent.ended', trigger: 'Subagent finished', payload: '{ sessionKey, subagentId, result, durationMs }' },
29
+ { subject: 'agent.events.context.compacted', trigger: 'Context compressed', payload: '{ sessionKey }' },
30
+ { subject: 'agent.events.cron.*', trigger: 'Cron trigger', payload: '(user-defined)' },
31
+ { subject: 'agent.events.custom.*', trigger: 'Custom event', payload: '(user-defined)' },
32
+ ];
33
+ </script>
34
+
35
+ <Modal {open} title="Events Reference" {onClose} class="max-w-3xl">
36
+ <div class="max-h-[60vh] overflow-y-auto -mx-6 px-6">
37
+ <p class="text-xs text-muted-foreground mb-3">
38
+ All events published by the NATS plugin. Each event also includes a <code class="text-xs">timestamp</code> field.
39
+ </p>
40
+ <Table.Root>
41
+ <Table.Header>
42
+ <Table.Row>
43
+ <Table.Head>Subject</Table.Head>
44
+ <Table.Head>Trigger</Table.Head>
45
+ <Table.Head>Payload</Table.Head>
46
+ </Table.Row>
47
+ </Table.Header>
48
+ <Table.Body>
49
+ {#each events as ev}
50
+ <Table.Row>
51
+ <Table.Cell class="font-mono text-xs whitespace-nowrap">{ev.subject}</Table.Cell>
52
+ <Table.Cell class="text-xs">{ev.trigger}</Table.Cell>
53
+ <Table.Cell class="font-mono text-xs text-muted-foreground">{ev.payload}</Table.Cell>
54
+ </Table.Row>
55
+ {/each}
56
+ </Table.Body>
57
+ </Table.Root>
58
+ </div>
59
+ </Modal>
@@ -1,15 +1,19 @@
1
1
  <script lang="ts">
2
2
  import * as Table from '$lib/components/ui/table';
3
3
  import { Badge } from '$lib/components/ui/badge';
4
+ import { Button } from '$lib/components/ui/button';
4
5
  import * as Card from '$lib/components/ui/card';
5
6
  import type { SubjectMetric } from '$lib/api';
6
7
  import { relativeAge } from '$lib/utils';
8
+ import CircleHelp from '@lucide/svelte/icons/circle-help';
9
+ import EventsReferenceModal from './EventsReferenceModal.svelte';
7
10
 
8
11
  interface Props {
9
12
  metrics: SubjectMetric[];
10
13
  }
11
14
 
12
15
  let { metrics }: Props = $props();
16
+ let showEventsRef = $state(false);
13
17
 
14
18
  let totalPublished = $derived(metrics.reduce((acc, m) => acc + m.published, 0));
15
19
  let totalConsumed = $derived(metrics.reduce((acc, m) => acc + m.consumed, 0));
@@ -17,7 +21,12 @@
17
21
 
18
22
  <Card.Root>
19
23
  <Card.Header class="pb-2">
20
- <Card.Title class="text-sm font-medium">Queue Metrics</Card.Title>
24
+ <div class="flex items-center justify-between">
25
+ <Card.Title class="text-sm font-medium">Queue Metrics</Card.Title>
26
+ <Button variant="ghost" size="icon-sm" onclick={() => showEventsRef = true} title="Events reference">
27
+ <CircleHelp size={14} />
28
+ </Button>
29
+ </div>
21
30
  </Card.Header>
22
31
  <Card.Content>
23
32
  <div class="flex items-center gap-2 mb-3">
@@ -58,3 +67,5 @@
58
67
  {/if}
59
68
  </Card.Content>
60
69
  </Card.Root>
70
+
71
+ <EventsReferenceModal open={showEventsRef} onClose={() => showEventsRef = false} />
@@ -8,6 +8,7 @@
8
8
  onClose: () => void;
9
9
  children: Snippet;
10
10
  actions?: Snippet;
11
+ class?: string;
11
12
  }
12
13
 
13
14
  let {
@@ -16,6 +17,7 @@
16
17
  onClose,
17
18
  children,
18
19
  actions,
20
+ class: className,
19
21
  }: Props = $props();
20
22
 
21
23
  function handleKeydown(e: KeyboardEvent) {
@@ -31,7 +33,7 @@
31
33
  >
32
34
  <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
33
35
  <div class="fixed inset-0" onclick={onClose}></div>
34
- <div class="relative z-10 w-full max-w-lg rounded-lg border bg-background p-6 shadow-lg">
36
+ <div class="relative z-10 w-full {className ?? 'max-w-lg'} rounded-lg border bg-background p-6 shadow-lg">
35
37
  <div class="flex items-center justify-between mb-4">
36
38
  <h3 class="text-lg font-semibold">{title}</h3>
37
39
  <Button variant="ghost" size="icon-sm" onclick={onClose}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnixal/openclaw-nats-plugin",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "NATS JetStream event-driven plugin for OpenClaw",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -78,6 +78,52 @@ export default function (api: any) {
78
78
  }, { priority: 8 });
79
79
  }, { priority: 99 });
80
80
 
81
+ // ── Gateway stop ───────────────────────────────────────────────
82
+
83
+ api.on('gateway_stop', (event: any) => {
84
+ void publishToSidecar('agent.events.gateway.stopped', {
85
+ reason: event.reason,
86
+ timestamp: new Date().toISOString(),
87
+ });
88
+ }, { priority: 99 });
89
+
90
+ // ── Message received ─────────────────────────────────────────────
91
+
92
+ api.on('message_received', (event: any) => {
93
+ void publishToSidecar('agent.events.message.received', {
94
+ from: event.from,
95
+ content: event.content,
96
+ metadata: event.metadata,
97
+ timestamp: new Date().toISOString(),
98
+ });
99
+ }, { priority: 99 });
100
+
101
+ // ── LLM output ───────────────────────────────────────────────────
102
+
103
+ api.on('llm_output', (event: any) => {
104
+ void publishToSidecar('agent.events.llm.output', {
105
+ sessionKey: event.sessionId,
106
+ runId: event.runId,
107
+ provider: event.provider,
108
+ model: event.model,
109
+ usage: event.usage,
110
+ timestamp: new Date().toISOString(),
111
+ });
112
+ }, { priority: 99 });
113
+
114
+ // ── Subagent spawning ────────────────────────────────────────────
115
+
116
+ api.on('subagent_spawning', (event: any) => {
117
+ void publishToSidecar('agent.events.subagent.spawning', {
118
+ childSessionKey: event.childSessionKey,
119
+ agentId: event.agentId,
120
+ label: event.label,
121
+ mode: event.mode,
122
+ timestamp: new Date().toISOString(),
123
+ });
124
+ return { status: 'ok' };
125
+ }, { priority: 99 });
126
+
81
127
  // ── Agent Tools ─────────────────────────────────────────────────
82
128
 
83
129
  const SIDECAR_URL = process.env.NATS_SIDECAR_URL || 'http://127.0.0.1:3104';
@@ -58,14 +58,9 @@ export class ConsumerController extends BaseController {
58
58
  try {
59
59
  const injectStart = performance.now();
60
60
  await this.gatewayClient.inject({
61
- target: route.target,
61
+ to: route.target,
62
62
  message: this.formatMessage(envelope),
63
- metadata: {
64
- source: 'nats',
65
- eventId: envelope.id,
66
- subject: envelope.subject,
67
- priority: (ctx.enrichments['priority'] as number) ?? envelope.meta?.priority ?? 5,
68
- },
63
+ eventId: envelope.id,
69
64
  });
70
65
  const lagMs = Math.round(performance.now() - injectStart);
71
66
  await this.routerService.recordDelivery(route.id, envelope.subject, lagMs);
@@ -3,14 +3,11 @@ import path from 'node:path';
3
3
  import { loadOrCreateIdentity, publicKeyToBase64Url, signChallenge, type DeviceIdentity } from './device-identity';
4
4
 
5
5
  export interface GatewayInjectPayload {
6
- target: string;
6
+ /** OpenClaw session key or recipient address (maps to `to` in the send frame) */
7
+ to: string;
7
8
  message: string;
8
- metadata?: {
9
- source: 'nats';
10
- eventId: string;
11
- subject: string;
12
- priority: number;
13
- };
9
+ /** Internal tracking metadata — NOT sent to gateway (additionalProperties: false) */
10
+ eventId?: string;
14
11
  }
15
12
 
16
13
  export class GatewayRpcError extends Error {
@@ -266,10 +263,9 @@ export class GatewayClientService extends BaseService implements OnModuleInit, O
266
263
  id,
267
264
  method: 'send',
268
265
  params: {
269
- target: payload.target,
266
+ to: payload.to,
270
267
  message: payload.message,
271
- metadata: payload.metadata,
272
- idempotencyKey: payload.metadata?.eventId ?? String(this.requestId),
268
+ idempotencyKey: payload.eventId ?? String(this.requestId),
273
269
  },
274
270
  });
275
271
  return promise;
@@ -56,7 +56,7 @@ export class LogRepository extends BaseService {
56
56
  .select({ count: sql<number>`count(*)` })
57
57
  .from(executionLogs)
58
58
  .where(this.buildWhereConditions(filters));
59
- return result[0]?.count ?? 0;
59
+ return (result[0] as unknown as { count: number })?.count ?? 0;
60
60
  }
61
61
 
62
62
  async findRecent(limit: number = 20): Promise<DbExecutionLog[]> {
@@ -58,7 +58,7 @@ export class LogService extends BaseService {
58
58
  }
59
59
  }
60
60
 
61
- async logError(entityType: 'route' | 'cron', entityId: string, subject: string, error: unknown): Promise<void> {
61
+ async logError(entityType: 'route' | 'cron' | 'timer', entityId: string, subject: string, error: unknown): Promise<void> {
62
62
  try {
63
63
  const detail = error instanceof Error
64
64
  ? JSON.stringify({ message: error.message, stack: error.stack })
@@ -49,7 +49,7 @@ export class PendingRepository extends BaseService {
49
49
  .select({ count: sql<number>`count(*)` })
50
50
  .from(pendingEvents)
51
51
  .where(isNull(pendingEvents.deliveredAt));
52
- return result[0]?.count ?? 0;
52
+ return (result[0] as unknown as { count: number })?.count ?? 0;
53
53
  }
54
54
 
55
55
  async cleanup(ttlSeconds: number): Promise<number> {
@@ -82,6 +82,6 @@ export class RouterRepository extends BaseService {
82
82
  const result = await this.db
83
83
  .select({ count: sql<number>`count(*)` })
84
84
  .from(eventRoutes);
85
- return result[0]?.count ?? 0;
85
+ return (result[0] as unknown as { count: number })?.count ?? 0;
86
86
  }
87
87
  }