opencode-codex-memory 0.7.2 → 0.7.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 +6 -6
- package/dist/src/git-baseline.js +9 -12
- package/dist/src/phase2.js +5 -4
- package/dist/src/store.d.ts +1 -0
- package/dist/src/store.js +2 -1
- package/dist/src/v2/plugin.js +5 -3
- package/dist/src/v2/service.d.ts +6 -2
- package/dist/src/v2/service.js +18 -3
- package/dist/src/v2/shim.d.ts +12 -2
- package/dist/src/v2/shim.js +106 -33
- package/dist/tools/control.js +16 -0
- package/package.json +1 -1
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.
|
|
55
|
+
"plugin": ["opencode-codex-memory@0.7.4"]
|
|
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.
|
|
75
|
+
"plugins": [{ "package": "opencode-codex-memory@0.7.4" }],
|
|
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.
|
|
201
|
+
["opencode-codex-memory@0.7.4", { "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.
|
|
268
|
+
"opencode-codex-memory@0.7.4",
|
|
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.
|
|
325
|
+
["opencode-codex-memory@0.7.4", { "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.
|
|
352
|
+
"opencode-codex-memory@0.7.4",
|
|
353
353
|
{
|
|
354
354
|
"claude_import": {
|
|
355
355
|
"enabled": true,
|
package/dist/src/git-baseline.js
CHANGED
|
@@ -29,7 +29,7 @@ async function ensureInit(dir) {
|
|
|
29
29
|
const gitDir = path.join(dir, ".git");
|
|
30
30
|
let recreate = false;
|
|
31
31
|
try {
|
|
32
|
-
recreate =
|
|
32
|
+
recreate = gitMetadataUnusable(gitDir);
|
|
33
33
|
}
|
|
34
34
|
catch (err) {
|
|
35
35
|
if (err.code !== "ENOENT")
|
|
@@ -41,17 +41,14 @@ async function ensureInit(dir) {
|
|
|
41
41
|
await isogit.init({ fs, dir });
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
return false;
|
|
44
|
+
/**
|
|
45
|
+
* Only the `.git` entry itself. A recursive walk of git objects hangs on
|
|
46
|
+
* Windows junctions / symlink cycles and is not needed: we only refuse a
|
|
47
|
+
* `.git` that is a symlink or a non-directory.
|
|
48
|
+
*/
|
|
49
|
+
function gitMetadataUnusable(gitDir) {
|
|
50
|
+
const st = fs.lstatSync(gitDir);
|
|
51
|
+
return st.isSymbolicLink() || !st.isDirectory();
|
|
55
52
|
}
|
|
56
53
|
// statusMatrix rows are [filepath, head, workdir, stage]; head !== workdir
|
|
57
54
|
// means the working tree differs from HEAD (added, modified, or deleted).
|
package/dist/src/phase2.js
CHANGED
|
@@ -2,7 +2,7 @@ import { checkRateLimit, isProviderCapacityError, noteProviderCapacityExhausted
|
|
|
2
2
|
import { ensureLayout, rebuildRawMemories, writeRolloutSummaries, pruneExtensionResources, writeWorkspaceDiff, validateConsolidationArtifacts, removeMemorySymlinks, } from "./workspace.js";
|
|
3
3
|
import { ensureBaseline, captureWorkspaceDiff, resetBaseline, DIFF_ARTIFACT } from "./git-baseline.js";
|
|
4
4
|
import { consolidateViaSubagent, getPluginInput, SubagentCancelledError, SubagentShutdownError, } from "./llm.js";
|
|
5
|
-
import { hostSessionLiveness } from "./host-client.js";
|
|
5
|
+
import { hostSessionLiveness, withHostTimeout } from "./host-client.js";
|
|
6
6
|
import { invalidateCache } from "./source.js";
|
|
7
7
|
import { memoryRoot } from "./paths.js";
|
|
8
8
|
import { abortPhase2Consolidation, beginPhase2AbortScope, endPhase2AbortScope, isPluginShuttingDown, } from "./lifecycle.js";
|
|
@@ -16,6 +16,7 @@ export const DEFAULT_PHASE2_OPTIONS = {
|
|
|
16
16
|
// Export runs only after a successful phase 2 (fresh, validated artifacts) and
|
|
17
17
|
// must never fail the run — Codex's workspace is best-effort foreign territory.
|
|
18
18
|
const PHASE2_LIVE_CHECK_CONCURRENCY = 8;
|
|
19
|
+
const GIT_TIMEOUT_MS = 120_000;
|
|
19
20
|
/**
|
|
20
21
|
* Codex get_phase2_input_selection re-validates each row against the live
|
|
21
22
|
* threads table. We only drop a row on a confirmed 404 (same as session.deleted);
|
|
@@ -132,7 +133,7 @@ export async function runPhase2(store, opts = DEFAULT_PHASE2_OPTIONS) {
|
|
|
132
133
|
// ad-hoc notes added since then reach consolidation. Stale stage-1
|
|
133
134
|
// output pruning happens in phase 1, before the rate gate (codex
|
|
134
135
|
// start.rs ordering).
|
|
135
|
-
if (!await ensureBaseline()) {
|
|
136
|
+
if (!await withHostTimeout(ensureBaseline(), GIT_TIMEOUT_MS, "ensureBaseline")) {
|
|
136
137
|
store.markPhase2Failed(claim.ownershipToken, "git baseline failed");
|
|
137
138
|
return { status: "baseline_failed" };
|
|
138
139
|
}
|
|
@@ -171,7 +172,7 @@ export async function runPhase2(store, opts = DEFAULT_PHASE2_OPTIONS) {
|
|
|
171
172
|
console.warn("[opencode-codex-memory] claude import sync failed:", err);
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
|
-
const diff = await captureWorkspaceDiff();
|
|
175
|
+
const diff = await withHostTimeout(captureWorkspaceDiff(), GIT_TIMEOUT_MS, "captureWorkspaceDiff");
|
|
175
176
|
if (releaseIfShuttingDown(store, claim.ownershipToken)) {
|
|
176
177
|
return { status: "shutting_down" };
|
|
177
178
|
}
|
|
@@ -273,7 +274,7 @@ export async function runPhase2(store, opts = DEFAULT_PHASE2_OPTIONS) {
|
|
|
273
274
|
store.markPhase2Failed(claim.ownershipToken, `failed_invalid_artifacts: ${artifacts.reason}`);
|
|
274
275
|
return { status: "failed_invalid_artifacts" };
|
|
275
276
|
}
|
|
276
|
-
if (!await resetBaseline()) {
|
|
277
|
+
if (!await withHostTimeout(resetBaseline(), GIT_TIMEOUT_MS, "resetBaseline")) {
|
|
277
278
|
store.markPhase2Failed(claim.ownershipToken, "baseline reset failed");
|
|
278
279
|
return { status: "baseline_reset_failed" };
|
|
279
280
|
}
|
package/dist/src/store.d.ts
CHANGED
package/dist/src/store.js
CHANGED
|
@@ -434,7 +434,7 @@ export class MemoryStore {
|
|
|
434
434
|
*/
|
|
435
435
|
phase2JobSnapshot() {
|
|
436
436
|
const row = this.db
|
|
437
|
-
.prepare(`SELECT status, finished_at, last_error, retry_at, lease_until, last_success_watermark FROM memory_jobs
|
|
437
|
+
.prepare(`SELECT status, started_at, finished_at, last_error, retry_at, lease_until, last_success_watermark FROM memory_jobs
|
|
438
438
|
WHERE kind='memory_consolidate_global' AND job_key='global'`)
|
|
439
439
|
.get();
|
|
440
440
|
if (!row)
|
|
@@ -452,6 +452,7 @@ export class MemoryStore {
|
|
|
452
452
|
return {
|
|
453
453
|
status: row.status,
|
|
454
454
|
last_error: row.last_error,
|
|
455
|
+
started_at: row.started_at,
|
|
455
456
|
finished_at: row.finished_at,
|
|
456
457
|
retry_at: row.retry_at,
|
|
457
458
|
lease_until: row.lease_until,
|
package/dist/src/v2/plugin.js
CHANGED
|
@@ -30,7 +30,7 @@ import { hostMcpStatus } from "../host-client.js";
|
|
|
30
30
|
import { recordDiagnostic } from "../diagnostics.js";
|
|
31
31
|
import { resetAgentHealth } from "../agent-health.js";
|
|
32
32
|
import { applyPluginOptions, handleSessionDeleted } from "../index.js";
|
|
33
|
-
import { setV2Context, buildV1ClientShim, } from "./shim.js";
|
|
33
|
+
import { setV2Context, buildV1ClientShim, rememberV2Session, } from "./shim.js";
|
|
34
34
|
import { ensureV2Agents } from "./agents.js";
|
|
35
35
|
import { buildV2Tools } from "./tools.js";
|
|
36
36
|
import { MemoryStatusRpc } from "./status-rpc.js";
|
|
@@ -178,7 +178,8 @@ async function classifyExternalContextTool(toolName) {
|
|
|
178
178
|
}
|
|
179
179
|
return false;
|
|
180
180
|
}
|
|
181
|
-
function stampAndPump(sid) {
|
|
181
|
+
function stampAndPump(sid, directory) {
|
|
182
|
+
rememberV2Session(sid, directory ?? null);
|
|
182
183
|
try {
|
|
183
184
|
getStore().stampMemoryModeIfAbsent(sid, pluginOptions.generate_memories ? "enabled" : "disabled");
|
|
184
185
|
}
|
|
@@ -336,7 +337,7 @@ export async function setup(ctx) {
|
|
|
336
337
|
return;
|
|
337
338
|
if (!markV2TurnSeen(sid))
|
|
338
339
|
return;
|
|
339
|
-
stampAndPump(sid);
|
|
340
|
+
stampAndPump(sid, ctx.location?.directory);
|
|
340
341
|
}
|
|
341
342
|
catch (err) {
|
|
342
343
|
console.error("[opencode-codex-memory] v2 prompt hook error:", err);
|
|
@@ -436,6 +437,7 @@ export async function setup(ctx) {
|
|
|
436
437
|
if (e.type === "session.execution.succeeded" || e.type === "session.execution.ended") {
|
|
437
438
|
const sid = sessionIdFromV2Event(data);
|
|
438
439
|
if (sid && !isMemorySubSession(sid)) {
|
|
440
|
+
rememberV2Session(sid, ctx.location?.directory);
|
|
439
441
|
trackBackgroundTask(triggerPhase1(sid));
|
|
440
442
|
}
|
|
441
443
|
}
|
package/dist/src/v2/service.d.ts
CHANGED
|
@@ -64,10 +64,14 @@ export declare function serviceHeaders(endpoint: V2ServiceEndpoint): Record<stri
|
|
|
64
64
|
export declare function parseReadyStatus(body: unknown): V2ServiceStatus | null;
|
|
65
65
|
export declare function readRegisteredEndpoint(file?: string): Promise<V2ServiceEndpoint | undefined>;
|
|
66
66
|
export declare function fetchServiceStatus(endpoint: V2ServiceEndpoint, headers: Record<string, string> | undefined, signal?: AbortSignal): Promise<V2ServiceStatus>;
|
|
67
|
+
/** Local loopback only — IDE `serve --port 0` shares opencode.db with `--service`. */
|
|
68
|
+
export declare function isLoopbackEndpointUrl(url: string): boolean;
|
|
67
69
|
/**
|
|
68
70
|
* Find a ready, registered OpenCode service without starting or replacing one.
|
|
69
|
-
* A missing service is a normal unavailable result
|
|
70
|
-
*
|
|
71
|
+
* A missing service is a normal unavailable result. A PID mismatch on a
|
|
72
|
+
* non-loopback URL is a safety failure (would operate on another host).
|
|
73
|
+
* Loopback PID mismatch is the IDE isolated-serve case: same machine, shared
|
|
74
|
+
* session database, so global list/get/messages still go through that service.
|
|
71
75
|
*/
|
|
72
76
|
export declare function discoverOwnService(dependencies?: V2ServiceDependencies, timeoutMs?: number): Promise<{
|
|
73
77
|
endpoint: V2ServiceEndpoint;
|
package/dist/src/v2/service.js
CHANGED
|
@@ -154,10 +154,22 @@ async function probeEndpoint(deps, endpoint, client, signal) {
|
|
|
154
154
|
}
|
|
155
155
|
return fetchServiceStatus(endpoint, deps.service.headers(endpoint), signal);
|
|
156
156
|
}
|
|
157
|
+
/** Local loopback only — IDE `serve --port 0` shares opencode.db with `--service`. */
|
|
158
|
+
export function isLoopbackEndpointUrl(url) {
|
|
159
|
+
try {
|
|
160
|
+
const hostname = new URL(url).hostname;
|
|
161
|
+
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
157
167
|
/**
|
|
158
168
|
* Find a ready, registered OpenCode service without starting or replacing one.
|
|
159
|
-
* A missing service is a normal unavailable result
|
|
160
|
-
*
|
|
169
|
+
* A missing service is a normal unavailable result. A PID mismatch on a
|
|
170
|
+
* non-loopback URL is a safety failure (would operate on another host).
|
|
171
|
+
* Loopback PID mismatch is the IDE isolated-serve case: same machine, shared
|
|
172
|
+
* session database, so global list/get/messages still go through that service.
|
|
161
173
|
*/
|
|
162
174
|
export async function discoverOwnService(dependencies, timeoutMs = SERVICE_REQUEST_TIMEOUT_MS) {
|
|
163
175
|
const deps = dependencies ?? testDependencies ?? productionDependencies();
|
|
@@ -168,7 +180,10 @@ export async function discoverOwnService(dependencies, timeoutMs = SERVICE_REQUE
|
|
|
168
180
|
const controller = new AbortController();
|
|
169
181
|
const health = await withServiceTimeout(probeEndpoint(deps, endpoint, client, controller.signal), timeoutMs, controller);
|
|
170
182
|
if (health.pid !== process.pid) {
|
|
171
|
-
|
|
183
|
+
if (!isLoopbackEndpointUrl(endpoint.url)) {
|
|
184
|
+
throw new Error(`registered OpenCode service PID ${String(health.pid)} does not match plugin host PID ${process.pid}`);
|
|
185
|
+
}
|
|
186
|
+
console.warn(`[opencode-codex-memory] registered OpenCode service PID ${String(health.pid)} is a different local process than plugin host PID ${process.pid}; using it for global session list`);
|
|
172
187
|
}
|
|
173
188
|
return { endpoint, client, health };
|
|
174
189
|
}
|
package/dist/src/v2/shim.d.ts
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* run byte-identical) by presenting a V1-shaped client façade backed by the
|
|
6
6
|
* V2 plugin context. Only genuinely missing V2 surfaces are adapted:
|
|
7
7
|
*
|
|
8
|
-
* - session list/discovery → the
|
|
9
|
-
*
|
|
8
|
+
* - session list/discovery → ctx.session.list when the host exposes it,
|
|
9
|
+
* else the authenticated public client for THIS process's registered
|
|
10
|
+
* service. A PID mismatch (IDE `serve --port 0` vs `serve --service`)
|
|
11
|
+
* does not list another host; it falls back to sessions this process
|
|
12
|
+
* has observed.
|
|
10
13
|
* - session.prompt agent/system/model/format/variant → V2 create-time
|
|
11
14
|
* agent/model (via switchAgent/switchModel) + generate.text for the
|
|
12
15
|
* json_schema extraction path (V2 prompts carry text only).
|
|
@@ -27,6 +30,13 @@ export declare function setV2Context(ctx: V2Context | null): void;
|
|
|
27
30
|
export declare function isReleasedSubSession(id: string): boolean;
|
|
28
31
|
/** Stable synthetic id for extraction helpers (see create below). */
|
|
29
32
|
export declare const EXTRACT_STUB_SESSION_ID = "codex-memory-extract-stub";
|
|
33
|
+
/**
|
|
34
|
+
* Isolated OpenCode 2 serves (IntelliJ/desktop `serve --port 0`) are not the
|
|
35
|
+
* registered `--service` process, so global session.list is unavailable.
|
|
36
|
+
* Remember sessions this process has actually seen so phase 1 can still
|
|
37
|
+
* extract them.
|
|
38
|
+
*/
|
|
39
|
+
export declare function rememberV2Session(id: string, directory?: string | null, title?: string): void;
|
|
30
40
|
/** Test seam. */
|
|
31
41
|
export declare function resetV2ShimStateForTest(): void;
|
|
32
42
|
/**
|
package/dist/src/v2/shim.js
CHANGED
|
@@ -59,9 +59,58 @@ export function isReleasedSubSession(id) {
|
|
|
59
59
|
}
|
|
60
60
|
/** Stable synthetic id for extraction helpers (see create below). */
|
|
61
61
|
export const EXTRACT_STUB_SESSION_ID = "codex-memory-extract-stub";
|
|
62
|
+
const OBSERVED_CAP = 5000;
|
|
63
|
+
const observedSessions = new Map();
|
|
64
|
+
/**
|
|
65
|
+
* Isolated OpenCode 2 serves (IntelliJ/desktop `serve --port 0`) are not the
|
|
66
|
+
* registered `--service` process, so global session.list is unavailable.
|
|
67
|
+
* Remember sessions this process has actually seen so phase 1 can still
|
|
68
|
+
* extract them.
|
|
69
|
+
*/
|
|
70
|
+
export function rememberV2Session(id, directory, title) {
|
|
71
|
+
if (!id || id === EXTRACT_STUB_SESSION_ID)
|
|
72
|
+
return;
|
|
73
|
+
if (releasedSubSessions.has(id))
|
|
74
|
+
return;
|
|
75
|
+
observedSessions.delete(id);
|
|
76
|
+
observedSessions.set(id, {
|
|
77
|
+
updated_at: Date.now(),
|
|
78
|
+
directory: directory ?? null,
|
|
79
|
+
title: typeof title === "string" ? title : "",
|
|
80
|
+
});
|
|
81
|
+
while (observedSessions.size > OBSERVED_CAP) {
|
|
82
|
+
const oldest = observedSessions.keys().next().value;
|
|
83
|
+
if (oldest === undefined)
|
|
84
|
+
break;
|
|
85
|
+
observedSessions.delete(oldest);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function listObservedSessions(limit, cursor, search) {
|
|
89
|
+
const timestampCursor = typeof cursor === "number" ? cursor : undefined;
|
|
90
|
+
const needle = typeof search === "string" && search.length > 0 ? search.toLowerCase() : undefined;
|
|
91
|
+
const rows = [...observedSessions.entries()].sort((a, b) => b[1].updated_at - a[1].updated_at);
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const [id, rec] of rows) {
|
|
94
|
+
if (timestampCursor !== undefined && rec.updated_at >= timestampCursor)
|
|
95
|
+
continue;
|
|
96
|
+
if (needle && !`${id}\n${rec.title}`.toLowerCase().includes(needle))
|
|
97
|
+
continue;
|
|
98
|
+
out.push({
|
|
99
|
+
id,
|
|
100
|
+
parentID: null,
|
|
101
|
+
...(rec.title ? { title: rec.title } : {}),
|
|
102
|
+
directory: rec.directory,
|
|
103
|
+
time: { updated: rec.updated_at },
|
|
104
|
+
});
|
|
105
|
+
if (out.length >= limit)
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
62
110
|
/** Test seam. */
|
|
63
111
|
export function resetV2ShimStateForTest() {
|
|
64
112
|
releasedSubSessions.clear();
|
|
113
|
+
observedSessions.clear();
|
|
65
114
|
invalidateOwnService();
|
|
66
115
|
}
|
|
67
116
|
// ---------------------------------------------------------------------------
|
|
@@ -367,10 +416,13 @@ export function buildV1ClientShim() {
|
|
|
367
416
|
if (typeof localList === "function") {
|
|
368
417
|
return paginateSessionList((input) => localList(input), limit, cursor, search);
|
|
369
418
|
}
|
|
370
|
-
const client = await
|
|
371
|
-
if (typeof client.session.list
|
|
419
|
+
const client = await ownServiceClient();
|
|
420
|
+
if (client && typeof client.session.list === "function") {
|
|
421
|
+
return paginateSessionList((input) => client.session.list(input), limit, cursor, search);
|
|
422
|
+
}
|
|
423
|
+
if (client)
|
|
372
424
|
throw new Error("registered service does not support session.list");
|
|
373
|
-
return
|
|
425
|
+
return { data: listObservedSessions(limit, cursor, search) };
|
|
374
426
|
}
|
|
375
427
|
const session = {
|
|
376
428
|
create: async (opts) => {
|
|
@@ -408,25 +460,30 @@ export function buildV1ClientShim() {
|
|
|
408
460
|
try {
|
|
409
461
|
if (isReleasedSubSession(opts.path.id))
|
|
410
462
|
throw Object.assign(new Error("SessionNotFound"), { _tag: "SessionNotFoundError" });
|
|
411
|
-
const client = await
|
|
412
|
-
if (typeof client
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
463
|
+
const client = await ownServiceClient();
|
|
464
|
+
if (typeof client?.message?.list === "function") {
|
|
465
|
+
const messages = [];
|
|
466
|
+
const seenCursors = new Set();
|
|
467
|
+
let cursor;
|
|
468
|
+
while (true) {
|
|
469
|
+
const response = await client.message.list(cursor ? { sessionID: opts.path.id, cursor } : { sessionID: opts.path.id, order: "asc" });
|
|
470
|
+
const rows = responseRows(response);
|
|
471
|
+
if (!rows)
|
|
472
|
+
throw new Error("registered service returned an invalid message list");
|
|
473
|
+
messages.push(...rows);
|
|
474
|
+
const next = responseNextCursor(response);
|
|
475
|
+
if (!next || seenCursors.has(next))
|
|
476
|
+
break;
|
|
477
|
+
seenCursors.add(next);
|
|
478
|
+
cursor = next;
|
|
479
|
+
}
|
|
480
|
+
return { data: adaptV2Messages(messages) };
|
|
428
481
|
}
|
|
429
|
-
|
|
482
|
+
const raw = await ctx().session.context({
|
|
483
|
+
sessionID: opts.path.id,
|
|
484
|
+
});
|
|
485
|
+
const rows = Array.isArray(raw) ? raw : responseRows(raw) ?? [];
|
|
486
|
+
return { data: adaptV2Messages(rows) };
|
|
430
487
|
}
|
|
431
488
|
catch (e) {
|
|
432
489
|
return { error: e };
|
|
@@ -463,18 +520,23 @@ export function buildV1ClientShim() {
|
|
|
463
520
|
}
|
|
464
521
|
try {
|
|
465
522
|
const client = await ownServiceClient();
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
523
|
+
if (client?.session?.get) {
|
|
524
|
+
const info = await client.session.get({ sessionID: opts.path.id });
|
|
525
|
+
if (info?.error && isNotFoundError(info.error)) {
|
|
526
|
+
markReleased(opts.path.id);
|
|
527
|
+
return {};
|
|
528
|
+
}
|
|
529
|
+
const data = und(info);
|
|
530
|
+
if (data && typeof data === "object") {
|
|
531
|
+
return { error: shutdownError ?? new Error("session still exists after interrupt") };
|
|
532
|
+
}
|
|
475
533
|
return { error: shutdownError ?? new Error("session still exists after interrupt") };
|
|
476
534
|
}
|
|
477
|
-
|
|
535
|
+
// Isolated serve: no session.remove on plugin ctx. interrupt+wait already
|
|
536
|
+
// finished, so the helper is idle. Holding the phase-2 lease until it
|
|
537
|
+
// expires would block consolidation for an hour.
|
|
538
|
+
markReleased(opts.path.id);
|
|
539
|
+
return {};
|
|
478
540
|
}
|
|
479
541
|
catch (e) {
|
|
480
542
|
if (isNotFoundError(e)) {
|
|
@@ -489,8 +551,19 @@ export function buildV1ClientShim() {
|
|
|
489
551
|
if (isReleasedSubSession(opts.path.id)) {
|
|
490
552
|
return { response: { status: 404 }, error: { _tag: "SessionNotFoundError" } };
|
|
491
553
|
}
|
|
492
|
-
const client = await
|
|
493
|
-
|
|
554
|
+
const client = await ownServiceClient();
|
|
555
|
+
if (client?.session?.get) {
|
|
556
|
+
try {
|
|
557
|
+
const info = und(await client.session.get({ sessionID: opts.path.id }));
|
|
558
|
+
return { data: info };
|
|
559
|
+
}
|
|
560
|
+
catch (e) {
|
|
561
|
+
if (isNotFoundError(e))
|
|
562
|
+
return { response: { status: 404 }, error: e };
|
|
563
|
+
return { error: e };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const info = und(await ctx().session.get({ sessionID: opts.path.id }));
|
|
494
567
|
return { data: info };
|
|
495
568
|
}
|
|
496
569
|
catch (e) {
|
package/dist/tools/control.js
CHANGED
|
@@ -192,6 +192,16 @@ export const memory_reset = tool({
|
|
|
192
192
|
function fmtUnixSec(sec) {
|
|
193
193
|
return sec ? new Date(sec * 1000).toISOString() : "none";
|
|
194
194
|
}
|
|
195
|
+
function fmtElapsedSec(startedAtSec) {
|
|
196
|
+
if (!startedAtSec)
|
|
197
|
+
return "unknown";
|
|
198
|
+
const sec = Math.max(0, Math.floor(Date.now() / 1000) - startedAtSec);
|
|
199
|
+
if (sec < 60)
|
|
200
|
+
return `${sec}s`;
|
|
201
|
+
if (sec < 3600)
|
|
202
|
+
return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
203
|
+
return `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;
|
|
204
|
+
}
|
|
195
205
|
function fmtWatermarkMs(ms) {
|
|
196
206
|
if (ms === 0)
|
|
197
207
|
return "0 (no consumed inputs)";
|
|
@@ -229,6 +239,9 @@ export const memory_inspect = tool({
|
|
|
229
239
|
? [
|
|
230
240
|
`phase2_status: ${phase2.status}`,
|
|
231
241
|
`phase2_last_error: ${phase2.last_error ?? "none"}`,
|
|
242
|
+
`phase2_started_at: ${fmtUnixSec(phase2.started_at)}`,
|
|
243
|
+
`phase2_running_for: ${phase2.status === "running" ? fmtElapsedSec(phase2.started_at) : "n/a"}`,
|
|
244
|
+
`phase2_lease_until: ${fmtUnixSec(phase2.lease_until)}`,
|
|
232
245
|
`phase2_retry_at: ${fmtUnixSec(phase2.retry_at)}`,
|
|
233
246
|
`phase2_last_attempt_finished_at: ${fmtUnixSec(phase2.finished_at)}`,
|
|
234
247
|
`phase2_last_success_watermark: ${fmtWatermarkMs(phase2.last_success_watermark)}`,
|
|
@@ -238,6 +251,9 @@ export const memory_inspect = tool({
|
|
|
238
251
|
: [
|
|
239
252
|
"phase2_status: none",
|
|
240
253
|
"phase2_last_error: none",
|
|
254
|
+
"phase2_started_at: none",
|
|
255
|
+
"phase2_running_for: n/a",
|
|
256
|
+
"phase2_lease_until: none",
|
|
241
257
|
"phase2_retry_at: none",
|
|
242
258
|
"phase2_last_attempt_finished_at: none",
|
|
243
259
|
"phase2_last_success_watermark: none",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-codex-memory",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.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",
|