switchroom 0.12.27 → 0.12.29

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.
Files changed (48) hide show
  1. package/dist/cli/switchroom.js +4 -2
  2. package/package.json +2 -1
  3. package/telegram-plugin/dist/gateway/gateway.js +113 -7
  4. package/telegram-plugin/gateway/gateway.ts +52 -9
  5. package/telegram-plugin/gateway/prefix-warmup.ts +123 -0
  6. package/telegram-plugin/stderr-timestamps.ts +106 -0
  7. package/telegram-plugin/tests/prefix-warmup.test.ts +175 -0
  8. package/telegram-plugin/tests/stderr-timestamps.test.ts +113 -0
  9. package/vendor/hindsight-memory/.claude-plugin/plugin.json +8 -0
  10. package/vendor/hindsight-memory/CHANGELOG.md +32 -0
  11. package/vendor/hindsight-memory/LICENSE +21 -0
  12. package/vendor/hindsight-memory/README.md +329 -0
  13. package/vendor/hindsight-memory/hooks/hooks.json +49 -0
  14. package/vendor/hindsight-memory/scripts/drain_pending.py +190 -0
  15. package/vendor/hindsight-memory/scripts/lib/__init__.py +0 -0
  16. package/vendor/hindsight-memory/scripts/lib/bank.py +122 -0
  17. package/vendor/hindsight-memory/scripts/lib/client.py +204 -0
  18. package/vendor/hindsight-memory/scripts/lib/config.py +180 -0
  19. package/vendor/hindsight-memory/scripts/lib/content.py +493 -0
  20. package/vendor/hindsight-memory/scripts/lib/daemon.py +334 -0
  21. package/vendor/hindsight-memory/scripts/lib/directives.py +119 -0
  22. package/vendor/hindsight-memory/scripts/lib/gateway_ipc.py +126 -0
  23. package/vendor/hindsight-memory/scripts/lib/llm.py +146 -0
  24. package/vendor/hindsight-memory/scripts/lib/pending.py +218 -0
  25. package/vendor/hindsight-memory/scripts/lib/state.py +196 -0
  26. package/vendor/hindsight-memory/scripts/recall.py +873 -0
  27. package/vendor/hindsight-memory/scripts/retain.py +286 -0
  28. package/vendor/hindsight-memory/scripts/session_end.py +122 -0
  29. package/vendor/hindsight-memory/scripts/session_start.py +76 -0
  30. package/vendor/hindsight-memory/scripts/setup_hooks.py +115 -0
  31. package/vendor/hindsight-memory/scripts/tests/__init__.py +0 -0
  32. package/vendor/hindsight-memory/scripts/tests/test_directives.py +211 -0
  33. package/vendor/hindsight-memory/scripts/tests/test_gateway_ipc.py +205 -0
  34. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +621 -0
  35. package/vendor/hindsight-memory/settings.json +37 -0
  36. package/vendor/hindsight-memory/skills/setup.md +24 -0
  37. package/vendor/hindsight-memory/tests/conftest.py +94 -0
  38. package/vendor/hindsight-memory/tests/test_bank.py +142 -0
  39. package/vendor/hindsight-memory/tests/test_client.py +232 -0
  40. package/vendor/hindsight-memory/tests/test_config.py +128 -0
  41. package/vendor/hindsight-memory/tests/test_content.py +471 -0
  42. package/vendor/hindsight-memory/tests/test_drain_pending.py +192 -0
  43. package/vendor/hindsight-memory/tests/test_hooks.py +808 -0
  44. package/vendor/hindsight-memory/tests/test_manifest.py +14 -0
  45. package/vendor/hindsight-memory/tests/test_pending.py +152 -0
  46. package/vendor/hindsight-memory/tests/test_recall_exit_codes.py +325 -0
  47. package/vendor/hindsight-memory/tests/test_session_end_pending.py +205 -0
  48. package/vendor/hindsight-memory/tests/test_state.py +125 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Tests for the prefix-cache warmup module.
3
+ *
4
+ * Per cold-start TTFO RFC Option A: fire a synthetic inbound on
5
+ * bridge-up so Anthropic's prefix cache is warm by the user's next
6
+ * real message. These tests pin: env gate, cooldown, missing-target
7
+ * skip, message shape (meta.source="warmup", correct text), and
8
+ * cooldown debounce across multiple bridge reconnects.
9
+ */
10
+
11
+ import { describe, expect, it, beforeEach, vi } from 'vitest'
12
+ import {
13
+ __resetForTests,
14
+ maybeFireWarmup,
15
+ WARMUP_TEXT,
16
+ } from '../gateway/prefix-warmup'
17
+ import type { WarmupCtx } from '../gateway/prefix-warmup'
18
+
19
+ beforeEach(() => {
20
+ __resetForTests()
21
+ delete process.env.SWITCHROOM_PREFIX_WARMUP
22
+ })
23
+
24
+ function makeCtx(overrides?: Partial<WarmupCtx>): {
25
+ ctx: WarmupCtx
26
+ send: ReturnType<typeof vi.fn>
27
+ logs: string[]
28
+ } {
29
+ const send = vi.fn()
30
+ const logs: string[] = []
31
+ const ctx: WarmupCtx = {
32
+ selfAgent: 'test-agent',
33
+ client: { send, agentName: 'test-agent', id: 'c1', isAlive: () => true, lastHeartbeat: 0, close: () => {} } as never,
34
+ resolveBootTarget: () => ({ chatId: '12345', threadId: undefined }),
35
+ log: (l: string) => logs.push(l),
36
+ now: () => 1_000_000,
37
+ ...overrides,
38
+ }
39
+ return { ctx, send, logs }
40
+ }
41
+
42
+ describe('maybeFireWarmup — env gate', () => {
43
+ it('skipped when SWITCHROOM_PREFIX_WARMUP is unset', () => {
44
+ const { ctx, send } = makeCtx()
45
+ expect(maybeFireWarmup(ctx)).toBe(false)
46
+ expect(send).not.toHaveBeenCalled()
47
+ })
48
+
49
+ it('skipped when SWITCHROOM_PREFIX_WARMUP is 0', () => {
50
+ process.env.SWITCHROOM_PREFIX_WARMUP = '0'
51
+ const { ctx, send } = makeCtx()
52
+ expect(maybeFireWarmup(ctx)).toBe(false)
53
+ expect(send).not.toHaveBeenCalled()
54
+ })
55
+
56
+ it('fires when SWITCHROOM_PREFIX_WARMUP=1', () => {
57
+ process.env.SWITCHROOM_PREFIX_WARMUP = '1'
58
+ const { ctx, send } = makeCtx()
59
+ expect(maybeFireWarmup(ctx)).toBe(true)
60
+ expect(send).toHaveBeenCalledTimes(1)
61
+ })
62
+ })
63
+
64
+ describe('maybeFireWarmup — message shape', () => {
65
+ beforeEach(() => {
66
+ process.env.SWITCHROOM_PREFIX_WARMUP = '1'
67
+ })
68
+
69
+ it('tags meta.source="warmup"', () => {
70
+ const { ctx, send } = makeCtx()
71
+ maybeFireWarmup(ctx)
72
+ const msg = send.mock.calls[0][0]
73
+ expect(msg.meta.source).toBe('warmup')
74
+ })
75
+
76
+ it('uses synthetic messageId=0 and userId=0', () => {
77
+ const { ctx, send } = makeCtx()
78
+ maybeFireWarmup(ctx)
79
+ const msg = send.mock.calls[0][0]
80
+ expect(msg.messageId).toBe(0)
81
+ expect(msg.userId).toBe(0)
82
+ expect(msg.user).toBe('switchroom-warmup')
83
+ })
84
+
85
+ it('text carries the NO_REPLY instruction', () => {
86
+ const { ctx, send } = makeCtx()
87
+ maybeFireWarmup(ctx)
88
+ const msg = send.mock.calls[0][0]
89
+ expect(msg.text).toBe(WARMUP_TEXT)
90
+ expect(msg.text).toContain('__WARMUP_PING__')
91
+ expect(msg.text).toContain('NO_REPLY')
92
+ })
93
+
94
+ it('routes to the resolved boot chat', () => {
95
+ process.env.SWITCHROOM_PREFIX_WARMUP = '1'
96
+ const { ctx, send } = makeCtx({
97
+ resolveBootTarget: () => ({ chatId: 'CHAT-9', threadId: 42 }),
98
+ })
99
+ maybeFireWarmup(ctx)
100
+ const msg = send.mock.calls[0][0]
101
+ expect(msg.chatId).toBe('CHAT-9')
102
+ expect(msg.threadId).toBe(42)
103
+ })
104
+
105
+ it('omits threadId when boot target has none', () => {
106
+ const { ctx, send } = makeCtx()
107
+ maybeFireWarmup(ctx)
108
+ const msg = send.mock.calls[0][0]
109
+ expect(msg.threadId).toBeUndefined()
110
+ })
111
+ })
112
+
113
+ describe('maybeFireWarmup — cooldown', () => {
114
+ beforeEach(() => {
115
+ process.env.SWITCHROOM_PREFIX_WARMUP = '1'
116
+ })
117
+
118
+ it('second call within cooldown is skipped', () => {
119
+ const { ctx, send, logs } = makeCtx()
120
+ expect(maybeFireWarmup(ctx)).toBe(true)
121
+ // Same now() — well within cooldown.
122
+ expect(maybeFireWarmup(ctx)).toBe(false)
123
+ expect(send).toHaveBeenCalledTimes(1)
124
+ expect(logs.some((l) => l.includes('reason=cooldown'))).toBe(true)
125
+ })
126
+
127
+ it('call after cooldown elapses fires again', () => {
128
+ let t = 1_000_000
129
+ const ctx = makeCtx({ now: () => t }).ctx
130
+ expect(maybeFireWarmup(ctx)).toBe(true)
131
+ t += 5 * 60_000 + 1 // just past cooldown
132
+ expect(maybeFireWarmup(ctx)).toBe(true)
133
+ })
134
+
135
+ it('cooldown is per-agent', () => {
136
+ const a = makeCtx({ selfAgent: 'agent-a' })
137
+ const b = makeCtx({ selfAgent: 'agent-b' })
138
+ expect(maybeFireWarmup(a.ctx)).toBe(true)
139
+ expect(maybeFireWarmup(b.ctx)).toBe(true)
140
+ expect(maybeFireWarmup(a.ctx)).toBe(false) // a in cooldown
141
+ expect(maybeFireWarmup(b.ctx)).toBe(false) // b in cooldown
142
+ })
143
+ })
144
+
145
+ describe('maybeFireWarmup — missing boot target', () => {
146
+ beforeEach(() => {
147
+ process.env.SWITCHROOM_PREFIX_WARMUP = '1'
148
+ })
149
+
150
+ it('skipped when no boot chat resolves', () => {
151
+ const { ctx, send, logs } = makeCtx({ resolveBootTarget: () => null })
152
+ expect(maybeFireWarmup(ctx)).toBe(false)
153
+ expect(send).not.toHaveBeenCalled()
154
+ expect(logs.some((l) => l.includes('reason=no-boot-chat-target'))).toBe(true)
155
+ })
156
+ })
157
+
158
+ describe('maybeFireWarmup — send error handling', () => {
159
+ beforeEach(() => {
160
+ process.env.SWITCHROOM_PREFIX_WARMUP = '1'
161
+ })
162
+
163
+ it('caught send throw returns false and does not mark cooldown', () => {
164
+ const send = vi.fn(() => {
165
+ throw new Error('boom')
166
+ })
167
+ const ctx = makeCtx({
168
+ client: { send } as never,
169
+ }).ctx
170
+ expect(maybeFireWarmup(ctx)).toBe(false)
171
+ // Cooldown NOT recorded — next call should attempt again.
172
+ expect(maybeFireWarmup(ctx)).toBe(false) // still throws
173
+ expect(send).toHaveBeenCalledTimes(2)
174
+ })
175
+ })
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Tests for the line-buffered stderr timestamp wrapper.
3
+ */
4
+
5
+ import { describe, expect, it, beforeEach } from 'vitest'
6
+ import {
7
+ __resetForTests,
8
+ __getPartialBufferForTests,
9
+ installStderrTimestamps,
10
+ stampLines,
11
+ } from '../stderr-timestamps'
12
+
13
+ beforeEach(() => {
14
+ __resetForTests()
15
+ })
16
+
17
+ describe('stampLines (pure)', () => {
18
+ it('stamps a single complete line', () => {
19
+ const t = () => '2026-05-20T18:00:00.000Z'
20
+ expect(stampLines('hello world\n', t)).toBe('[2026-05-20T18:00:00.000Z] hello world\n')
21
+ })
22
+
23
+ it('returns empty for empty input', () => {
24
+ const t = () => '2026-05-20T18:00:00.000Z'
25
+ expect(stampLines('', t)).toBe('')
26
+ })
27
+
28
+ it('stamps multiple lines in one chunk', () => {
29
+ const t = () => 'T'
30
+ expect(stampLines('a\nb\nc\n', t)).toBe('[T] a\n[T] b\n[T] c\n')
31
+ })
32
+
33
+ it('buffers a partial line until the newline arrives', () => {
34
+ const t = () => 'T'
35
+ // Partial chunk: no newline → no output, content buffered.
36
+ expect(stampLines('partial', t)).toBe('')
37
+ expect(__getPartialBufferForTests()).toBe('partial')
38
+ // Newline finishes the buffered line.
39
+ expect(stampLines('-end\n', t)).toBe('[T] partial-end\n')
40
+ expect(__getPartialBufferForTests()).toBe('')
41
+ })
42
+
43
+ it('handles partial + complete in one chunk', () => {
44
+ const t = () => 'T'
45
+ expect(stampLines('line1\nstart-of-2', t)).toBe('[T] line1\n')
46
+ expect(__getPartialBufferForTests()).toBe('start-of-2')
47
+ expect(stampLines('-end\n', t)).toBe('[T] start-of-2-end\n')
48
+ })
49
+
50
+ it('handles chunk that ends exactly on a newline', () => {
51
+ const t = () => 'T'
52
+ expect(stampLines('exact\n', t)).toBe('[T] exact\n')
53
+ expect(__getPartialBufferForTests()).toBe('')
54
+ })
55
+
56
+ it('handles a multi-newline chunk with trailing partial', () => {
57
+ const t = () => 'T'
58
+ expect(stampLines('a\nb\nc', t)).toBe('[T] a\n[T] b\n')
59
+ expect(__getPartialBufferForTests()).toBe('c')
60
+ })
61
+ })
62
+
63
+ describe('installStderrTimestamps (integration)', () => {
64
+ it('kill switch SWITCHROOM_LOG_TIMESTAMPS=0 prevents install', () => {
65
+ const env: NodeJS.ProcessEnv = { SWITCHROOM_LOG_TIMESTAMPS: '0' }
66
+ const result = installStderrTimestamps(env)
67
+ expect(result).toBe(false)
68
+ })
69
+
70
+ it('default install returns true', () => {
71
+ const result = installStderrTimestamps({})
72
+ expect(result).toBe(true)
73
+ })
74
+
75
+ it('second install is a no-op (idempotent)', () => {
76
+ expect(installStderrTimestamps({})).toBe(true)
77
+ expect(installStderrTimestamps({})).toBe(true)
78
+ })
79
+
80
+ it('wrapped write emits ISO-timestamped lines', () => {
81
+ installStderrTimestamps({})
82
+ // Capture by replacing the write FUNCTION the wrapper forwards to.
83
+ // The wrapper calls the ORIGINAL bound `process.stderr.write`. We
84
+ // can validate end-to-end by writing and then reading back from a
85
+ // captured forward — but easier: pipe the wrapped output to a
86
+ // string by hooking process.stderr.write a second time.
87
+ const captured: string[] = []
88
+ const prev = process.stderr.write
89
+ process.stderr.write = ((chunk: string | Uint8Array) => {
90
+ const text = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')
91
+ captured.push(text)
92
+ return true
93
+ }) as typeof process.stderr.write
94
+ try {
95
+ // The OUTER wrapper (installStderrTimestamps) was installed first,
96
+ // then we layered the capture on top. Calling process.stderr.write
97
+ // hits the CAPTURE, which means we'd capture the un-stamped text.
98
+ // Reverse: call the STAMPED wrapper directly by invoking through
99
+ // the installed function reference. Easiest path: just exercise
100
+ // stampLines (already pure-tested above) and rely on the install
101
+ // returning true to know we wired the wrapper.
102
+ //
103
+ // This test is best-effort end-to-end; the pure tests are the
104
+ // load-bearing contract.
105
+ process.stderr.write('test\n')
106
+ } finally {
107
+ process.stderr.write = prev
108
+ }
109
+ // The capture above is positioned ABOVE the stamper, so what it
110
+ // captures is what callers see. We at least verify it didn't crash.
111
+ expect(captured.length).toBeGreaterThan(0)
112
+ })
113
+ })
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "hindsight-memory",
3
+ "description": "Automatic long-term memory for Claude Code via Hindsight. Recalls relevant memories before each prompt and retains conversation transcripts after each response.",
4
+ "version": "0.4.0",
5
+ "author": {"name": "Hindsight Team", "url": "https://vectorize.io/hindsight"},
6
+ "license": "MIT",
7
+ "keywords": ["memory", "hindsight", "recall", "retain"]
8
+ }
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+
7
+ - `{user_id}` template variable for `retainTags` and `retainMetadata`, resolved
8
+ from the `HINDSIGHT_USER_ID` env var (empty string if unset). Enables
9
+ machine-independent per-user memory scoping without hardcoding user ids in
10
+ `settings.json`.
11
+
12
+ ### Changed
13
+
14
+ - Tags that resolve to an empty namespace content (e.g. `"user:"` when
15
+ `HINDSIGHT_USER_ID` is unset) are now dropped from retain requests. Previously
16
+ such tags were sent as-is. Tags without `:` are unaffected.
17
+
18
+ ## [0.1.0] - 2025-03-23
19
+
20
+ ### Added
21
+ - Initial release: Claude Code plugin for Hindsight long-term memory
22
+ - Auto-recall on every user prompt via `UserPromptSubmit` hook — injects relevant memories as `additionalContext`
23
+ - Auto-retain after every response via async `Stop` hook — extracts and stores conversation transcript
24
+ - Session lifecycle hooks (`SessionStart` health check, `SessionEnd` daemon cleanup)
25
+ - Three connection modes: external API, auto-managed local daemon (`uvx hindsight-embed`), existing local server
26
+ - Dynamic bank IDs with configurable granularity (`agent`, `project`, `session`, `channel`, `user`)
27
+ - Channel-agnostic: works with Claude Code Channels (Telegram, Discord, Slack) and interactive sessions
28
+ - Zero pip dependencies — pure Python stdlib (`urllib`, `fcntl`, `subprocess`)
29
+ - 34 configuration options via `settings.json` with env var overrides
30
+ - LLM auto-detection from `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `GROQ_API_KEY`
31
+ - Chunked retention with sliding window (`retainEveryNTurns` + `retainOverlapTurns`)
32
+ - Memory tag stripping to prevent retain feedback loops
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Vectorize AI, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,329 @@
1
+ # Hindsight Memory Plugin for Claude Code
2
+
3
+ Biomimetic long-term memory for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) using [Hindsight](https://vectorize.io/hindsight). Automatically captures conversations and intelligently recalls relevant context — a complete port of [`hindsight-openclaw`](../openclaw/) adapted to Claude Code's hook-based plugin architecture.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # 1. Add the Hindsight marketplace and install the plugin
9
+ claude plugin marketplace add vectorize-io/hindsight
10
+ claude plugin install hindsight-memory
11
+
12
+ # 2. Configure your LLM provider for memory extraction
13
+ # Option A: OpenAI (auto-detected)
14
+ export OPENAI_API_KEY="sk-your-key"
15
+
16
+ # Option B: Anthropic (auto-detected)
17
+ export ANTHROPIC_API_KEY="your-key"
18
+
19
+ # Option C: No API key needed (uses Claude Code's own model — personal/local use only)
20
+ # See: https://vectorize.io/hindsight/developer/models#claude-code-setup-claude-promax
21
+ export HINDSIGHT_LLM_PROVIDER=claude-code
22
+
23
+ # Option D: Connect to an external Hindsight server instead of running locally
24
+ mkdir -p ~/.hindsight
25
+ echo '{"hindsightApiUrl": "https://your-hindsight-server.com"}' > ~/.hindsight/claude-code.json
26
+
27
+ # 3. Start Claude Code — the plugin activates automatically
28
+ claude
29
+ ```
30
+
31
+ That's it! The plugin will automatically start capturing and recalling memories.
32
+
33
+ ## Features
34
+
35
+ - **Auto-recall** — on every user prompt, queries Hindsight for relevant memories and injects them as context (invisible to the chat transcript, visible to Claude)
36
+ - **Auto-retain** — after every response (or every N turns), extracts and retains conversation content to Hindsight for long-term storage
37
+ - **Daemon management** — can auto-start/stop `hindsight-embed` locally or connect to an external Hindsight server
38
+ - **Dynamic bank IDs** — supports per-agent, per-project, or per-session memory isolation
39
+ - **Channel-agnostic** — works with Claude Code Channels (Telegram, Discord, Slack) or interactive sessions
40
+ - **Zero dependencies** — pure Python stdlib, no pip install required
41
+
42
+ ## Architecture
43
+
44
+ The plugin uses all four Claude Code hook events:
45
+
46
+ | Hook | Event | Purpose |
47
+ |------|-------|---------|
48
+ | `session_start.py` | `SessionStart` | Health check — verify Hindsight is reachable |
49
+ | `recall.py` | `UserPromptSubmit` | **Auto-recall** — query memories, inject as `additionalContext` |
50
+ | `retain.py` | `Stop` | **Auto-retain** — extract transcript, POST to Hindsight (async) |
51
+ | `session_end.py` | `SessionEnd` | Cleanup — stop auto-managed daemon if started |
52
+
53
+ ### Library Modules
54
+
55
+ | Module | Purpose |
56
+ |--------|---------|
57
+ | `lib/client.py` | Hindsight REST API client (stdlib `urllib`) |
58
+ | `lib/config.py` | Configuration loader (settings.json + env overrides) |
59
+ | `lib/daemon.py` | `hindsight-embed` daemon lifecycle (start/stop/health) |
60
+ | `lib/bank.py` | Bank ID derivation + mission management |
61
+ | `lib/content.py` | Content processing (transcript parsing, memory formatting, tag stripping) |
62
+ | `lib/state.py` | File-based state persistence with `fcntl` locking |
63
+ | `lib/llm.py` | LLM provider auto-detection for daemon mode |
64
+
65
+ ### How Recall Works
66
+
67
+ 1. User sends a prompt → `UserPromptSubmit` hook fires
68
+ 2. Plugin resolves Hindsight API URL (external, local, or auto-start daemon)
69
+ 3. Derives bank ID (static or dynamic from project context)
70
+ 4. Composes query from current prompt + optional prior turns
71
+ 5. Calls Hindsight recall API
72
+ 6. Formats memories into `<hindsight_memories>` block
73
+ 7. Outputs via `hookSpecificOutput.additionalContext` — Claude sees it, user doesn't
74
+
75
+ ### How Retain Works
76
+
77
+ 1. Claude responds → `Stop` hook fires (async, non-blocking)
78
+ 2. Reads conversation transcript from Claude Code's JSONL file
79
+ 3. Applies chunked retention logic (every N turns with sliding window)
80
+ 4. Strips `<hindsight_memories>` tags to prevent feedback loops
81
+ 5. Extracts text from channel messages (Telegram reply tool calls, etc.)
82
+ 6. POSTs formatted transcript to Hindsight retain API
83
+
84
+ ## Connection Modes
85
+
86
+ The plugin supports three connection modes, matching the Openclaw plugin:
87
+
88
+ ### 1. External API (recommended for production)
89
+
90
+ Connect to a running Hindsight server (cloud or self-hosted). No local LLM needed — the server handles fact extraction.
91
+
92
+ ```json
93
+ {
94
+ "hindsightApiUrl": "https://your-hindsight-server.com",
95
+ "hindsightApiToken": "your-token"
96
+ }
97
+ ```
98
+
99
+ ### 2. Local Daemon (auto-managed)
100
+
101
+ The plugin automatically starts and stops `hindsight-embed` via `uvx`. Requires an LLM provider API key for local fact extraction.
102
+
103
+ ```json
104
+ {
105
+ "hindsightApiUrl": "",
106
+ "apiPort": 9077
107
+ }
108
+ ```
109
+
110
+ Set an LLM provider:
111
+ ```bash
112
+ export OPENAI_API_KEY="sk-your-key" # Auto-detected, uses gpt-4o-mini
113
+ # or
114
+ export ANTHROPIC_API_KEY="your-key" # Auto-detected, uses claude-3-5-haiku
115
+ ```
116
+
117
+ ### 3. Existing Local Server
118
+
119
+ If you already have `hindsight-embed` running, leave `hindsightApiUrl` empty and set `apiPort` to match your server's port. The plugin will detect it automatically.
120
+
121
+ ## Configuration
122
+
123
+ All settings live in `~/.hindsight/claude-code.json`. Every setting can also be overridden via environment variables. The plugin ships with sensible defaults — you only need to configure what you want to change.
124
+
125
+ **Loading order** (later entries win):
126
+ 1. Built-in defaults (hardcoded in the plugin)
127
+ 2. Plugin `settings.json` (ships with the plugin, at `CLAUDE_PLUGIN_ROOT/settings.json`)
128
+ 3. User config (`~/.hindsight/claude-code.json` — recommended for your overrides)
129
+ 4. Environment variables
130
+
131
+ ---
132
+
133
+ ### Connection & Daemon
134
+
135
+ These settings control how the plugin connects to the Hindsight API.
136
+
137
+ | Setting | Env Var | Default | Description |
138
+ |---------|---------|---------|-------------|
139
+ | `hindsightApiUrl` | `HINDSIGHT_API_URL` | `""` (empty) | URL of an external Hindsight API server. When empty, the plugin uses a local daemon instead. |
140
+ | `hindsightApiToken` | `HINDSIGHT_API_TOKEN` | `null` | Authentication token for the external API. Only needed when `hindsightApiUrl` is set. |
141
+ | `apiPort` | `HINDSIGHT_API_PORT` | `9077` | Port used by the local `hindsight-embed` daemon. Change this if you run multiple instances or have a port conflict. |
142
+ | `daemonIdleTimeout` | `HINDSIGHT_DAEMON_IDLE_TIMEOUT` | `0` | Seconds of inactivity before the local daemon shuts itself down. `0` means the daemon stays running until the session ends. |
143
+ | `embedVersion` | `HINDSIGHT_EMBED_VERSION` | `"latest"` | Which version of `hindsight-embed` to install via `uvx`. Pin to a specific version (e.g. `"0.5.2"`) for reproducibility. |
144
+ | `embedPackagePath` | `HINDSIGHT_EMBED_PACKAGE_PATH` | `null` | Local filesystem path to a `hindsight-embed` checkout. When set, the plugin runs from this path instead of installing via `uvx`. Useful for development. |
145
+
146
+ ---
147
+
148
+ ### LLM Provider (local daemon only)
149
+
150
+ These settings configure which LLM the local daemon uses for fact extraction. They are **ignored** when connecting to an external API (the server uses its own LLM configuration).
151
+
152
+ | Setting | Env Var | Default | Description |
153
+ |---------|---------|---------|-------------|
154
+ | `llmProvider` | `HINDSIGHT_LLM_PROVIDER` | auto-detect | Which LLM provider to use. Supported values: `openai`, `anthropic`, `gemini`, `groq`, `ollama`, `openai-codex`, `claude-code`. When omitted, the plugin auto-detects by checking for API key env vars in order: `OPENAI_API_KEY` → `ANTHROPIC_API_KEY` → `GEMINI_API_KEY` → `GROQ_API_KEY`. |
155
+ | `llmModel` | `HINDSIGHT_LLM_MODEL` | provider default | Override the default model for the chosen provider (e.g. `"gpt-4o"`, `"claude-sonnet-4-20250514"`). When omitted, the Hindsight API picks a sensible default for each provider. |
156
+ | `llmApiKeyEnv` | — | provider standard | Name of the environment variable that holds the API key. Normally auto-detected (e.g. `OPENAI_API_KEY` for the `openai` provider). Set this only if your key is in a non-standard env var. |
157
+
158
+ ---
159
+
160
+ ### Memory Bank
161
+
162
+ A **bank** is an isolated memory store — like a separate "brain." These settings control which bank the plugin reads from and writes to.
163
+
164
+ | Setting | Env Var | Default | Description |
165
+ |---------|---------|---------|-------------|
166
+ | `bankId` | `HINDSIGHT_BANK_ID` | `"claude_code"` | The bank ID to use when `dynamicBankId` is `false`. All sessions share this single bank. |
167
+ | `bankMission` | `HINDSIGHT_BANK_MISSION` | generic assistant prompt | A short description of the agent's identity and purpose. Sent to Hindsight when creating or updating the bank, and used during recall to contextualize results. |
168
+ | `retainMission` | — | extraction prompt | Instructions for the fact extraction LLM — tells it *what* to extract from conversations (e.g. "Extract technical decisions and user preferences"). |
169
+ | `dynamicBankId` | `HINDSIGHT_DYNAMIC_BANK_ID` | `false` | When `true`, the plugin derives a unique bank ID from context fields (see `dynamicBankGranularity`), giving each combination its own isolated memory. |
170
+ | `dynamicBankGranularity` | — | `["agent", "project"]` | Which context fields to combine when building a dynamic bank ID. Available fields: `agent` (agent name), `project` (working directory), `session` (session ID), `channel` (channel ID), `user` (user ID). |
171
+ | `bankIdPrefix` | — | `""` | A string prepended to all bank IDs — both static and dynamic. Useful for namespacing (e.g. `"prod"` or `"staging"`). |
172
+ | `agentName` | `HINDSIGHT_AGENT_NAME` | `"claude-code"` | Name used for the `agent` field in dynamic bank ID derivation. |
173
+
174
+ ---
175
+
176
+ ### Auto-Recall
177
+
178
+ Auto-recall runs on every user prompt. It queries Hindsight for relevant memories and injects them into Claude's context as invisible `additionalContext` (the user doesn't see them in the chat transcript).
179
+
180
+ | Setting | Env Var | Default | Description |
181
+ |---------|---------|---------|-------------|
182
+ | `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Master switch for auto-recall. Set to `false` to disable memory retrieval entirely. |
183
+ | `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Controls how hard Hindsight searches for memories. `"low"` = fast, fewer strategies; `"mid"` = balanced; `"high"` = thorough, slower. Affects latency directly. |
184
+ | `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `1024` | Maximum number of tokens in the recalled memory block. Lower values reduce context usage but may truncate relevant memories. |
185
+ | `recallTypes` | — | `["world", "experience"]` | Which memory types to retrieve. `"world"` = general facts; `"experience"` = personal experiences; `"observation"` = raw observations. |
186
+ | `recallContextTurns` | `HINDSIGHT_RECALL_CONTEXT_TURNS` | `1` | How many prior conversation turns to include when composing the recall query. `1` = only the latest user message; higher values give more context but may dilute the query. |
187
+ | `recallMaxQueryChars` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | `800` | Maximum character length of the query sent to Hindsight. Longer queries are truncated. |
188
+ | `recallRoles` | — | `["user", "assistant"]` | Which message roles to include when building the recall query from prior turns. |
189
+ | `recallPromptPreamble` | — | built-in string | Text placed above the recalled memories in the injected context block. Customize this to change how Claude interprets the memories. |
190
+
191
+ ---
192
+
193
+ ### Auto-Retain
194
+
195
+ Auto-retain runs after Claude responds. It extracts the conversation transcript and sends it to Hindsight for long-term storage and fact extraction.
196
+
197
+ | Setting | Env Var | Default | Description |
198
+ |---------|---------|---------|-------------|
199
+ | `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Master switch for auto-retain. Set to `false` to disable memory storage entirely. |
200
+ | `retainMode` | `HINDSIGHT_RETAIN_MODE` | `"full-session"` | Retention strategy. `"full-session"` sends the full conversation transcript (with chunking). |
201
+ | `retainEveryNTurns` | — | `10` | How often to retain. `1` = every turn; `10` = every 10th turn. Higher values reduce API calls but delay memory capture. Values > 1 enable **chunked retention** with a sliding window. |
202
+ | `retainOverlapTurns` | — | `2` | When chunked retention fires, this many extra turns from the previous chunk are included for continuity. Total window size = `retainEveryNTurns + retainOverlapTurns`. |
203
+ | `retainRoles` | — | `["user", "assistant"]` | Which message roles to include in the retained transcript. |
204
+ | `retainToolCalls` | — | `true` | Whether to include tool calls (function invocations and results) in the retained transcript. Captures structured actions like file reads, searches, and code edits. |
205
+ | `retainTags` | — | `["{session_id}"]` | Tags attached to the retained document. Supports template placeholders: `{session_id}`, `{bank_id}`, `{timestamp}`, and `{user_id}` (resolved from `HINDSIGHT_USER_ID` env var; empty string if unset). Tags whose resolved form ends in an empty namespace part (e.g. `"user:"` when `HINDSIGHT_USER_ID` is unset) are dropped from the outgoing request. See [Template variables](#template-variables-for-retaintags-and-retainmetadata) below. |
206
+ | `retainMetadata` | — | `{}` | Arbitrary key-value metadata attached to the retained document. Same template placeholders as `retainTags`. |
207
+ | `retainContext` | — | `"claude-code"` | A label attached to retained memories identifying their source. Useful when multiple integrations write to the same bank. |
208
+
209
+ #### Template variables for `retainTags` and `retainMetadata`
210
+
211
+ | Variable | Source |
212
+ |----------|--------|
213
+ | `{session_id}` | Current Claude Code session ID |
214
+ | `{bank_id}` | Resolved bank ID (per `bankGranularity`) |
215
+ | `{timestamp}` | ISO 8601 UTC at retain time |
216
+ | `{user_id}` | Value of `HINDSIGHT_USER_ID` env var (empty string if unset) |
217
+
218
+ ##### Example: per-user memory scoping
219
+
220
+ ```json
221
+ {
222
+ "retainTags": ["user:{user_id}", "session:{session_id}"]
223
+ }
224
+ ```
225
+
226
+ Set `HINDSIGHT_USER_ID=<opaque-user-id>` in your shell profile (`.zshrc`,
227
+ `.bashrc`, etc.). If the env var is unset, the `user:` tag is dropped from the
228
+ outgoing retain request and the rest of the tags are sent as-is — so the same
229
+ `settings.json` works across machines whether you've set the env var or not.
230
+
231
+ Downstream, `recall` can filter by `tags=["user:alice"]` to isolate memories
232
+ authored by a specific user from a shared bank.
233
+
234
+ ---
235
+
236
+ ### Debug
237
+
238
+ | Setting | Env Var | Default | Description |
239
+ |---------|---------|---------|-------------|
240
+ | `debug` | `HINDSIGHT_DEBUG` | `false` | Enable verbose logging to stderr. All log lines are prefixed with `[Hindsight]`. Useful for diagnosing connection issues, recall/retain behavior, and bank ID derivation. |
241
+
242
+ ## Claude Code Channels
243
+
244
+ With [Claude Code Channels](https://docs.anthropic.com/en/docs/claude-code), Claude Code can operate as a persistent background agent connected to Telegram, Discord, Slack, and other messaging platforms. This plugin gives Channel-based agents the same long-term memory that `hindsight-openclaw` provides for Openclaw agents.
245
+
246
+ For Channel agents, set these environment variables in your Channel configuration:
247
+
248
+ ```bash
249
+ # Per-channel/per-user memory isolation
250
+ export HINDSIGHT_CHANNEL_ID="telegram-group-12345"
251
+ export HINDSIGHT_USER_ID="user-67890"
252
+ ```
253
+
254
+ And enable dynamic bank IDs:
255
+
256
+ ```json
257
+ {
258
+ "dynamicBankId": true,
259
+ "dynamicBankGranularity": ["agent", "channel", "user"]
260
+ }
261
+ ```
262
+
263
+ ## Troubleshooting
264
+
265
+ ### Plugin not activating
266
+
267
+ - Verify installation: check that `.claude-plugin/plugin.json` exists in the installed plugin directory
268
+ - Check Claude Code logs for `[Hindsight]` messages (enable `"debug": true` in `~/.hindsight/claude-code.json`)
269
+
270
+ ### Recall returning no memories
271
+
272
+ - Verify the Hindsight server is reachable: `curl http://localhost:9077/health`
273
+ - Check that the bank has retained content: memories need at least one retain cycle
274
+ - Try increasing `recallBudget` to `"mid"` or `"high"`
275
+
276
+ ### Daemon not starting
277
+
278
+ - Ensure `uvx` is installed: `pip install uv` or `brew install uv`
279
+ - Check that an LLM API key is set (required for local daemon)
280
+ - Review daemon logs: `tail -f ~/.hindsight/profiles/claude-code.log`
281
+ - Try starting manually: `uvx hindsight-embed@latest daemon --profile claude-code start`
282
+
283
+ ### High latency on recall
284
+
285
+ - The recall hook has a 12-second timeout. If Hindsight is slow:
286
+ - Use `recallBudget: "low"` (fewer retrieval strategies)
287
+ - Reduce `recallMaxTokens`
288
+ - Consider using an external API with a faster server
289
+
290
+ ### State file issues
291
+
292
+ - State is stored in `$CLAUDE_PLUGIN_DATA/state/`
293
+ - To reset: delete the `state/` directory
294
+ - Turn counts, bank missions, and daemon state are tracked here
295
+
296
+ ## Development
297
+
298
+ To test local changes to `hindsight-embed`:
299
+
300
+ ```json
301
+ {
302
+ "embedPackagePath": "/path/to/hindsight-embed"
303
+ }
304
+ ```
305
+
306
+ The plugin will use `uv run --directory <path> hindsight-embed` instead of `uvx hindsight-embed@latest`.
307
+
308
+ To view daemon logs:
309
+
310
+ ```bash
311
+ # Check daemon status
312
+ uvx hindsight-embed@latest daemon --profile claude-code status
313
+
314
+ # View logs
315
+ tail -f ~/.hindsight/profiles/claude-code.log
316
+
317
+ # List profiles
318
+ uvx hindsight-embed@latest profile list
319
+ ```
320
+
321
+ ## Links
322
+
323
+ - [Hindsight Documentation](https://vectorize.io/hindsight)
324
+ - [Claude Code Documentation](https://docs.anthropic.com/en/docs/claude-code)
325
+ - [GitHub Repository](https://github.com/vectorize-io/hindsight)
326
+
327
+ ## License
328
+
329
+ MIT