opencode-claude-memory 2.3.0 → 2.3.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/dist/host/v2/agents.d.ts +2 -1
- package/dist/host/v2/agents.js +4 -0
- package/dist/host/v2/events.d.ts +8 -0
- package/dist/host/v2/events.js +49 -2
- package/dist/host/v2/fork.d.ts +3 -0
- package/dist/host/v2/fork.js +9 -1
- package/dist/host/v2/host.d.ts +3 -1
- package/dist/host/v2/host.js +5 -0
- package/dist/host/v2/plugin.js +14 -4
- package/package.json +1 -1
package/dist/host/v2/agents.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentEditor } from "@opencode/plugin/promise/agent";
|
|
2
|
-
import type
|
|
2
|
+
import { type MemoryAgents } from "../../config.js";
|
|
3
3
|
import type { PermissionRule } from "./fork.js";
|
|
4
|
+
export declare function isMemoryAgent(agent: unknown, agents?: MemoryAgents): boolean;
|
|
4
5
|
export type { PermissionRule };
|
|
5
6
|
export declare const BASELINE_PROBE_AGENT = "opencode-memory-baseline-probe";
|
|
6
7
|
export declare const PERMISSIONS_PROBE_AGENT = "opencode-memory-permissions-probe";
|
package/dist/host/v2/agents.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { memoryAgentDefaults } from "../../agents.js";
|
|
2
|
+
import { MEMORY_AGENTS } from "../../config.js";
|
|
3
|
+
export function isMemoryAgent(agent, agents = MEMORY_AGENTS) {
|
|
4
|
+
return typeof agent === "string" && (agent === agents.extract || agent === agents.dream || agent === agents.recall);
|
|
5
|
+
}
|
|
2
6
|
// A throwaway id used to read the host's default ruleset inside the same draft.
|
|
3
7
|
export const BASELINE_PROBE_AGENT = "opencode-memory-baseline-probe";
|
|
4
8
|
// A hidden, deny-all agent that is kept: every rule after its own PROBE_RULES was appended to all
|
package/dist/host/v2/events.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type MemoryAgents } from "../../config.js";
|
|
1
2
|
import type { ExtractionCoordinator } from "../../extraction/ExtractionCoordinator.js";
|
|
2
3
|
import type { RecalledMemory } from "../../recall/format.js";
|
|
3
4
|
import { type RecallCoordinator } from "../../recall/RecallCoordinator.js";
|
|
@@ -9,19 +10,26 @@ export type V2Event = {
|
|
|
9
10
|
} | null;
|
|
10
11
|
data?: Record<string, unknown>;
|
|
11
12
|
};
|
|
13
|
+
export declare const MAX_IGNORED_FORKS = 512;
|
|
12
14
|
export type V2EventDeps = {
|
|
13
15
|
directory: string;
|
|
14
16
|
owned: OwnedSessions;
|
|
15
17
|
recall: Pick<RecallCoordinator, "forget">;
|
|
16
18
|
extraction: Pick<ExtractionCoordinator, "onSessionIdle" | "onSessionStatus" | "onSessionDeleted">;
|
|
19
|
+
agents?: MemoryAgents;
|
|
17
20
|
now?: () => number;
|
|
18
21
|
};
|
|
19
22
|
export declare class V2SessionEvents {
|
|
20
23
|
private readonly deps;
|
|
21
24
|
private readonly sessions;
|
|
22
25
|
private readonly recalled;
|
|
26
|
+
private readonly seenForks;
|
|
27
|
+
private readonly retainedForks;
|
|
23
28
|
private readonly now;
|
|
24
29
|
constructor(deps: V2EventDeps);
|
|
30
|
+
isIgnoredFork(sessionID: string): boolean;
|
|
31
|
+
ignoreFork(sessionID: string, persistent?: boolean): void;
|
|
32
|
+
untrack(sessionID: string): void;
|
|
25
33
|
track(sessionID: string): void;
|
|
26
34
|
isTracked(sessionID: string): boolean;
|
|
27
35
|
surfacedKeys(sessionID: string): ReadonlySet<string>;
|
package/dist/host/v2/events.js
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
1
|
+
// V2 events → coordinator calls.
|
|
2
|
+
//
|
|
3
|
+
// The event stream is global across server locations, and the session execution events carry no
|
|
4
|
+
// location at all, so the adapter only acts on sessions it knows belong to its own location: those
|
|
5
|
+
// created here (`session.created` with this location) or seen by the location-scoped prompt and
|
|
6
|
+
// context hooks. Turn boundaries: `session.execution.started` = busy, `succeeded` / `failed` /
|
|
7
|
+
// `interrupted` = idle. The deprecated `session.status` / `session.idle` events are honoured too.
|
|
8
|
+
import { MEMORY_AGENTS } from "../../config.js";
|
|
1
9
|
import { SESSION_STATE_TTL_MS } from "../../recall/RecallCoordinator.js";
|
|
2
10
|
import { surfaceKey } from "../../store/scan.js";
|
|
11
|
+
import { isMemoryAgent } from "./agents.js";
|
|
12
|
+
import { isMemoryForkMetadata } from "./fork.js";
|
|
3
13
|
const IDLE_EVENTS = [
|
|
4
14
|
"session.execution.succeeded",
|
|
5
15
|
"session.execution.failed",
|
|
6
16
|
"session.execution.interrupted",
|
|
7
17
|
"session.idle",
|
|
8
18
|
];
|
|
19
|
+
export const MAX_IGNORED_FORKS = 512;
|
|
20
|
+
function rememberBounded(set, id, maxSize = MAX_IGNORED_FORKS) {
|
|
21
|
+
if (set.has(id))
|
|
22
|
+
set.delete(id);
|
|
23
|
+
set.add(id);
|
|
24
|
+
while (set.size > maxSize) {
|
|
25
|
+
const oldest = set.values().next().value;
|
|
26
|
+
if (oldest === undefined)
|
|
27
|
+
break;
|
|
28
|
+
set.delete(oldest);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
9
31
|
export class V2SessionEvents {
|
|
10
32
|
deps;
|
|
11
33
|
// Tracked sessions → last time a hook or event saw them. Entries idle for longer than
|
|
@@ -16,13 +38,32 @@ export class V2SessionEvents {
|
|
|
16
38
|
// model request and does not keep it in the history, so the recalled section is re-injected on
|
|
17
39
|
// every request (not only the first one of a turn) and its keys are what the selector skips.
|
|
18
40
|
recalled = new Map();
|
|
41
|
+
// Bounded sets of plugin forks that carry no `parentID` on V2: `seenForks` captures forks
|
|
42
|
+
// announced on `session.created` (including when it races ahead of `session.create()`), while
|
|
43
|
+
// `retainedForks` keeps forks whose cleanup delete failed or that a context hook observed under
|
|
44
|
+
// a memory agent so successful forks cannot evict them after the 60 s `owned` grace expires.
|
|
45
|
+
seenForks = new Set();
|
|
46
|
+
retainedForks = new Set();
|
|
19
47
|
now;
|
|
20
48
|
constructor(deps) {
|
|
21
49
|
this.deps = deps;
|
|
22
50
|
this.now = deps.now ?? Date.now;
|
|
23
51
|
}
|
|
52
|
+
isIgnoredFork(sessionID) {
|
|
53
|
+
return this.seenForks.has(sessionID) || this.retainedForks.has(sessionID);
|
|
54
|
+
}
|
|
55
|
+
// Marks a session as a plugin-owned fork and clears any accidental user-session state.
|
|
56
|
+
ignoreFork(sessionID, persistent = false) {
|
|
57
|
+
rememberBounded(persistent ? this.retainedForks : this.seenForks, sessionID);
|
|
58
|
+
this.forget(sessionID);
|
|
59
|
+
}
|
|
60
|
+
// Drops any user-session tracking recorded before `session.create()` resolved (e.g., from an
|
|
61
|
+
// unannotated `session.created` race), without permanently retaining normal deleted forks.
|
|
62
|
+
untrack(sessionID) {
|
|
63
|
+
this.forget(sessionID);
|
|
64
|
+
}
|
|
24
65
|
track(sessionID) {
|
|
25
|
-
if (this.deps.owned.has(sessionID))
|
|
66
|
+
if (this.deps.owned.has(sessionID) || this.isIgnoredFork(sessionID))
|
|
26
67
|
return;
|
|
27
68
|
const now = this.now();
|
|
28
69
|
this.evictStale(now);
|
|
@@ -65,16 +106,22 @@ export class V2SessionEvents {
|
|
|
65
106
|
return;
|
|
66
107
|
if (type === "session.deleted") {
|
|
67
108
|
this.deps.owned.release(sessionID, 5_000);
|
|
109
|
+
this.seenForks.delete(sessionID);
|
|
110
|
+
this.retainedForks.delete(sessionID);
|
|
68
111
|
this.forget(sessionID);
|
|
69
112
|
return;
|
|
70
113
|
}
|
|
71
114
|
if (type === "session.created") {
|
|
115
|
+
if (isMemoryForkMetadata(data.metadata) || isMemoryAgent(data.agent, this.deps.agents ?? MEMORY_AGENTS)) {
|
|
116
|
+
this.ignoreFork(sessionID);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
72
119
|
const directory = data.location?.directory ?? event.location?.directory;
|
|
73
120
|
if (directory === this.deps.directory && !data.parentID)
|
|
74
121
|
this.track(sessionID);
|
|
75
122
|
return;
|
|
76
123
|
}
|
|
77
|
-
if (this.deps.owned.has(sessionID) || !this.sessions.has(sessionID))
|
|
124
|
+
if (this.deps.owned.has(sessionID) || this.isIgnoredFork(sessionID) || !this.sessions.has(sessionID))
|
|
78
125
|
return;
|
|
79
126
|
this.sessions.set(sessionID, this.now());
|
|
80
127
|
if (type === "session.execution.started") {
|
package/dist/host/v2/fork.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare const FORK_CREATE_TIMEOUT_MS = 30000;
|
|
|
3
3
|
export declare const FORK_CLEANUP_TIMEOUT_MS = 15000;
|
|
4
4
|
export declare const FORK_READ_TIMEOUT_MS = 30000;
|
|
5
5
|
export declare const FORK_SETTLE_POLL_MS = 250;
|
|
6
|
+
export declare const FORK_METADATA_KEY = "opencodeClaudeMemoryFork";
|
|
7
|
+
export declare function isMemoryForkMetadata(metadata: unknown): boolean;
|
|
6
8
|
export type PermissionRule = {
|
|
7
9
|
action: string;
|
|
8
10
|
resource: string;
|
|
@@ -31,6 +33,7 @@ export type V2SessionApi = {
|
|
|
31
33
|
title?: string;
|
|
32
34
|
agent?: string;
|
|
33
35
|
model?: ModelRef;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
34
37
|
permissions?: PermissionRule[];
|
|
35
38
|
}): Promise<{
|
|
36
39
|
id: string;
|
package/dist/host/v2/fork.js
CHANGED
|
@@ -11,6 +11,14 @@ export const FORK_CLEANUP_TIMEOUT_MS = 15_000;
|
|
|
11
11
|
export const FORK_READ_TIMEOUT_MS = 30_000;
|
|
12
12
|
// `wait` can return before an enqueued prompt was admitted; re-check this often until the deadline.
|
|
13
13
|
export const FORK_SETTLE_POLL_MS = 250;
|
|
14
|
+
// Durable session annotation so V2 forks stay identifiable without `parentID` (setting `parentID`
|
|
15
|
+
// causes OpenCode 2.x to post a `<subagent>` report into the parent chat and wake its model).
|
|
16
|
+
export const FORK_METADATA_KEY = "opencodeClaudeMemoryFork";
|
|
17
|
+
export function isMemoryForkMetadata(metadata) {
|
|
18
|
+
return (typeof metadata === "object" &&
|
|
19
|
+
metadata !== null &&
|
|
20
|
+
metadata[FORK_METADATA_KEY] === true);
|
|
21
|
+
}
|
|
14
22
|
export class ForkSessionTimeoutError extends Error {
|
|
15
23
|
constructor(title, timeoutMs) {
|
|
16
24
|
super(`${title} timed out after ${timeoutMs}ms`);
|
|
@@ -66,10 +74,10 @@ export async function runV2Fork(session, input) {
|
|
|
66
74
|
const cleanupTimeoutMs = input.cleanupTimeoutMs ?? FORK_CLEANUP_TIMEOUT_MS;
|
|
67
75
|
const pollMs = input.pollMs ?? FORK_SETTLE_POLL_MS;
|
|
68
76
|
const created = await deadline(`${input.title} session.create`, createTimeoutMs, () => session.create({
|
|
69
|
-
...(input.parentSessionID ? { parentID: input.parentSessionID } : {}),
|
|
70
77
|
title: input.title,
|
|
71
78
|
agent: input.agent,
|
|
72
79
|
...(input.model ? { model: input.model } : {}),
|
|
80
|
+
metadata: { [FORK_METADATA_KEY]: true },
|
|
73
81
|
permissions: input.permissions,
|
|
74
82
|
}));
|
|
75
83
|
const forkID = created?.id;
|
package/dist/host/v2/host.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Logger } from "../../util/log.js";
|
|
2
|
-
import { type GenerateInput, type MemoryHost } from "../types.js";
|
|
2
|
+
import { type ForkCleanupStage, type GenerateInput, type MemoryHost } from "../types.js";
|
|
3
3
|
import { type ModelRef, type PermissionRule, type V2SessionApi } from "./fork.js";
|
|
4
4
|
export type V2AgentInfo = {
|
|
5
5
|
model?: ModelRef;
|
|
@@ -15,6 +15,8 @@ export type V2HostOptions = {
|
|
|
15
15
|
}>;
|
|
16
16
|
getAgent: (name: string) => Promise<V2AgentInfo | undefined>;
|
|
17
17
|
sandboxFor: (name: string) => PermissionRule[];
|
|
18
|
+
onForkCreated?: (sessionID: string) => void;
|
|
19
|
+
onForkCleanupFailed?: (sessionID: string, stage: ForkCleanupStage) => void;
|
|
18
20
|
log?: Logger;
|
|
19
21
|
};
|
|
20
22
|
export declare class V2Host implements MemoryHost {
|
package/dist/host/v2/host.js
CHANGED
|
@@ -69,12 +69,17 @@ export class V2Host {
|
|
|
69
69
|
onCreated: (id) => {
|
|
70
70
|
if (input.system)
|
|
71
71
|
this.forkSystems.set(id, input.system);
|
|
72
|
+
this.options.onForkCreated?.(id);
|
|
72
73
|
input.onCreated?.(id);
|
|
73
74
|
},
|
|
74
75
|
onFinished: (id) => {
|
|
75
76
|
this.forkSystems.delete(id);
|
|
76
77
|
input.onFinished?.(id);
|
|
77
78
|
},
|
|
79
|
+
onCleanupFailed: (id, stage, error) => {
|
|
80
|
+
this.options.onForkCleanupFailed?.(id, stage);
|
|
81
|
+
input.onCleanupFailed?.(id, stage, error);
|
|
82
|
+
},
|
|
78
83
|
});
|
|
79
84
|
}
|
|
80
85
|
}
|
package/dist/host/v2/plugin.js
CHANGED
|
@@ -11,7 +11,7 @@ import { buildMemoryToolSpecs } from "../../tools.js";
|
|
|
11
11
|
import { getErrorMessage } from "../../util/log.js";
|
|
12
12
|
import { OwnedSessions } from "../../util/ownedSessions.js";
|
|
13
13
|
import { withDeadline } from "../../util/timeout.js";
|
|
14
|
-
import { applyMemoryAgents, sandboxRules } from "./agents.js";
|
|
14
|
+
import { applyMemoryAgents, isMemoryAgent, sandboxRules } from "./agents.js";
|
|
15
15
|
import { consumeEvents, V2SessionEvents } from "./events.js";
|
|
16
16
|
import { V2Host } from "./host.js";
|
|
17
17
|
import { createFileLogger } from "./log.js";
|
|
@@ -45,6 +45,7 @@ export const createV2Setup = (env, homeDir) => async (ctx) => {
|
|
|
45
45
|
const log = config.readOnly ? () => { } : createFileLogger(store.stateDir);
|
|
46
46
|
const owned = new OwnedSessions();
|
|
47
47
|
const defaults = memoryAgentDefaults(config.agents, config.readOnly);
|
|
48
|
+
let events;
|
|
48
49
|
const host = new V2Host({
|
|
49
50
|
session: ctx.session,
|
|
50
51
|
generateText: (input) => ctx.generate.text(input),
|
|
@@ -53,12 +54,17 @@ export const createV2Setup = (env, homeDir) => async (ctx) => {
|
|
|
53
54
|
return agent?.data;
|
|
54
55
|
},
|
|
55
56
|
sandboxFor: (name) => sandboxRules(defaults[name]?.allowedTools ?? []),
|
|
57
|
+
onForkCreated: (id) => events?.untrack(id),
|
|
58
|
+
onForkCleanupFailed: (id, stage) => {
|
|
59
|
+
if (stage === "delete")
|
|
60
|
+
events?.ignoreFork(id, true);
|
|
61
|
+
},
|
|
56
62
|
log,
|
|
57
63
|
});
|
|
58
64
|
const deps = { store, config, host, owned, log };
|
|
59
65
|
const recall = new RecallCoordinator(deps);
|
|
60
66
|
const extraction = new ExtractionCoordinator(deps);
|
|
61
|
-
|
|
67
|
+
events = new V2SessionEvents({ directory, owned, recall, extraction, agents: config.agents });
|
|
62
68
|
const registrations = [];
|
|
63
69
|
const register = async (what, call) => {
|
|
64
70
|
registrations.push(await bounded(what, SETUP_CALL_TIMEOUT_MS, call));
|
|
@@ -83,7 +89,7 @@ export const createV2Setup = (env, homeDir) => async (ctx) => {
|
|
|
83
89
|
// Before the prompt is admitted: start the recall selector for this turn.
|
|
84
90
|
await register("session.hook(prompt)", () => ctx.session.hook("prompt", (event) => {
|
|
85
91
|
const { sessionID } = event;
|
|
86
|
-
if (owned.has(sessionID) || host.systemFor(sessionID) !== undefined)
|
|
92
|
+
if (owned.has(sessionID) || events.isIgnoredFork(sessionID) || host.systemFor(sessionID) !== undefined)
|
|
87
93
|
return;
|
|
88
94
|
events.track(sessionID);
|
|
89
95
|
const text = event.prompt?.text;
|
|
@@ -105,8 +111,12 @@ export const createV2Setup = (env, homeDir) => async (ctx) => {
|
|
|
105
111
|
event.system.push({ type: "text", text: forkSystem });
|
|
106
112
|
return;
|
|
107
113
|
}
|
|
108
|
-
if (owned.has(sessionID))
|
|
114
|
+
if (owned.has(sessionID) || events.isIgnoredFork(sessionID))
|
|
115
|
+
return;
|
|
116
|
+
if (isMemoryAgent(event.agent, config.agents)) {
|
|
117
|
+
events.ignoreFork(sessionID, true);
|
|
109
118
|
return;
|
|
119
|
+
}
|
|
110
120
|
events.track(sessionID);
|
|
111
121
|
// Everything recalled in this session so far, not only this turn's selection: the system
|
|
112
122
|
// prompt is rebuilt for every request, so a memory shown once would otherwise vanish after
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-claude-memory",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode plugin for Claude Code memory: persistent, local-first shared memory with Claude Code-compatible Markdown files, LLM recall, in-process auto extraction, and auto-dream consolidation",
|
|
6
6
|
"main": "dist/index.js",
|