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 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.3"]
55
+ "plugin": ["opencode-codex-memory@0.6.4"]
56
56
  }
57
57
  ```
58
58
 
59
- **Pin the version** (here and for any OpenCode plugin). OpenCode installs a
60
- plugin spec once into its package cache and never re-resolves it, so a bare
61
- `"opencode-codex-memory"` is not "always latest" — it freezes at whatever
62
- was latest the first time OpenCode started. With a version tag you decide
63
- which release runs, and bumping the tag installs the new one. Check
64
- [npm](https://www.npmjs.com/package/opencode-codex-memory) for the current
65
- version.
66
-
67
- Since updating is then just editing version numbers, it's worth automating
68
- with a custom command — one that reads your `opencode.json`, compares each
69
- pin against npm, and rewrites the ones you confirm. If you'd rather not
70
- write your own, this repo has one you can copy:
71
- [`docs/commands/update-plugins.md`](./docs/commands/update-plugins.md) →
72
- `~/.config/opencode/command/update-plugins.md`, then run `/update-plugins`.
73
- It's documentation, not part of the package: nothing about it is installed
74
- with the plugin, and it works for every pinned plugin in your config, not
75
- just this one.
76
-
77
- (While developing locally, point it at an absolute path to your checkout
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.3", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
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.3",
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.3", { "claude_import": { "enabled": true } }]
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.3",
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;
@@ -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
- // missing variant on the model is a no-op.
343
- variant: "low",
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: "medium",
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",
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",