opencode-codex-memory 0.1.3 → 0.1.6
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/dist/opencode.json +37 -0
- package/dist/src/capture.d.ts +19 -0
- package/dist/src/capture.js +120 -0
- package/dist/src/citation.d.ts +14 -0
- package/dist/src/citation.js +81 -0
- package/dist/src/db.d.ts +3 -0
- package/dist/src/db.js +78 -0
- package/dist/src/git-baseline.d.ts +24 -0
- package/dist/src/git-baseline.js +150 -0
- package/dist/src/index.d.ts +163 -0
- package/dist/src/index.js +365 -0
- package/dist/src/llm.d.ts +19 -0
- package/dist/src/llm.js +251 -0
- package/dist/src/path-guard.d.ts +10 -0
- package/dist/src/path-guard.js +44 -0
- package/dist/src/paths.d.ts +4 -0
- package/dist/src/paths.js +23 -0
- package/dist/src/phase1.d.ts +11 -0
- package/dist/src/phase1.js +104 -0
- package/dist/src/phase2.d.ts +11 -0
- package/dist/src/phase2.js +83 -0
- package/dist/src/ratelimit.d.ts +5 -0
- package/dist/src/ratelimit.js +20 -0
- package/dist/src/redact.d.ts +8 -0
- package/dist/src/redact.js +37 -0
- package/dist/src/source.d.ts +3 -0
- package/dist/src/source.js +46 -0
- package/dist/src/store.d.ts +96 -0
- package/dist/src/store.js +346 -0
- package/dist/src/token.d.ts +8 -0
- package/dist/src/token.js +19 -0
- package/dist/src/workspace.d.ts +8 -0
- package/dist/src/workspace.js +194 -0
- package/dist/tools/control.d.ts +29 -0
- package/dist/tools/control.js +153 -0
- package/dist/tools/memory.d.ts +52 -0
- package/dist/tools/memory.js +322 -0
- package/package.json +24 -6
- package/src/capture.ts +0 -135
- package/src/citation.ts +0 -94
- package/src/db.ts +0 -80
- package/src/git-baseline.ts +0 -162
- package/src/index.ts +0 -366
- package/src/llm.ts +0 -267
- package/src/path-guard.ts +0 -44
- package/src/paths.ts +0 -29
- package/src/phase1.ts +0 -116
- package/src/phase2.ts +0 -99
- package/src/ratelimit.ts +0 -26
- package/src/redact.ts +0 -44
- package/src/source.ts +0 -59
- package/src/store.ts +0 -430
- package/src/token.ts +0 -21
- package/src/workspace.ts +0 -181
- package/tools/control.ts +0 -145
- package/tools/memory.ts +0 -318
- /package/{src → dist/src}/templates/consolidation.md +0 -0
- /package/{src → dist/src}/templates/read_path.md +0 -0
- /package/{src → dist/src}/templates/stage_one_input.md +0 -0
- /package/{src → dist/src}/templates/stage_one_system.md +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { PluginInput, PluginOptions } from "@opencode-ai/plugin";
|
|
2
|
+
export declare function takeNewCitations(partKey: string, ids: string[]): string[];
|
|
3
|
+
declare const _default: {
|
|
4
|
+
id: string;
|
|
5
|
+
server(input: PluginInput, opts?: PluginOptions): Promise<{
|
|
6
|
+
tool: {
|
|
7
|
+
memory_read: {
|
|
8
|
+
description: string;
|
|
9
|
+
args: {
|
|
10
|
+
path: import("zod").ZodString;
|
|
11
|
+
line_offset: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
12
|
+
max_lines: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
13
|
+
};
|
|
14
|
+
execute(args: {
|
|
15
|
+
path: string;
|
|
16
|
+
line_offset?: number | undefined;
|
|
17
|
+
max_lines?: number | undefined;
|
|
18
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
19
|
+
};
|
|
20
|
+
memory_search: {
|
|
21
|
+
description: string;
|
|
22
|
+
args: {
|
|
23
|
+
query: import("zod").ZodOptional<import("zod").ZodString>;
|
|
24
|
+
case_sensitive: import("zod").ZodDefault<import("zod").ZodBoolean>;
|
|
25
|
+
since: import("zod").ZodOptional<import("zod").ZodString>;
|
|
26
|
+
until: import("zod").ZodOptional<import("zod").ZodString>;
|
|
27
|
+
limit: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
28
|
+
};
|
|
29
|
+
execute(args: {
|
|
30
|
+
case_sensitive: boolean;
|
|
31
|
+
limit: number;
|
|
32
|
+
query?: string | undefined;
|
|
33
|
+
since?: string | undefined;
|
|
34
|
+
until?: string | undefined;
|
|
35
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
36
|
+
};
|
|
37
|
+
memory_list: {
|
|
38
|
+
description: string;
|
|
39
|
+
args: {
|
|
40
|
+
path: import("zod").ZodDefault<import("zod").ZodString>;
|
|
41
|
+
max_results: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
42
|
+
};
|
|
43
|
+
execute(args: {
|
|
44
|
+
path: string;
|
|
45
|
+
max_results: number;
|
|
46
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
47
|
+
};
|
|
48
|
+
memory_add_note: {
|
|
49
|
+
description: string;
|
|
50
|
+
args: {
|
|
51
|
+
note: import("zod").ZodString;
|
|
52
|
+
title: import("zod").ZodOptional<import("zod").ZodString>;
|
|
53
|
+
};
|
|
54
|
+
execute(args: {
|
|
55
|
+
note: string;
|
|
56
|
+
title?: string | undefined;
|
|
57
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
58
|
+
};
|
|
59
|
+
memory_reset: {
|
|
60
|
+
description: string;
|
|
61
|
+
args: {
|
|
62
|
+
confirm: import("zod").ZodBoolean;
|
|
63
|
+
};
|
|
64
|
+
execute(args: {
|
|
65
|
+
confirm: boolean;
|
|
66
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
67
|
+
};
|
|
68
|
+
memory_inspect: {
|
|
69
|
+
description: string;
|
|
70
|
+
args: {};
|
|
71
|
+
execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
72
|
+
};
|
|
73
|
+
memory_mode: {
|
|
74
|
+
description: string;
|
|
75
|
+
args: {
|
|
76
|
+
mode: import("zod").ZodEnum<{
|
|
77
|
+
enabled: "enabled";
|
|
78
|
+
disabled: "disabled";
|
|
79
|
+
polluted: "polluted";
|
|
80
|
+
}>;
|
|
81
|
+
sessionId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
82
|
+
};
|
|
83
|
+
execute(args: {
|
|
84
|
+
mode: "enabled" | "disabled" | "polluted";
|
|
85
|
+
sessionId?: string | undefined;
|
|
86
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
87
|
+
};
|
|
88
|
+
} | {
|
|
89
|
+
memory_reset: {
|
|
90
|
+
description: string;
|
|
91
|
+
args: {
|
|
92
|
+
confirm: import("zod").ZodBoolean;
|
|
93
|
+
};
|
|
94
|
+
execute(args: {
|
|
95
|
+
confirm: boolean;
|
|
96
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
97
|
+
};
|
|
98
|
+
memory_inspect: {
|
|
99
|
+
description: string;
|
|
100
|
+
args: {};
|
|
101
|
+
execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
102
|
+
};
|
|
103
|
+
memory_mode: {
|
|
104
|
+
description: string;
|
|
105
|
+
args: {
|
|
106
|
+
mode: import("zod").ZodEnum<{
|
|
107
|
+
enabled: "enabled";
|
|
108
|
+
disabled: "disabled";
|
|
109
|
+
polluted: "polluted";
|
|
110
|
+
}>;
|
|
111
|
+
sessionId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
112
|
+
};
|
|
113
|
+
execute(args: {
|
|
114
|
+
mode: "enabled" | "disabled" | "polluted";
|
|
115
|
+
sessionId?: string | undefined;
|
|
116
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
117
|
+
};
|
|
118
|
+
memory_read?: undefined;
|
|
119
|
+
memory_search?: undefined;
|
|
120
|
+
memory_list?: undefined;
|
|
121
|
+
memory_add_note?: undefined;
|
|
122
|
+
};
|
|
123
|
+
config(input: {
|
|
124
|
+
agent?: Record<string, unknown>;
|
|
125
|
+
}): Promise<void>;
|
|
126
|
+
"experimental.chat.system.transform"(input: {
|
|
127
|
+
sessionID?: string;
|
|
128
|
+
model: unknown;
|
|
129
|
+
}, output: {
|
|
130
|
+
system: string[];
|
|
131
|
+
}): Promise<void>;
|
|
132
|
+
"experimental.chat.messages.transform"(_input: unknown, output: {
|
|
133
|
+
messages: {
|
|
134
|
+
info: {
|
|
135
|
+
role?: string;
|
|
136
|
+
};
|
|
137
|
+
parts: {
|
|
138
|
+
type: string;
|
|
139
|
+
text?: string;
|
|
140
|
+
}[];
|
|
141
|
+
}[];
|
|
142
|
+
}): Promise<void>;
|
|
143
|
+
event(input: {
|
|
144
|
+
event: {
|
|
145
|
+
type: string;
|
|
146
|
+
properties: unknown;
|
|
147
|
+
};
|
|
148
|
+
}): Promise<void>;
|
|
149
|
+
dispose(): Promise<void>;
|
|
150
|
+
}>;
|
|
151
|
+
};
|
|
152
|
+
export default _default;
|
|
153
|
+
/**
|
|
154
|
+
* Registers the memorize / memorize-extract sub-agents through the config
|
|
155
|
+
* hook so installing the plugin requires no manual agent setup. Definitions
|
|
156
|
+
* are read from the plugin's bundled opencode.json (single source of truth
|
|
157
|
+
* with the dev checkout). A user-defined agent of the same name always wins —
|
|
158
|
+
* only missing entries are filled. opencode-specific packaging: codex ships
|
|
159
|
+
* its memory agents inside the binary.
|
|
160
|
+
*/
|
|
161
|
+
export declare function injectAgentDefinitions(config: {
|
|
162
|
+
agent?: Record<string, unknown>;
|
|
163
|
+
}): void;
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { ensureMemoryLayout, buildMemorySystemPrompt, invalidateCache } from "./source.js";
|
|
2
|
+
import { stripCitations, extractCitedSessionIds } from "./citation.js";
|
|
3
|
+
import { memory_read, memory_search, memory_list, memory_add_note } from "../tools/memory.js";
|
|
4
|
+
import { memory_reset, memory_inspect, memory_mode } from "../tools/control.js";
|
|
5
|
+
import { MemoryStore } from "./store.js";
|
|
6
|
+
import { runPhase1 } from "./phase1.js";
|
|
7
|
+
import { runPhase2 } from "./phase2.js";
|
|
8
|
+
import { setPluginInput, cleanupOldSubSessions, isMemorySubSession } from "./llm.js";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
let phase1InFlight = false;
|
|
12
|
+
let pluginClient = null;
|
|
13
|
+
// Configured MCP server names, fetched lazily; null until first successful fetch.
|
|
14
|
+
let mcpServerNames = null;
|
|
15
|
+
// Option names and defaults mirror codex's MemoriesToml/MemoriesConfig
|
|
16
|
+
// (codex-rs/config/src/types.rs). Keep them 1:1 so the drift script and manual
|
|
17
|
+
// syncing stay trivial; do not rename for taste.
|
|
18
|
+
let pluginOptions = {
|
|
19
|
+
generate_memories: true,
|
|
20
|
+
use_memories: true,
|
|
21
|
+
dedicated_tools: true,
|
|
22
|
+
disable_on_external_context: false,
|
|
23
|
+
max_raw_memories_for_consolidation: 256,
|
|
24
|
+
max_unused_days: 30,
|
|
25
|
+
max_rollout_age_days: 10,
|
|
26
|
+
max_rollouts_per_startup: 2,
|
|
27
|
+
min_rollout_idle_hours: 6,
|
|
28
|
+
};
|
|
29
|
+
// Deliberately uncached: openDb() is already a singleton, and caching a store
|
|
30
|
+
// here would hold a stale handle across closeDb() (e.g. after memory_reset).
|
|
31
|
+
function getStore() {
|
|
32
|
+
return new MemoryStore();
|
|
33
|
+
}
|
|
34
|
+
// Citation blocks arrive via message.part.updated once per streaming delta,
|
|
35
|
+
// so the same completed block is seen many times. Track which session ids
|
|
36
|
+
// were already recorded per part to count each citation once.
|
|
37
|
+
const recordedCitations = new Map();
|
|
38
|
+
const MAX_TRACKED_PARTS = 500;
|
|
39
|
+
export function takeNewCitations(partKey, ids) {
|
|
40
|
+
let seen = recordedCitations.get(partKey);
|
|
41
|
+
if (!seen) {
|
|
42
|
+
seen = new Set();
|
|
43
|
+
recordedCitations.set(partKey, seen);
|
|
44
|
+
if (recordedCitations.size > MAX_TRACKED_PARTS) {
|
|
45
|
+
const oldest = recordedCitations.keys().next().value;
|
|
46
|
+
if (oldest !== undefined)
|
|
47
|
+
recordedCitations.delete(oldest);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const fresh = ids.filter((id) => !seen.has(id));
|
|
51
|
+
for (const id of fresh)
|
|
52
|
+
seen.add(id);
|
|
53
|
+
return fresh;
|
|
54
|
+
}
|
|
55
|
+
export default {
|
|
56
|
+
id: "opencode-codex-memory",
|
|
57
|
+
async server(input, opts) {
|
|
58
|
+
setPluginInput(input);
|
|
59
|
+
pluginClient = input.client;
|
|
60
|
+
if (opts)
|
|
61
|
+
applyPluginOptions(opts);
|
|
62
|
+
void cleanupOldSubSessions().catch(() => { });
|
|
63
|
+
return buildHooks();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const KNOWN_OPTION_KEYS = new Set([
|
|
67
|
+
"generate_memories",
|
|
68
|
+
"use_memories",
|
|
69
|
+
"dedicated_tools",
|
|
70
|
+
"disable_on_external_context",
|
|
71
|
+
"extract_model",
|
|
72
|
+
"consolidation_model",
|
|
73
|
+
"max_raw_memories_for_consolidation",
|
|
74
|
+
"max_unused_days",
|
|
75
|
+
"max_rollout_age_days",
|
|
76
|
+
"max_rollouts_per_startup",
|
|
77
|
+
"min_rollout_idle_hours",
|
|
78
|
+
]);
|
|
79
|
+
// codex clamps numeric knobs in From<MemoriesToml> for MemoriesConfig
|
|
80
|
+
// (config/src/types.rs); mirror the exact ranges. Non-finite values fall back
|
|
81
|
+
// to the default.
|
|
82
|
+
function clampInt(value, min, max, fallback) {
|
|
83
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
84
|
+
return fallback;
|
|
85
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
86
|
+
}
|
|
87
|
+
function applyPluginOptions(opts) {
|
|
88
|
+
for (const key of Object.keys(opts)) {
|
|
89
|
+
if (!KNOWN_OPTION_KEYS.has(key)) {
|
|
90
|
+
// codex uses deny_unknown_fields; a plugin can only warn. Covers typos
|
|
91
|
+
// and the deliberately unimplemented min_rate_limit_remaining_percent.
|
|
92
|
+
console.warn(`[opencode-codex-memory] unknown/unsupported option '${key}' ignored`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (typeof opts.generate_memories === "boolean")
|
|
96
|
+
pluginOptions.generate_memories = opts.generate_memories;
|
|
97
|
+
if (typeof opts.use_memories === "boolean")
|
|
98
|
+
pluginOptions.use_memories = opts.use_memories;
|
|
99
|
+
if (typeof opts.dedicated_tools === "boolean")
|
|
100
|
+
pluginOptions.dedicated_tools = opts.dedicated_tools;
|
|
101
|
+
if (typeof opts.disable_on_external_context === "boolean")
|
|
102
|
+
pluginOptions.disable_on_external_context = opts.disable_on_external_context;
|
|
103
|
+
if (typeof opts.extract_model === "string")
|
|
104
|
+
pluginOptions.extract_model = opts.extract_model;
|
|
105
|
+
if (typeof opts.consolidation_model === "string")
|
|
106
|
+
pluginOptions.consolidation_model = opts.consolidation_model;
|
|
107
|
+
if ("max_raw_memories_for_consolidation" in opts)
|
|
108
|
+
pluginOptions.max_raw_memories_for_consolidation = clampInt(opts.max_raw_memories_for_consolidation, 1, 4096, 256);
|
|
109
|
+
if ("max_unused_days" in opts)
|
|
110
|
+
pluginOptions.max_unused_days = clampInt(opts.max_unused_days, 0, 365, 30);
|
|
111
|
+
if ("max_rollout_age_days" in opts)
|
|
112
|
+
pluginOptions.max_rollout_age_days = clampInt(opts.max_rollout_age_days, 0, 90, 10);
|
|
113
|
+
if ("max_rollouts_per_startup" in opts)
|
|
114
|
+
pluginOptions.max_rollouts_per_startup = clampInt(opts.max_rollouts_per_startup, 1, 128, 2);
|
|
115
|
+
if ("min_rollout_idle_hours" in opts)
|
|
116
|
+
pluginOptions.min_rollout_idle_hours = clampInt(opts.min_rollout_idle_hours, 1, 48, 6);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* codex marks every MCP server as memory-polluting unconditionally
|
|
120
|
+
* (codex-mcp server.rs pollutes_memory: true). opencode registers MCP tools
|
|
121
|
+
* as "<server>_<tool>", so match tool names against the configured server
|
|
122
|
+
* list. Fails closed to the web-tools-only check when the list is unavailable.
|
|
123
|
+
*/
|
|
124
|
+
async function isExternalContextTool(toolName) {
|
|
125
|
+
if (toolName === "websearch" || toolName === "webfetch")
|
|
126
|
+
return true;
|
|
127
|
+
if (!mcpServerNames && pluginClient) {
|
|
128
|
+
try {
|
|
129
|
+
const res = await pluginClient.mcp.status();
|
|
130
|
+
const servers = res?.data ?? res;
|
|
131
|
+
if (servers && typeof servers === "object") {
|
|
132
|
+
mcpServerNames = new Set(Object.keys(servers));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// MCP status unavailable (older opencode); keep web-tools-only checks.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!mcpServerNames)
|
|
140
|
+
return false;
|
|
141
|
+
for (const server of mcpServerNames) {
|
|
142
|
+
if (toolName.startsWith(`${server}_`))
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Registers the memorize / memorize-extract sub-agents through the config
|
|
149
|
+
* hook so installing the plugin requires no manual agent setup. Definitions
|
|
150
|
+
* are read from the plugin's bundled opencode.json (single source of truth
|
|
151
|
+
* with the dev checkout). A user-defined agent of the same name always wins —
|
|
152
|
+
* only missing entries are filled. opencode-specific packaging: codex ships
|
|
153
|
+
* its memory agents inside the binary.
|
|
154
|
+
*/
|
|
155
|
+
export function injectAgentDefinitions(config) {
|
|
156
|
+
let defs;
|
|
157
|
+
try {
|
|
158
|
+
const raw = fs.readFileSync(path.join(import.meta.dirname, "..", "opencode.json"), "utf8");
|
|
159
|
+
defs = JSON.parse(raw).agent ?? {};
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
console.warn("[opencode-codex-memory] could not load bundled agent definitions:", err);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
config.agent ??= {};
|
|
166
|
+
for (const [name, def] of Object.entries(defs)) {
|
|
167
|
+
if (!config.agent[name])
|
|
168
|
+
config.agent[name] = def;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function buildHooks() {
|
|
172
|
+
const base = {
|
|
173
|
+
async config(input) {
|
|
174
|
+
try {
|
|
175
|
+
// The write pipeline is the only consumer of the sub-agents; with
|
|
176
|
+
// generation off they would just pollute the user's agent list.
|
|
177
|
+
if (!pluginOptions.generate_memories)
|
|
178
|
+
return;
|
|
179
|
+
injectAgentDefinitions(input);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
console.error("[opencode-codex-memory] config hook error:", err);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
async "experimental.chat.system.transform"(input, output) {
|
|
186
|
+
try {
|
|
187
|
+
if (!pluginOptions.use_memories)
|
|
188
|
+
return;
|
|
189
|
+
if (input.sessionID && isMemorySubSession(input.sessionID))
|
|
190
|
+
return;
|
|
191
|
+
ensureMemoryLayout();
|
|
192
|
+
const memoryPrompt = buildMemorySystemPrompt();
|
|
193
|
+
if (memoryPrompt) {
|
|
194
|
+
output.system.push(memoryPrompt);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
console.error("[opencode-codex-memory] system.transform error:", err);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
async "experimental.chat.messages.transform"(_input, output) {
|
|
202
|
+
try {
|
|
203
|
+
for (const msg of output.messages) {
|
|
204
|
+
if (msg.info?.role !== "assistant")
|
|
205
|
+
continue;
|
|
206
|
+
for (const part of msg.parts) {
|
|
207
|
+
if (part.type === "text" && typeof part.text === "string" && part.text.includes("<memory-citation>")) {
|
|
208
|
+
const before = part.text;
|
|
209
|
+
part.text = stripCitations(part.text);
|
|
210
|
+
if (part.text.includes("<memory-citation>")) {
|
|
211
|
+
console.warn("[opencode-codex-memory] citation marker still present after stripCitations — hook contract may have changed");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
console.error("[opencode-codex-memory] messages.transform error:", err);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
async event(input) {
|
|
222
|
+
try {
|
|
223
|
+
const ev = input.event;
|
|
224
|
+
if (ev.type === "message.part.updated") {
|
|
225
|
+
const part = ev.properties.part;
|
|
226
|
+
if (!part || part.type !== "text" || typeof part.text !== "string")
|
|
227
|
+
return;
|
|
228
|
+
if (part.sessionID && isMemorySubSession(part.sessionID))
|
|
229
|
+
return;
|
|
230
|
+
if (!part.text.includes("<memory-citation>"))
|
|
231
|
+
return;
|
|
232
|
+
let ids = [];
|
|
233
|
+
try {
|
|
234
|
+
ids = extractCitedSessionIds(part.text);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const fresh = takeNewCitations(`${part.sessionID ?? ""}:${part.id ?? ""}`, ids);
|
|
240
|
+
if (fresh.length > 0) {
|
|
241
|
+
try {
|
|
242
|
+
getStore().recordUsage(fresh);
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
console.error("[opencode-codex-memory] recordUsage failed:", e);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (ev.type === "tool.execute.after") {
|
|
251
|
+
// Mirrors codex: external context (web search or any MCP tool) only
|
|
252
|
+
// pollutes the session's memory when disable_on_external_context is
|
|
253
|
+
// enabled. Off by default.
|
|
254
|
+
if (!pluginOptions.disable_on_external_context)
|
|
255
|
+
return;
|
|
256
|
+
const props = ev.properties;
|
|
257
|
+
const toolName = props?.tool ?? "";
|
|
258
|
+
if (props.sessionID && (await isExternalContextTool(toolName))) {
|
|
259
|
+
try {
|
|
260
|
+
getStore().markPolluted(props.sessionID);
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
console.error("[opencode-codex-memory] markPolluted failed:", e);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (ev.type === "session.deleted") {
|
|
269
|
+
// Mirrors codex delete_thread_memory: drop the extracted memory and
|
|
270
|
+
// its job when the session is deleted; the file disappears at the
|
|
271
|
+
// next phase-2 rebuild and the diff drives forgetting.
|
|
272
|
+
const props = ev.properties;
|
|
273
|
+
const sid = props?.info?.id;
|
|
274
|
+
if (sid) {
|
|
275
|
+
try {
|
|
276
|
+
getStore().deleteSessionMemory(sid);
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
console.error("[opencode-codex-memory] deleteSessionMemory failed:", e);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (ev.type === "session.idle") {
|
|
285
|
+
const props = ev.properties;
|
|
286
|
+
const sid = props?.sessionID;
|
|
287
|
+
if (!sid || isMemorySubSession(sid))
|
|
288
|
+
return;
|
|
289
|
+
// codex stamps memory_mode at thread creation from generate_memories:
|
|
290
|
+
// sessions seen while generation is off stay excluded permanently,
|
|
291
|
+
// even if the option is re-enabled later.
|
|
292
|
+
try {
|
|
293
|
+
getStore().stampMemoryModeIfAbsent(sid, pluginOptions.generate_memories ? "enabled" : "disabled");
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
console.error("[opencode-codex-memory] stampMemoryModeIfAbsent failed:", e);
|
|
297
|
+
}
|
|
298
|
+
void triggerPhase1(sid);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
console.error("[opencode-codex-memory] event error:", err);
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
async dispose() {
|
|
307
|
+
invalidateCache();
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
// Control tools (reset/inspect/mode) are always available. The memory
|
|
311
|
+
// read/search/list/add-note tools require BOTH use_memories and
|
|
312
|
+
// dedicated_tools, mirroring codex's MemoriesExtension: use_memories=false
|
|
313
|
+
// disables the whole extension including its tools (extension.rs).
|
|
314
|
+
const tool = pluginOptions.use_memories && pluginOptions.dedicated_tools
|
|
315
|
+
? {
|
|
316
|
+
memory_read,
|
|
317
|
+
memory_search,
|
|
318
|
+
memory_list,
|
|
319
|
+
memory_add_note,
|
|
320
|
+
memory_reset,
|
|
321
|
+
memory_inspect,
|
|
322
|
+
memory_mode,
|
|
323
|
+
}
|
|
324
|
+
: {
|
|
325
|
+
memory_reset,
|
|
326
|
+
memory_inspect,
|
|
327
|
+
memory_mode,
|
|
328
|
+
};
|
|
329
|
+
return { ...base, tool };
|
|
330
|
+
}
|
|
331
|
+
async function triggerPhase1(currentSessionId) {
|
|
332
|
+
if (phase1InFlight || !pluginOptions.generate_memories)
|
|
333
|
+
return;
|
|
334
|
+
phase1InFlight = true;
|
|
335
|
+
try {
|
|
336
|
+
await runPhase1(getStore(), {
|
|
337
|
+
maxAgeDays: pluginOptions.max_rollout_age_days,
|
|
338
|
+
minIdleHours: pluginOptions.min_rollout_idle_hours,
|
|
339
|
+
maxClaimed: pluginOptions.max_rollouts_per_startup,
|
|
340
|
+
excludeSession: currentSessionId,
|
|
341
|
+
extractModel: pluginOptions.extract_model,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
console.error("[opencode-codex-memory] phase1 error:", err);
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
phase1InFlight = false;
|
|
349
|
+
}
|
|
350
|
+
void triggerPhase2();
|
|
351
|
+
}
|
|
352
|
+
async function triggerPhase2() {
|
|
353
|
+
try {
|
|
354
|
+
// runPhase2 has its own in-flight guard
|
|
355
|
+
await runPhase2(getStore(), {
|
|
356
|
+
maxRaw: pluginOptions.max_raw_memories_for_consolidation,
|
|
357
|
+
maxUnusedDays: pluginOptions.max_unused_days,
|
|
358
|
+
extensionRetentionDays: 7,
|
|
359
|
+
consolidationModel: pluginOptions.consolidation_model,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
console.error("[opencode-codex-memory] phase2 error:", err);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
export interface ExtractionResult {
|
|
3
|
+
raw_memory: string;
|
|
4
|
+
rollout_summary: string;
|
|
5
|
+
rollout_slug: string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function setPluginInput(input: PluginInput): void;
|
|
8
|
+
export declare function isMemorySubSession(sessionId: string): boolean;
|
|
9
|
+
export interface ExtractOptions {
|
|
10
|
+
cwd?: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Returns null when the extractor reported a no-op (nothing worth remembering). */
|
|
14
|
+
export declare function extractViaSubagent(sessionId: string, transcript: string, opts?: ExtractOptions): Promise<ExtractionResult | null>;
|
|
15
|
+
export declare function consolidateViaSubagent(memoryRoot: string, diffFileName: string, model?: string): Promise<void>;
|
|
16
|
+
export declare function cleanupOldSubSessions(maxAgeMinutes?: number): Promise<void>;
|
|
17
|
+
export declare function fillTemplate(tmpl: string, vars: Record<string, string>): string;
|
|
18
|
+
/** Parses the stage-1 JSON output. Returns null for the all-empty no-op response. */
|
|
19
|
+
export declare function parseExtraction(raw: string): ExtractionResult | null;
|