@zilliz/memsearch-opencode 0.3.7 → 0.3.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/index.ts CHANGED
@@ -13,13 +13,15 @@
13
13
 
14
14
  import type { Plugin } from "@opencode-ai/plugin";
15
15
  import { tool } from "@opencode-ai/plugin";
16
- import { execSync, exec, spawnSync } from "node:child_process";
16
+ import { execSync, exec, spawn, spawnSync } from "node:child_process";
17
17
  import {
18
18
  readFileSync,
19
19
  existsSync,
20
20
  mkdirSync,
21
21
  readdirSync,
22
22
  realpathSync,
23
+ unlinkSync,
24
+ writeFileSync,
23
25
  } from "node:fs";
24
26
  import { join, dirname } from "node:path";
25
27
  import { fileURLToPath } from "node:url";
@@ -70,11 +72,48 @@ function deriveCollectionName(projectDir: string): string {
70
72
 
71
73
  /**
72
74
  * Summarize the N most recent daily .md files for cold-start context.
73
- * Extracts headings (## Session, ### turns) and bullet content from each
74
- * file so the agent sees the structure of past days (what sessions existed,
75
- * what topics came up), not just the tail of whichever file is newest.
75
+ * Extracts recent non-empty session sections so empty SessionStart headings
76
+ * do not crowd out useful context.
76
77
  */
77
- function getRecentMemories(
78
+ function recentMemoryPreviewLines(content: string, maxLines: number): string[] {
79
+ const sections: string[][] = [];
80
+ let current: string[] = [];
81
+ let hasBody = false;
82
+
83
+ const flush = () => {
84
+ if (current.length > 0 && hasBody) {
85
+ sections.push(current);
86
+ }
87
+ current = [];
88
+ hasBody = false;
89
+ };
90
+
91
+ for (const rawLine of content.split("\n")) {
92
+ const line = rawLine.trimEnd();
93
+ if (/^##\s/.test(line)) {
94
+ flush();
95
+ current = [line];
96
+ continue;
97
+ }
98
+ if (/^#{3,4}\s/.test(line)) {
99
+ current.push(line);
100
+ continue;
101
+ }
102
+ if (line.startsWith("- ") || line.startsWith("[User]") || line.startsWith("[Assistant]")) {
103
+ current.push(line);
104
+ hasBody = true;
105
+ }
106
+ }
107
+
108
+ flush();
109
+ return sections.flat().slice(-maxLines);
110
+ }
111
+
112
+ export function isDailyJournalFile(file: string): boolean {
113
+ return /^\d{4}-\d{2}-\d{2}\.md$/.test(file);
114
+ }
115
+
116
+ export function getRecentMemories(
78
117
  memDir: string,
79
118
  count = 2,
80
119
  maxLinesPerFile = 30
@@ -82,7 +121,7 @@ function getRecentMemories(
82
121
  if (!existsSync(memDir)) return "";
83
122
 
84
123
  const files = readdirSync(memDir)
85
- .filter((f) => f.endsWith(".md"))
124
+ .filter(isDailyJournalFile)
86
125
  .sort()
87
126
  .slice(-count);
88
127
 
@@ -92,9 +131,7 @@ function getRecentMemories(
92
131
  for (const file of files) {
93
132
  try {
94
133
  const content = readFileSync(join(memDir, file), "utf-8");
95
- const lines = content.split("\n")
96
- .filter((l) => /^#{2,4}\s/.test(l) || l.startsWith("- ") || l.startsWith("[User]") || l.startsWith("[Assistant]"))
97
- .slice(0, maxLinesPerFile);
134
+ const lines = recentMemoryPreviewLines(content, maxLinesPerFile);
98
135
  if (lines.length > 0) {
99
136
  summary.push(`[${file}]`, ...lines);
100
137
  }
@@ -113,6 +150,34 @@ function shellEscape(s: string): string {
113
150
  return s.replace(/'/g, "'\\''");
114
151
  }
115
152
 
153
+ /** Marks the start of memsearch's injected block within a system message. */
154
+ export const MEMSEARCH_SYSTEM_MARKER = "[memsearch] Memory available.";
155
+
156
+ /**
157
+ * Merge memsearch's memory context into an `output.system` array without
158
+ * growing it. Some backends (litellm/vllm serving e.g. Qwen models) reject a
159
+ * multi-entry `output.system` array with "system message must be first", so
160
+ * the memory block is folded into the first entry instead of pushed as a new
161
+ * one. If a memsearch block from a previous transform call is already present
162
+ * (identified by MEMSEARCH_SYSTEM_MARKER), it is replaced in place rather than
163
+ * appended again, so repeated calls against the same output stay idempotent.
164
+ */
165
+ export function mergeSystemMemoryContext(
166
+ system: string[] | undefined,
167
+ memoryText: string
168
+ ): string[] {
169
+ if (!Array.isArray(system) || system.length === 0) {
170
+ return [memoryText];
171
+ }
172
+ const result = [...system];
173
+ const existing = result[0];
174
+ const markerIndex = existing.indexOf(MEMSEARCH_SYSTEM_MARKER);
175
+ const base =
176
+ markerIndex === -1 ? existing : existing.slice(0, markerIndex).replace(/\n+$/, "");
177
+ result[0] = base ? `${base}\n\n${memoryText}` : memoryText;
178
+ return result;
179
+ }
180
+
116
181
  /**
117
182
  * Start the capture daemon as a background process.
118
183
  * The daemon polls OpenCode's SQLite for completed turns and writes to daily .md files.
@@ -122,6 +187,7 @@ function startCaptureDaemon(
122
187
  collectionName: string,
123
188
  memsearchCmd: string
124
189
  ): void {
190
+ const stateDir = join(projectDir, ".memsearch");
125
191
  const pidFile = join(projectDir, ".memsearch", ".capture.pid");
126
192
  const daemonScript = join(PLUGIN_DIR, "scripts", "capture-daemon.py");
127
193
 
@@ -136,21 +202,40 @@ function startCaptureDaemon(
136
202
  return; // Already running
137
203
  } catch {
138
204
  // Process is dead, clean up stale PID file
205
+ try { unlinkSync(pidFile); } catch { /* ignore */ }
139
206
  }
140
207
  }
141
208
  } catch { /* ignore */ }
142
209
  }
143
210
 
144
- // Start daemon in background
145
- exec(
146
- `python3 "${daemonScript}" "${projectDir}" "${collectionName}" ` +
147
- `--memsearch-cmd "${shellEscape(memsearchCmd)}" --poll-interval 10 &`,
148
- {
149
- timeout: 5000,
150
- env: { ...process.env, MEMSEARCH_NO_WATCH: "1" },
151
- },
152
- () => { /* ignore */ }
153
- );
211
+ try {
212
+ mkdirSync(stateDir, { recursive: true });
213
+ const child = spawn(
214
+ "python3",
215
+ [
216
+ daemonScript,
217
+ projectDir,
218
+ collectionName,
219
+ "--memsearch-cmd",
220
+ memsearchCmd,
221
+ "--poll-interval",
222
+ "10",
223
+ "--parent-pid",
224
+ String(process.pid),
225
+ ],
226
+ {
227
+ detached: true,
228
+ stdio: "ignore",
229
+ env: { ...process.env, MEMSEARCH_NO_WATCH: "1" },
230
+ }
231
+ );
232
+ child.unref();
233
+ if (child.pid) {
234
+ try { writeFileSync(pidFile, String(child.pid), "utf-8"); } catch { /* ignore */ }
235
+ }
236
+ } catch {
237
+ // Capture is best-effort; tools still work without the background daemon.
238
+ }
154
239
  }
155
240
 
156
241
  /**
@@ -349,9 +434,10 @@ const MemsearchPlugin: Plugin = async ({ project, directory, worktree }) => {
349
434
  try {
350
435
  const context = getRecentMemories(memoryDir);
351
436
  if (context) {
352
- output.system.push(
353
- `[memsearch] Memory available. You have access to memory_search, memory_get, and memory_transcript tools for recalling past sessions.\n\n${context}`
354
- );
437
+ const memoryText =
438
+ `${MEMSEARCH_SYSTEM_MARKER} You have access to memory_search, ` +
439
+ `memory_get, and memory_transcript tools for recalling past sessions.\n\n${context}`;
440
+ output.system = mergeSystemMemoryContext(output.system, memoryText);
355
441
  }
356
442
  } catch { /* ignore */ }
357
443
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zilliz/memsearch-opencode",
3
- "version": "0.3.7",
3
+ "version": "0.3.11",
4
4
  "description": "memsearch plugin for OpenCode — semantic memory search across sessions",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -23,6 +23,9 @@
23
23
  "semantic-search",
24
24
  "milvus"
25
25
  ],
26
+ "scripts": {
27
+ "test": "node --test"
28
+ },
26
29
  "author": "memsearch contributors",
27
30
  "license": "MIT",
28
31
  "peerDependencies": {
@@ -481,6 +481,21 @@ def wake_maintenance(project_dir: str) -> None:
481
481
  )
482
482
 
483
483
 
484
+ def process_is_alive(pid: int) -> bool:
485
+ """Return whether a process exists without sending it a signal."""
486
+ if pid <= 0:
487
+ return True
488
+ try:
489
+ os.kill(pid, 0)
490
+ except ProcessLookupError:
491
+ return False
492
+ except PermissionError:
493
+ return True
494
+ except OSError:
495
+ return True
496
+ return True
497
+
498
+
484
499
  def get_session_ids(conn: sqlite3.Connection, project_dir: str) -> list[str]:
485
500
  """Find OpenCode sessions that belong to the given project directory."""
486
501
  sessions = conn.execute(
@@ -764,6 +779,7 @@ def main() -> None:
764
779
  parser.add_argument("collection_name", help="Milvus collection name")
765
780
  parser.add_argument("--memsearch-cmd", default="memsearch", help="memsearch command")
766
781
  parser.add_argument("--poll-interval", type=int, default=10, help="Poll interval in seconds")
782
+ parser.add_argument("--parent-pid", type=int, default=0, help="Exit when this parent process is gone")
767
783
  args = parser.parse_args()
768
784
 
769
785
  db_path = get_db_path()
@@ -791,6 +807,9 @@ def main() -> None:
791
807
  tail_turn_cache: dict[str, TailTurnObservation] = {}
792
808
 
793
809
  while True:
810
+ if args.parent_pid and not process_is_alive(args.parent_pid):
811
+ cleanup()
812
+
794
813
  any_new = False
795
814
  conn = None
796
815
  try:
@@ -820,6 +839,8 @@ def main() -> None:
820
839
  if conn is not None:
821
840
  conn.close()
822
841
 
842
+ if args.parent_pid and not process_is_alive(args.parent_pid):
843
+ cleanup()
823
844
  time.sleep(args.poll_interval)
824
845
 
825
846
 
@@ -15,7 +15,7 @@ When this skill is triggered, inspect the user's request text. If there is no co
15
15
 
16
16
  - Empty request or "check": diagnose current MemSearch setup.
17
17
  - "Show/get setting": read the requested resolved/global/project value.
18
- - "Set/enable/disable/change": make a safe project-scoped change after confirming ambiguous choices.
18
+ - "Set/enable/disable/change": choose global vs project scope explicitly; use global config for trusted plugin automation/provider/prompt/endpoint settings and project config only for allowlisted local indexing knobs.
19
19
  - "Not capturing/search empty/no memory": troubleshoot files, config, and index health.
20
20
  - "Use OpenAI/Gemini/Anthropic/native/model": configure provider routing.
21
21
  - "PROJECT.md/USER.md/profile/review": configure advanced maintenance.
@@ -114,14 +114,33 @@ Config is resolved from built-in defaults, global config, project config, env re
114
114
 
115
115
  Use `memsearch config list --resolved` for effective behavior, `--global` for global overrides, and `--project` for repository-specific overrides.
116
116
 
117
+ Since v0.4.11, project-local `.memsearch.toml` is restricted before it is merged.
118
+ Only these low-risk local indexing keys are honored from project config:
119
+
120
+ - `milvus.collection`
121
+ - `embedding.batch_size`
122
+ - `chunking.max_chunk_size`
123
+ - `chunking.overlap_lines`
124
+ - `watch.debounce_ms`
125
+
126
+ Trusted settings are ignored or rejected in project config. Put these in global
127
+ config (`~/.memsearch/config.toml`) or pass explicit CLI flags instead:
128
+
129
+ - provider/model/API endpoint/API key settings
130
+ - `[llm]` and `[llm.providers.*]`
131
+ - `[prompts]`
132
+ - plugin automation such as `plugins.opencode.project_review.enabled`,
133
+ `plugins.opencode.user_profile.enabled`, and
134
+ `plugins.opencode.memory_to_skill.enabled`
135
+
117
136
  Default recommendation:
118
137
 
119
138
  - Put reusable defaults in global config so users do not repeat setup in every project.
120
- - Put only project-specific overrides in project config.
121
- - Use global config for named LLM providers, common model choices, normal input/output defaults, and shared prompt defaults.
122
- - Use project config for one repo's enable/disable switches, unusual output files, special intervals, or repo-specific providers.
139
+ - Put only allowlisted local indexing overrides in project config.
140
+ - Use global config for named LLM providers, common model choices, plugin enable/disable switches, task intervals, install paths, and shared prompt defaults.
141
+ - If the user wants advanced maintenance enabled for all projects, set the plugin keys globally. The default relative `input_dir` / `output_file` values still resolve inside each current project.
123
142
 
124
- Maintenance `input_dir` and `output_file` may be relative even when configured globally. They are resolved from the current project directory at runtime. For custom prompt paths, prefer absolute paths in global config and relative paths in project config.
143
+ Maintenance `input_dir` and `output_file` may be relative even when configured globally. They are resolved from the current project directory at runtime, so a global `output_file = ".memsearch/PROJECT.md"` writes to each project's own `.memsearch/PROJECT.md`. For custom prompt paths, prefer absolute paths in global config; project prompt paths are not trusted.
125
144
 
126
145
  OpenCode plugin keys:
127
146
 
@@ -193,7 +212,7 @@ If advanced maintenance or `memory_to_skill` seems silent, check
193
212
  `.memsearch/.maintenance-state.json` for `<plugin>.<task>.last_error` and
194
213
  `last_failed_at`; background hook errors may not surface in the chat.
195
214
 
196
- Before enabling advanced maintenance, ask which provider to use, whether the default 24-hour interval is acceptable, and whether `.memsearch/PROJECT.md` / `.memsearch/USER.md` are acceptable output files.
215
+ Before enabling advanced maintenance, ask which provider to use, whether the default 24-hour interval is acceptable, whether `.memsearch/PROJECT.md` / `.memsearch/USER.md` are acceptable output files, and whether the user wants the enablement global. Do not write plugin automation keys with `--project`; v0.4.11+ project config ignores or rejects them.
197
216
 
198
217
  Prompt overrides:
199
218
 
@@ -207,8 +226,8 @@ memory_to_skill = ""
207
226
 
208
227
  Empty prompt paths mean use the built-in MemSearch prompts. Custom prompt files may use `{{AGENT_NAME}}`, `{{TASK_NAME}}`, `{{PROJECT_DIR}}`, `{{INPUT_DIR}}`, and `{{OUTPUT_FILE}}`; the runner appends existing output, recent journals, and digest automatically.
209
228
 
210
- Use `memsearch config set` for changes. After changing anything, show the command, the resolved value, and whether a new session is needed.
229
+ Use `memsearch config set` for changes. For trusted keys such as `plugins.*`, `[llm.providers.*]`, `[prompts]`, `embedding.provider`, or `milvus.uri`, set global config by omitting `--project`. Use `--project` only for allowlisted local indexing keys. After changing anything, show the command, the resolved value, and whether a new session is needed.
211
230
 
212
231
  MemSearch TOML changes are read lazily by the CLI, capture daemon, and maintenance runner, so values such as `plugins.opencode.summarize.*`, `plugins.opencode.project_review.*`, `plugins.opencode.user_profile.*`, `[llm.providers.*]`, `[prompts]`, `milvus.*`, and `embedding.*` usually apply on the next capture, recall, index, or maintenance invocation. Restart OpenCode after `opencode.json` or plugin package changes; if capture behavior still looks stale after TOML edits, restart OpenCode or the capture daemon. In final diagnostic/change summaries, make clear that this is MemSearch memory configuration, not OpenCode's own memory/config system.
213
232
 
214
- When useful, remind the user that they can either continue using this `memory-config` skill for guided configuration, or manually run `memsearch config init` for global interactive setup, `memsearch config init --project` for project interactive setup, and `memsearch config set/get/list` for direct CLI changes.
233
+ When useful, remind the user that they can either continue using this `memory-config` skill for guided configuration, or manually run `memsearch config init` for global interactive setup, `memsearch config init --project` for allowlisted project indexing setup, and `memsearch config set/get/list` for direct CLI changes.
@@ -90,14 +90,19 @@ The background pass mines automatically when enabled, starting from the summarie
90
90
 
91
91
  ```bash
92
92
  memsearch config get plugins.opencode.memory_to_skill.enabled 2>/dev/null || echo "false"
93
- # enable the background pass (do not enable silently)
94
- memsearch config set plugins.opencode.memory_to_skill.enabled true --project
93
+ # enable the background pass globally (do not enable silently)
94
+ memsearch config set plugins.opencode.memory_to_skill.enabled true
95
95
  # how eagerly history-mining distils (default 3; lower = more eager)
96
- memsearch config set plugins.opencode.memory_to_skill.min_occurrences 3 --project
96
+ memsearch config set plugins.opencode.memory_to_skill.min_occurrences 3
97
97
  # pre-set install targets (otherwise you are asked at install time)
98
- memsearch config set plugins.opencode.memory_to_skill.paths '[".agents/skills"]' --project
98
+ memsearch config set plugins.opencode.memory_to_skill.paths '[".agents/skills"]'
99
99
  ```
100
100
 
101
+ Since v0.4.11, project-local `.memsearch.toml` accepts only allowlisted local
102
+ indexing keys. Do not use `--project` for `plugins.*` settings such as
103
+ `memory_to_skill.enabled`, `min_occurrences`, or `paths`; put them in global
104
+ config instead.
105
+
101
106
  Note: `enabled` only gates the **background** (session-end) pass. The explicit
102
107
  commands above (`skills add`, `skills install`) always work, and you can mine history (C) directly.
103
108