opencode-codex-memory 0.6.5 → 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.
Files changed (41) hide show
  1. package/README.md +26 -28
  2. package/dist/opencode.json +1 -1
  3. package/dist/src/citation.d.ts +9 -0
  4. package/dist/src/citation.js +68 -11
  5. package/dist/src/db.js +10 -0
  6. package/dist/src/host-client.d.ts +1 -0
  7. package/dist/src/host-client.js +1 -0
  8. package/dist/src/index.d.ts +12 -2
  9. package/dist/src/index.js +32 -5
  10. package/dist/src/llm.d.ts +6 -0
  11. package/dist/src/llm.js +21 -10
  12. package/dist/src/phase2.d.ts +2 -0
  13. package/dist/src/phase2.js +1 -1
  14. package/dist/src/rollout-input.d.ts +6 -0
  15. package/dist/src/rollout-input.js +111 -0
  16. package/dist/src/store.d.ts +17 -1
  17. package/dist/src/store.js +90 -4
  18. package/dist/src/v2/agents.d.ts +53 -0
  19. package/dist/src/v2/agents.js +204 -0
  20. package/dist/src/v2/citation-overlay.d.ts +7 -0
  21. package/dist/src/v2/citation-overlay.js +52 -0
  22. package/dist/src/v2/index.d.ts +7 -0
  23. package/dist/src/v2/index.js +10 -0
  24. package/dist/src/v2/injection.d.ts +14 -0
  25. package/dist/src/v2/injection.js +19 -0
  26. package/dist/src/v2/plugin.d.ts +7 -0
  27. package/dist/src/v2/plugin.js +482 -0
  28. package/dist/src/v2/service.d.ts +78 -0
  29. package/dist/src/v2/service.js +195 -0
  30. package/dist/src/v2/shim.d.ts +47 -0
  31. package/dist/src/v2/shim.js +591 -0
  32. package/dist/src/v2/status-rpc.d.ts +197 -0
  33. package/dist/src/v2/status-rpc.js +159 -0
  34. package/dist/src/v2/status.d.ts +3 -0
  35. package/dist/src/v2/status.js +83 -0
  36. package/dist/src/v2/tools.d.ts +33 -0
  37. package/dist/src/v2/tools.js +57 -0
  38. package/dist/src/v2/tui.d.ts +3 -0
  39. package/dist/src/v2/tui.js +750 -0
  40. package/opencode.json +1 -1
  41. package/package.json +38 -2
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Shared server↔TUI wire contract for the memory sidebar status.
3
+ *
4
+ * Dependency-free on purpose: the TUI bundle runs in the CLI sandbox, where
5
+ * only `@opencode/plugin/tui` (+ rendering peers) is guaranteed to resolve.
6
+ * `Rpc.define` is a runtime passthrough (validation only), so a plain object
7
+ * with JSON Schema nodes is an equivalent portable definition.
8
+ */
9
+ export declare const MEMORY_STATUS_ACTIVITIES: readonly ["idle", "extracting", "consolidating", "retrying", "error", "read_only", "disabled", "stopping"];
10
+ export type MemoryStatusActivity = (typeof MEMORY_STATUS_ACTIVITIES)[number];
11
+ export interface MemoryStatus {
12
+ activity: MemoryStatusActivity;
13
+ useMemories: boolean;
14
+ generateMemories: boolean;
15
+ extractModel: string | null;
16
+ consolidationModel: string | null;
17
+ codexImport: boolean;
18
+ lastSuccessAt: number | null;
19
+ retryAt: number | null;
20
+ warnings: string[];
21
+ /** Per-session read/write mode when a sessionID was supplied. */
22
+ sessionMode: "enabled" | "disabled" | "polluted" | null;
23
+ /** Root directory of the memory workspace. */
24
+ memoryRoot: string;
25
+ /** Estimated memory tokens injected into model requests (chars/4). */
26
+ injected: {
27
+ sessionTokens: number;
28
+ sessionRequests: number;
29
+ totalTokens: number;
30
+ totalRequests: number;
31
+ };
32
+ }
33
+ export declare const MemoryStatusRpc: {
34
+ readonly id: "opencode-codex-memory";
35
+ readonly methods: {
36
+ readonly status: {
37
+ readonly input: {
38
+ readonly type: "object";
39
+ readonly properties: {
40
+ readonly sessionID: {
41
+ readonly type: "string";
42
+ };
43
+ };
44
+ readonly additionalProperties: false;
45
+ };
46
+ readonly output: {
47
+ readonly type: "object";
48
+ readonly properties: {
49
+ readonly activity: {
50
+ readonly type: "string";
51
+ readonly enum: readonly ["idle", "extracting", "consolidating", "retrying", "error", "read_only", "disabled", "stopping"];
52
+ };
53
+ readonly useMemories: {
54
+ readonly type: "boolean";
55
+ };
56
+ readonly generateMemories: {
57
+ readonly type: "boolean";
58
+ };
59
+ readonly extractModel: {
60
+ readonly type: readonly ["string", "null"];
61
+ };
62
+ readonly consolidationModel: {
63
+ readonly type: readonly ["string", "null"];
64
+ };
65
+ readonly codexImport: {
66
+ readonly type: "boolean";
67
+ };
68
+ readonly lastSuccessAt: {
69
+ readonly type: readonly ["number", "null"];
70
+ };
71
+ readonly retryAt: {
72
+ readonly type: readonly ["number", "null"];
73
+ };
74
+ readonly warnings: {
75
+ readonly type: "array";
76
+ readonly items: {
77
+ readonly type: "string";
78
+ };
79
+ };
80
+ readonly sessionMode: {
81
+ readonly type: readonly ["string", "null"];
82
+ readonly enum: readonly ["enabled", "disabled", "polluted", null];
83
+ };
84
+ readonly memoryRoot: {
85
+ readonly type: "string";
86
+ };
87
+ readonly injected: {
88
+ readonly type: "object";
89
+ readonly properties: {
90
+ readonly sessionTokens: {
91
+ readonly type: "number";
92
+ };
93
+ readonly sessionRequests: {
94
+ readonly type: "number";
95
+ };
96
+ readonly totalTokens: {
97
+ readonly type: "number";
98
+ };
99
+ readonly totalRequests: {
100
+ readonly type: "number";
101
+ };
102
+ };
103
+ readonly required: readonly ["sessionTokens", "sessionRequests", "totalTokens", "totalRequests"];
104
+ readonly additionalProperties: false;
105
+ };
106
+ };
107
+ readonly required: readonly ["activity", "useMemories", "generateMemories", "extractModel", "consolidationModel", "codexImport", "lastSuccessAt", "retryAt", "warnings", "sessionMode", "memoryRoot", "injected"];
108
+ readonly additionalProperties: false;
109
+ };
110
+ };
111
+ /** Runtime-only toggle of a boolean plugin option (until server restart). */
112
+ readonly setOption: {
113
+ readonly input: {
114
+ readonly type: "object";
115
+ readonly properties: {
116
+ readonly key: {
117
+ readonly type: "string";
118
+ readonly enum: readonly ["use_memories", "generate_memories"];
119
+ };
120
+ readonly value: {
121
+ readonly type: "boolean";
122
+ };
123
+ };
124
+ readonly required: readonly ["key", "value"];
125
+ readonly additionalProperties: false;
126
+ };
127
+ readonly output: {
128
+ readonly type: "object";
129
+ readonly properties: {
130
+ readonly ok: {
131
+ readonly type: "boolean";
132
+ };
133
+ };
134
+ readonly required: readonly ["ok"];
135
+ readonly additionalProperties: false;
136
+ };
137
+ };
138
+ /** Same effect as the memory_mode tool. */
139
+ readonly setSessionMode: {
140
+ readonly input: {
141
+ readonly type: "object";
142
+ readonly properties: {
143
+ readonly sessionID: {
144
+ readonly type: "string";
145
+ };
146
+ readonly mode: {
147
+ readonly type: "string";
148
+ readonly enum: readonly ["enabled", "disabled"];
149
+ };
150
+ };
151
+ readonly required: readonly ["sessionID", "mode"];
152
+ readonly additionalProperties: false;
153
+ };
154
+ readonly output: {
155
+ readonly type: "object";
156
+ readonly properties: {
157
+ readonly ok: {
158
+ readonly type: "boolean";
159
+ };
160
+ };
161
+ readonly required: readonly ["ok"];
162
+ readonly additionalProperties: false;
163
+ };
164
+ };
165
+ /** Run extraction + consolidation now, bypassing the success cooldown (not the lease). */
166
+ readonly consolidateNow: {
167
+ readonly input: {
168
+ readonly type: "object";
169
+ readonly properties: {};
170
+ readonly additionalProperties: false;
171
+ };
172
+ readonly output: {
173
+ readonly type: "object";
174
+ readonly properties: {
175
+ readonly status: {
176
+ readonly type: "string";
177
+ };
178
+ };
179
+ readonly required: readonly ["status"];
180
+ readonly additionalProperties: false;
181
+ };
182
+ };
183
+ };
184
+ readonly events: {
185
+ readonly changed: {
186
+ readonly schema: {
187
+ readonly type: "object";
188
+ readonly properties: {};
189
+ readonly additionalProperties: false;
190
+ };
191
+ };
192
+ };
193
+ };
194
+ /** Runtime guard for the untyped RPC client result (no zod in the TUI bundle). */
195
+ export declare function isMemoryStatus(value: unknown): value is MemoryStatus;
196
+ /** Throwing parser for tests and call sites that want a typed value. */
197
+ export declare function parseMemoryStatus(value: unknown): MemoryStatus;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Shared server↔TUI wire contract for the memory sidebar status.
3
+ *
4
+ * Dependency-free on purpose: the TUI bundle runs in the CLI sandbox, where
5
+ * only `@opencode/plugin/tui` (+ rendering peers) is guaranteed to resolve.
6
+ * `Rpc.define` is a runtime passthrough (validation only), so a plain object
7
+ * with JSON Schema nodes is an equivalent portable definition.
8
+ */
9
+ export const MEMORY_STATUS_ACTIVITIES = [
10
+ "idle",
11
+ "extracting",
12
+ "consolidating",
13
+ "retrying",
14
+ "error",
15
+ "read_only",
16
+ "disabled",
17
+ "stopping",
18
+ ];
19
+ const INJECTED_SCHEMA = {
20
+ type: "object",
21
+ properties: {
22
+ sessionTokens: { type: "number" },
23
+ sessionRequests: { type: "number" },
24
+ totalTokens: { type: "number" },
25
+ totalRequests: { type: "number" },
26
+ },
27
+ required: ["sessionTokens", "sessionRequests", "totalTokens", "totalRequests"],
28
+ additionalProperties: false,
29
+ };
30
+ export const MemoryStatusRpc = {
31
+ id: "opencode-codex-memory",
32
+ methods: {
33
+ status: {
34
+ input: {
35
+ type: "object",
36
+ properties: { sessionID: { type: "string" } },
37
+ additionalProperties: false,
38
+ },
39
+ output: {
40
+ type: "object",
41
+ properties: {
42
+ activity: { type: "string", enum: [...MEMORY_STATUS_ACTIVITIES] },
43
+ useMemories: { type: "boolean" },
44
+ generateMemories: { type: "boolean" },
45
+ extractModel: { type: ["string", "null"] },
46
+ consolidationModel: { type: ["string", "null"] },
47
+ codexImport: { type: "boolean" },
48
+ lastSuccessAt: { type: ["number", "null"] },
49
+ retryAt: { type: ["number", "null"] },
50
+ warnings: { type: "array", items: { type: "string" } },
51
+ sessionMode: { type: ["string", "null"], enum: ["enabled", "disabled", "polluted", null] },
52
+ memoryRoot: { type: "string" },
53
+ injected: INJECTED_SCHEMA,
54
+ },
55
+ required: [
56
+ "activity",
57
+ "useMemories",
58
+ "generateMemories",
59
+ "extractModel",
60
+ "consolidationModel",
61
+ "codexImport",
62
+ "lastSuccessAt",
63
+ "retryAt",
64
+ "warnings",
65
+ "sessionMode",
66
+ "memoryRoot",
67
+ "injected",
68
+ ],
69
+ additionalProperties: false,
70
+ },
71
+ },
72
+ /** Runtime-only toggle of a boolean plugin option (until server restart). */
73
+ setOption: {
74
+ input: {
75
+ type: "object",
76
+ properties: {
77
+ key: { type: "string", enum: ["use_memories", "generate_memories"] },
78
+ value: { type: "boolean" },
79
+ },
80
+ required: ["key", "value"],
81
+ additionalProperties: false,
82
+ },
83
+ output: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"], additionalProperties: false },
84
+ },
85
+ /** Same effect as the memory_mode tool. */
86
+ setSessionMode: {
87
+ input: {
88
+ type: "object",
89
+ properties: {
90
+ sessionID: { type: "string" },
91
+ mode: { type: "string", enum: ["enabled", "disabled"] },
92
+ },
93
+ required: ["sessionID", "mode"],
94
+ additionalProperties: false,
95
+ },
96
+ output: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"], additionalProperties: false },
97
+ },
98
+ /** Run extraction + consolidation now, bypassing the success cooldown (not the lease). */
99
+ consolidateNow: {
100
+ input: { type: "object", properties: {}, additionalProperties: false },
101
+ output: {
102
+ type: "object",
103
+ properties: { status: { type: "string" } },
104
+ required: ["status"],
105
+ additionalProperties: false,
106
+ },
107
+ },
108
+ },
109
+ events: {
110
+ changed: { schema: { type: "object", properties: {}, additionalProperties: false } },
111
+ },
112
+ };
113
+ function isRecord(value) {
114
+ return typeof value === "object" && value !== null && !Array.isArray(value);
115
+ }
116
+ /** Runtime guard for the untyped RPC client result (no zod in the TUI bundle). */
117
+ export function isMemoryStatus(value) {
118
+ if (!isRecord(value))
119
+ return false;
120
+ if (typeof value.activity !== "string" ||
121
+ !MEMORY_STATUS_ACTIVITIES.includes(value.activity)) {
122
+ return false;
123
+ }
124
+ for (const key of ["useMemories", "generateMemories", "codexImport"]) {
125
+ if (typeof value[key] !== "boolean")
126
+ return false;
127
+ }
128
+ for (const key of ["extractModel", "consolidationModel", "lastSuccessAt", "retryAt"]) {
129
+ const v = value[key];
130
+ if (v !== null && typeof v !== (key === "extractModel" || key === "consolidationModel" ? "string" : "number")) {
131
+ return false;
132
+ }
133
+ }
134
+ if (!Array.isArray(value.warnings) || !value.warnings.every((w) => typeof w === "string")) {
135
+ return false;
136
+ }
137
+ if (value.sessionMode !== null &&
138
+ value.sessionMode !== "enabled" &&
139
+ value.sessionMode !== "disabled" &&
140
+ value.sessionMode !== "polluted") {
141
+ return false;
142
+ }
143
+ if (typeof value.memoryRoot !== "string")
144
+ return false;
145
+ const injected = value.injected;
146
+ if (!isRecord(injected))
147
+ return false;
148
+ for (const key of ["sessionTokens", "sessionRequests", "totalTokens", "totalRequests"]) {
149
+ if (typeof injected[key] !== "number")
150
+ return false;
151
+ }
152
+ return true;
153
+ }
154
+ /** Throwing parser for tests and call sites that want a typed value. */
155
+ export function parseMemoryStatus(value) {
156
+ if (!isMemoryStatus(value))
157
+ throw new Error("invalid memory status payload");
158
+ return value;
159
+ }
@@ -0,0 +1,3 @@
1
+ import type { MemoryStatus } from "./status-rpc.js";
2
+ /** Read the same snapshots as memory_inspect; never claim or advance a job. */
3
+ export declare function readMemoryStatus(sessionID?: string | null): MemoryStatus;
@@ -0,0 +1,83 @@
1
+ import { MemoryStore } from "../store.js";
2
+ import { pluginOptions, getConfigWarnings } from "../options.js";
3
+ import { getAgentHealth } from "../agent-health.js";
4
+ import { isPhase2InFlight } from "../phase2.js";
5
+ import { isPluginShuttingDown } from "../lifecycle.js";
6
+ import { activeProviderCapacityBackoffs } from "../ratelimit.js";
7
+ import { resolveCodexInterop } from "../codex-interop.js";
8
+ import { injectionTotals, sessionInjection } from "./injection.js";
9
+ import { memoryRoot } from "../paths.js";
10
+ /** Read the same snapshots as memory_inspect; never claim or advance a job. */
11
+ export function readMemoryStatus(sessionID) {
12
+ const store = new MemoryStore();
13
+ const phase1 = store.stage1JobSnapshot();
14
+ const phase2 = store.phase2JobSnapshot();
15
+ const options = pluginOptions;
16
+ const now = Date.now();
17
+ const session = sessionInjection(sessionID);
18
+ const total = injectionTotals();
19
+ const retryTimes = [
20
+ ...activeProviderCapacityBackoffs().map((backoff) => backoff.retry_at),
21
+ ...phase1.recent_errors.map((error) => error.retry_at),
22
+ phase2?.retry_at,
23
+ ].filter((time) => time != null && time * 1000 > now);
24
+ const retryAt = retryTimes.length ? Math.min(...retryTimes) * 1000 : null;
25
+ const warnings = [...getConfigWarnings()];
26
+ const health = getAgentHealth();
27
+ if (options.generate_memories && health.observed) {
28
+ // V2 extraction is sessionless; only the consolidator agent is used.
29
+ warnings.push(...health.agents.memorize.issues.map((issue) => `memorize: ${issue}`));
30
+ }
31
+ if (phase2?.last_error)
32
+ warnings.push("Consolidation failed; see memory_inspect for details.");
33
+ if (phase1.by_failure_class.other_exhausted > 0)
34
+ warnings.push("Some extraction jobs exhausted their retries.");
35
+ if (phase1.by_failure_class.provider_capacity > 0)
36
+ warnings.push("Some extraction jobs hit provider capacity limits.");
37
+ const codexImport = options.codex_interop.import && resolveCodexInterop(options.codex_interop) !== null;
38
+ if (options.codex_interop.import && !codexImport)
39
+ warnings.push("Codex import is misconfigured.");
40
+ // A `running` row this process does not own is either another opencode
41
+ // instance's live job or an orphaned lease (e.g. a server restart killed the
42
+ // owner mid-run). Neither is "consolidating" here; surface it instead.
43
+ const foreignLease = !isPhase2InFlight() &&
44
+ phase2?.status === "running" &&
45
+ phase2.lease_until != null &&
46
+ phase2.lease_until * 1000 > now;
47
+ if (foreignLease) {
48
+ warnings.push(`Consolidation lease held by another process until ${new Date(phase2.lease_until * 1000).toLocaleTimeString()}.`);
49
+ }
50
+ let activity = "idle";
51
+ if (isPluginShuttingDown())
52
+ activity = "stopping";
53
+ else if (isPhase2InFlight())
54
+ activity = "consolidating";
55
+ else if ((phase1.by_status.running ?? 0) > 0)
56
+ activity = "extracting";
57
+ else if (!options.generate_memories)
58
+ activity = options.use_memories ? "read_only" : "disabled";
59
+ else if (retryAt !== null)
60
+ activity = "retrying";
61
+ else if (warnings.length > 0)
62
+ activity = "error";
63
+ return {
64
+ activity,
65
+ useMemories: options.use_memories,
66
+ generateMemories: options.generate_memories,
67
+ extractModel: options.extract_model ?? null,
68
+ consolidationModel: options.consolidation_model ?? null,
69
+ codexImport,
70
+ // finished_at on a failed attempt is NOT a successful consolidation.
71
+ lastSuccessAt: phase2?.success_finished_at != null ? phase2.success_finished_at * 1000 : null,
72
+ retryAt,
73
+ warnings,
74
+ sessionMode: sessionID ? store.getMemoryMode(sessionID) : null,
75
+ memoryRoot: memoryRoot(),
76
+ injected: {
77
+ sessionTokens: session.tokens,
78
+ sessionRequests: session.requests,
79
+ totalTokens: total.tokens,
80
+ totalRequests: total.requests,
81
+ },
82
+ };
83
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * opencode2 tool registration.
3
+ *
4
+ * The memory tools' logic lives in tools/*.ts (V1 tool() definitions) and is
5
+ * reused verbatim: each V1 definition already bundles {description, args
6
+ * (zod raw shape), execute}. V2 accepts any StandardSchema as input, which
7
+ * zod v4 satisfies natively, so the adapter just re-wraps the same validate
8
+ * + execute path and maps the result shape:
9
+ * V1 string | {output, metadata} → V2 {content, metadata}
10
+ * No V1 file is touched and behavior is identical by construction.
11
+ */
12
+ import { z } from "zod";
13
+ interface V2ToolDefinition {
14
+ name: string;
15
+ description: string;
16
+ input: z.ZodTypeAny;
17
+ execute: (input: any, ctx: {
18
+ sessionID: string;
19
+ messageID: string;
20
+ agent: string;
21
+ abort?: AbortSignal;
22
+ }) => Promise<{
23
+ content: string | unknown[];
24
+ metadata?: unknown;
25
+ }>;
26
+ }
27
+ /**
28
+ * Same gating as the V1 entry: the read/search/list/add-note tools require
29
+ * BOTH use_memories and dedicated_tools (codex MemoriesExtension); the
30
+ * control tools are always available.
31
+ */
32
+ export declare function buildV2Tools(): V2ToolDefinition[];
33
+ export {};
@@ -0,0 +1,57 @@
1
+ /**
2
+ * opencode2 tool registration.
3
+ *
4
+ * The memory tools' logic lives in tools/*.ts (V1 tool() definitions) and is
5
+ * reused verbatim: each V1 definition already bundles {description, args
6
+ * (zod raw shape), execute}. V2 accepts any StandardSchema as input, which
7
+ * zod v4 satisfies natively, so the adapter just re-wraps the same validate
8
+ * + execute path and maps the result shape:
9
+ * V1 string | {output, metadata} → V2 {content, metadata}
10
+ * No V1 file is touched and behavior is identical by construction.
11
+ */
12
+ import { z } from "zod";
13
+ import { memory_read, memory_search, memory_list, memory_add_note } from "../../tools/memory.js";
14
+ import { memory_reset, memory_inspect, memory_mode } from "../../tools/control.js";
15
+ import { pluginOptions } from "../options.js";
16
+ function adaptTool(name, v1) {
17
+ return {
18
+ name,
19
+ description: v1.description,
20
+ input: z.object(v1.args),
21
+ async execute(input, tctx) {
22
+ const v1ctx = {
23
+ sessionID: tctx.sessionID,
24
+ messageID: tctx.messageID,
25
+ agent: tctx.agent,
26
+ directory: "",
27
+ worktree: "",
28
+ abort: tctx.abort instanceof AbortSignal ? tctx.abort : new AbortController().signal,
29
+ metadata: () => { },
30
+ ask: async () => { },
31
+ };
32
+ const res = await v1.execute(input, v1ctx);
33
+ if (typeof res === "string")
34
+ return { content: res };
35
+ return { content: res.output ?? "", ...(res.metadata !== undefined ? { metadata: res.metadata } : {}) };
36
+ },
37
+ };
38
+ }
39
+ const ALL_MEMORY_TOOLS = [
40
+ ["memory_read", memory_read],
41
+ ["memory_search", memory_search],
42
+ ["memory_list", memory_list],
43
+ ["memory_add_note", memory_add_note],
44
+ ["memory_reset", memory_reset],
45
+ ["memory_inspect", memory_inspect],
46
+ ["memory_mode", memory_mode],
47
+ ];
48
+ const CONTROL_ONLY = new Set(["memory_reset", "memory_inspect", "memory_mode"]);
49
+ /**
50
+ * Same gating as the V1 entry: the read/search/list/add-note tools require
51
+ * BOTH use_memories and dedicated_tools (codex MemoriesExtension); the
52
+ * control tools are always available.
53
+ */
54
+ export function buildV2Tools() {
55
+ const full = pluginOptions.use_memories && pluginOptions.dedicated_tools;
56
+ return ALL_MEMORY_TOOLS.filter(([name]) => full || CONTROL_ONLY.has(name)).map(([name, v1]) => adaptTool(name, v1));
57
+ }
@@ -0,0 +1,3 @@
1
+ import { Plugin } from "@opencode/plugin/tui";
2
+ declare const _default: Plugin.Definition;
3
+ export default _default;