@rvboris/opencode-mempalace 0.1.0 → 0.2.0
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 +114 -158
- package/README.ru.md +115 -159
- package/dist/bridge/mempalace_adapter.py +67 -35
- package/dist/plugin/hooks/event.d.ts +3 -10
- package/dist/plugin/hooks/event.js +77 -44
- package/dist/plugin/hooks/system.d.ts +5 -7
- package/dist/plugin/hooks/system.js +16 -19
- package/dist/plugin/hooks/tool.d.ts +2 -10
- package/dist/plugin/hooks/tool.js +4 -49
- package/dist/plugin/index.js +0 -2
- package/dist/plugin/lib/adapter.d.ts +2 -1
- package/dist/plugin/lib/adapter.js +41 -8
- package/dist/plugin/lib/autosave.d.ts +16 -21
- package/dist/plugin/lib/autosave.js +47 -101
- package/dist/plugin/lib/config.d.ts +4 -3
- package/dist/plugin/lib/config.js +58 -19
- package/dist/plugin/lib/constants.d.ts +80 -0
- package/dist/plugin/lib/constants.js +87 -0
- package/dist/plugin/lib/context.js +17 -16
- package/dist/plugin/lib/enforcement.js +2 -8
- package/dist/plugin/lib/log.d.ts +2 -7
- package/dist/plugin/lib/log.js +3 -2
- package/dist/plugin/lib/opencode.d.ts +5 -0
- package/dist/plugin/lib/opencode.js +16 -0
- package/dist/plugin/lib/scope.d.ts +2 -2
- package/dist/plugin/lib/scope.js +3 -2
- package/dist/plugin/lib/types.d.ts +79 -0
- package/dist/plugin/lib/types.js +15 -0
- package/dist/plugin/tools/mempalace-memory.d.ts +4 -8
- package/dist/plugin/tools/mempalace-memory.js +31 -23
- package/package.json +11 -2
- package/dist/plugin/hooks/chat-params.d.ts +0 -5
- package/dist/plugin/hooks/chat-params.js +0 -8
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
import json
|
|
3
3
|
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
from contextlib import redirect_stdout
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from pathlib import Path
|
|
4
8
|
|
|
9
|
+
from mempalace.config import MempalaceConfig
|
|
10
|
+
from mempalace.convo_miner import mine_convos
|
|
5
11
|
from mempalace.mcp_server import (
|
|
6
12
|
tool_add_drawer,
|
|
7
13
|
tool_diary_write,
|
|
@@ -10,43 +16,69 @@ from mempalace.mcp_server import (
|
|
|
10
16
|
)
|
|
11
17
|
|
|
12
18
|
|
|
13
|
-
def
|
|
14
|
-
payload = json.load(sys.stdin)
|
|
15
|
-
|
|
16
|
-
mode = payload["mode"]
|
|
17
|
-
if mode == "search":
|
|
18
|
-
result = tool_search(
|
|
19
|
-
query=payload["query"],
|
|
20
|
-
limit=payload.get("limit", 5),
|
|
21
|
-
wing=payload.get("wing"),
|
|
22
|
-
room=payload.get("room"),
|
|
23
|
-
)
|
|
24
|
-
elif mode == "save":
|
|
25
|
-
result = tool_add_drawer(
|
|
26
|
-
wing=payload["wing"],
|
|
27
|
-
room=payload["room"],
|
|
28
|
-
content=payload["content"],
|
|
29
|
-
added_by=payload.get("added_by", "opencode"),
|
|
30
|
-
)
|
|
31
|
-
elif mode == "kg_add":
|
|
32
|
-
result = tool_kg_add(
|
|
33
|
-
subject=payload["subject"],
|
|
34
|
-
predicate=payload["predicate"],
|
|
35
|
-
object=payload["object"],
|
|
36
|
-
valid_from=payload.get("valid_from", ""),
|
|
37
|
-
source_closet=payload.get("source_closet", ""),
|
|
38
|
-
)
|
|
39
|
-
elif mode == "diary_write":
|
|
40
|
-
result = tool_diary_write(
|
|
41
|
-
agent_name=payload["agent_name"],
|
|
42
|
-
entry=payload["entry"],
|
|
43
|
-
topic=payload.get("topic", "autosave"),
|
|
44
|
-
)
|
|
45
|
-
else:
|
|
46
|
-
result = {"success": False, "error": f"Unknown mode: {mode}"}
|
|
47
|
-
|
|
19
|
+
def write_result(result: dict) -> None:
|
|
48
20
|
output = json.dumps(result, ensure_ascii=False)
|
|
49
21
|
sys.stdout.buffer.write(output.encode("utf-8", errors="replace"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> int:
|
|
25
|
+
try:
|
|
26
|
+
payload = json.load(sys.stdin)
|
|
27
|
+
|
|
28
|
+
mode = payload["mode"]
|
|
29
|
+
if mode == "search":
|
|
30
|
+
result = tool_search(
|
|
31
|
+
query=payload["query"],
|
|
32
|
+
limit=payload.get("limit", 5),
|
|
33
|
+
wing=payload.get("wing"),
|
|
34
|
+
room=payload.get("room"),
|
|
35
|
+
)
|
|
36
|
+
elif mode == "save":
|
|
37
|
+
result = tool_add_drawer(
|
|
38
|
+
wing=payload["wing"],
|
|
39
|
+
room=payload["room"],
|
|
40
|
+
content=payload["content"],
|
|
41
|
+
added_by=payload.get("added_by", "opencode"),
|
|
42
|
+
)
|
|
43
|
+
elif mode == "kg_add":
|
|
44
|
+
result = tool_kg_add(
|
|
45
|
+
subject=payload["subject"],
|
|
46
|
+
predicate=payload["predicate"],
|
|
47
|
+
object=payload["object"],
|
|
48
|
+
valid_from=payload.get("valid_from", ""),
|
|
49
|
+
source_closet=payload.get("source_closet", ""),
|
|
50
|
+
)
|
|
51
|
+
elif mode == "diary_write":
|
|
52
|
+
result = tool_diary_write(
|
|
53
|
+
agent_name=payload["agent_name"],
|
|
54
|
+
entry=payload["entry"],
|
|
55
|
+
topic=payload.get("topic", "autosave"),
|
|
56
|
+
)
|
|
57
|
+
elif mode == "mine_messages":
|
|
58
|
+
palace_path = payload.get("palace_path") or MempalaceConfig().palace_path
|
|
59
|
+
extract_mode = payload.get("extract_mode", "general")
|
|
60
|
+
with tempfile.TemporaryDirectory(prefix="mempalace-autosave-") as tmpdir:
|
|
61
|
+
transcript_path = Path(tmpdir) / "session.txt"
|
|
62
|
+
transcript_path.write_text(payload["transcript"], encoding="utf-8")
|
|
63
|
+
with redirect_stdout(StringIO()):
|
|
64
|
+
mine_convos(
|
|
65
|
+
convo_dir=tmpdir,
|
|
66
|
+
palace_path=palace_path,
|
|
67
|
+
wing=payload.get("wing"),
|
|
68
|
+
agent=payload.get("agent", "opencode"),
|
|
69
|
+
extract_mode=extract_mode,
|
|
70
|
+
)
|
|
71
|
+
result = {
|
|
72
|
+
"success": True,
|
|
73
|
+
"mode": "mine_messages",
|
|
74
|
+
"wing": payload.get("wing"),
|
|
75
|
+
}
|
|
76
|
+
else:
|
|
77
|
+
result = {"success": False, "error": f"Unknown mode: {mode}"}
|
|
78
|
+
except Exception as error:
|
|
79
|
+
result = {"success": False, "error": str(error)}
|
|
80
|
+
|
|
81
|
+
write_result(result)
|
|
50
82
|
return 0
|
|
51
83
|
|
|
52
84
|
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
type
|
|
2
|
-
|
|
3
|
-
project: any;
|
|
4
|
-
directory: string;
|
|
5
|
-
worktree: string;
|
|
6
|
-
$: any;
|
|
7
|
-
};
|
|
8
|
-
export declare const eventHooks: (ctx: PluginContext) => {
|
|
1
|
+
import { type EventHookContext, type SessionEvent } from "../lib/types";
|
|
2
|
+
export declare const eventHooks: (ctx: EventHookContext) => {
|
|
9
3
|
event: ({ event }: {
|
|
10
|
-
event:
|
|
4
|
+
event: SessionEvent;
|
|
11
5
|
}) => Promise<void>;
|
|
12
6
|
"experimental.session.compacting": (input: {
|
|
13
7
|
sessionID?: string;
|
|
@@ -16,4 +10,3 @@ export declare const eventHooks: (ctx: PluginContext) => {
|
|
|
16
10
|
prompt?: string;
|
|
17
11
|
}) => Promise<void>;
|
|
18
12
|
};
|
|
19
|
-
export {};
|
|
@@ -1,7 +1,28 @@
|
|
|
1
|
-
import { AutosaveReason, AutosaveStatus,
|
|
1
|
+
import { AutosaveReason, AutosaveStatus, buildMessageSnapshot, getMessageSnapshot, getSessionState, markAutosaveComplete, markKeywordSavePending, markFailed, markRetrievalPending, setMessageSnapshot, shouldScheduleAutosave, } from "../lib/autosave";
|
|
2
|
+
import { executeAdapter } from "../lib/adapter";
|
|
2
3
|
import { loadConfig } from "../lib/config";
|
|
4
|
+
import { COMPACTION_CONTEXT_MESSAGE, DEFAULT_AGENT_NAME, LOG_MESSAGES } from "../lib/constants";
|
|
5
|
+
import { getProjectName, loadSessionMessages } from "../lib/opencode";
|
|
6
|
+
import { redactSecrets } from "../lib/privacy";
|
|
7
|
+
import { getProjectScope } from "../lib/scope";
|
|
3
8
|
import { writeLog } from "../lib/log";
|
|
9
|
+
import { SESSION_EVENT_TYPES } from "../lib/types";
|
|
4
10
|
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
+
const TRACKED_EVENT_TYPES = new Set(SESSION_EVENT_TYPES);
|
|
12
|
+
const AUTOSAVE_TRIGGER_EVENT_TYPES = new Set([
|
|
13
|
+
"session.idle",
|
|
14
|
+
"session.compacted",
|
|
15
|
+
"session.deleted",
|
|
16
|
+
"session.error",
|
|
17
|
+
]);
|
|
18
|
+
const isTrackedEventType = (value) => TRACKED_EVENT_TYPES.has(value);
|
|
19
|
+
const isAutosaveTriggerEventType = (value) => {
|
|
20
|
+
return AUTOSAVE_TRIGGER_EVENT_TYPES.has(value);
|
|
21
|
+
};
|
|
22
|
+
const getSessionId = (event) => {
|
|
23
|
+
const properties = event.properties;
|
|
24
|
+
return properties?.sessionID ?? properties?.info?.id;
|
|
25
|
+
};
|
|
5
26
|
const toReason = (eventType) => {
|
|
6
27
|
if (eventType === "session.compacted")
|
|
7
28
|
return AutosaveReason.Compacted;
|
|
@@ -9,55 +30,42 @@ const toReason = (eventType) => {
|
|
|
9
30
|
return AutosaveReason.Error;
|
|
10
31
|
return AutosaveReason.Idle;
|
|
11
32
|
};
|
|
12
|
-
const loadMessages = async (ctx, sessionId) => {
|
|
13
|
-
const response = await ctx.client.session.messages({ path: { id: sessionId } });
|
|
14
|
-
return response?.data ?? response ?? [];
|
|
15
|
-
};
|
|
16
33
|
export const eventHooks = (ctx) => {
|
|
17
34
|
return {
|
|
18
35
|
event: async ({ event }) => {
|
|
19
36
|
try {
|
|
20
|
-
if (!
|
|
37
|
+
if (!isTrackedEventType(event.type))
|
|
21
38
|
return;
|
|
22
|
-
const sessionId = event
|
|
39
|
+
const sessionId = getSessionId(event);
|
|
23
40
|
if (!sessionId) {
|
|
24
|
-
await writeLog("WARN",
|
|
41
|
+
await writeLog("WARN", LOG_MESSAGES.autosaveEventMissingSessionId, { eventType: event.type });
|
|
25
42
|
return;
|
|
26
43
|
}
|
|
27
44
|
const config = await loadConfig();
|
|
28
|
-
if (event
|
|
29
|
-
const
|
|
30
|
-
|
|
45
|
+
if (event.type === "session.updated" || event.type === "message.updated") {
|
|
46
|
+
const snapshot = buildMessageSnapshot(await loadSessionMessages(ctx.client, sessionId));
|
|
47
|
+
setMessageSnapshot(sessionId, snapshot);
|
|
31
48
|
if (config.retrievalEnabled) {
|
|
32
|
-
markRetrievalPending(sessionId, userDigest);
|
|
49
|
+
markRetrievalPending(sessionId, snapshot.userDigest);
|
|
33
50
|
}
|
|
34
|
-
const lastUserMessage =
|
|
51
|
+
const lastUserMessage = snapshot.lastUserMessage;
|
|
35
52
|
if (lastUserMessage && config.keywordSaveEnabled && config.keywordPatterns.length) {
|
|
36
53
|
const keywordPattern = new RegExp(`\\b(${config.keywordPatterns.map(escapeRegex).join("|")})\\b`, "i");
|
|
37
54
|
if (config.keywordSaveEnabled && keywordPattern.test(lastUserMessage)) {
|
|
38
55
|
markKeywordSavePending(sessionId);
|
|
39
|
-
await writeLog("INFO",
|
|
56
|
+
await writeLog("INFO", LOG_MESSAGES.keywordTriggeredAutosaveHintDetected, { sessionId });
|
|
40
57
|
}
|
|
41
58
|
}
|
|
42
59
|
}
|
|
43
|
-
if (!
|
|
60
|
+
if (!isAutosaveTriggerEventType(event.type))
|
|
44
61
|
return;
|
|
45
|
-
await writeLog("INFO",
|
|
46
|
-
eventType: event
|
|
62
|
+
await writeLog("INFO", LOG_MESSAGES.autosaveTriggerReceived, {
|
|
63
|
+
eventType: event.type,
|
|
47
64
|
sessionId,
|
|
48
65
|
});
|
|
49
|
-
|
|
50
|
-
if (state.status === AutosaveStatus.Running && event?.type === "session.idle") {
|
|
51
|
-
const finalized = finalizeAutosave(sessionId);
|
|
52
|
-
await writeLog("INFO", "finalized autosave turn", {
|
|
53
|
-
sessionId,
|
|
54
|
-
status: finalized.status,
|
|
55
|
-
toolCalls: finalized.successfulToolCalls.length,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
if (event?.type === "session.error") {
|
|
66
|
+
if (event.type === "session.error") {
|
|
59
67
|
const failed = markFailed(sessionId);
|
|
60
|
-
await writeLog("ERROR",
|
|
68
|
+
await writeLog("ERROR", LOG_MESSAGES.autosaveFailedOnSessionError, {
|
|
61
69
|
sessionId,
|
|
62
70
|
retryCount: failed.retryCount,
|
|
63
71
|
});
|
|
@@ -65,45 +73,70 @@ export const eventHooks = (ctx) => {
|
|
|
65
73
|
}
|
|
66
74
|
if (!config.autosaveEnabled)
|
|
67
75
|
return;
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
76
|
+
const cachedSnapshot = getMessageSnapshot(sessionId);
|
|
77
|
+
const snapshot = cachedSnapshot ?? buildMessageSnapshot(await loadSessionMessages(ctx.client, sessionId));
|
|
78
|
+
if (!cachedSnapshot) {
|
|
79
|
+
setMessageSnapshot(sessionId, snapshot);
|
|
80
|
+
}
|
|
81
|
+
const { transcript, transcriptDigest, userDigest } = snapshot;
|
|
71
82
|
if (!shouldScheduleAutosave(sessionId, userDigest, transcriptDigest)) {
|
|
72
|
-
await writeLog("INFO",
|
|
83
|
+
await writeLog("INFO", LOG_MESSAGES.skippingAutosaveState, {
|
|
73
84
|
sessionId,
|
|
74
|
-
reason: toReason(event
|
|
85
|
+
reason: toReason(event.type),
|
|
75
86
|
userDigest,
|
|
76
87
|
transcriptDigest,
|
|
77
88
|
status: getSessionState(sessionId).status,
|
|
78
89
|
});
|
|
79
90
|
return;
|
|
80
91
|
}
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
const redactedTranscript = redactSecrets(transcript);
|
|
93
|
+
if (!redactedTranscript.trim()) {
|
|
94
|
+
markAutosaveComplete(sessionId, userDigest, transcriptDigest, AutosaveStatus.Noop);
|
|
95
|
+
await writeLog("INFO", LOG_MESSAGES.autosaveSkippedEmptyTranscript, { sessionId });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const wing = getProjectScope(getProjectName(ctx.project), config.projectWingPrefix).wing;
|
|
99
|
+
const result = await executeAdapter(ctx.$, {
|
|
100
|
+
mode: "mine_messages",
|
|
101
|
+
transcript: redactedTranscript,
|
|
102
|
+
wing,
|
|
103
|
+
extract_mode: config.autoMineExtractMode,
|
|
104
|
+
agent: DEFAULT_AGENT_NAME,
|
|
105
|
+
});
|
|
106
|
+
if (result?.success === false) {
|
|
107
|
+
const failed = markFailed(sessionId);
|
|
108
|
+
await writeLog("ERROR", LOG_MESSAGES.autosaveMiningFailed, {
|
|
109
|
+
sessionId,
|
|
110
|
+
retryCount: failed.retryCount,
|
|
111
|
+
result,
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const completed = markAutosaveComplete(sessionId, userDigest, transcriptDigest, AutosaveStatus.Saved);
|
|
116
|
+
await writeLog("INFO", LOG_MESSAGES.autosaveMinedSessionContext, {
|
|
83
117
|
sessionId,
|
|
84
|
-
reason: toReason(event
|
|
118
|
+
reason: toReason(event.type),
|
|
85
119
|
userDigest,
|
|
86
120
|
transcriptDigest,
|
|
121
|
+
status: completed.status,
|
|
122
|
+
wing,
|
|
87
123
|
});
|
|
88
124
|
}
|
|
89
125
|
catch (error) {
|
|
90
|
-
await writeLog("ERROR",
|
|
126
|
+
await writeLog("ERROR", LOG_MESSAGES.eventHookFailed, {
|
|
91
127
|
error: error instanceof Error ? error.message : String(error),
|
|
92
128
|
});
|
|
93
129
|
}
|
|
94
130
|
},
|
|
95
131
|
"experimental.session.compacting": async (input, output) => {
|
|
96
132
|
if (!input.sessionID) {
|
|
97
|
-
await writeLog("WARN",
|
|
133
|
+
await writeLog("WARN", LOG_MESSAGES.compactionHookMissingSessionId, {});
|
|
98
134
|
return;
|
|
99
135
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return;
|
|
103
|
-
output.context.push("MemPalace autosave is pending. Before answering after compaction, persist durable facts, decisions, preferences, outcomes, and diary notes using MemPalace MCP tools. Do not dump the full transcript.");
|
|
104
|
-
await writeLog("INFO", "injected compaction autosave context", {
|
|
136
|
+
output.context.push(COMPACTION_CONTEXT_MESSAGE);
|
|
137
|
+
await writeLog("INFO", LOG_MESSAGES.injectedCompactionAutosaveContext, {
|
|
105
138
|
sessionId: input.sessionID,
|
|
106
|
-
reason:
|
|
139
|
+
reason: AutosaveReason.Compacted,
|
|
107
140
|
});
|
|
108
141
|
},
|
|
109
142
|
};
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"experimental.chat.system.transform": (_input: {}, output: {
|
|
1
|
+
import type { SystemHookContext } from "../lib/types";
|
|
2
|
+
export declare const systemHooks: (ctx: SystemHookContext) => {
|
|
3
|
+
"experimental.chat.system.transform": (input: {
|
|
4
|
+
sessionID?: string;
|
|
5
|
+
}, output: {
|
|
7
6
|
system: string[];
|
|
8
7
|
}) => Promise<void>;
|
|
9
8
|
};
|
|
10
|
-
export {};
|
|
@@ -1,22 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildMessageSnapshot, clearKeywordSavePending, getMessageSnapshot, getSessionState, markRetrievalInjected, setMessageSnapshot, } from "../lib/autosave";
|
|
2
2
|
import { loadConfig } from "../lib/config";
|
|
3
|
-
import {
|
|
3
|
+
import { LOG_MESSAGES } from "../lib/constants";
|
|
4
|
+
import { buildKeywordSaveInstruction, buildRetrievalInstruction } from "../lib/context";
|
|
4
5
|
import { writeLog } from "../lib/log";
|
|
6
|
+
import { getProjectName, loadSessionMessages } from "../lib/opencode";
|
|
5
7
|
export const systemHooks = (ctx) => {
|
|
6
8
|
return {
|
|
7
|
-
"experimental.chat.system.transform": async (
|
|
9
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
8
10
|
try {
|
|
9
|
-
const sessionId =
|
|
11
|
+
const sessionId = input.sessionID;
|
|
10
12
|
if (!sessionId)
|
|
11
13
|
return;
|
|
12
14
|
const config = await loadConfig();
|
|
13
15
|
const state = getSessionState(sessionId);
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
16
|
+
const cachedSnapshot = getMessageSnapshot(sessionId);
|
|
17
|
+
const snapshot = cachedSnapshot ?? buildMessageSnapshot(await loadSessionMessages(ctx.client, sessionId));
|
|
18
|
+
if (!cachedSnapshot) {
|
|
19
|
+
setMessageSnapshot(sessionId, snapshot);
|
|
20
|
+
}
|
|
21
|
+
const lastUserMessage = snapshot.lastUserMessage;
|
|
17
22
|
if (config.retrievalEnabled && state.retrievalPending && lastUserMessage) {
|
|
18
23
|
output.system.push(buildRetrievalInstruction({
|
|
19
|
-
projectName: ctx.project
|
|
24
|
+
projectName: getProjectName(ctx.project),
|
|
20
25
|
projectWingPrefix: config.projectWingPrefix,
|
|
21
26
|
userWingPrefix: config.userWingPrefix,
|
|
22
27
|
maxInjectedItems: config.maxInjectedItems,
|
|
@@ -24,24 +29,16 @@ export const systemHooks = (ctx) => {
|
|
|
24
29
|
lastUserMessage,
|
|
25
30
|
}));
|
|
26
31
|
markRetrievalInjected(sessionId);
|
|
27
|
-
await writeLog("INFO",
|
|
32
|
+
await writeLog("INFO", LOG_MESSAGES.injectedRetrievalInstruction, { sessionId });
|
|
28
33
|
}
|
|
29
34
|
if (state.keywordSavePending) {
|
|
30
35
|
output.system.push(buildKeywordSaveInstruction());
|
|
31
36
|
clearKeywordSavePending(sessionId);
|
|
32
|
-
await writeLog("INFO",
|
|
37
|
+
await writeLog("INFO", LOG_MESSAGES.injectedKeywordSaveInstruction, { sessionId });
|
|
33
38
|
}
|
|
34
|
-
if (state.status !== AutosaveStatus.Pending || !state.pendingReason)
|
|
35
|
-
return;
|
|
36
|
-
output.system.push(buildAutosaveInstruction(state.pendingReason));
|
|
37
|
-
startAutosave(sessionId);
|
|
38
|
-
await writeLog("INFO", "injected hidden autosave instruction", {
|
|
39
|
-
sessionId,
|
|
40
|
-
reason: state.pendingReason,
|
|
41
|
-
});
|
|
42
39
|
}
|
|
43
40
|
catch (error) {
|
|
44
|
-
await writeLog("ERROR",
|
|
41
|
+
await writeLog("ERROR", LOG_MESSAGES.systemTransformHookFailed, {
|
|
45
42
|
error: error instanceof Error ? error.message : String(error),
|
|
46
43
|
});
|
|
47
44
|
}
|
|
@@ -3,15 +3,7 @@ export declare const toolHooks: () => {
|
|
|
3
3
|
tool: string;
|
|
4
4
|
sessionID?: string;
|
|
5
5
|
callID?: string;
|
|
6
|
-
},
|
|
7
|
-
args:
|
|
8
|
-
}) => Promise<void>;
|
|
9
|
-
"tool.execute.after": (input: {
|
|
10
|
-
tool: string;
|
|
11
|
-
sessionID?: string;
|
|
12
|
-
callID?: string;
|
|
13
|
-
}, output: {
|
|
14
|
-
output: string;
|
|
15
|
-
metadata: any;
|
|
6
|
+
}, _output: {
|
|
7
|
+
args: Record<string, unknown>;
|
|
16
8
|
}) => Promise<void>;
|
|
17
9
|
};
|
|
@@ -1,60 +1,15 @@
|
|
|
1
|
+
import { LOG_MESSAGES, TOOL_ERROR_MESSAGES } from "../lib/constants";
|
|
1
2
|
import { writeLog } from "../lib/log";
|
|
2
|
-
import { AutosaveStatus, getSessionState, markMutationToolCall, recordSuccessfulTool, shouldCountSuccessfulTool } from "../lib/autosave";
|
|
3
3
|
import { isDirectMempalaceMutationTool } from "../lib/enforcement";
|
|
4
|
-
const isMempalaceTool = (tool) => tool === "mempalace_memory";
|
|
5
|
-
const isSuccessfulToolOutput = (output) => {
|
|
6
|
-
const text = `${output.output || ""} ${JSON.stringify(output.metadata || {})}`.toLowerCase();
|
|
7
|
-
return !text.includes("\"success\":false") && !text.includes("error");
|
|
8
|
-
};
|
|
9
|
-
const getMetadataSessionId = (metadata) => {
|
|
10
|
-
return metadata?.sessionID || metadata?.sessionId || metadata?.session?.id;
|
|
11
|
-
};
|
|
12
4
|
export const toolHooks = () => {
|
|
13
5
|
return {
|
|
14
|
-
"tool.execute.before": async (input,
|
|
6
|
+
"tool.execute.before": async (input, _output) => {
|
|
15
7
|
if (isDirectMempalaceMutationTool(input.tool)) {
|
|
16
|
-
await writeLog("WARN",
|
|
8
|
+
await writeLog("WARN", LOG_MESSAGES.blockedDirectMempalaceMutationTool, {
|
|
17
9
|
tool: input.tool,
|
|
18
10
|
sessionId: input.sessionID,
|
|
19
11
|
});
|
|
20
|
-
throw new Error(
|
|
21
|
-
}
|
|
22
|
-
if (input.tool !== "mempalace_memory" || !input.sessionID)
|
|
23
|
-
return;
|
|
24
|
-
const mode = output.args?.mode;
|
|
25
|
-
if (mode === "save" || mode === "kg_add" || mode === "diary_write") {
|
|
26
|
-
markMutationToolCall(input.sessionID, input.callID);
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
"tool.execute.after": async (input, output) => {
|
|
30
|
-
try {
|
|
31
|
-
if (!input.sessionID) {
|
|
32
|
-
await writeLog("WARN", "tool.execute.after missing sessionID", { tool: input.tool });
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
if (!isMempalaceTool(input.tool))
|
|
36
|
-
return;
|
|
37
|
-
const state = getSessionState(input.sessionID);
|
|
38
|
-
if (state.status !== AutosaveStatus.Running)
|
|
39
|
-
return;
|
|
40
|
-
const metadataSessionId = getMetadataSessionId(output.metadata);
|
|
41
|
-
if (metadataSessionId && metadataSessionId !== input.sessionID)
|
|
42
|
-
return;
|
|
43
|
-
if (!isSuccessfulToolOutput(output))
|
|
44
|
-
return;
|
|
45
|
-
if (!shouldCountSuccessfulTool(input.sessionID, input.tool, input.callID))
|
|
46
|
-
return;
|
|
47
|
-
recordSuccessfulTool(input.sessionID, input.tool, input.callID);
|
|
48
|
-
await writeLog("INFO", "observed mempalace tool during autosave", {
|
|
49
|
-
sessionId: input.sessionID,
|
|
50
|
-
tool: input.tool,
|
|
51
|
-
count: getSessionState(input.sessionID).successfulToolCalls.length,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
await writeLog("ERROR", "tool.execute.after hook failed", {
|
|
56
|
-
error: error instanceof Error ? error.message : String(error),
|
|
57
|
-
});
|
|
12
|
+
throw new Error(TOOL_ERROR_MESSAGES.directMutationBlocked);
|
|
58
13
|
}
|
|
59
14
|
},
|
|
60
15
|
};
|
package/dist/plugin/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { chatParamHooks } from "./hooks/chat-params";
|
|
2
1
|
import { eventHooks } from "./hooks/event";
|
|
3
2
|
import { systemHooks } from "./hooks/system";
|
|
4
3
|
import { toolHooks } from "./hooks/tool";
|
|
@@ -7,7 +6,6 @@ import { mempalaceMemoryTool } from "./tools/mempalace-memory";
|
|
|
7
6
|
export const MempalaceAutosavePlugin = async (ctx) => {
|
|
8
7
|
setLogger(ctx.client);
|
|
9
8
|
return {
|
|
10
|
-
...chatParamHooks(),
|
|
11
9
|
...eventHooks(ctx),
|
|
12
10
|
...systemHooks(ctx),
|
|
13
11
|
...toolHooks(),
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import type { AdapterRequest, AdapterResponse } from "./types";
|
|
2
|
+
export declare const executeAdapter: (_shell: unknown, payload: AdapterRequest, retries?: number) => Promise<AdapterResponse>;
|
|
@@ -1,35 +1,68 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { DEFAULT_ADAPTER_TIMEOUT_MS, ENV_KEYS, TOOL_ERROR_MESSAGES } from "./constants";
|
|
4
5
|
const getAdapterPath = () => {
|
|
5
6
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
return path.resolve(here, "..", "..", "bridge", "mempalace_adapter.py");
|
|
7
8
|
};
|
|
8
|
-
const getPythonCommand = () => process.env.
|
|
9
|
+
const getPythonCommand = () => process.env[ENV_KEYS.adapterPython] || "python";
|
|
10
|
+
const getAdapterTimeoutMs = () => {
|
|
11
|
+
const raw = process.env[ENV_KEYS.adapterTimeoutMs];
|
|
12
|
+
if (!raw)
|
|
13
|
+
return DEFAULT_ADAPTER_TIMEOUT_MS;
|
|
14
|
+
const parsed = Number(raw);
|
|
15
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ADAPTER_TIMEOUT_MS;
|
|
16
|
+
};
|
|
17
|
+
const isAdapterResponse = (value) => {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
};
|
|
9
20
|
export const executeAdapter = async (_shell, payload, retries = 3) => {
|
|
10
21
|
let lastError;
|
|
22
|
+
const timeoutMs = getAdapterTimeoutMs();
|
|
11
23
|
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
12
24
|
try {
|
|
13
25
|
const text = await new Promise((resolve, reject) => {
|
|
14
26
|
const child = spawn(getPythonCommand(), [getAdapterPath()], {
|
|
15
27
|
stdio: ["pipe", "pipe", "pipe"],
|
|
16
28
|
});
|
|
29
|
+
let settled = false;
|
|
17
30
|
const stdout = [];
|
|
18
31
|
const stderr = [];
|
|
32
|
+
const timeoutId = setTimeout(() => {
|
|
33
|
+
child.kill();
|
|
34
|
+
if (settled)
|
|
35
|
+
return;
|
|
36
|
+
settled = true;
|
|
37
|
+
reject(new Error(`${TOOL_ERROR_MESSAGES.adapterTimedOut} after ${timeoutMs}ms`));
|
|
38
|
+
}, timeoutMs);
|
|
39
|
+
const finish = (handler) => {
|
|
40
|
+
if (settled)
|
|
41
|
+
return;
|
|
42
|
+
settled = true;
|
|
43
|
+
clearTimeout(timeoutId);
|
|
44
|
+
handler();
|
|
45
|
+
};
|
|
19
46
|
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
20
47
|
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
21
|
-
child.on("error", reject);
|
|
48
|
+
child.on("error", (error) => finish(() => reject(error)));
|
|
22
49
|
child.on("close", (code) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
finish(() => {
|
|
51
|
+
if (code === 0) {
|
|
52
|
+
resolve(Buffer.concat(stdout).toString("utf8"));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
reject(new Error(Buffer.concat(stderr).toString("utf8") || `Adapter exited with code ${code}`));
|
|
56
|
+
});
|
|
28
57
|
});
|
|
29
58
|
child.stdin.write(JSON.stringify(payload), "utf8");
|
|
30
59
|
child.stdin.end();
|
|
31
60
|
});
|
|
32
|
-
|
|
61
|
+
const parsed = JSON.parse(text);
|
|
62
|
+
if (!isAdapterResponse(parsed)) {
|
|
63
|
+
throw new Error(TOOL_ERROR_MESSAGES.invalidAdapterPayload);
|
|
64
|
+
}
|
|
65
|
+
return parsed;
|
|
33
66
|
}
|
|
34
67
|
catch (error) {
|
|
35
68
|
lastError = error;
|