opencode-codex-memory 0.6.3 → 0.6.4
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 +30 -47
- package/dist/src/host-client.d.ts +8 -0
- package/dist/src/host-client.js +12 -0
- package/dist/src/llm.d.ts +2 -0
- package/dist/src/llm.js +62 -6
- package/dist/src/reasoning-variant.d.ts +5 -0
- package/dist/src/reasoning-variant.js +53 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,55 +48,33 @@ If you want the mental model before the details, jump to
|
|
|
48
48
|
|
|
49
49
|
## Install
|
|
50
50
|
|
|
51
|
-
1. Add the plugin to your `~/.config/opencode/opencode.json`:
|
|
51
|
+
**1. Add the plugin** to your `~/.config/opencode/opencode.json`:
|
|
52
52
|
|
|
53
53
|
```json
|
|
54
54
|
{
|
|
55
|
-
"plugin": ["opencode-codex-memory@0.6.
|
|
55
|
+
"plugin": ["opencode-codex-memory@0.6.4"]
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
instead of the package name.)
|
|
79
|
-
|
|
80
|
-
2. That's it. The memory workspace is created on first use. Installing the
|
|
81
|
-
plugin is the opt-in: background learning and summary injection are active
|
|
82
|
-
immediately (Codex ships the same system behind a default-off feature flag
|
|
83
|
-
with a consent prompt; a standalone memory plugin *is* the consent).
|
|
84
|
-
|
|
85
|
-
Requires OpenCode 1.18 or newer (official release). Git is bundled
|
|
86
|
-
(`isomorphic-git`) — no `git` binary or any other external tool needed.
|
|
87
|
-
|
|
88
|
-
The two restricted sub-agents that do the background learning (`memorize`,
|
|
89
|
-
`memorize-extract`) register themselves automatically while background learning
|
|
90
|
-
is enabled. To choose which models
|
|
91
|
-
they use, set the `extract_model` / `consolidation_model` plugin options (see
|
|
92
|
-
[Configuration](#configuration)) — don't override the agents for that. Defining
|
|
93
|
-
an agent with the same name in your own config is only for advanced tweaks
|
|
94
|
-
(e.g. permissions); your definition then replaces the shipped one. If you
|
|
95
|
-
override `memorize`, keep an `external_directory` allow for
|
|
96
|
-
`~/.local/share/opencode/memories/*` (e.g.
|
|
97
|
-
`"external_directory": { "$HOME/.local/share/opencode/memories/*": "allow" }`
|
|
98
|
-
after the wildcard deny) — the memory folder lives outside your project, and
|
|
99
|
-
without that grant OpenCode blocks the consolidator's file access.
|
|
59
|
+
Pin the version — OpenCode installs a plugin spec once and never re-resolves
|
|
60
|
+
it, so a bare `"opencode-codex-memory"` is not "always latest". Check
|
|
61
|
+
[npm](https://www.npmjs.com/package/opencode-codex-memory) for the current
|
|
62
|
+
version.
|
|
63
|
+
|
|
64
|
+
**2. That's it.** The memory workspace is created on first use; background
|
|
65
|
+
learning starts immediately.
|
|
66
|
+
|
|
67
|
+
Requires OpenCode 1.18 or newer. Models and other options: see
|
|
68
|
+
[Configuration](#configuration).
|
|
69
|
+
|
|
70
|
+
### Installation hints
|
|
71
|
+
|
|
72
|
+
To bump pins, copy
|
|
73
|
+
[`docs/commands/update-plugins.md`](./docs/commands/update-plugins.md) to
|
|
74
|
+
`~/.config/opencode/command/update-plugins.md` and run `/update-plugins`.
|
|
75
|
+
This command is not part of the plugin package — nothing about it is
|
|
76
|
+
installed with the plugin. It works for every pinned plugin in your config,
|
|
77
|
+
not just this one.
|
|
100
78
|
|
|
101
79
|
## Try it
|
|
102
80
|
|
|
@@ -254,7 +232,7 @@ To set options, turn the plugin entry into a `[name, options]` pair:
|
|
|
254
232
|
```json
|
|
255
233
|
{
|
|
256
234
|
"plugin": [
|
|
257
|
-
["opencode-codex-memory@0.6.
|
|
235
|
+
["opencode-codex-memory@0.6.4", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
|
|
258
236
|
]
|
|
259
237
|
}
|
|
260
238
|
```
|
|
@@ -288,6 +266,11 @@ on your own `memorize-extract`/`memorize` agent definition, if you overrode
|
|
|
288
266
|
one → the provider's default model. Note that the first two pass the model
|
|
289
267
|
explicitly, so they win over an agent-level `model`.
|
|
290
268
|
|
|
269
|
+
Reasoning effort follows Codex (`low` for extraction, `medium` for
|
|
270
|
+
consolidation) when the model lists that name. Otherwise the plugin picks the
|
|
271
|
+
nearest OpenCode variant on `none < minimal < low < medium < high < xhigh < max`
|
|
272
|
+
(ties go higher). No listed efforts → no pin, host default.
|
|
273
|
+
|
|
291
274
|
> Note: `dedicated_tools` defaults to `true` here (Codex defaults it to `false`).
|
|
292
275
|
> This is the one intentional default difference — the tools are a core part of a
|
|
293
276
|
> standalone memory plugin. Everything else matches Codex's defaults.
|
|
@@ -316,7 +299,7 @@ Off by default; no changes to Codex's own config are required.
|
|
|
316
299
|
{
|
|
317
300
|
"plugin": [
|
|
318
301
|
[
|
|
319
|
-
"opencode-codex-memory@0.6.
|
|
302
|
+
"opencode-codex-memory@0.6.4",
|
|
320
303
|
{ "codex_interop": { "import": true, "export": true } }
|
|
321
304
|
]
|
|
322
305
|
]
|
|
@@ -373,7 +356,7 @@ from the project memories Claude already keeps on your machine. **One-way only**
|
|
|
373
356
|
```json
|
|
374
357
|
{
|
|
375
358
|
"plugin": [
|
|
376
|
-
["opencode-codex-memory@0.6.
|
|
359
|
+
["opencode-codex-memory@0.6.4", { "claude_import": { "enabled": true } }]
|
|
377
360
|
]
|
|
378
361
|
}
|
|
379
362
|
```
|
|
@@ -400,7 +383,7 @@ Claude names each project with an opaque id (a folder under
|
|
|
400
383
|
{
|
|
401
384
|
"plugin": [
|
|
402
385
|
[
|
|
403
|
-
"opencode-codex-memory@0.6.
|
|
386
|
+
"opencode-codex-memory@0.6.4",
|
|
404
387
|
{
|
|
405
388
|
"claude_import": {
|
|
406
389
|
"enabled": true,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - AssistantMessage.info.structured: structured extraction result
|
|
11
11
|
* - client._client.get: experimental routes (no experimental.* namespace on V1)
|
|
12
12
|
* - mcp.status: present on host client, weakly typed in plugin package
|
|
13
|
+
* - provider.list model.variants omitted from V1 OpenAPI types
|
|
13
14
|
*/
|
|
14
15
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
15
16
|
export type HostHttpGet = (opts: {
|
|
@@ -45,6 +46,13 @@ export interface SessionCreateBody {
|
|
|
45
46
|
title?: string;
|
|
46
47
|
metadata?: Record<string, unknown>;
|
|
47
48
|
}
|
|
49
|
+
/** GET /provider. V1 SDK types omit model.variants; parse the payload loosely. */
|
|
50
|
+
export declare function hostProviderList(client: PluginInput["client"] | null | undefined, opts?: {
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}): Promise<{
|
|
53
|
+
error?: unknown;
|
|
54
|
+
data?: unknown;
|
|
55
|
+
}>;
|
|
48
56
|
/** session.create with metadata (SDK body type is incomplete). */
|
|
49
57
|
export declare function hostSessionCreate(client: PluginInput["client"], opts: {
|
|
50
58
|
directory: string;
|
package/dist/src/host-client.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - AssistantMessage.info.structured: structured extraction result
|
|
11
11
|
* - client._client.get: experimental routes (no experimental.* namespace on V1)
|
|
12
12
|
* - mcp.status: present on host client, weakly typed in plugin package
|
|
13
|
+
* - provider.list model.variants omitted from V1 OpenAPI types
|
|
13
14
|
*/
|
|
14
15
|
/** Hey-api transport on PluginInput.client for routes the V1 surface lags on. */
|
|
15
16
|
export function pluginHttpGet(client) {
|
|
@@ -64,6 +65,17 @@ export async function withHostTimeout(promise, ms, label, abort) {
|
|
|
64
65
|
clearTimeout(timer);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
68
|
+
/** GET /provider. V1 SDK types omit model.variants; parse the payload loosely. */
|
|
69
|
+
export async function hostProviderList(client, opts) {
|
|
70
|
+
const list = client?.provider?.list;
|
|
71
|
+
if (typeof list === "function") {
|
|
72
|
+
return (await list(opts?.signal ? { signal: opts.signal } : undefined));
|
|
73
|
+
}
|
|
74
|
+
const get = pluginHttpGet(client);
|
|
75
|
+
if (!get)
|
|
76
|
+
return { error: { message: "provider.list unavailable" } };
|
|
77
|
+
return get({ url: "/provider", ...(opts?.signal ? { signal: opts.signal } : {}) });
|
|
78
|
+
}
|
|
67
79
|
/** session.create with metadata (SDK body type is incomplete). */
|
|
68
80
|
export async function hostSessionCreate(client, opts) {
|
|
69
81
|
return client.session.create({
|
package/dist/src/llm.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare function setSubSessionCreateTimeoutForTest(ms?: number): void;
|
|
|
11
11
|
/** Test seam. */
|
|
12
12
|
export declare function setConfigGetTimeoutForTest(ms?: number): void;
|
|
13
13
|
/** Test seam. */
|
|
14
|
+
export declare function setProviderListTimeoutForTest(ms?: number): void;
|
|
15
|
+
/** Test seam. */
|
|
14
16
|
export declare function setStaleDeleteBatchTimeoutForTest(ms?: number): void;
|
|
15
17
|
export declare function isMemorySubSession(sessionId: string): boolean;
|
|
16
18
|
export declare function resolveExtractionModel(configured?: string): Promise<string | undefined>;
|
package/dist/src/llm.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { memoryRoot } from "./paths.js";
|
|
4
|
-
import { hostListSessionsGlobal, hostSessionCreate, hostSessionDeletionConfirmed, hostSessionPrompt, hostStructuredOutput, ignoreLateRejection, pluginHttpGet, withHostTimeout, } from "./host-client.js";
|
|
4
|
+
import { hostListSessionsGlobal, hostProviderList, hostSessionCreate, hostSessionDeletionConfirmed, hostSessionPrompt, hostStructuredOutput, ignoreLateRejection, pluginHttpGet, withHostTimeout, } from "./host-client.js";
|
|
5
|
+
import { catalogVariantKeys, nearestReasoningVariant } from "./reasoning-variant.js";
|
|
5
6
|
import { isPluginShuttingDown, pluginShutdownSignal } from "./lifecycle.js";
|
|
6
7
|
import { SCAN_LIMIT } from "./store.js";
|
|
7
8
|
import { isProviderCapacityError, ProviderCapacityError } from "./ratelimit.js";
|
|
@@ -12,6 +13,8 @@ export function setPluginInput(input) {
|
|
|
12
13
|
inputGeneration++;
|
|
13
14
|
configModels = null;
|
|
14
15
|
configModelsInFlight = null;
|
|
16
|
+
providerCatalog = null;
|
|
17
|
+
providerCatalogInFlight = null;
|
|
15
18
|
}
|
|
16
19
|
export function getPluginInput() {
|
|
17
20
|
return inputRef;
|
|
@@ -27,10 +30,12 @@ const SUBSESSION_CONFIRM_TIMEOUT_MS = 1_000;
|
|
|
27
30
|
const SUBSESSION_DELETE_TIMEOUT_MS = 10_000;
|
|
28
31
|
const SUBSESSION_CREATE_TIMEOUT_MS = 10_000;
|
|
29
32
|
const CONFIG_GET_TIMEOUT_MS = 5_000;
|
|
33
|
+
const PROVIDER_LIST_TIMEOUT_MS = 5_000;
|
|
30
34
|
const SUBSESSION_DELETE_CONCURRENCY = 8;
|
|
31
35
|
const SUBSESSION_DELETE_BATCH_TIMEOUT_MS = 30_000;
|
|
32
36
|
let createTimeoutMs = SUBSESSION_CREATE_TIMEOUT_MS;
|
|
33
37
|
let configGetTimeoutMs = CONFIG_GET_TIMEOUT_MS;
|
|
38
|
+
let providerListTimeoutMs = PROVIDER_LIST_TIMEOUT_MS;
|
|
34
39
|
let staleDeleteBatchTimeoutMs = SUBSESSION_DELETE_BATCH_TIMEOUT_MS;
|
|
35
40
|
/** Test seam. */
|
|
36
41
|
export function setSubSessionCreateTimeoutForTest(ms) {
|
|
@@ -41,6 +46,10 @@ export function setConfigGetTimeoutForTest(ms) {
|
|
|
41
46
|
configGetTimeoutMs = ms ?? CONFIG_GET_TIMEOUT_MS;
|
|
42
47
|
}
|
|
43
48
|
/** Test seam. */
|
|
49
|
+
export function setProviderListTimeoutForTest(ms) {
|
|
50
|
+
providerListTimeoutMs = ms ?? PROVIDER_LIST_TIMEOUT_MS;
|
|
51
|
+
}
|
|
52
|
+
/** Test seam. */
|
|
44
53
|
export function setStaleDeleteBatchTimeoutForTest(ms) {
|
|
45
54
|
staleDeleteBatchTimeoutMs = ms ?? SUBSESSION_DELETE_BATCH_TIMEOUT_MS;
|
|
46
55
|
}
|
|
@@ -142,6 +151,53 @@ function parseModelRef(ref) {
|
|
|
142
151
|
return null;
|
|
143
152
|
return { providerID: ref.slice(0, slash), modelID: ref.slice(slash + 1) };
|
|
144
153
|
}
|
|
154
|
+
const EXTRACT_VARIANT = "low";
|
|
155
|
+
const CONSOLIDATE_VARIANT = "medium";
|
|
156
|
+
let providerCatalog = null;
|
|
157
|
+
let providerCatalogInFlight = null;
|
|
158
|
+
async function getProviderCatalog() {
|
|
159
|
+
if (providerCatalog)
|
|
160
|
+
return providerCatalog.data;
|
|
161
|
+
if (providerCatalogInFlight)
|
|
162
|
+
return providerCatalogInFlight;
|
|
163
|
+
const input = getPluginInput();
|
|
164
|
+
if (!input)
|
|
165
|
+
return null;
|
|
166
|
+
const generation = inputGeneration;
|
|
167
|
+
const request = (async () => {
|
|
168
|
+
const controller = new AbortController();
|
|
169
|
+
try {
|
|
170
|
+
const res = await withHostTimeout(hostProviderList(input.client, { signal: controller.signal }), providerListTimeoutMs, "provider.list", controller);
|
|
171
|
+
if (res.error || res.data == null || generation !== inputGeneration)
|
|
172
|
+
return null;
|
|
173
|
+
providerCatalog = { data: res.data };
|
|
174
|
+
return res.data;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
})();
|
|
180
|
+
providerCatalogInFlight = request;
|
|
181
|
+
try {
|
|
182
|
+
return await request;
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
if (providerCatalogInFlight === request)
|
|
186
|
+
providerCatalogInFlight = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function resolveReasoningVariant(preferred, modelRef) {
|
|
190
|
+
const parsed = modelRef ? parseModelRef(modelRef) : null;
|
|
191
|
+
if (!parsed)
|
|
192
|
+
return preferred;
|
|
193
|
+
const catalog = await getProviderCatalog();
|
|
194
|
+
if (catalog == null)
|
|
195
|
+
return preferred;
|
|
196
|
+
const keys = catalogVariantKeys(catalog, parsed.providerID, parsed.modelID);
|
|
197
|
+
if (keys === undefined)
|
|
198
|
+
return preferred;
|
|
199
|
+
return nearestReasoningVariant(preferred, keys);
|
|
200
|
+
}
|
|
145
201
|
/**
|
|
146
202
|
* Thrown when a sub-agent prompt exceeds its budget. A distinct type (rather
|
|
147
203
|
* than matching on the message text) is what tells the catch below that the
|
|
@@ -338,9 +394,9 @@ export async function extractViaSubagent(sessionId, transcript, opts = {}) {
|
|
|
338
394
|
// call (toolChoice: required) — which is why memorize-extract must allow
|
|
339
395
|
// that one otherwise-denied tool.
|
|
340
396
|
format: { type: "json_schema", schema: EXTRACTION_SCHEMA },
|
|
341
|
-
// Codex extraction ReasoningEffort::Low. Host maps variant → reasoningEffort
|
|
342
|
-
//
|
|
343
|
-
variant:
|
|
397
|
+
// Codex extraction ReasoningEffort::Low. Host maps variant → reasoningEffort.
|
|
398
|
+
// If the model has no `low`, pick the nearest listed effort (host-only).
|
|
399
|
+
variant: await resolveReasoningVariant(EXTRACT_VARIANT, model),
|
|
344
400
|
});
|
|
345
401
|
// The captured JSON lands on AssistantMessage.structured (schema
|
|
346
402
|
// v1/session.ts; absent from the generated SDK type — see host-client.ts).
|
|
@@ -376,8 +432,8 @@ export async function consolidateViaSubagent(memoryRoot, diffFileName, model, si
|
|
|
376
432
|
model: resolved,
|
|
377
433
|
timeoutMs: CONSOLIDATION_TIMEOUT_MS,
|
|
378
434
|
signal,
|
|
379
|
-
// Codex consolidation ReasoningEffort::Medium.
|
|
380
|
-
variant:
|
|
435
|
+
// Codex consolidation ReasoningEffort::Medium. Nearest listed effort if missing.
|
|
436
|
+
variant: await resolveReasoningVariant(CONSOLIDATE_VARIANT, resolved),
|
|
381
437
|
});
|
|
382
438
|
}
|
|
383
439
|
catch (err) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const REASONING_VARIANT_LADDER: readonly ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
2
|
+
export type ReasoningVariant = (typeof REASONING_VARIANT_LADDER)[number];
|
|
3
|
+
export declare function nearestReasoningVariant(preferred: string, available: Iterable<string>): string | undefined;
|
|
4
|
+
export declare function enabledVariantKeys(variants: unknown): string[];
|
|
5
|
+
export declare function catalogVariantKeys(data: unknown, providerID: string, modelID: string): string[] | undefined;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const REASONING_VARIANT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
2
|
+
export function nearestReasoningVariant(preferred, available) {
|
|
3
|
+
const have = new Set(available);
|
|
4
|
+
if (have.has(preferred))
|
|
5
|
+
return preferred;
|
|
6
|
+
const prefIdx = REASONING_VARIANT_LADDER.indexOf(preferred);
|
|
7
|
+
if (prefIdx < 0)
|
|
8
|
+
return undefined;
|
|
9
|
+
let best;
|
|
10
|
+
let bestDist = Infinity;
|
|
11
|
+
let bestIdx = -1;
|
|
12
|
+
for (let idx = 0; idx < REASONING_VARIANT_LADDER.length; idx++) {
|
|
13
|
+
const name = REASONING_VARIANT_LADDER[idx];
|
|
14
|
+
if (!have.has(name))
|
|
15
|
+
continue;
|
|
16
|
+
const dist = Math.abs(idx - prefIdx);
|
|
17
|
+
if (dist < bestDist || (dist === bestDist && idx > bestIdx)) {
|
|
18
|
+
best = name;
|
|
19
|
+
bestDist = dist;
|
|
20
|
+
bestIdx = idx;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return best;
|
|
24
|
+
}
|
|
25
|
+
export function enabledVariantKeys(variants) {
|
|
26
|
+
if (!variants || typeof variants !== "object" || Array.isArray(variants))
|
|
27
|
+
return [];
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const [name, value] of Object.entries(variants)) {
|
|
30
|
+
if (value && typeof value === "object" && !Array.isArray(value) && value.disabled === true) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
out.push(name);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
export function catalogVariantKeys(data, providerID, modelID) {
|
|
38
|
+
if (!data || typeof data !== "object")
|
|
39
|
+
return undefined;
|
|
40
|
+
const root = data;
|
|
41
|
+
const all = Array.isArray(root.all) ? root.all : Array.isArray(root.providers) ? root.providers : undefined;
|
|
42
|
+
if (!all)
|
|
43
|
+
return undefined;
|
|
44
|
+
const provider = all.find((item) => item && typeof item === "object" && item.id === providerID);
|
|
45
|
+
if (!provider?.models || typeof provider.models !== "object")
|
|
46
|
+
return undefined;
|
|
47
|
+
const model = provider.models[modelID];
|
|
48
|
+
if (!model || typeof model !== "object" || Array.isArray(model))
|
|
49
|
+
return undefined;
|
|
50
|
+
if (!Object.prototype.hasOwnProperty.call(model, "variants"))
|
|
51
|
+
return undefined;
|
|
52
|
+
return enabledVariantKeys(model.variants);
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-codex-memory",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Persistent memory plugin for opencode — ports codex's two-phase memory system (extraction → consolidation → injection → citation feedback)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|