opencode-codex-memory 0.7.0 → 0.7.1

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
@@ -52,7 +52,7 @@ If you want the mental model — learning, remembering, forgetting — see
52
52
 
53
53
  ```json
54
54
  {
55
- "plugin": ["opencode-codex-memory@0.7.0"]
55
+ "plugin": ["opencode-codex-memory@0.7.1"]
56
56
  }
57
57
  ```
58
58
 
@@ -72,7 +72,7 @@ V2 plugin syntax:
72
72
 
73
73
  ```jsonc
74
74
  {
75
- "plugins": [{ "package": "opencode-codex-memory@0.7.0" }],
75
+ "plugins": [{ "package": "opencode-codex-memory@0.7.1" }],
76
76
  }
77
77
  ```
78
78
 
@@ -198,7 +198,7 @@ To set options, turn the plugin entry into a `[name, options]` pair:
198
198
  ```json
199
199
  {
200
200
  "plugin": [
201
- ["opencode-codex-memory@0.7.0", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
201
+ ["opencode-codex-memory@0.7.1", { "disable_on_external_context": true, "min_rollout_idle_hours": 2 }]
202
202
  ]
203
203
  }
204
204
  ```
@@ -265,7 +265,7 @@ Off by default; no changes to Codex's own config are required.
265
265
  {
266
266
  "plugin": [
267
267
  [
268
- "opencode-codex-memory@0.7.0",
268
+ "opencode-codex-memory@0.7.1",
269
269
  { "codex_interop": { "import": true, "export": true } }
270
270
  ]
271
271
  ]
@@ -322,7 +322,7 @@ from the project memories Claude already keeps on your machine. **One-way only**
322
322
  ```json
323
323
  {
324
324
  "plugin": [
325
- ["opencode-codex-memory@0.7.0", { "claude_import": { "enabled": true } }]
325
+ ["opencode-codex-memory@0.7.1", { "claude_import": { "enabled": true } }]
326
326
  ]
327
327
  }
328
328
  ```
@@ -349,7 +349,7 @@ Claude names each project with an opaque id (a folder under
349
349
  {
350
350
  "plugin": [
351
351
  [
352
- "opencode-codex-memory@0.7.0",
352
+ "opencode-codex-memory@0.7.1",
353
353
  {
354
354
  "claude_import": {
355
355
  "enabled": true,
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * The supported OpenCode 2 connection boundary.
3
3
  *
4
- * Server plugins do not receive the complete public client. The registered
5
- * local service does: the XDG `service.json` file is the discovery contract
6
- * (read-only never Service.ensure()). Auth headers are preserved, and
7
- * GET /api/status pid must match this process. 2.0.5 dropped JSON
8
- * /api/health (404 HTML/empty); 2.0.3 Service.discover() still probes that
9
- * path and throws on a non-object body, so this module never calls it.
4
+ * Server plugins do not receive the complete public client. `ctx` is the
5
+ * documented plugin API; global session list is missing there, so we talk
6
+ * HTTP via `@opencode/client` (a runtime dependency so the plugin cache
7
+ * actually installs it). Discovery reads XDG `service.json` (never
8
+ * Service.ensure() / Service.discover() — those still probe /api/health,
9
+ * which 2.0.5 404s). GET /api/status pid must match this process.
10
10
  */
11
11
  export interface V2ServiceEndpoint {
12
12
  url: string;
@@ -57,6 +57,10 @@ export interface V2ServiceDependencies {
57
57
  export declare function setV2ServiceDependenciesForTest(dependencies: V2ServiceDependencies | null): void;
58
58
  /** Forget a cached endpoint after a service restart or failed request. */
59
59
  export declare function invalidateOwnService(): void;
60
+ /** Last discoverOwnService failure, if ownServiceClient returned null. */
61
+ export declare function lastServiceFailure(): string | null;
62
+ /** Auth headers for the registered local service. */
63
+ export declare function serviceHeaders(endpoint: V2ServiceEndpoint): Record<string, string> | undefined;
60
64
  export declare function parseReadyStatus(body: unknown): V2ServiceStatus | null;
61
65
  export declare function readRegisteredEndpoint(file?: string): Promise<V2ServiceEndpoint | undefined>;
62
66
  export declare function fetchServiceStatus(endpoint: V2ServiceEndpoint, headers: Record<string, string> | undefined, signal?: AbortSignal): Promise<V2ServiceStatus>;
@@ -1,19 +1,22 @@
1
1
  /**
2
2
  * The supported OpenCode 2 connection boundary.
3
3
  *
4
- * Server plugins do not receive the complete public client. The registered
5
- * local service does: the XDG `service.json` file is the discovery contract
6
- * (read-only never Service.ensure()). Auth headers are preserved, and
7
- * GET /api/status pid must match this process. 2.0.5 dropped JSON
8
- * /api/health (404 HTML/empty); 2.0.3 Service.discover() still probes that
9
- * path and throws on a non-object body, so this module never calls it.
4
+ * Server plugins do not receive the complete public client. `ctx` is the
5
+ * documented plugin API; global session list is missing there, so we talk
6
+ * HTTP via `@opencode/client` (a runtime dependency so the plugin cache
7
+ * actually installs it). Discovery reads XDG `service.json` (never
8
+ * Service.ensure() / Service.discover() — those still probe /api/health,
9
+ * which 2.0.5 404s). GET /api/status pid must match this process.
10
10
  */
11
11
  import { readFile } from "node:fs/promises";
12
12
  import { homedir } from "node:os";
13
13
  import { join } from "node:path";
14
+ import { OpenCode } from "@opencode/client";
15
+ import { Service } from "@opencode/client/service";
14
16
  let testDependencies = null;
15
17
  let clientPromise = null;
16
- const SERVICE_REQUEST_TIMEOUT_MS = 1_000;
18
+ let lastFailure = null;
19
+ const SERVICE_REQUEST_TIMEOUT_MS = 3_000;
17
20
  /** Test seam: replace discovery without changing the production connection path. */
18
21
  export function setV2ServiceDependenciesForTest(dependencies) {
19
22
  testDependencies = dependencies;
@@ -22,6 +25,15 @@ export function setV2ServiceDependenciesForTest(dependencies) {
22
25
  /** Forget a cached endpoint after a service restart or failed request. */
23
26
  export function invalidateOwnService() {
24
27
  clientPromise = null;
28
+ lastFailure = null;
29
+ }
30
+ /** Last discoverOwnService failure, if ownServiceClient returned null. */
31
+ export function lastServiceFailure() {
32
+ return lastFailure;
33
+ }
34
+ /** Auth headers for the registered local service. */
35
+ export function serviceHeaders(endpoint) {
36
+ return Service.headers(endpoint);
25
37
  }
26
38
  export function parseReadyStatus(body) {
27
39
  const record = unwrapStatusRecord(body);
@@ -94,36 +106,40 @@ export async function readRegisteredEndpoint(file = registrationPath()) {
94
106
  async function fetchJson(url, headers, signal) {
95
107
  const response = await fetch(url, { headers, signal });
96
108
  const text = await response.text();
109
+ let body;
97
110
  if (!text)
98
- return undefined;
99
- try {
100
- return JSON.parse(text);
101
- }
102
- catch {
103
- return undefined;
111
+ body = undefined;
112
+ else {
113
+ try {
114
+ body = JSON.parse(text);
115
+ }
116
+ catch {
117
+ body = text;
118
+ }
104
119
  }
120
+ return { ok: response.ok, status: response.status, body };
105
121
  }
106
122
  export async function fetchServiceStatus(endpoint, headers, signal) {
107
- const statusBody = await fetchJson(new URL("/api/status", endpoint.url), headers, signal);
108
- const fromStatus = parseReadyStatus(statusBody);
123
+ const statusRes = await fetchJson(new URL("/api/status", endpoint.url), headers, signal);
124
+ const fromStatus = parseReadyStatus(statusRes.body);
109
125
  if (fromStatus)
110
126
  return fromStatus;
111
- const healthBody = await fetchJson(new URL("/api/health", endpoint.url), headers, signal);
112
- const fromHealth = parseReadyStatus(healthBody);
127
+ if (!statusRes.ok)
128
+ throw new Error(`GET /api/status ${String(statusRes.status)}`);
129
+ const healthRes = await fetchJson(new URL("/api/health", endpoint.url), headers, signal);
130
+ const fromHealth = parseReadyStatus(healthRes.body);
113
131
  if (fromHealth)
114
132
  return fromHealth;
115
133
  throw new Error("registered OpenCode service is not healthy");
116
134
  }
117
- async function productionDependencies() {
118
- const { Service } = await import("@opencode/client/service");
119
- const { OpenCode } = await import("@opencode/client");
135
+ function productionDependencies() {
120
136
  return {
121
137
  service: {
122
138
  discover: () => readRegisteredEndpoint(),
123
- headers: (endpoint) => Service.headers(endpoint),
139
+ headers: serviceHeaders,
124
140
  },
125
141
  make: (options) => OpenCode.make(options),
126
- probe: (endpoint, signal) => fetchServiceStatus(endpoint, Service.headers(endpoint), signal),
142
+ probe: (endpoint, signal) => fetchServiceStatus(endpoint, serviceHeaders(endpoint), signal),
127
143
  };
128
144
  }
129
145
  async function probeEndpoint(deps, endpoint, client, signal) {
@@ -144,7 +160,7 @@ async function probeEndpoint(deps, endpoint, client, signal) {
144
160
  * safety failure because it would make global memory operate on another host.
145
161
  */
146
162
  export async function discoverOwnService(dependencies, timeoutMs = SERVICE_REQUEST_TIMEOUT_MS) {
147
- const deps = dependencies ?? testDependencies ?? (await productionDependencies());
163
+ const deps = dependencies ?? testDependencies ?? productionDependencies();
148
164
  const endpoint = await withServiceTimeout(deps.service.discover(), timeoutMs);
149
165
  if (!endpoint)
150
166
  return null;
@@ -159,7 +175,13 @@ export async function discoverOwnService(dependencies, timeoutMs = SERVICE_REQUE
159
175
  /** Resolve the registered client once per live service; never start a service. */
160
176
  export async function ownServiceClient() {
161
177
  if (!clientPromise) {
162
- const request = discoverOwnService().then((found) => found?.client ?? null).catch((err) => {
178
+ const request = discoverOwnService()
179
+ .then((found) => {
180
+ lastFailure = found ? null : "no registered OpenCode 2 service.json";
181
+ return found?.client ?? null;
182
+ })
183
+ .catch((err) => {
184
+ lastFailure = err instanceof Error ? err.message : String(err);
163
185
  console.warn("[opencode-codex-memory] registered OpenCode service unavailable:", err);
164
186
  return null;
165
187
  });
@@ -1,5 +1,5 @@
1
1
  import { memoryRoot } from "../paths.js";
2
- import { invalidateOwnService, ownServiceClient } from "./service.js";
2
+ import { invalidateOwnService, lastServiceFailure, ownServiceClient } from "./service.js";
3
3
  let v2ctx = null;
4
4
  export function setV2Context(ctx) {
5
5
  v2ctx = ctx;
@@ -315,19 +315,19 @@ async function v2promptWithWait(sessionID, body, signal) {
315
315
  export function buildV1ClientShim() {
316
316
  async function serviceOrThrow() {
317
317
  const client = await ownServiceClient();
318
- if (!client)
319
- throw new Error("no healthy registered OpenCode 2 service for global memory operations");
318
+ if (!client) {
319
+ throw new Error(lastServiceFailure() ?? "no healthy registered OpenCode 2 service for global memory operations");
320
+ }
320
321
  return client;
321
322
  }
322
- async function listGlobalSessions(limit, cursor, search) {
323
- const client = await serviceOrThrow();
323
+ async function paginateSessionList(listFn, limit, cursor, search) {
324
324
  const pageSize = Math.min(Math.max(limit, 1), 5000);
325
325
  const out = [];
326
326
  const seenCursors = new Set();
327
327
  const timestampCursor = typeof cursor === "number" ? cursor : undefined;
328
328
  let next = typeof cursor === "string" ? cursor : undefined;
329
329
  while (out.length < limit) {
330
- const response = (await client.session.list?.({
330
+ const response = (await listFn({
331
331
  limit: pageSize,
332
332
  order: "desc",
333
333
  parentID: null,
@@ -362,6 +362,16 @@ export function buildV1ClientShim() {
362
362
  }
363
363
  return { data: out.slice(0, limit) };
364
364
  }
365
+ async function listGlobalSessions(limit, cursor, search) {
366
+ const localList = v2ctx ? v2ctx.session.list : undefined;
367
+ if (typeof localList === "function") {
368
+ return paginateSessionList((input) => localList(input), limit, cursor, search);
369
+ }
370
+ const client = await serviceOrThrow();
371
+ if (typeof client.session.list !== "function")
372
+ throw new Error("registered service does not support session.list");
373
+ return paginateSessionList((input) => client.session.list(input), limit, cursor, search);
374
+ }
365
375
  const session = {
366
376
  create: async (opts) => {
367
377
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codex-memory",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
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",
@@ -62,12 +62,12 @@
62
62
  "license": "Apache-2.0",
63
63
  "dependencies": {
64
64
  "@opencode-ai/plugin": "^1.18.0",
65
+ "@opencode/client": "2.0.5",
65
66
  "diff": "^9.0.0",
66
67
  "isomorphic-git": "^1.38.6",
67
68
  "xdg-basedir": "^5.1.0"
68
69
  },
69
70
  "devDependencies": {
70
- "@opencode/client": "2.0.3",
71
71
  "@opencode/plugin": "2.0.3",
72
72
  "zod": "4.1.8",
73
73
  "@opencode/theme": "2.0.3",