opencode-codex-memory 0.4.0 → 0.4.2
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 +163 -70
- package/dist/src/capture.js +15 -2
- package/dist/src/codex-interop.js +12 -13
- package/dist/src/db.js +17 -11
- package/dist/src/git-baseline.js +22 -0
- package/dist/src/index.d.ts +17 -1
- package/dist/src/index.js +129 -63
- package/dist/src/llm.d.ts +11 -1
- package/dist/src/llm.js +123 -11
- package/dist/src/options.d.ts +2 -0
- package/dist/src/options.js +5 -1
- package/dist/src/path-guard.d.ts +2 -0
- package/dist/src/path-guard.js +17 -0
- package/dist/src/phase1.js +1 -1
- package/dist/src/phase2.d.ts +2 -1
- package/dist/src/phase2.js +8 -12
- package/dist/src/redact.d.ts +8 -0
- package/dist/src/redact.js +210 -6
- package/dist/src/source.js +32 -16
- package/dist/src/store.d.ts +2 -2
- package/dist/src/store.js +26 -8
- package/dist/src/workspace.js +25 -20
- package/dist/tools/memory.js +3 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# OpenCode Codex Memory
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/opencode-codex-memory">
|
|
5
|
+
<img src="https://img.shields.io/npm/v/opencode-codex-memory?logo=npm&label=latest" alt="Latest npm version" />
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/opencode-codex-memory">
|
|
8
|
+
<img src="https://img.shields.io/npm/dt/opencode-codex-memory?logo=npm&label=downloads" alt="npm downloads" />
|
|
9
|
+
</a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
Persistent memory for [OpenCode](https://opencode.ai): your agent remembers what
|
|
4
13
|
it learned in past sessions — your conventions, your projects, the decisions you
|
|
5
14
|
made — and brings that context into new conversations automatically.
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
`~/.local/share/opencode/`.
|
|
10
|
-
|
|
11
|
-
Despite the name: **no codex subscription or OpenAI account is needed.** This
|
|
12
|
-
project ports the memory *design* from OpenAI's codex to opencode. It works out
|
|
16
|
+
Despite the name: **no Codex subscription or OpenAI account is needed.** This
|
|
17
|
+
project is a faithful port of the memory system in OpenAI's Codex. It works out
|
|
13
18
|
of the box with zero extra configuration and uses whatever models you already
|
|
14
|
-
have set up in
|
|
19
|
+
have set up in OpenCode.
|
|
20
|
+
|
|
21
|
+
**Local-first by design.** Memory is plain markdown files plus a small SQLite
|
|
22
|
+
database on your own machine — no memory service to sign up for, no MCP server,
|
|
23
|
+
no separate process, no sync. Installing it is one line in your `opencode.json`;
|
|
24
|
+
from there everything lives under `~/.local/share/opencode/`, so you can read it,
|
|
25
|
+
grep it, edit it, or delete it like anything else you own. Nothing leaves your
|
|
26
|
+
machine beyond the model calls OpenCode already makes.
|
|
15
27
|
|
|
16
28
|
If you *do* also use the Codex CLI: the plugin can share memory with Codex in
|
|
17
29
|
both directions — what one assistant learns on your machine, the other picks
|
|
@@ -20,24 +32,17 @@ up. Off by default, one config flag per direction; see
|
|
|
20
32
|
|
|
21
33
|
## Why
|
|
22
34
|
|
|
23
|
-
By default every
|
|
35
|
+
By default every OpenCode session starts from zero. You re-explain your build
|
|
24
36
|
commands, your code style, and the quirks of each repo over and over.
|
|
25
37
|
|
|
26
|
-
|
|
38
|
+
This plugin closes that loop. It reviews finished sessions in the
|
|
39
|
+
background, keeps what's durable — your preferences, how a repo is built, what
|
|
40
|
+
worked and what didn't — and puts that context back in front of the agent in
|
|
41
|
+
later conversations. You don't manage any of it; OpenCode just gets more useful
|
|
42
|
+
the more you use it.
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
durable facts — preferences, project structure, what worked and what didn't.
|
|
31
|
-
- **It consolidates.** Periodically it merges those notes into a compact,
|
|
32
|
-
searchable memory, pruning what's stale.
|
|
33
|
-
- **It remembers at the right time.** A short summary is injected into the system
|
|
34
|
-
prompt, and the agent can search the full memory on demand when a task looks
|
|
35
|
-
related to past work.
|
|
36
|
-
- **It self-corrects.** When the agent actually uses a memory it cites the source,
|
|
37
|
-
so useful memories rank higher over time and unused ones fade.
|
|
38
|
-
|
|
39
|
-
The result: opencode gets more useful the more you use it, without you managing
|
|
40
|
-
anything.
|
|
44
|
+
If you want the mental model before the details, jump to
|
|
45
|
+
[How it works](#how-it-works).
|
|
41
46
|
|
|
42
47
|
## Install
|
|
43
48
|
|
|
@@ -45,19 +50,37 @@ anything.
|
|
|
45
50
|
|
|
46
51
|
```json
|
|
47
52
|
{
|
|
48
|
-
|
|
53
|
+
"plugin": ["opencode-codex-memory@0.4.2"]
|
|
49
54
|
}
|
|
50
55
|
```
|
|
51
56
|
|
|
57
|
+
**Pin the version** (here and for any OpenCode plugin). OpenCode installs a
|
|
58
|
+
plugin spec once into its package cache and never re-resolves it, so a bare
|
|
59
|
+
`"opencode-codex-memory"` is not "always latest" — it freezes at whatever
|
|
60
|
+
was latest the first time OpenCode started. With a version tag you decide
|
|
61
|
+
which release runs, and bumping the tag installs the new one. Check
|
|
62
|
+
[npm](https://www.npmjs.com/package/opencode-codex-memory) for the current
|
|
63
|
+
version.
|
|
64
|
+
|
|
65
|
+
Since updating is then just editing version numbers, it's worth automating
|
|
66
|
+
with a custom command — one that reads your `opencode.json`, compares each
|
|
67
|
+
pin against npm, and rewrites the ones you confirm. If you'd rather not
|
|
68
|
+
write your own, this repo has one you can copy:
|
|
69
|
+
[`docs/commands/update-plugins.md`](./docs/commands/update-plugins.md) →
|
|
70
|
+
`~/.config/opencode/command/update-plugins.md`, then run `/update-plugins`.
|
|
71
|
+
It's documentation, not part of the package: nothing about it is installed
|
|
72
|
+
with the plugin, and it works for every pinned plugin in your config, not
|
|
73
|
+
just this one.
|
|
74
|
+
|
|
52
75
|
(While developing locally, point it at an absolute path to your checkout
|
|
53
76
|
instead of the package name.)
|
|
54
77
|
|
|
55
78
|
2. That's it. The memory workspace is created on first use. Installing the
|
|
56
79
|
plugin is the opt-in: background learning and summary injection are active
|
|
57
|
-
immediately (
|
|
80
|
+
immediately (Codex ships the same system behind a default-off feature flag
|
|
58
81
|
with a consent prompt; a standalone memory plugin *is* the consent).
|
|
59
82
|
|
|
60
|
-
Requires
|
|
83
|
+
Requires OpenCode 1.18 or newer (official release). Git is bundled
|
|
61
84
|
(`isomorphic-git`) — no `git` binary or any other external tool needed.
|
|
62
85
|
|
|
63
86
|
The two restricted sub-agents that do the background learning (`memorize`,
|
|
@@ -71,11 +94,11 @@ override `memorize`, keep an `external_directory` allow for
|
|
|
71
94
|
`~/.local/share/opencode/memories/*` (e.g.
|
|
72
95
|
`"external_directory": { "$HOME/.local/share/opencode/memories/*": "allow" }`
|
|
73
96
|
after the wildcard deny) — the memory folder lives outside your project, and
|
|
74
|
-
without that grant
|
|
97
|
+
without that grant OpenCode blocks the consolidator's file access.
|
|
75
98
|
|
|
76
99
|
## Try it
|
|
77
100
|
|
|
78
|
-
Just use
|
|
101
|
+
Just use OpenCode normally. Sessions that have been idle for a few hours get
|
|
79
102
|
reviewed in the background and memory starts building up — you don't have to do
|
|
80
103
|
anything. Come back the next day and ask something like *"what do you know about how I
|
|
81
104
|
work?"* or *"what was I doing in this repo?"* and the agent draws on what it
|
|
@@ -96,11 +119,59 @@ echo 'I prefer TypeScript strict mode and 2-space indentation.' \
|
|
|
96
119
|
> ~/.local/share/opencode/memories/memory_summary.md
|
|
97
120
|
```
|
|
98
121
|
|
|
122
|
+
## How it works
|
|
123
|
+
|
|
124
|
+
You don't need to know any of this to use the plugin. The design is Codex's,
|
|
125
|
+
ported as-is, and it does what any memory system has to do: decide what's worth
|
|
126
|
+
keeping, write it down so it can be found again, surface the right piece at the
|
|
127
|
+
right moment, and forget what stopped being useful.
|
|
128
|
+
|
|
129
|
+
Think of it as three jobs: two background writers and one reader. **Nothing here
|
|
130
|
+
runs while you're waiting for a reply** — an assistant that stops to take notes
|
|
131
|
+
mid-answer would be slower and more expensive, so the learning happens after the
|
|
132
|
+
fact, on transcripts of conversations that are already over.
|
|
133
|
+
|
|
134
|
+
**Phase 1 — read one finished session, write notes about it.** Once a
|
|
135
|
+
conversation has been idle long enough that it's clearly done (default 6 h), the
|
|
136
|
+
plugin fetches that transcript, strips secrets out of it, and hands it to a
|
|
137
|
+
cheap model with one question: *what from this is worth keeping?* The answer
|
|
138
|
+
comes back as structured data — a detailed note plus a short recap of the
|
|
139
|
+
session — and lands in a local SQLite database. One session in, one record out.
|
|
140
|
+
Sessions are independent, so this part is easy to parallelize and to retry when
|
|
141
|
+
it fails.
|
|
142
|
+
|
|
143
|
+
**Phase 2 — merge all those notes into one memory.** Every few hours at most
|
|
144
|
+
(and only one run at a time across all your OpenCode windows), a second pass
|
|
145
|
+
takes the most relevant per-session notes and rewrites the actual memory files:
|
|
146
|
+
`MEMORY.md` as the full index, `memory_summary.md` as the short version, and
|
|
147
|
+
`skills/` for procedures worth repeating. This is where the interesting work
|
|
148
|
+
happens — ten similar observations collapse into one rule, contradictions get
|
|
149
|
+
resolved, and notes nothing ever used age out. Forgetting is a feature: memory
|
|
150
|
+
that only grows is memory that stops being useful.
|
|
151
|
+
|
|
152
|
+
The split exists because the two halves have opposite needs. Phase 1 is
|
|
153
|
+
per-session and can run many at once; phase 2 touches the single shared memory,
|
|
154
|
+
so it has to be serialized. Keeping them apart means one slow or failing session
|
|
155
|
+
extraction can't corrupt or block the shared store.
|
|
156
|
+
|
|
157
|
+
**The read path — actually remembering.** Every turn, the short summary is
|
|
158
|
+
appended to the system prompt (capped at ~2500 tokens, so the cost is small and
|
|
159
|
+
predictable). That's the always-on layer. When a task looks related to past
|
|
160
|
+
work, the agent goes further and searches the full memory itself with the
|
|
161
|
+
`memory_*` tools — the equivalent of "I've seen this before, let me look it up"
|
|
162
|
+
rather than carrying everything around all the time.
|
|
163
|
+
|
|
164
|
+
**The feedback loop.** When the agent uses a memory, it cites it. The citation
|
|
165
|
+
is recorded and then stripped before it reaches your screen, and those usage
|
|
166
|
+
counts feed back into phase 2's ranking. Memories that keep proving useful get
|
|
167
|
+
kept and sharpened; memories nothing has touched in a month drop out. The system
|
|
168
|
+
finds out which of its own notes were worth writing.
|
|
169
|
+
|
|
99
170
|
## Where your data lives
|
|
100
171
|
|
|
101
172
|
```
|
|
102
173
|
~/.local/share/opencode/
|
|
103
|
-
├── memory.db # the plugin's own database (
|
|
174
|
+
├── memory.db # the plugin's own database (OpenCode's data is only accessed via its API)
|
|
104
175
|
└── memories/
|
|
105
176
|
├── memory_summary.md # compact summary injected into the system prompt
|
|
106
177
|
├── MEMORY.md # searchable index of everything learned
|
|
@@ -109,7 +180,7 @@ echo 'I prefer TypeScript strict mode and 2-space indentation.' \
|
|
|
109
180
|
└── extensions/ad_hoc/notes/ # things you explicitly asked it to remember
|
|
110
181
|
```
|
|
111
182
|
|
|
112
|
-
The location follows
|
|
183
|
+
The location follows OpenCode's own data directory — `$XDG_DATA_HOME/opencode`
|
|
113
184
|
when that's set, otherwise `~/.local/share/opencode` (same resolution on macOS,
|
|
114
185
|
Linux, and Windows).
|
|
115
186
|
|
|
@@ -120,8 +191,12 @@ those too.)
|
|
|
120
191
|
|
|
121
192
|
## Privacy & safety
|
|
122
193
|
|
|
123
|
-
- **Local only.**
|
|
124
|
-
|
|
194
|
+
- **Local only.** There is no remote storage option to enable, by accident or
|
|
195
|
+
otherwise: Codex keeps memory storage behind a backend interface whose only
|
|
196
|
+
implementation today is the local filesystem, and this port implements that
|
|
197
|
+
path and nothing else. Nothing is sent anywhere except through your existing
|
|
198
|
+
OpenCode provider, using your existing credentials; the plugin holds no keys
|
|
199
|
+
of its own.
|
|
125
200
|
- **Secrets are redacted** (API keys, tokens, private keys, passwords) from
|
|
126
201
|
session transcripts and extracted memories before anything is written or sent
|
|
127
202
|
to a model. Notes you explicitly dictate ("remember that ...") are stored as
|
|
@@ -133,14 +208,14 @@ those too.)
|
|
|
133
208
|
- **Reset is safe.** `memory_reset` refuses to run if the memory folder is a
|
|
134
209
|
symlink, so it can't be tricked into deleting something else.
|
|
135
210
|
- **Web/MCP sessions:** by default, sessions that used web search, fetch, or MCP
|
|
136
|
-
tools are still eligible for memory (matching
|
|
211
|
+
tools are still eligible for memory (matching Codex). If you'd rather exclude
|
|
137
212
|
them so scraped or external content can't enter your memory, set
|
|
138
213
|
`disable_on_external_context: true`.
|
|
139
214
|
|
|
140
215
|
## Configuration
|
|
141
216
|
|
|
142
217
|
Optional plugin options (all have sensible defaults). Names and defaults match
|
|
143
|
-
|
|
218
|
+
Codex's `[memories]` config so the two stay easy to compare:
|
|
144
219
|
|
|
145
220
|
| Option | Default | Meaning |
|
|
146
221
|
|---|---|---|
|
|
@@ -148,8 +223,8 @@ codex's `[memories]` config so the two stay easy to compare:
|
|
|
148
223
|
| `use_memories` | `true` | Inject the memory summary into the system prompt |
|
|
149
224
|
| `dedicated_tools` | `true` | Expose the `memory_read`/`memory_search`/`memory_list`/`memory_add_note` tools |
|
|
150
225
|
| `disable_on_external_context` | `false` | Exclude sessions that used web/MCP tools from memory |
|
|
151
|
-
| `extract_model` |
|
|
152
|
-
| `consolidation_model` |
|
|
226
|
+
| `extract_model` | OpenCode `small_model`, else see below | Model used for per-session extraction |
|
|
227
|
+
| `consolidation_model` | OpenCode `model`, else see below | Model used for consolidation |
|
|
153
228
|
| `max_raw_memories_for_consolidation` | `256` | How many raw memories feed each consolidation pass |
|
|
154
229
|
| `max_rollout_age_days` | `10` | Ignore sessions older than this for extraction |
|
|
155
230
|
| `min_rollout_idle_hours` | `6` | How long a session must be idle before it's eligible |
|
|
@@ -162,16 +237,16 @@ To set options, turn the plugin entry into a `[name, options]` pair:
|
|
|
162
237
|
```json
|
|
163
238
|
{
|
|
164
239
|
"plugin": [
|
|
165
|
-
["opencode-codex-memory", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
|
|
240
|
+
["opencode-codex-memory@0.4.2", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
|
|
166
241
|
]
|
|
167
242
|
}
|
|
168
243
|
```
|
|
169
244
|
|
|
170
|
-
See the [
|
|
245
|
+
See the [OpenCode plugin docs](https://opencode.ai/docs/plugins/) for details.
|
|
171
246
|
|
|
172
|
-
Numeric options are clamped to
|
|
247
|
+
Numeric options are clamped to Codex's valid ranges; unknown option keys are
|
|
173
248
|
ignored with a warning. Setting `use_memories: false` also hides the memory
|
|
174
|
-
tools, matching
|
|
249
|
+
tools, matching Codex's extension gating.
|
|
175
250
|
|
|
176
251
|
**Verifying your configuration:** the plugin never hard-fails on bad options.
|
|
177
252
|
To check what actually took effect, ask the agent to run `memory_inspect` — it
|
|
@@ -180,32 +255,32 @@ unknown or malformed keys (typos included), and shows the resolved Codex
|
|
|
180
255
|
interop state. A mistyped option shows up there twice: as a warning, and as
|
|
181
256
|
the default value appearing where you expected your setting.
|
|
182
257
|
|
|
183
|
-
Model selection mirrors
|
|
184
|
-
split using
|
|
185
|
-
`small_model` from your `opencode.json` is used (
|
|
186
|
-
when `consolidation_model` is unset, your main `model` is used (
|
|
258
|
+
Model selection mirrors Codex's cheap-extraction / capable-consolidation
|
|
259
|
+
split using OpenCode's own concepts: when `extract_model` is unset, the
|
|
260
|
+
`small_model` from your `opencode.json` is used (Codex uses `gpt-5.4-mini`);
|
|
261
|
+
when `consolidation_model` is unset, your main `model` is used (Codex uses
|
|
187
262
|
`gpt-5.4`). If neither is configured, the learning sub-agents fall back to
|
|
188
263
|
their own agent-level `model` (if you defined one), else the provider default.
|
|
189
|
-
(
|
|
264
|
+
(OpenCode's *automatic* small-model pick is internal to OpenCode and not
|
|
190
265
|
exposed to plugins — set `small_model` explicitly to get the cheap extraction
|
|
191
266
|
path.)
|
|
192
267
|
|
|
193
268
|
The full precedence per phase: plugin option (`extract_model` /
|
|
194
|
-
`consolidation_model`) →
|
|
269
|
+
`consolidation_model`) → OpenCode config (`small_model` / `model`) → a `model`
|
|
195
270
|
on your own `memorize-extract`/`memorize` agent definition, if you overrode
|
|
196
271
|
one → the provider's default model. Note that the first two pass the model
|
|
197
272
|
explicitly, so they win over an agent-level `model`.
|
|
198
273
|
|
|
199
|
-
> Note: `dedicated_tools` defaults to `true` here (
|
|
274
|
+
> Note: `dedicated_tools` defaults to `true` here (Codex defaults it to `false`).
|
|
200
275
|
> This is the one intentional default difference — the tools are a core part of a
|
|
201
|
-
> standalone memory plugin. Everything else matches
|
|
276
|
+
> standalone memory plugin. Everything else matches Codex's defaults.
|
|
202
277
|
>
|
|
203
278
|
> Turning `dedicated_tools` off keeps background learning, summary injection,
|
|
204
|
-
> and citation tracking working. The injected guidance switches to
|
|
279
|
+
> and citation tracking working. The injected guidance switches to Codex's
|
|
205
280
|
> file-based mode — the agent reads the memory files with its normal file
|
|
206
281
|
> tools and writes "remember this" notes directly into
|
|
207
282
|
> `extensions/ad_hoc/notes/`. Caveat: the memory folder lives outside your
|
|
208
|
-
> project, so
|
|
283
|
+
> project, so OpenCode raises an `external_directory` permission prompt the
|
|
209
284
|
> first time an agent touches it (allow-always covers later access); agents
|
|
210
285
|
> whose permissions deny that ask cannot use file-based mode. The dedicated
|
|
211
286
|
> tools have no such friction — that's why they are the default. The
|
|
@@ -221,7 +296,7 @@ directions:
|
|
|
221
296
|
```json
|
|
222
297
|
{
|
|
223
298
|
"plugin": [
|
|
224
|
-
["opencode-codex-memory", { "codex_interop": { "import": true, "export": true } }]
|
|
299
|
+
["opencode-codex-memory@0.4.2", { "codex_interop": { "import": true, "export": true } }]
|
|
225
300
|
]
|
|
226
301
|
}
|
|
227
302
|
```
|
|
@@ -245,28 +320,46 @@ Both sides mark imported content with a provenance tag (`[from codex]` /
|
|
|
245
320
|
don't ping-pong between the two systems. This follows the same extension
|
|
246
321
|
mechanism Codex itself uses to import Claude memories.
|
|
247
322
|
|
|
248
|
-
##
|
|
323
|
+
## Why one global memory?
|
|
324
|
+
|
|
325
|
+
There's a single store for everything you do, not one per project — and that's
|
|
326
|
+
the design choice most likely to surprise you, so it's worth explaining where it
|
|
327
|
+
came from.
|
|
328
|
+
|
|
329
|
+
Codex *started* with per-project memory: a separate bucket per directory, plus a
|
|
330
|
+
user-level scope on top. It **deliberately removed that** in early 2026 and
|
|
331
|
+
collapsed everything into one global root — one store, one lock, one
|
|
332
|
+
consolidation pass — for simplicity.
|
|
333
|
+
|
|
334
|
+
Simplicity is easy to underrate here — until you try to draw the boundaries
|
|
335
|
+
yourself. Scoping forces a question that often has no good answer: which project does
|
|
336
|
+
"prefers table-driven tests" belong to? Monorepos, worktrees, and sibling repos
|
|
337
|
+
of the same stack all blur the line, and the most valuable lessons — the ones
|
|
338
|
+
about how *you* work — belong to no project at all. Per-scope stores also each
|
|
339
|
+
consolidate over a thinner slice of evidence than the whole.
|
|
340
|
+
|
|
341
|
+
Project awareness didn't disappear; it moved out of storage and into the prompt.
|
|
342
|
+
Memories carry the project they came from, and the consolidator is told to keep
|
|
343
|
+
per-project detail separable — soft "this looks like it belongs to that project"
|
|
344
|
+
hints rather than hard partitions. You get the cross-project transfer (your
|
|
345
|
+
conventions follow you into a new repo on day one) while project-specific facts
|
|
346
|
+
stay recognizable as such.
|
|
249
347
|
|
|
250
|
-
|
|
348
|
+
The cost is real: with one store, an unrelated project's details can surface in
|
|
349
|
+
the summary. Codex judged that cheaper than the alternative, and this port
|
|
350
|
+
mirrors that decision rather than layering scoping back on top.
|
|
251
351
|
|
|
252
|
-
|
|
253
|
-
is global.** There's a single store for all your work, not one per project. That's
|
|
254
|
-
not an accident of the port — it's codex's own hard-won shape. codex *started* with
|
|
255
|
-
per-project memory (a separate bucket per directory, plus a user scope) and
|
|
256
|
-
**deliberately removed it** in early 2026, collapsing everything into one global
|
|
257
|
-
root for simplicity: one store, one lock, one consolidation pass. Project awareness
|
|
258
|
-
didn't disappear — it moved out of storage and into the prompt, as soft "this looks
|
|
259
|
-
like it belongs to that project" hints rather than hard partitions. This port
|
|
260
|
-
mirrors that exactly.
|
|
352
|
+
## Contributing
|
|
261
353
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
354
|
+
The port follows Codex closely: same two-phase pipeline, same on-disk artifacts,
|
|
355
|
+
same prompts (adapted only where OpenCode differs). If you want the full design
|
|
356
|
+
and the trade-offs, see [`ARCHITECTURE.md`](./ARCHITECTURE.md); contributor
|
|
357
|
+
guidance lives in [`CONTRIBUTING.md`](./CONTRIBUTING.md) and
|
|
358
|
+
[`AGENTS.md`](./AGENTS.md) — in short: this repo exists to port Codex's memory
|
|
359
|
+
system to OpenCode, and PRs that break that parity will be rejected.
|
|
267
360
|
|
|
268
361
|
## License
|
|
269
362
|
|
|
270
363
|
Apache 2.0 — the same license as [OpenAI Codex](https://github.com/openai/codex),
|
|
271
364
|
whose memory system this project ports. See [`LICENSE`](./LICENSE) and
|
|
272
|
-
[`NOTICE`](./NOTICE). Not affiliated with the
|
|
365
|
+
[`NOTICE`](./NOTICE). Not affiliated with the Codex project.
|
package/dist/src/capture.js
CHANGED
|
@@ -127,6 +127,11 @@ function extractText(msg) {
|
|
|
127
127
|
// parts carry `text`, so they must be dropped before the text check.
|
|
128
128
|
if (msg.type === "reasoning")
|
|
129
129
|
return undefined;
|
|
130
|
+
// opencode itself drops `ignored` text parts when building model messages
|
|
131
|
+
// (session/message-v2.ts), e.g. ACP content addressed only to the user.
|
|
132
|
+
// The assistant never saw them, so they are not conversation.
|
|
133
|
+
if (msg.ignored === true)
|
|
134
|
+
return undefined;
|
|
130
135
|
if (typeof msg.text === "string")
|
|
131
136
|
return msg.text;
|
|
132
137
|
if (msg.type === "tool") {
|
|
@@ -135,8 +140,16 @@ function extractText(msg) {
|
|
|
135
140
|
// the extractor's strongest evidence — do not slice them per call.
|
|
136
141
|
const tool = msg.tool ?? "unknown";
|
|
137
142
|
const input = msg.state?.input ? JSON.stringify(msg.state.input) : "";
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
// `output` exists only on status:"completed"; a failed call carries
|
|
144
|
+
// `error` instead (schema v1/session.ts ToolStateError). codex persists
|
|
145
|
+
// failed calls too (rollout policy: FunctionCallOutput => true), and "X
|
|
146
|
+
// failed with Y" is often the most memorable part of a session.
|
|
147
|
+
const result = typeof msg.state?.output === "string"
|
|
148
|
+
? msg.state.output
|
|
149
|
+
: typeof msg.state?.error === "string"
|
|
150
|
+
? `[error] ${msg.state.error}`
|
|
151
|
+
: "";
|
|
152
|
+
return `[tool: ${tool}] ${input}${result ? "\n" + result : ""}`;
|
|
140
153
|
}
|
|
141
154
|
if (msg.type === "step-start" || msg.type === "step-finish")
|
|
142
155
|
return undefined;
|
|
@@ -2,6 +2,7 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import os from "os";
|
|
4
4
|
import { memoryRoot } from "./paths.js";
|
|
5
|
+
import { safeResolveUnderRoot } from "./path-guard.js";
|
|
5
6
|
/**
|
|
6
7
|
* Codex interop: memory exchange with an upstream Codex CLI installation on
|
|
7
8
|
* the same machine, in both directions, through the generic extensions
|
|
@@ -235,8 +236,7 @@ function writeIfChanged(file, content) {
|
|
|
235
236
|
* target workspace changed. Never creates the extension while the source has
|
|
236
237
|
* nothing to offer.
|
|
237
238
|
*/
|
|
238
|
-
function syncExtension(sourceRoot,
|
|
239
|
-
const resDir = path.join(extDir, "resources", subdir);
|
|
239
|
+
function syncExtension(sourceRoot, targetRoot, extension, subdir, instructions) {
|
|
240
240
|
// An unreachable source ROOT is not a deletion signal: a missing/mistyped
|
|
241
241
|
// codex home (or an env context without CODEX_HOME) must not trigger the
|
|
242
242
|
// forgetting path. Keep existing copies untouched and do nothing.
|
|
@@ -247,6 +247,8 @@ function syncExtension(sourceRoot, extDir, subdir, instructions) {
|
|
|
247
247
|
catch { }
|
|
248
248
|
if (!rootIsDir)
|
|
249
249
|
return false;
|
|
250
|
+
const extensionDir = safeResolveUnderRoot(targetRoot, path.join("extensions", extension));
|
|
251
|
+
const resDir = safeResolveUnderRoot(targetRoot, path.join("extensions", extension, "resources", subdir));
|
|
250
252
|
const sourceAvailable = ARTIFACTS.some((name) => readIfFile(path.join(sourceRoot, name)) !== null);
|
|
251
253
|
if (!sourceAvailable) {
|
|
252
254
|
// Root exists but the artifacts are gone (e.g. codex memory cleared):
|
|
@@ -259,19 +261,18 @@ function syncExtension(sourceRoot, extDir, subdir, instructions) {
|
|
|
259
261
|
return true;
|
|
260
262
|
}
|
|
261
263
|
let changed = false;
|
|
262
|
-
if (writeIfChanged(path.join(
|
|
264
|
+
if (writeIfChanged(path.join(extensionDir, "instructions.md"), instructions))
|
|
263
265
|
changed = true;
|
|
264
266
|
for (const name of ARTIFACTS) {
|
|
265
267
|
const source = readIfFile(path.join(sourceRoot, name));
|
|
266
268
|
const target = path.join(resDir, name);
|
|
267
269
|
if (source === null) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
catch { }
|
|
270
|
+
try {
|
|
271
|
+
fs.lstatSync(target);
|
|
272
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
273
|
+
changed = true;
|
|
274
274
|
}
|
|
275
|
+
catch { }
|
|
275
276
|
continue;
|
|
276
277
|
}
|
|
277
278
|
if (writeIfChanged(target, source))
|
|
@@ -287,8 +288,7 @@ function syncExtension(sourceRoot, extDir, subdir, instructions) {
|
|
|
287
288
|
* Returns true when the plugin workspace changed.
|
|
288
289
|
*/
|
|
289
290
|
export function syncCodexImport(codexMemoryRoot) {
|
|
290
|
-
|
|
291
|
-
return syncExtension(codexMemoryRoot, extDir, "codex", IMPORT_INSTRUCTIONS);
|
|
291
|
+
return syncExtension(codexMemoryRoot, memoryRoot(), IMPORT_EXTENSION, "codex", IMPORT_INSTRUCTIONS);
|
|
292
292
|
}
|
|
293
293
|
/**
|
|
294
294
|
* Export direction: our consolidated memory -> Codex's
|
|
@@ -312,6 +312,5 @@ export function exportToCodexMemory(codexMemoryRoot) {
|
|
|
312
312
|
const summary = readIfFile(path.join(memoryRoot(), "memory_summary.md"));
|
|
313
313
|
if (summary === null || summary.toString("utf8").split(/\r?\n/, 1)[0] !== "v1")
|
|
314
314
|
return false;
|
|
315
|
-
|
|
316
|
-
return syncExtension(memoryRoot(), extDir, "opencode", EXPORT_INSTRUCTIONS);
|
|
315
|
+
return syncExtension(memoryRoot(), codexMemoryRoot, EXPORT_EXTENSION, "opencode", EXPORT_INSTRUCTIONS);
|
|
317
316
|
}
|
package/dist/src/db.js
CHANGED
|
@@ -47,18 +47,24 @@ export function openDb() {
|
|
|
47
47
|
return dbInstance;
|
|
48
48
|
const dbPath = memoryDbPath();
|
|
49
49
|
const db = new Database(dbPath, { create: true, readwrite: true, strict: false });
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
try {
|
|
51
|
+
// Match codex's memories-DB open options (runtime.rs): WAL, NORMAL sync,
|
|
52
|
+
// 5s busy timeout for cross-process access, incremental auto-vacuum.
|
|
53
|
+
db.run("PRAGMA journal_mode=WAL");
|
|
54
|
+
db.run("PRAGMA synchronous=NORMAL");
|
|
55
|
+
db.run("PRAGMA busy_timeout=5000");
|
|
56
|
+
db.run("PRAGMA auto_vacuum=INCREMENTAL");
|
|
57
|
+
runMigrations(db);
|
|
58
|
+
dbInstance = db;
|
|
59
|
+
return db;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
db.close();
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
59
65
|
}
|
|
60
66
|
function runMigrations(db) {
|
|
61
|
-
db.
|
|
67
|
+
db.run(`CREATE TABLE IF NOT EXISTS schema_version (
|
|
62
68
|
version INTEGER NOT NULL,
|
|
63
69
|
applied_at INTEGER NOT NULL
|
|
64
70
|
)`);
|
|
@@ -67,7 +73,7 @@ function runMigrations(db) {
|
|
|
67
73
|
if (currentVersion >= 1)
|
|
68
74
|
return;
|
|
69
75
|
for (const stmt of SCHEMA_V1)
|
|
70
|
-
db.
|
|
76
|
+
db.run(stmt);
|
|
71
77
|
db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (?, ?)").run(1, Date.now());
|
|
72
78
|
}
|
|
73
79
|
export function closeDb() {
|
package/dist/src/git-baseline.js
CHANGED
|
@@ -17,10 +17,32 @@ function removeDiffArtifact(dir) {
|
|
|
17
17
|
}
|
|
18
18
|
async function ensureInit(dir) {
|
|
19
19
|
const gitDir = path.join(dir, ".git");
|
|
20
|
+
let recreate = false;
|
|
21
|
+
try {
|
|
22
|
+
recreate = containsSymlink(gitDir) || !fs.lstatSync(gitDir).isDirectory();
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (err.code !== "ENOENT")
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
if (recreate)
|
|
29
|
+
fs.rmSync(gitDir, { recursive: true, force: true });
|
|
20
30
|
if (!fs.existsSync(gitDir)) {
|
|
21
31
|
await isogit.init({ fs, dir });
|
|
22
32
|
}
|
|
23
33
|
}
|
|
34
|
+
function containsSymlink(root) {
|
|
35
|
+
const st = fs.lstatSync(root);
|
|
36
|
+
if (st.isSymbolicLink())
|
|
37
|
+
return true;
|
|
38
|
+
if (!st.isDirectory())
|
|
39
|
+
return false;
|
|
40
|
+
for (const name of fs.readdirSync(root)) {
|
|
41
|
+
if (containsSymlink(path.join(root, name)))
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
24
46
|
// statusMatrix rows are [filepath, head, workdir, stage]; head !== workdir
|
|
25
47
|
// means the working tree differs from HEAD (added, modified, or deleted).
|
|
26
48
|
async function stageAll(dir) {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -189,7 +189,23 @@ declare const _default: {
|
|
|
189
189
|
"chat.message"(input: {
|
|
190
190
|
sessionID?: string;
|
|
191
191
|
}): Promise<void>;
|
|
192
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Dedicated plugin hook (NOT an event-bus type). Marks the session polluted
|
|
194
|
+
* at INVOCATION, mirroring codex: mcp_tool_call.rs calls
|
|
195
|
+
* maybe_mark_thread_memory_mode_polluted inside handle_approved_mcp_tool_call
|
|
196
|
+
* BEFORE the call runs, and web search marks on the completed response item
|
|
197
|
+
* (stream_events_utils.rs response_item_may_include_external_context).
|
|
198
|
+
*
|
|
199
|
+
* Deliberately not tool.execute.after: opencode does not guarantee that hook
|
|
200
|
+
* (session/tools.ts awaits execute() with no ensuring/catchAll, and an abort
|
|
201
|
+
* interrupts the fiber), so a failed or cancelled websearch/webfetch/MCP call
|
|
202
|
+
* left the session unmarked while its output had already entered the
|
|
203
|
+
* transcript. Marking early over-marks a permission-denied call, which is the
|
|
204
|
+
* safe direction for an opt-in guard.
|
|
205
|
+
*
|
|
206
|
+
* Pollution remains gated by disable_on_external_context, off by default.
|
|
207
|
+
*/
|
|
208
|
+
"tool.execute.before"(input: {
|
|
193
209
|
tool: string;
|
|
194
210
|
sessionID: string;
|
|
195
211
|
callID: string;
|