@parkgogogo/openclaw-reflection 0.1.5 → 0.1.6

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # OpenClaw Reflection
1
+ <h1 align="center">OpenClaw Reflection</h1>
2
2
 
3
3
  <p align="center">
4
4
  <img src="./assets/openclaw-reflection-logo.png" alt="OpenClaw Reflection logo" width="180" />
@@ -6,12 +6,14 @@
6
6
 
7
7
  <p align="center"><strong>Make OpenClaw's native memory system sharper without replacing it.</strong></p>
8
8
 
9
- ![OpenClaw Plugin](https://img.shields.io/badge/OpenClaw-Plugin-111111?style=flat-square)
10
- ![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178c6?style=flat-square)
11
- ![memory_gate 18 cases](https://img.shields.io/badge/memory_gate-18%20benchmark%20cases-2ea043?style=flat-square)
12
- ![write_guardian 14 cases](https://img.shields.io/badge/write_guardian-14%20benchmark%20cases-2ea043?style=flat-square)
9
+ <p align="center">
10
+ <img alt="OpenClaw Plugin" src="https://img.shields.io/badge/OpenClaw-Plugin-111111?style=flat-square" />
11
+ <img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-5.x-3178c6?style=flat-square" />
12
+ <img alt="memory_gate 18 cases" src="https://img.shields.io/badge/memory_gate-18%20benchmark%20cases-2ea043?style=flat-square" />
13
+ <img alt="write_guardian 14 cases" src="https://img.shields.io/badge/write_guardian-14%20benchmark%20cases-2ea043?style=flat-square" />
14
+ </p>
13
15
 
14
- Chinese version: [README.zh-CN.md](./README.zh-CN.md)
16
+ <p align="center"><a href="./README.zh-CN.md">中文文档</a></p>
15
17
 
16
18
  OpenClaw Reflection is an additive layer on top of OpenClaw's built-in Markdown memory system. It captures message flow, keeps thread noise out of long-term memory, writes durable knowledge into the same human-readable memory files OpenClaw already uses, and periodically consolidates them so your agent gets sharper over time instead of messier.
17
19
 
@@ -112,6 +114,7 @@ Once the gateway restarts, Reflection will begin listening to `message_received`
112
114
  - Reflection now writes an independent write_guardian audit log to:
113
115
  - `<workspaceDir>/.openclaw-reflection/write-guardian.log.jsonl`
114
116
  - When `logLevel` is `debug`, Reflection also overwrites `logs/debug.json` with the latest raw `message_received` callback payload.
117
+ - When `write_guardian` successfully writes durable memory, Reflection reacts to the triggering user message with `📝`.
115
118
  - Register command: `reflections`
116
119
  - Returns the most recent 10 write_guardian behaviors (written/refused/failed/skipped), including decision, target file, and reason.
117
120
 
package/README.zh-CN.md CHANGED
@@ -1,4 +1,4 @@
1
- # OpenClaw Reflection
1
+ <h1 align="center">OpenClaw Reflection</h1>
2
2
 
3
3
  <p align="center">
4
4
  <img src="./assets/openclaw-reflection-logo.png" alt="OpenClaw Reflection logo" width="180" />
@@ -8,10 +8,12 @@
8
8
 
9
9
  英文版: [README.md](./README.md)
10
10
 
11
- ![OpenClaw Plugin](https://img.shields.io/badge/OpenClaw-Plugin-111111?style=flat-square)
12
- ![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178c6?style=flat-square)
13
- ![memory_gate 18 cases](https://img.shields.io/badge/memory_gate-18%20benchmark%20cases-2ea043?style=flat-square)
14
- ![write_guardian 14 cases](https://img.shields.io/badge/write_guardian-14%20benchmark%20cases-2ea043?style=flat-square)
11
+ <p align="center">
12
+ <img alt="OpenClaw Plugin" src="https://img.shields.io/badge/OpenClaw-Plugin-111111?style=flat-square" />
13
+ <img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-5.x-3178c6?style=flat-square" />
14
+ <img alt="memory_gate 18 cases" src="https://img.shields.io/badge/memory_gate-18%20benchmark%20cases-2ea043?style=flat-square" />
15
+ <img alt="write_guardian 14 cases" src="https://img.shields.io/badge/write_guardian-14%20benchmark%20cases-2ea043?style=flat-square" />
16
+ </p>
15
17
 
16
18
  OpenClaw Reflection 是叠加在 OpenClaw 原生 Markdown memory 之上的一层增强插件。它负责监听消息流,过滤线程噪音,把真正长期有效的信息写回 OpenClaw 的核心记忆文件,并定期整理这些文件,避免长期使用后越记越乱。
17
19
 
@@ -107,6 +109,7 @@ Gateway 重启后,Reflection 就会开始监听 `message_received` 和 `before
107
109
  - Reflection 现在会给 write_guardian 单独写一份审计日志:
108
110
  - `<workspaceDir>/.openclaw-reflection/write-guardian.log.jsonl`
109
111
  - 当 `logLevel` 为 `debug` 时,Reflection 还会把最近一次 `message_received` callback 的原始 payload 覆盖写入 `logs/debug.json`。
112
+ - 当 `write_guardian` 成功写入长期记忆时,Reflection 会给触发这次写入的用户消息补一个 `📝` reaction。
110
113
  - 注册命令:`reflections`
111
114
  - 返回最近 10 条 write_guardian 行为(written/refused/failed/skipped),包含 decision、目标文件和原因。
112
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parkgogogo/openclaw-reflection",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "OpenClaw plugin that enhances native Markdown memory with filtering, curation, and consolidation",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  handleBeforeMessageWrite,
21
21
  handleMessageReceived,
22
22
  } from "./message-handler.js";
23
+ import { OpenClawMessageReactionService } from "./message-reaction.js";
23
24
  import { SessionBufferManager } from "./session-manager.js";
24
25
  import type {
25
26
  LLMService,
@@ -275,6 +276,7 @@ export default function activate(api: PluginAPI): void {
275
276
  let memoryGate: MemoryGateAnalyzer | undefined;
276
277
  let writeGuardian: WriteGuardian | undefined;
277
278
  let writeGuardianAuditLog: WriteGuardianAuditLog | undefined;
279
+ const reactionService = new OpenClawMessageReactionService(logger);
278
280
 
279
281
  if (config.memoryGate.enabled && llmService) {
280
282
  memoryGate = new MemoryGateAnalyzer(llmService, logger);
@@ -342,7 +344,8 @@ export default function activate(api: PluginAPI): void {
342
344
  context,
343
345
  memoryGate,
344
346
  writeGuardian,
345
- config.memoryGate.windowSize
347
+ config.memoryGate.windowSize,
348
+ reactionService
346
349
  );
347
350
  } else {
348
351
  logger.warn("PluginLifecycle", "Callback skipped: buffer manager missing", {
@@ -1,5 +1,12 @@
1
1
  import type { SessionBufferManager } from "./session-manager.js";
2
- import type { Logger, ReflectionMessage } from "./types.js";
2
+ import type {
3
+ Logger,
4
+ MessageHookContext,
5
+ MessageReactionInput,
6
+ MessageReactionService,
7
+ MessageReceivedHookEvent,
8
+ ReflectionMessage,
9
+ } from "./types.js";
3
10
  import { MemoryGateAnalyzer, type MemoryGateOutput } from "./memory-gate/index.js";
4
11
  import { WriteGuardian } from "./write-guardian/index.js";
5
12
  import { ulid } from "ulid";
@@ -26,19 +33,6 @@ interface MessageEvent {
26
33
  channelId?: string;
27
34
  }
28
35
 
29
- interface MessageHookContext {
30
- channelId?: string;
31
- accountId?: string;
32
- conversationId?: string;
33
- }
34
-
35
- interface MessageReceivedHookEvent {
36
- from?: string;
37
- content?: string;
38
- timestamp?: number;
39
- metadata?: Record<string, unknown>;
40
- }
41
-
42
36
  interface BeforeMessageWriteEvent {
43
37
  message?: {
44
38
  role?: string;
@@ -480,6 +474,7 @@ function createReflectionMessage(
480
474
  messageId: event.message?.id,
481
475
  from: event.from,
482
476
  to: event.to,
477
+ accountId: event.accountId,
483
478
  success: event.success,
484
479
  };
485
480
 
@@ -507,6 +502,80 @@ function findLatestMessageByRole(
507
502
  return "";
508
503
  }
509
504
 
505
+ function findLatestUserReactionTarget(
506
+ messages: ReflectionMessage[]
507
+ ): MessageReactionInput | null {
508
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
509
+ const message = messages[index];
510
+ if (message.role !== "user") {
511
+ continue;
512
+ }
513
+
514
+ const messageId = getNonEmptyString(message.metadata?.messageId);
515
+ const target = getNonEmptyString(message.metadata?.to);
516
+ const channelId = getNonEmptyString(message.channelId);
517
+
518
+ if (!messageId || !target || !channelId) {
519
+ continue;
520
+ }
521
+
522
+ return {
523
+ channelId,
524
+ accountId: getNonEmptyString(message.metadata?.accountId),
525
+ target,
526
+ messageId,
527
+ emoji: "📝",
528
+ };
529
+ }
530
+
531
+ return null;
532
+ }
533
+
534
+ function isReactionService(
535
+ value: unknown
536
+ ): value is MessageReactionService {
537
+ return (
538
+ typeof value === "object" &&
539
+ value !== null &&
540
+ "reactToMessage" in value &&
541
+ typeof (value as { reactToMessage?: unknown }).reactToMessage === "function"
542
+ );
543
+ }
544
+
545
+ function resolveHandlerOptions(
546
+ memoryGateWindowSizeOrReactionService: number | MessageReactionService | undefined,
547
+ reactionServiceOrWindowSize: MessageReactionService | number | undefined
548
+ ): {
549
+ memoryGateWindowSize: number;
550
+ reactionService?: MessageReactionService;
551
+ } {
552
+ const defaultOptions = {
553
+ memoryGateWindowSize: DEFAULT_MEMORY_GATE_WINDOW_SIZE,
554
+ reactionService: undefined,
555
+ } as const;
556
+
557
+ if (typeof memoryGateWindowSizeOrReactionService === "number") {
558
+ return {
559
+ memoryGateWindowSize: memoryGateWindowSizeOrReactionService,
560
+ reactionService: isReactionService(reactionServiceOrWindowSize)
561
+ ? reactionServiceOrWindowSize
562
+ : undefined,
563
+ };
564
+ }
565
+
566
+ if (isReactionService(memoryGateWindowSizeOrReactionService)) {
567
+ return {
568
+ memoryGateWindowSize:
569
+ typeof reactionServiceOrWindowSize === "number"
570
+ ? reactionServiceOrWindowSize
571
+ : DEFAULT_MEMORY_GATE_WINDOW_SIZE,
572
+ reactionService: memoryGateWindowSizeOrReactionService,
573
+ };
574
+ }
575
+
576
+ return defaultOptions;
577
+ }
578
+
510
579
  function isUpdateDecision(
511
580
  decision: MemoryGateOutput["decision"]
512
581
  ): decision is
@@ -530,7 +599,8 @@ async function triggerMemoryGate(
530
599
  memoryGate: MemoryGateAnalyzer,
531
600
  writeGuardian: WriteGuardian | undefined,
532
601
  logger: Logger,
533
- memoryGateWindowSize: number
602
+ memoryGateWindowSize: number,
603
+ reactionService?: MessageReactionService
534
604
  ): Promise<void> {
535
605
  const normalizedWindowSize = Number.isInteger(memoryGateWindowSize)
536
606
  ? Math.max(memoryGateWindowSize, 1)
@@ -578,6 +648,11 @@ async function triggerMemoryGate(
578
648
  },
579
649
  sessionKey
580
650
  );
651
+
652
+ const reactionTarget = findLatestUserReactionTarget(sessionMessages);
653
+ if (reactionService && reactionTarget) {
654
+ await reactionService.reactToMessage(reactionTarget);
655
+ }
581
656
  } else if (writeResult.status === "refused") {
582
657
  logger.info(
583
658
  "MessageHandler",
@@ -698,8 +773,14 @@ function handleAgentMessage(
698
773
  hookContext?: unknown,
699
774
  memoryGate?: MemoryGateAnalyzer,
700
775
  writeGuardian?: WriteGuardian,
701
- memoryGateWindowSize = DEFAULT_MEMORY_GATE_WINDOW_SIZE
776
+ memoryGateWindowSizeOrReactionService?: number | MessageReactionService,
777
+ reactionServiceOrWindowSize?: MessageReactionService | number
702
778
  ): void {
779
+ const { memoryGateWindowSize, reactionService } = resolveHandlerOptions(
780
+ memoryGateWindowSizeOrReactionService,
781
+ reactionServiceOrWindowSize
782
+ );
783
+
703
784
  const normalizedEvent = normalizeSentEvent(event, hookContext);
704
785
  logHookPayloadDebug(
705
786
  logger,
@@ -773,7 +854,8 @@ function handleAgentMessage(
773
854
  memoryGate,
774
855
  writeGuardian,
775
856
  logger,
776
- memoryGateWindowSize
857
+ memoryGateWindowSize,
858
+ reactionService
777
859
  )
778
860
  );
779
861
  }
@@ -786,7 +868,8 @@ export function handleMessageSent(
786
868
  hookContext?: unknown,
787
869
  memoryGate?: MemoryGateAnalyzer,
788
870
  writeGuardian?: WriteGuardian,
789
- memoryGateWindowSize = DEFAULT_MEMORY_GATE_WINDOW_SIZE
871
+ memoryGateWindowSizeOrReactionService?: number | MessageReactionService,
872
+ reactionServiceOrWindowSize?: MessageReactionService | number
790
873
  ): void {
791
874
  handleAgentMessage(
792
875
  event,
@@ -796,7 +879,8 @@ export function handleMessageSent(
796
879
  hookContext,
797
880
  memoryGate,
798
881
  writeGuardian,
799
- memoryGateWindowSize
882
+ memoryGateWindowSizeOrReactionService,
883
+ reactionServiceOrWindowSize
800
884
  );
801
885
  }
802
886
 
@@ -807,7 +891,8 @@ export function handleBeforeMessageWrite(
807
891
  hookContext?: unknown,
808
892
  memoryGate?: MemoryGateAnalyzer,
809
893
  writeGuardian?: WriteGuardian,
810
- memoryGateWindowSize = DEFAULT_MEMORY_GATE_WINDOW_SIZE
894
+ memoryGateWindowSizeOrReactionService?: number | MessageReactionService,
895
+ reactionServiceOrWindowSize?: MessageReactionService | number
811
896
  ): void {
812
897
  const normalizedEvent = normalizeBeforeMessageWriteEvent(event, hookContext);
813
898
  logHookPayloadDebug(
@@ -834,7 +919,8 @@ export function handleBeforeMessageWrite(
834
919
  hookContext,
835
920
  memoryGate,
836
921
  writeGuardian,
837
- memoryGateWindowSize
922
+ memoryGateWindowSizeOrReactionService,
923
+ reactionServiceOrWindowSize
838
924
  );
839
925
  }
840
926
 
@@ -0,0 +1,96 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import type { Logger, MessageReactionInput, MessageReactionService } from "./types.js";
4
+
5
+ const execFileAsync = promisify(execFile);
6
+
7
+ type CommandRunner = (command: string, args: string[]) => Promise<void>;
8
+
9
+ async function defaultCommandRunner(command: string, args: string[]): Promise<void> {
10
+ await execFileAsync(command, args);
11
+ }
12
+
13
+ function getNonEmptyString(value: string | undefined): string | undefined {
14
+ if (typeof value !== "string") {
15
+ return undefined;
16
+ }
17
+
18
+ const trimmed = value.trim();
19
+ return trimmed.length > 0 ? trimmed : undefined;
20
+ }
21
+
22
+ function getErrorMessage(error: unknown): string {
23
+ if (error instanceof Error) {
24
+ return error.message;
25
+ }
26
+
27
+ return String(error);
28
+ }
29
+
30
+ export class OpenClawMessageReactionService implements MessageReactionService {
31
+ private readonly logger: Logger;
32
+ private readonly commandRunner: CommandRunner;
33
+
34
+ constructor(
35
+ logger: Logger,
36
+ commandRunner: CommandRunner = defaultCommandRunner
37
+ ) {
38
+ this.logger = logger;
39
+ this.commandRunner = commandRunner;
40
+ }
41
+
42
+ async reactToMessage(input: MessageReactionInput): Promise<boolean> {
43
+ const channelId = getNonEmptyString(input.channelId);
44
+ const accountId = getNonEmptyString(input.accountId);
45
+ const target = getNonEmptyString(input.target);
46
+ const messageId = getNonEmptyString(input.messageId);
47
+ const emoji = getNonEmptyString(input.emoji);
48
+
49
+ if (!channelId || !target || !messageId || !emoji) {
50
+ this.logger.warn("MessageReaction", "Skipped reaction with incomplete target", {
51
+ channelId,
52
+ accountId,
53
+ target,
54
+ messageId,
55
+ emoji,
56
+ });
57
+ return false;
58
+ }
59
+
60
+ const args = [
61
+ "message",
62
+ "react",
63
+ "--channel",
64
+ channelId,
65
+ ...(accountId ? ["--account", accountId] : []),
66
+ "--target",
67
+ target,
68
+ "--message-id",
69
+ messageId,
70
+ "--emoji",
71
+ emoji,
72
+ ];
73
+
74
+ try {
75
+ await this.commandRunner("openclaw", args);
76
+ this.logger.info("MessageReaction", "Applied reaction to message", {
77
+ channelId,
78
+ accountId,
79
+ target,
80
+ messageId,
81
+ emoji,
82
+ });
83
+ return true;
84
+ } catch (error) {
85
+ this.logger.warn("MessageReaction", "Failed to apply reaction to message", {
86
+ channelId,
87
+ accountId,
88
+ target,
89
+ messageId,
90
+ emoji,
91
+ reason: getErrorMessage(error),
92
+ });
93
+ return false;
94
+ }
95
+ }
96
+ }
package/src/types.ts CHANGED
@@ -38,10 +38,49 @@ export interface ReflectionMessage {
38
38
  from?: string;
39
39
  to?: string;
40
40
  messageId?: string;
41
+ accountId?: string;
41
42
  success?: boolean;
42
43
  };
43
44
  }
44
45
 
46
+ export interface MessageHookContext {
47
+ channelId?: string;
48
+ accountId?: string;
49
+ conversationId?: string;
50
+ }
51
+
52
+ export interface MessageReceivedHookMetadata {
53
+ to?: string;
54
+ provider?: string;
55
+ surface?: string;
56
+ originatingChannel?: string;
57
+ originatingTo?: string;
58
+ messageId?: string;
59
+ senderId?: string;
60
+ senderName?: string;
61
+ senderUsername?: string;
62
+ guildId?: string;
63
+ }
64
+
65
+ export interface MessageReceivedHookEvent {
66
+ from?: string;
67
+ content?: string;
68
+ timestamp?: number;
69
+ metadata?: MessageReceivedHookMetadata;
70
+ }
71
+
72
+ export interface MessageReactionInput {
73
+ channelId: string;
74
+ target: string;
75
+ messageId: string;
76
+ emoji: string;
77
+ accountId?: string;
78
+ }
79
+
80
+ export interface MessageReactionService {
81
+ reactToMessage(input: MessageReactionInput): Promise<boolean>;
82
+ }
83
+
45
84
  export interface LogEntry {
46
85
  timestamp: string;
47
86
  level: LogLevel;