opencode-codex-memory 0.1.8 → 0.2.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 +59 -23
- package/dist/opencode.json +4 -4
- package/dist/src/capture.d.ts +29 -4
- package/dist/src/capture.js +111 -57
- package/dist/src/git-baseline.js +43 -34
- package/dist/src/index.d.ts +56 -4
- package/dist/src/index.js +167 -36
- package/dist/src/llm.d.ts +1 -0
- package/dist/src/llm.js +5 -2
- package/dist/src/path-guard.d.ts +9 -0
- package/dist/src/path-guard.js +23 -1
- package/dist/src/paths.d.ts +0 -1
- package/dist/src/paths.js +0 -3
- package/dist/src/phase1.d.ts +1 -0
- package/dist/src/phase1.js +13 -7
- package/dist/src/phase2.d.ts +2 -0
- package/dist/src/phase2.js +14 -2
- package/dist/src/redact.js +5 -2
- package/dist/src/source.d.ts +1 -1
- package/dist/src/source.js +23 -1
- package/dist/src/store.d.ts +5 -0
- package/dist/src/store.js +50 -18
- package/dist/src/templates/consolidation.md +15 -13
- package/dist/src/templates/read_path.md +3 -9
- package/dist/src/templates/stage_one_input.md +1 -1
- package/dist/src/workspace.js +8 -3
- package/dist/tools/control.js +40 -14
- package/dist/tools/memory.d.ts +20 -4
- package/dist/tools/memory.js +183 -58
- package/opencode.json +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,11 @@ It's a single plugin. No core changes, no MCP server, no separate process, no
|
|
|
8
8
|
cloud service. Everything stays on your machine under
|
|
9
9
|
`~/.local/share/opencode/`.
|
|
10
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
|
|
13
|
+
of the box with zero extra configuration and uses whatever models you already
|
|
14
|
+
have set up in opencode.
|
|
15
|
+
|
|
11
16
|
## Why
|
|
12
17
|
|
|
13
18
|
By default every opencode session starts from zero. You re-explain your build
|
|
@@ -15,9 +20,9 @@ commands, your code style, and the quirks of each repo over and over.
|
|
|
15
20
|
|
|
16
21
|
opencode-codex-memory closes that loop:
|
|
17
22
|
|
|
18
|
-
- **It learns in the background.**
|
|
19
|
-
|
|
20
|
-
what worked and what didn't.
|
|
23
|
+
- **It learns in the background.** Once a session has been idle for a while
|
|
24
|
+
(default 6 h), a later background pass reviews the transcript and extracts
|
|
25
|
+
durable facts — preferences, project structure, what worked and what didn't.
|
|
21
26
|
- **It consolidates.** Periodically it merges those notes into a compact,
|
|
22
27
|
searchable memory, pruning what's stale.
|
|
23
28
|
- **It remembers at the right time.** A short summary is injected into the system
|
|
@@ -44,22 +49,30 @@ anything.
|
|
|
44
49
|
|
|
45
50
|
2. That's it. The memory workspace is created on first use. Installing the
|
|
46
51
|
plugin is the opt-in: background learning and summary injection are active
|
|
47
|
-
immediately (codex ships the same system behind
|
|
48
|
-
consent prompt; a standalone memory plugin *is* the consent).
|
|
52
|
+
immediately (codex ships the same system behind a default-off feature flag
|
|
53
|
+
with a consent prompt; a standalone memory plugin *is* the consent).
|
|
49
54
|
|
|
50
55
|
Requires only opencode (official release). Git is bundled (`isomorphic-git`) —
|
|
51
56
|
no `git` binary or any other external tool needed.
|
|
52
57
|
|
|
53
58
|
The two restricted sub-agents that do the background learning (`memorize`,
|
|
54
|
-
`memorize-extract`) register themselves automatically
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
`memorize-extract`) register themselves automatically while background learning
|
|
60
|
+
is enabled. To choose which models
|
|
61
|
+
they use, set the `extract_model` / `consolidation_model` plugin options (see
|
|
62
|
+
[Configuration](#configuration)) — don't override the agents for that. Defining
|
|
63
|
+
an agent with the same name in your own config is only for advanced tweaks
|
|
64
|
+
(e.g. permissions); your definition then replaces the shipped one. If you
|
|
65
|
+
override `memorize`, keep an `external_directory` allow for
|
|
66
|
+
`~/.local/share/opencode/memories/*` (e.g.
|
|
67
|
+
`"external_directory": { "$HOME/.local/share/opencode/memories/*": "allow" }`
|
|
68
|
+
after the wildcard deny) — the memory folder lives outside your project, and
|
|
69
|
+
without that grant opencode blocks the consolidator's file access.
|
|
57
70
|
|
|
58
71
|
## Try it
|
|
59
72
|
|
|
60
|
-
Just use opencode normally.
|
|
61
|
-
the background and starts building
|
|
62
|
-
back
|
|
73
|
+
Just use opencode normally. Sessions that have been idle for a few hours get
|
|
74
|
+
reviewed in the background and memory starts building up — you don't have to do
|
|
75
|
+
anything. Come back the next day and ask something like *"what do you know about how I
|
|
63
76
|
work?"* or *"what was I doing in this repo?"* and the agent draws on what it
|
|
64
77
|
learned. The more you use it, the more it knows.
|
|
65
78
|
|
|
@@ -82,7 +95,7 @@ echo 'I prefer TypeScript strict mode and 2-space indentation.' \
|
|
|
82
95
|
|
|
83
96
|
```
|
|
84
97
|
~/.local/share/opencode/
|
|
85
|
-
├── memory.db # the plugin's own database (
|
|
98
|
+
├── memory.db # the plugin's own database (opencode's is only ever read)
|
|
86
99
|
└── memories/
|
|
87
100
|
├── memory_summary.md # compact summary injected into the system prompt
|
|
88
101
|
├── MEMORY.md # searchable index of everything learned
|
|
@@ -92,16 +105,20 @@ echo 'I prefer TypeScript strict mode and 2-space indentation.' \
|
|
|
92
105
|
```
|
|
93
106
|
|
|
94
107
|
It's all plain files and a local SQLite database. Read them, edit them, delete
|
|
95
|
-
them
|
|
108
|
+
them — it's yours. (The `memories/` folder also holds a few working files and
|
|
109
|
+
an internal `.git/` the plugin uses for change tracking; `memory_reset` wipes
|
|
110
|
+
those too.)
|
|
96
111
|
|
|
97
112
|
## Privacy & safety
|
|
98
113
|
|
|
99
114
|
- **Local only.** Nothing is sent anywhere except through your existing opencode
|
|
100
115
|
provider, using your existing credentials. The plugin holds no keys of its own.
|
|
101
|
-
- **Secrets are redacted** (API keys, tokens, private keys, passwords)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
116
|
+
- **Secrets are redacted** (API keys, tokens, private keys, passwords) from
|
|
117
|
+
session transcripts and extracted memories before anything is written or sent
|
|
118
|
+
to a model. Notes you explicitly dictate ("remember that ...") are stored as
|
|
119
|
+
you said them.
|
|
120
|
+
- **The learning agents are sandboxed** — every tool except reading and editing
|
|
121
|
+
the memory files is denied, including shell and network access.
|
|
105
122
|
- **Reset is safe.** `memory_reset` refuses to run if the memory folder is a
|
|
106
123
|
symlink, so it can't be tricked into deleting something else.
|
|
107
124
|
- **Web/MCP sessions:** by default, sessions that used web search, fetch, or MCP
|
|
@@ -120,8 +137,8 @@ codex's `[memories]` config so the two stay easy to compare:
|
|
|
120
137
|
| `use_memories` | `true` | Inject the memory summary into the system prompt |
|
|
121
138
|
| `dedicated_tools` | `true` | Expose the `memory_read`/`memory_search`/`memory_list`/`memory_add_note` tools |
|
|
122
139
|
| `disable_on_external_context` | `false` | Exclude sessions that used web/MCP tools from memory |
|
|
123
|
-
| `extract_model` | opencode `small_model`, else
|
|
124
|
-
| `consolidation_model` | opencode `model`, else
|
|
140
|
+
| `extract_model` | opencode `small_model`, else see below | Model used for per-session extraction |
|
|
141
|
+
| `consolidation_model` | opencode `model`, else see below | Model used for consolidation |
|
|
125
142
|
| `max_raw_memories_for_consolidation` | `256` | How many raw memories feed each consolidation pass |
|
|
126
143
|
| `max_rollout_age_days` | `10` | Ignore sessions older than this for extraction |
|
|
127
144
|
| `min_rollout_idle_hours` | `6` | How long a session must be idle before it's eligible |
|
|
@@ -148,14 +165,33 @@ Model selection mirrors codex's cheap-extraction / capable-consolidation
|
|
|
148
165
|
split using opencode's own concepts: when `extract_model` is unset, the
|
|
149
166
|
`small_model` from your `opencode.json` is used (codex uses `gpt-5.4-mini`);
|
|
150
167
|
when `consolidation_model` is unset, your main `model` is used (codex uses
|
|
151
|
-
`gpt-5.4`). If neither is configured,
|
|
152
|
-
model
|
|
153
|
-
|
|
154
|
-
extraction
|
|
168
|
+
`gpt-5.4`). If neither is configured, the learning sub-agents fall back to
|
|
169
|
+
their own agent-level `model` (if you defined one), else the provider default.
|
|
170
|
+
(opencode's *automatic* small-model pick is internal to opencode and not
|
|
171
|
+
exposed to plugins — set `small_model` explicitly to get the cheap extraction
|
|
172
|
+
path.)
|
|
173
|
+
|
|
174
|
+
The full precedence per phase: plugin option (`extract_model` /
|
|
175
|
+
`consolidation_model`) → opencode config (`small_model` / `model`) → a `model`
|
|
176
|
+
on your own `memorize-extract`/`memorize` agent definition, if you overrode
|
|
177
|
+
one → the provider's default model. Note that the first two pass the model
|
|
178
|
+
explicitly, so they win over an agent-level `model`.
|
|
155
179
|
|
|
156
180
|
> Note: `dedicated_tools` defaults to `true` here (codex defaults it to `false`).
|
|
157
181
|
> This is the one intentional default difference — the tools are a core part of a
|
|
158
182
|
> standalone memory plugin. Everything else matches codex's defaults.
|
|
183
|
+
>
|
|
184
|
+
> Turning `dedicated_tools` off keeps background learning, summary injection,
|
|
185
|
+
> and citation tracking working. The injected guidance switches to codex's
|
|
186
|
+
> file-based mode — the agent reads the memory files with its normal file
|
|
187
|
+
> tools and writes "remember this" notes directly into
|
|
188
|
+
> `extensions/ad_hoc/notes/`. Caveat: the memory folder lives outside your
|
|
189
|
+
> project, so opencode raises an `external_directory` permission prompt the
|
|
190
|
+
> first time an agent touches it (allow-always covers later access); agents
|
|
191
|
+
> whose permissions deny that ask cannot use file-based mode. The dedicated
|
|
192
|
+
> tools have no such friction — that's why they are the default. The
|
|
193
|
+
> maintenance tools (`memory_reset`, `memory_inspect`, `memory_mode`) stay
|
|
194
|
+
> available either way.
|
|
159
195
|
|
|
160
196
|
## Under the hood
|
|
161
197
|
|
package/dist/opencode.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"memorize-extract": {
|
|
22
22
|
"mode": "subagent",
|
|
23
|
-
"prompt": "You are a memory extraction agent.
|
|
23
|
+
"prompt": "You are a memory extraction agent. The session transcript is provided inline in the prompt. Extract raw_memory, rollout_summary, and rollout_slug as JSON. Exclude AGENTS.md/instruction content. Redact secrets.",
|
|
24
24
|
"permission": {
|
|
25
25
|
"*": "deny",
|
|
26
26
|
"bash": "deny",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"websearch": "deny",
|
|
29
29
|
"task": "deny",
|
|
30
30
|
"todowrite": "deny",
|
|
31
|
-
"read": "
|
|
31
|
+
"read": "deny",
|
|
32
32
|
"write": "deny",
|
|
33
33
|
"edit": "deny",
|
|
34
|
-
"glob": "
|
|
35
|
-
"grep": "
|
|
34
|
+
"glob": "deny",
|
|
35
|
+
"grep": "deny"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|
package/dist/src/capture.d.ts
CHANGED
|
@@ -1,19 +1,44 @@
|
|
|
1
|
-
import { MemoryStore } from "./store.js";
|
|
1
|
+
import type { MemoryStore } from "./store.js";
|
|
2
2
|
export interface SessionRow {
|
|
3
3
|
id: string;
|
|
4
4
|
updated_at: number;
|
|
5
5
|
directory: string | null;
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Global session discovery through the official API: opencode's session.list
|
|
9
|
+
* is project-scoped, so enumerate projects (project.list) and list each one
|
|
10
|
+
* with scope=project (routes the request to that project's instance AND
|
|
11
|
+
* widens the filter from the session directory to the whole project).
|
|
12
|
+
* Instance contexts created this way are cached by the host for the process
|
|
13
|
+
* lifetime, and the whole pass is rate-limited (30s min interval).
|
|
14
|
+
*
|
|
15
|
+
* Fail-safe at two levels: a failed project.list skips the pass ([]), a
|
|
16
|
+
* failed per-project session.list skips that project — neither claims or
|
|
17
|
+
* finalizes any job. Transcript loading must NOT be fail-safe — see
|
|
18
|
+
* loadTranscript.
|
|
19
|
+
*/
|
|
20
|
+
export declare function listRecentSessions(limit?: number): Promise<SessionRow[]>;
|
|
8
21
|
export interface TranscriptMessage {
|
|
9
22
|
type: string;
|
|
10
23
|
role?: string;
|
|
11
24
|
text?: string;
|
|
12
25
|
}
|
|
13
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Transcript loading uses the official API — the same surface opencode's own
|
|
28
|
+
* UI renders history from; the session-scoped route resolves the right
|
|
29
|
+
* instance even for sessions from other projects.
|
|
30
|
+
*
|
|
31
|
+
* Errors PROPAGATE. An empty result must mean "session has no extractable
|
|
32
|
+
* content" — a swallowed error here used to surface as a successful
|
|
33
|
+
* no-output extraction, which deletes any previous extraction for the
|
|
34
|
+
* session (codex: load_rollout_items errors fail the job, which retries
|
|
35
|
+
* under its lease/backoff). A claimed session normally has messages, so a
|
|
36
|
+
* legitimately empty result is logged for observability.
|
|
37
|
+
*/
|
|
38
|
+
export declare function loadTranscript(sessionId: string): Promise<TranscriptMessage[]>;
|
|
14
39
|
export interface EligibilityOptions {
|
|
15
40
|
maxAgeDays: number;
|
|
16
41
|
minIdleHours: number;
|
|
17
42
|
excludeSession?: string;
|
|
18
43
|
}
|
|
19
|
-
export declare function selectEligibleSessions(store: MemoryStore, opts: EligibilityOptions): SessionRow[]
|
|
44
|
+
export declare function selectEligibleSessions(store: MemoryStore, opts: EligibilityOptions): Promise<SessionRow[]>;
|
package/dist/src/capture.js
CHANGED
|
@@ -1,76 +1,130 @@
|
|
|
1
|
-
import { Database } from "bun:sqlite";
|
|
2
|
-
import { opencodeDbPath } from "./paths.js";
|
|
3
1
|
import { SCAN_LIMIT } from "./store.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const p = opencodeDbPath();
|
|
2
|
+
import { getPluginInput } from "./llm.js";
|
|
3
|
+
const API_TIMEOUT_MS = 60_000;
|
|
4
|
+
async function withTimeout(promise, ms, label) {
|
|
5
|
+
let timer;
|
|
9
6
|
try {
|
|
10
|
-
|
|
7
|
+
return await Promise.race([
|
|
8
|
+
promise,
|
|
9
|
+
new Promise((_, reject) => {
|
|
10
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
11
|
+
}),
|
|
12
|
+
]);
|
|
11
13
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
finally {
|
|
15
|
+
clearTimeout(timer);
|
|
14
16
|
}
|
|
15
|
-
return opencodeDb;
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Global session discovery through the official API: opencode's session.list
|
|
20
|
+
* is project-scoped, so enumerate projects (project.list) and list each one
|
|
21
|
+
* with scope=project (routes the request to that project's instance AND
|
|
22
|
+
* widens the filter from the session directory to the whole project).
|
|
23
|
+
* Instance contexts created this way are cached by the host for the process
|
|
24
|
+
* lifetime, and the whole pass is rate-limited (30s min interval).
|
|
25
|
+
*
|
|
26
|
+
* Fail-safe at two levels: a failed project.list skips the pass ([]), a
|
|
27
|
+
* failed per-project session.list skips that project — neither claims or
|
|
28
|
+
* finalizes any job. Transcript loading must NOT be fail-safe — see
|
|
29
|
+
* loadTranscript.
|
|
30
|
+
*/
|
|
31
|
+
export async function listRecentSessions(limit = SCAN_LIMIT) {
|
|
32
|
+
const client = getPluginInput()?.client;
|
|
33
|
+
if (!client?.project?.list || !client?.session?.list)
|
|
20
34
|
return [];
|
|
35
|
+
let projects;
|
|
21
36
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
WHERE parent_id IS NULL AND title NOT LIKE 'codex-memory-%'
|
|
27
|
-
ORDER BY time_updated DESC LIMIT ?`)
|
|
28
|
-
.all(limit);
|
|
37
|
+
const res = await withTimeout(client.project.list(), API_TIMEOUT_MS, "project.list");
|
|
38
|
+
if (!res || res.error || !Array.isArray(res.data))
|
|
39
|
+
throw new Error(`project.list failed: ${JSON.stringify(res?.error ?? {})}`);
|
|
40
|
+
projects = res.data;
|
|
29
41
|
}
|
|
30
|
-
catch {
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.warn("[opencode-codex-memory] project discovery failed; skipping pass:", err);
|
|
31
44
|
return [];
|
|
32
45
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
const all = [];
|
|
47
|
+
for (const project of projects) {
|
|
48
|
+
if (!project?.worktree)
|
|
49
|
+
continue;
|
|
50
|
+
try {
|
|
51
|
+
const res = await withTimeout(client.session.list({
|
|
52
|
+
// scope/roots/limit are in the server's ListQuery since 1.17; the
|
|
53
|
+
// pinned SDK types lag behind, hence the cast at the call site.
|
|
54
|
+
query: { directory: project.worktree, scope: "project", roots: true, limit },
|
|
55
|
+
}), API_TIMEOUT_MS, "session.list");
|
|
56
|
+
if (!res || res.error || !Array.isArray(res.data))
|
|
57
|
+
throw new Error(JSON.stringify(res?.error ?? {}));
|
|
58
|
+
for (const s of res.data) {
|
|
59
|
+
// Top-level sessions only: task-tool children are summarized into
|
|
60
|
+
// their parent, and the plugin's own sub-sessions must never be
|
|
61
|
+
// memorized (roots=true drops children server-side; keep both belts).
|
|
62
|
+
if (!s?.id || s.parentID)
|
|
63
|
+
continue;
|
|
64
|
+
if (s.title && s.title.startsWith("codex-memory-"))
|
|
65
|
+
continue;
|
|
66
|
+
all.push({ id: s.id, updated_at: s.time?.updated ?? 0, directory: s.directory ?? null });
|
|
52
67
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
type: parsed.type ?? "unknown",
|
|
62
|
-
role,
|
|
63
|
-
text: extractText(parsed),
|
|
64
|
-
};
|
|
65
|
-
});
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.warn(`[opencode-codex-memory] session.list failed for ${project.worktree}; skipping project:`, err);
|
|
71
|
+
}
|
|
66
72
|
}
|
|
67
|
-
|
|
73
|
+
all.sort((a, b) => b.updated_at - a.updated_at);
|
|
74
|
+
return all.slice(0, limit);
|
|
75
|
+
}
|
|
76
|
+
/** Official transcript surface: GET /session/{id}/message via the plugin's authenticated client. */
|
|
77
|
+
async function fetchMessagesViaApi(sessionId) {
|
|
78
|
+
const client = getPluginInput()?.client;
|
|
79
|
+
if (typeof client?.session?.messages !== "function") {
|
|
80
|
+
throw new Error("plugin client unavailable; cannot load transcript");
|
|
81
|
+
}
|
|
82
|
+
const res = await withTimeout(client.session.messages({ path: { id: sessionId } }), API_TIMEOUT_MS, "session.messages");
|
|
83
|
+
if (!res || res.error || !Array.isArray(res.data)) {
|
|
84
|
+
throw new Error(`session.messages failed: ${JSON.stringify(res?.error ?? {})}`);
|
|
85
|
+
}
|
|
86
|
+
return res.data;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Transcript loading uses the official API — the same surface opencode's own
|
|
90
|
+
* UI renders history from; the session-scoped route resolves the right
|
|
91
|
+
* instance even for sessions from other projects.
|
|
92
|
+
*
|
|
93
|
+
* Errors PROPAGATE. An empty result must mean "session has no extractable
|
|
94
|
+
* content" — a swallowed error here used to surface as a successful
|
|
95
|
+
* no-output extraction, which deletes any previous extraction for the
|
|
96
|
+
* session (codex: load_rollout_items errors fail the job, which retries
|
|
97
|
+
* under its lease/backoff). A claimed session normally has messages, so a
|
|
98
|
+
* legitimately empty result is logged for observability.
|
|
99
|
+
*/
|
|
100
|
+
export async function loadTranscript(sessionId) {
|
|
101
|
+
const rows = await fetchMessagesViaApi(sessionId);
|
|
102
|
+
if (rows.length === 0) {
|
|
103
|
+
console.warn(`[opencode-codex-memory] session.messages returned no messages for claimed session ${sessionId}`);
|
|
68
104
|
return [];
|
|
69
105
|
}
|
|
106
|
+
// One entry per part — the granularity extraction expects.
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const row of rows) {
|
|
109
|
+
const role = row?.info?.role;
|
|
110
|
+
for (const part of row?.parts ?? []) {
|
|
111
|
+
out.push({
|
|
112
|
+
type: part?.type ?? "unknown",
|
|
113
|
+
role,
|
|
114
|
+
text: extractText(part),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
70
119
|
}
|
|
71
120
|
function extractText(msg) {
|
|
72
121
|
if (!msg)
|
|
73
122
|
return undefined;
|
|
123
|
+
// codex excludes reasoning items from extraction transcripts
|
|
124
|
+
// (rollout policy: ResponseItem::Reasoning => false); opencode reasoning
|
|
125
|
+
// parts carry `text`, so they must be dropped before the text check.
|
|
126
|
+
if (msg.type === "reasoning")
|
|
127
|
+
return undefined;
|
|
74
128
|
if (typeof msg.text === "string")
|
|
75
129
|
return msg.text;
|
|
76
130
|
if (msg.type === "tool") {
|
|
@@ -98,11 +152,11 @@ function extractText(msg) {
|
|
|
98
152
|
}
|
|
99
153
|
return undefined;
|
|
100
154
|
}
|
|
101
|
-
export function selectEligibleSessions(store, opts) {
|
|
155
|
+
export async function selectEligibleSessions(store, opts) {
|
|
102
156
|
const now = Date.now();
|
|
103
157
|
const minUpdated = now - opts.maxAgeDays * 24 * 60 * 60 * 1000;
|
|
104
158
|
const maxUpdated = now - opts.minIdleHours * 60 * 60 * 1000;
|
|
105
|
-
const sessions = listRecentSessions();
|
|
159
|
+
const sessions = await listRecentSessions();
|
|
106
160
|
return sessions.filter((s) => {
|
|
107
161
|
if (opts.excludeSession && s.id === opts.excludeSession)
|
|
108
162
|
return false;
|
package/dist/src/git-baseline.js
CHANGED
|
@@ -62,8 +62,8 @@ async function commitBaseline(dir) {
|
|
|
62
62
|
* without any commit gets a fresh baseline.
|
|
63
63
|
*/
|
|
64
64
|
export async function ensureBaseline() {
|
|
65
|
+
const dir = memoryRoot();
|
|
65
66
|
try {
|
|
66
|
-
const dir = memoryRoot();
|
|
67
67
|
removeDiffArtifact(dir);
|
|
68
68
|
await ensureInit(dir);
|
|
69
69
|
if (!(await hasHeadCommit(dir))) {
|
|
@@ -72,8 +72,20 @@ export async function ensureBaseline() {
|
|
|
72
72
|
return true;
|
|
73
73
|
}
|
|
74
74
|
catch (err) {
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// codex ensure_git_baseline_repository: unusable/corrupt git metadata is
|
|
76
|
+
// recovered by a destructive fresh re-init (reset_git_repository_sync)
|
|
77
|
+
// instead of failing the job forever.
|
|
78
|
+
console.error("[opencode-codex-memory] ensureBaseline error, re-initializing baseline:", err);
|
|
79
|
+
try {
|
|
80
|
+
fs.rmSync(path.join(dir, ".git"), { recursive: true, force: true });
|
|
81
|
+
await isogit.init({ fs, dir });
|
|
82
|
+
await commitBaseline(dir);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch (err2) {
|
|
86
|
+
console.error("[opencode-codex-memory] baseline re-init failed:", err2);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
91
|
async function readBaselineText(dir, headOid, filepath) {
|
|
@@ -93,41 +105,38 @@ function readWorkdirText(dir, filepath) {
|
|
|
93
105
|
return "";
|
|
94
106
|
}
|
|
95
107
|
}
|
|
108
|
+
// Throws on failure: codex fails the phase-2 job on workspace-status errors
|
|
109
|
+
// (failed_workspace_status). Swallowing the error here would make an errored
|
|
110
|
+
// diff indistinguishable from "no changes" and falsely mark the job succeeded.
|
|
96
111
|
export async function captureWorkspaceDiff() {
|
|
112
|
+
const dir = memoryRoot();
|
|
113
|
+
await ensureInit(dir);
|
|
114
|
+
removeDiffArtifact(dir);
|
|
115
|
+
const matrix = await isogit.statusMatrix({ fs, dir });
|
|
116
|
+
const changedRows = matrix.filter(([filepath, head, workdir]) => head !== workdir && filepath !== DIFF_ARTIFACT);
|
|
117
|
+
const changes = changedRows.map(([filepath, head, workdir]) => {
|
|
118
|
+
if (head === 0)
|
|
119
|
+
return { status: "A", path: filepath };
|
|
120
|
+
if (workdir === 0)
|
|
121
|
+
return { status: "D", path: filepath };
|
|
122
|
+
return { status: "M", path: filepath };
|
|
123
|
+
});
|
|
124
|
+
let headOid = null;
|
|
97
125
|
try {
|
|
98
|
-
|
|
99
|
-
await ensureInit(dir);
|
|
100
|
-
removeDiffArtifact(dir);
|
|
101
|
-
const matrix = await isogit.statusMatrix({ fs, dir });
|
|
102
|
-
const changedRows = matrix.filter(([filepath, head, workdir]) => head !== workdir && filepath !== DIFF_ARTIFACT);
|
|
103
|
-
const changes = changedRows.map(([filepath, head, workdir]) => {
|
|
104
|
-
if (head === 0)
|
|
105
|
-
return { status: "A", path: filepath };
|
|
106
|
-
if (workdir === 0)
|
|
107
|
-
return { status: "D", path: filepath };
|
|
108
|
-
return { status: "M", path: filepath };
|
|
109
|
-
});
|
|
110
|
-
let headOid = null;
|
|
111
|
-
try {
|
|
112
|
-
headOid = await isogit.resolveRef({ fs, dir, ref: "HEAD" });
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// no commits yet — every file diffs against empty
|
|
116
|
-
}
|
|
117
|
-
const patches = [];
|
|
118
|
-
for (const [filepath, head, workdir] of changedRows) {
|
|
119
|
-
const oldText = head === 1 && headOid ? await readBaselineText(dir, headOid, filepath) : "";
|
|
120
|
-
const newText = workdir === 0 ? "" : readWorkdirText(dir, filepath);
|
|
121
|
-
// No per-file cap: codex renders every file's patch in full and relies
|
|
122
|
-
// on the global 4 MiB truncation in writeWorkspaceDiff.
|
|
123
|
-
patches.push(createPatch(filepath, oldText, newText));
|
|
124
|
-
}
|
|
125
|
-
return { changes, unifiedDiff: patches.join("\n") };
|
|
126
|
+
headOid = await isogit.resolveRef({ fs, dir, ref: "HEAD" });
|
|
126
127
|
}
|
|
127
|
-
catch
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
catch {
|
|
129
|
+
// no commits yet — every file diffs against empty
|
|
130
|
+
}
|
|
131
|
+
const patches = [];
|
|
132
|
+
for (const [filepath, head, workdir] of changedRows) {
|
|
133
|
+
const oldText = head === 1 && headOid ? await readBaselineText(dir, headOid, filepath) : "";
|
|
134
|
+
const newText = workdir === 0 ? "" : readWorkdirText(dir, filepath);
|
|
135
|
+
// No per-file cap: codex renders every file's patch in full and relies
|
|
136
|
+
// on the global 4 MiB truncation in writeWorkspaceDiff.
|
|
137
|
+
patches.push(createPatch(filepath, oldText, newText));
|
|
130
138
|
}
|
|
139
|
+
return { changes, unifiedDiff: patches.join("\n") };
|
|
131
140
|
}
|
|
132
141
|
/**
|
|
133
142
|
* Mirrors codex reset_git_repository: delete .git and re-create a fresh
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { MemoryStore } from "./store.js";
|
|
2
2
|
import type { PluginInput, PluginOptions } from "@opencode-ai/plugin";
|
|
3
3
|
export declare function takeNewCitations(partKey: string, ids: string[]): string[];
|
|
4
|
+
export declare function markTurnSeen(sessionId: string): boolean;
|
|
5
|
+
export declare function shouldHandleIdle(sessionId: string, now?: number): boolean;
|
|
4
6
|
export declare function handleSessionDeleted(sessionId: string, store?: Pick<MemoryStore, "deleteSessionMemory">, schedulePhase2?: () => void): void;
|
|
5
7
|
declare const _default: {
|
|
6
8
|
id: string;
|
|
@@ -22,16 +24,32 @@ declare const _default: {
|
|
|
22
24
|
memory_search: {
|
|
23
25
|
description: string;
|
|
24
26
|
args: {
|
|
25
|
-
|
|
27
|
+
queries: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
|
|
28
|
+
match_mode: import("zod").ZodDefault<import("zod").ZodEnum<{
|
|
29
|
+
any: "any";
|
|
30
|
+
all_on_same_line: "all_on_same_line";
|
|
31
|
+
all_within_lines: "all_within_lines";
|
|
32
|
+
}>>;
|
|
33
|
+
line_count: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
34
|
+
path: import("zod").ZodOptional<import("zod").ZodString>;
|
|
35
|
+
cursor: import("zod").ZodOptional<import("zod").ZodString>;
|
|
36
|
+
context_lines: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
26
37
|
case_sensitive: import("zod").ZodDefault<import("zod").ZodBoolean>;
|
|
38
|
+
normalized: import("zod").ZodDefault<import("zod").ZodBoolean>;
|
|
27
39
|
since: import("zod").ZodOptional<import("zod").ZodString>;
|
|
28
40
|
until: import("zod").ZodOptional<import("zod").ZodString>;
|
|
29
|
-
|
|
41
|
+
max_results: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
30
42
|
};
|
|
31
43
|
execute(args: {
|
|
44
|
+
match_mode: "any" | "all_on_same_line" | "all_within_lines";
|
|
45
|
+
context_lines: number;
|
|
32
46
|
case_sensitive: boolean;
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
normalized: boolean;
|
|
48
|
+
max_results: number;
|
|
49
|
+
queries?: string[] | undefined;
|
|
50
|
+
line_count?: number | undefined;
|
|
51
|
+
path?: string | undefined;
|
|
52
|
+
cursor?: string | undefined;
|
|
35
53
|
since?: string | undefined;
|
|
36
54
|
until?: string | undefined;
|
|
37
55
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
@@ -131,6 +149,22 @@ declare const _default: {
|
|
|
131
149
|
}, output: {
|
|
132
150
|
system: string[];
|
|
133
151
|
}): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Fires at text-end, before opencode persists the final part text
|
|
154
|
+
* (session/processor.ts): the returned text replaces the stored one.
|
|
155
|
+
* Primary citation seam — records usage and strips the block so neither
|
|
156
|
+
* the UI nor history ever shows citation markup (matches codex, which
|
|
157
|
+
* strips from the displayed/persisted message). The event and
|
|
158
|
+
* messages.transform paths below stay as fallbacks for older opencode
|
|
159
|
+
* hosts and for history persisted before this hook existed.
|
|
160
|
+
*/
|
|
161
|
+
"experimental.text.complete"(input: {
|
|
162
|
+
sessionID: string;
|
|
163
|
+
messageID: string;
|
|
164
|
+
partID: string;
|
|
165
|
+
}, output: {
|
|
166
|
+
text: string;
|
|
167
|
+
}): Promise<void>;
|
|
134
168
|
"experimental.chat.messages.transform"(_input: unknown, output: {
|
|
135
169
|
messages: {
|
|
136
170
|
info: {
|
|
@@ -142,6 +176,24 @@ declare const _default: {
|
|
|
142
176
|
}[];
|
|
143
177
|
}[];
|
|
144
178
|
}): Promise<void>;
|
|
179
|
+
/**
|
|
180
|
+
* Turn start. codex stamps memory_mode at thread creation (session.rs) and
|
|
181
|
+
* schedules memory work per startup/turn; the first user message is the
|
|
182
|
+
* closest plugin-visible moment. Stamping here (instead of waiting for the
|
|
183
|
+
* first idle) means a session created while generate_memories=false keeps
|
|
184
|
+
* its 'disabled' stamp even if the option flips mid-session, and the
|
|
185
|
+
* phase-1 pump no longer depends on idle events at all. The idle path
|
|
186
|
+
* below stays as a second pump trigger; both are cheap (stamp is INSERT OR
|
|
187
|
+
* IGNORE, the pump is gated by in-flight/rate/claim guards).
|
|
188
|
+
*/
|
|
189
|
+
"chat.message"(input: {
|
|
190
|
+
sessionID?: string;
|
|
191
|
+
}): Promise<void>;
|
|
192
|
+
"tool.execute.after"(input: {
|
|
193
|
+
tool: string;
|
|
194
|
+
sessionID: string;
|
|
195
|
+
callID: string;
|
|
196
|
+
}): Promise<void>;
|
|
145
197
|
event(input: {
|
|
146
198
|
event: {
|
|
147
199
|
type: string;
|