opencode-memoir 2.1.1 → 3.0.0

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
@@ -2,7 +2,8 @@
2
2
 
3
3
  OpenCode plugin for [Memoir](https://github.com/zhangfengcdt/memoir): git-versioned, taxonomy-structured memory for coding agents.
4
4
 
5
- Dynamically loads the `memoir-mcp` MCP server.
5
+ Launches the globally installed `memoir-mcp` console script and registers it as
6
+ a project-scoped remote MCP server.
6
7
 
7
8
  ## Install
8
9
 
@@ -29,6 +30,24 @@ OpenCode downloads and resolves the package from npm automatically. To pin a ver
29
30
  1. Install the plugin (see above)
30
31
  2. Start coding — the agent can use `memoir_memoir_recall`, `memoir_memoir_remember`, `memoir_memoir_get` MCP tools automatically
31
32
 
33
+ Automatic capture works without additional OpenCode flags. By default, capture
34
+ runs through the regular foreground subagent path for compatibility with older
35
+ OpenCode installations.
36
+
37
+ To run capture as a native background task, enable OpenCode's experimental
38
+ background-subagent support before starting OpenCode:
39
+
40
+ ```bash
41
+ OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true opencode
42
+ ```
43
+
44
+ With this flag, capture no longer blocks the parent session while the memoir
45
+ subagent extracts and stores durable facts.
46
+
47
+ OpenCode's umbrella `OPENCODE_EXPERIMENTAL=true` also enables background
48
+ subagents unless `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` explicitly
49
+ overrides it. The plugin mirrors that precedence.
50
+
32
51
  ## Store configuration
33
52
 
34
53
  The plugin delegates store path resolution to `memoir-mcp`. Override via `MEMOIR_STORE` env var or `store` plugin option:
@@ -46,25 +65,43 @@ The plugin delegates store path resolution to `memoir-mcp`. Override via `MEMOIR
46
65
  All optional:
47
66
 
48
67
  | Variable | Effect |
49
- |---|---|
68
+ | --- | --- |
50
69
  | `MEMOIR_STORE` | Override store path (passed to memoir-mcp as `--store`) |
51
- | `MEMOIR_DEBUG=1` | Emit diagnostic logs to stderr (prefixed `[memoir]`) |
52
- | `MEMOIR_AUTO_SAVE=1` | Auto-save session marker on dispose (default: disabled) |
53
- | `MEMOIR_REMINDER_INTERVAL=N` | Periodic save/recall reminder every N messages (default: 5, 0 to disable) |
70
+ | `MEMOIR_DEBUG=1` | Add verbose diagnostics and error stacks to the configured Memoir log; without it, normal lifecycle and concise error messages are still logged |
71
+ | `MEMOIR_LOG` | Log destination: unset uses `$XDG_STATE_HOME/opencode/memoir-plugin-YYYY-MM-DD.log`; `stderr` enables live stderr; any other value is an explicit file path |
72
+ | `MEMOIR_AUTO_SAVE` | Captures the previous completed turn when the next real user message arrives. **Enabled by default**; set `=0` to disable |
73
+ | `MEMOIR_AGENT_MODEL` | Model for the `memoir` subagent, as `provider/model`. Falls back to `small_model` → `model` → openCode default |
74
+ | `MEMOIR_CAPTURE_MIN_CHARS` | Local pre-filter; only transcripts at least this long are captured (default: 16, `0` = capture everything) |
75
+ | `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` | Optional OpenCode feature flag. Runs capture as a native background job; explicit `false` overrides the umbrella experimental flag |
54
76
 
55
77
  ## Hooks
56
78
 
57
79
  | Hook | Purpose |
58
- |---|---|
59
- | `config` | Registers `memoir-mcp` as a dynamic MCP server; adds `/memoir:onboard` slash command |
80
+ | --- | --- |
81
+ | `config` | Registers the `memoir` subagent, one project-scoped `memoir-mcp` remote MCP server, and the `/memoir:onboard` slash command |
60
82
  | `shell.env` | Injects `MEMOIR_STORE` into shell environment |
61
- | `chat.message` | Increments message counter; auto-matches memoir branch to current git branch |
62
- | `experimental.chat.system.transform` | Startup hint (once/session); periodic save/recall reminder |
63
- | `dispose` | Optionally saves session marker; clears all pending state |
83
+ | `chat.message` | Captures the previous completed turn, tracks real parent messages, and auto-matches the memoir branch; waits for `promptAsync` to accept the visible capture task while ignoring synthetic and memoir-child messages |
84
+ | `tool.execute.before` | Marks memoir's `task` invocation with `background: true` when OpenCode background subagents are enabled |
85
+ | `tool.execute.after` + `event` | Tracks foreground completion and known background child idle/error events so checkout does not overlap an active capture |
86
+ | `experimental.chat.system.transform` | Compact status after real parent messages + startup hint and proactive recall once per session |
87
+ | `dispose` | Saves session markers when `MEMOIR_AUTO_SAVE=1` is explicit, closes the project MCP process, and clears instance state |
64
88
 
65
89
  ## How it works
66
90
 
67
- Instead of wrapping the `memoir` CLI and re-implementing tools in TypeScript, this plugin registers `memoir-mcp` as a dynamic MCP server. OpenCode starts the server via the `memoir-mcp` command, and all memoir tools (`memoir_memoir_recall`, `memoir_memoir_remember`, `memoir_memoir_get`, etc.) are available natively to the LLM.
91
+ Instead of wrapping the `memoir` CLI and re-implementing tools in TypeScript, this plugin registers `memoir-mcp` as a **remote** MCP server — one HTTP process per plugin/project instance. All memoir tools (`memoir_memoir_recall`, `memoir_memoir_remember`, `memoir_memoir_get`, etc.) are available natively to the main LLM.
92
+
93
+ Capturing is done by a dedicated visible, collapsible `memoir` subagent. It can use the dynamic `memoir_*` tool namespace except for the store-global `memoir_memoir_checkout`; branch checkout remains owned by the plugin so a subagent cannot move the shared store. Every non-Memoir tool remains denied. The capture task includes the live MCP tool names and descriptions so a small local model does not have to infer the catalog. Deterministic settings keep it suitable for that model. On each real `chat.message`, the plugin captures the previous completed turn: the incoming user message has not entered the transcript yet. This one-turn delay prevents capture activity from triggering another capture. Capture is submitted as a supported `SubtaskPartInput` through `promptAsync`; the hook waits for OpenCode's immediate `204 No Content` acceptance, not for subagent execution. When native background subagents are enabled, `tool.execute.before` adds `background: true` before `TaskTool.execute` starts the child.
94
+
95
+ The subagent is instructed to finish with a compact report of confirmed writes (`Captured N memories`, taxonomy paths, and brief reasons), or a single reason why nothing was captured. OpenCode returns that report to the parent agent through the normal task result. The prompt requires the report to reflect actual tool outcomes rather than intended writes.
96
+
97
+ After each real parent-session message, every model call in that turn receives a compact status from `memoir_status`, such as `[memoir] fix6 · memory available (31 memories)`. Synthetic notifications and memoir child activity do not update it, and the status does not instruct the model to perform recall.
98
+
99
+ - With `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`, the plugin adds `background: true` in `tool.execute.before`. OpenCode runs the child through its native `BackgroundJob` path and immediately releases the parent session.
100
+ - `OPENCODE_EXPERIMENTAL=true` has the same effect unless the dedicated background-subagent flag is explicitly false, matching OpenCode's native precedence.
101
+ - Without either enabling flag, the plugin does not add `background`; OpenCode runs the same capture task through the original foreground subagent path. Memory capture remains available, but the parent session may wait for it to finish.
102
+ - Before changing the shared memoir branch, the plugin waits for active foreground tasks (`tool.execute.after`) and native background tasks (the known child session's terminal idle/error event). A timeout defers checkout instead of moving the store underneath a running capture.
103
+
104
+ At session start, `experimental.chat.system.transform` injects a recall hint plus a `memoir_summarize` snapshot so the agent begins with prior context.
68
105
 
69
106
  ## Development
70
107
 
@@ -76,7 +113,7 @@ Instead of wrapping the `memoir` CLI and re-implementing tools in TypeScript, th
76
113
  ### Setup
77
114
 
78
115
  ```bash
79
- npm install
116
+ make install # install dependencies and Git hooks
80
117
  npm run build # typecheck + emit dist
81
118
  npm test # run the test suite
82
119
  ```
@@ -84,11 +121,17 @@ npm test # run the test suite
84
121
  ### Source layout
85
122
 
86
123
  | File | Responsibility |
87
- |---|---|
124
+ | --- | --- |
88
125
  | `src/index.ts` | Plugin entry: MCP registration + all hooks + dispose |
89
- | `src/store.ts` | Store path derivation, branch auto-match, `callMemoir` CLI helper |
90
- | `src/memory-saver.ts` | Per-session message counter for periodic reminders |
91
- | `src/debug.ts` | Conditional stderr logger (`MEMOIR_DEBUG=1`) |
126
+ | `src/mcp-client.ts` | Project-scoped MCP process/client lifecycle and tool calls |
127
+ | `src/capture.ts` | Transcript extraction, filtering, task construction, and capture dispatch |
128
+ | `src/capture-lifecycle.ts` | Foreground/background capture tracking and checkout drain |
129
+ | `src/subagent.ts` | Subagent permissions, model selection, and OpenCode task dispatch |
130
+ | `src/store.ts` | Store path derivation and serialized store-branch matching |
131
+ | `src/path.ts` | Symlink-safe project and store path helpers |
132
+ | `src/prompts.ts` | Cached prompt-template loader |
133
+ | `src/turn-status.ts` | Compact model-facing status formatter |
134
+ | `src/debug.ts` | File/stderr lifecycle and debug logger |
92
135
 
93
136
  ## Publishing
94
137
 
@@ -97,4 +140,3 @@ Releases are fully automated — no manual `npm version` or `npm publish`.
97
140
  1. Land changes on `main` via PR using [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, etc.)
98
141
  2. On push to `main`, `.github/workflows/semantic.yaml` runs [semantic-release](https://semantic-release.gitbook.io/)
99
142
  3. The new tag triggers `.github/workflows/publish-npm.yaml`, which builds and runs `npm publish --provenance`
100
-
@@ -0,0 +1,12 @@
1
+ /** Track actual memoir task execution, including native background jobs. */
2
+ export declare class CaptureLifecycle {
3
+ private readonly active;
4
+ private readonly callsByChild;
5
+ begin(callID: string): void;
6
+ background(callID: string, childSessionID: string): void;
7
+ finishCall(callID: string): void;
8
+ finishSession(sessionID: string): void;
9
+ drain(timeoutMs?: number): Promise<boolean>;
10
+ clear(): void;
11
+ }
12
+ //# sourceMappingURL=capture-lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-lifecycle.d.ts","sourceRoot":"","sources":["../src/capture-lifecycle.ts"],"names":[],"mappings":"AAQA,4EAA4E;AAC5E,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;IAC3D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6B;IAE1D,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAS3B,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI;IAOxD,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAQhC,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAKhC,KAAK,CAAC,SAAS,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAejD,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,52 @@
1
+ import type { MemoirToolInfo } from "./mcp-client.js";
2
+ interface ChatPart {
3
+ type?: string;
4
+ text?: string;
5
+ [key: string]: unknown;
6
+ }
7
+ export interface ChatMessage {
8
+ info?: {
9
+ id?: string;
10
+ role?: string;
11
+ };
12
+ parts?: ChatPart[];
13
+ }
14
+ /**
15
+ * Render a list of messages into a plain-text transcript.
16
+ * Skips empty (no text) messages so tool-only turns stay quiet.
17
+ */
18
+ export declare function formatSessionTranscript(messages: ChatMessage[]): string;
19
+ /**
20
+ * Extract the most recent completed turn: the last assistant message that has
21
+ * text, plus the user message(s) immediately preceding it. Returns null when
22
+ * there is no completed assistant turn yet.
23
+ */
24
+ export declare function lastTurnTranscript(messages: ChatMessage[]): string | null;
25
+ /**
26
+ * Local, LLM-free pre-filter. When MEMOIR_CAPTURE_MIN_CHARS (default 16) is
27
+ * > 0, only transcripts at least that long are worth sending to the subagent.
28
+ * A value of 0 disables the filter (capture everything).
29
+ */
30
+ export declare function shouldCaptureTurn(transcript: string | null, minChars?: number): boolean;
31
+ /**
32
+ * Build the capture task handed to the memoir subagent. It applies the
33
+ * durability checks, writes each fact with an explicit 3-level taxonomy path,
34
+ * and returns a compact report based only on confirmed tool outcomes.
35
+ */
36
+ export declare function buildTurnCaptureTask(transcript: string, tools?: MemoirToolInfo[]): string;
37
+ /**
38
+ * Capture the latest completed turn of a session into memoir via the subagent.
39
+ *
40
+ * Fire-and-forget: the caller should NOT await the subagent's completion.
41
+ * Deduped per session by the last assistant message id, so a turn is written
42
+ * at most once. Skips entirely when MEMOIR_AUTO_SAVE=0.
43
+ *
44
+ * @param client SDK client (transcript source + subagent spawner)
45
+ * @param sessionID parent session id
46
+ * @param directory working directory for the child session (unused — the
47
+ * running opencode instance is already bound to it; kept for call stability)
48
+ * @param lastCaptured per-session map of already-captured assistant ids
49
+ */
50
+ export declare function captureTurn(client: unknown, sessionID: string, _directory: string, lastCaptured: Map<string, string>, tools?: MemoirToolInfo[]): Promise<void>;
51
+ export {};
52
+ //# sourceMappingURL=capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAQtD,UAAU,QAAQ;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;CACpB;AAyDD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,MAAM,CASvE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,MAAM,GAAG,IAAI,CAoBzE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,QAAQ,SAAoB,GAC3B,OAAO,CAGT;AAQD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,GAAE,cAAc,EAAO,GAAG,MAAM,CAW7F;AAeD;;;;;;;;;;;;GAYG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACjC,KAAK,GAAE,cAAc,EAAO,GAC3B,OAAO,CAAC,IAAI,CAAC,CA6Cf"}
package/dist/debug.d.ts CHANGED
@@ -1,6 +1,3 @@
1
- /**
2
- * Debug logger writing to stderr. Prefixes with [memoir] for easy filtering.
3
- * Only writes when MEMOIR_DEBUG=1 is set.
4
- */
5
- export declare function debugLog(...args: unknown[]): void;
1
+ /** Single logging entrypoint; MEMOIR_DEBUG=1 controls detail, not emission. */
2
+ export declare function log(...args: unknown[]): void;
6
3
  //# sourceMappingURL=debug.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"debug.d.ts","sourceRoot":"","sources":["../src/debug.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAIjD"}
1
+ {"version":3,"file":"debug.d.ts","sourceRoot":"","sources":["../src/debug.ts"],"names":[],"mappings":"AAkEA,+EAA+E;AAC/E,wBAAgB,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAE5C"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAoJxE,QAAA,MAAM,MAAM,EAAE,YAGb,CAAC;AAEF,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA+B,YAAY,EAAE,MAAM,qBAAqB,CAAC;AA+UrF,QAAA,MAAM,MAAM,EAAE,YAGb,CAAC;AAEF,eAAe,MAAM,CAAC"}