@rvboris/opencode-mempalace 0.2.0 → 0.3.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.
@@ -8,17 +8,22 @@ from pathlib import Path
8
8
 
9
9
  from mempalace.config import MempalaceConfig
10
10
  from mempalace.convo_miner import mine_convos
11
- from mempalace.mcp_server import (
12
- tool_add_drawer,
13
- tool_diary_write,
14
- tool_kg_add,
15
- tool_search,
16
- )
11
+ import mempalace.mcp_server as mcp_server
12
+
13
+
14
+ restore_stdout = getattr(mcp_server, "_restore_stdout", None)
15
+ if callable(restore_stdout):
16
+ restore_stdout()
17
17
 
18
18
 
19
19
  def write_result(result: dict) -> None:
20
20
  output = json.dumps(result, ensure_ascii=False)
21
21
  sys.stdout.buffer.write(output.encode("utf-8", errors="replace"))
22
+ sys.stdout.buffer.flush()
23
+
24
+
25
+ def write_utf8_text(path: Path, text: str) -> None:
26
+ path.write_bytes(text.encode("utf-8", errors="replace"))
22
27
 
23
28
 
24
29
  def main() -> int:
@@ -27,21 +32,21 @@ def main() -> int:
27
32
 
28
33
  mode = payload["mode"]
29
34
  if mode == "search":
30
- result = tool_search(
35
+ result = mcp_server.tool_search(
31
36
  query=payload["query"],
32
37
  limit=payload.get("limit", 5),
33
38
  wing=payload.get("wing"),
34
39
  room=payload.get("room"),
35
40
  )
36
41
  elif mode == "save":
37
- result = tool_add_drawer(
42
+ result = mcp_server.tool_add_drawer(
38
43
  wing=payload["wing"],
39
44
  room=payload["room"],
40
45
  content=payload["content"],
41
46
  added_by=payload.get("added_by", "opencode"),
42
47
  )
43
48
  elif mode == "kg_add":
44
- result = tool_kg_add(
49
+ result = mcp_server.tool_kg_add(
45
50
  subject=payload["subject"],
46
51
  predicate=payload["predicate"],
47
52
  object=payload["object"],
@@ -49,7 +54,7 @@ def main() -> int:
49
54
  source_closet=payload.get("source_closet", ""),
50
55
  )
51
56
  elif mode == "diary_write":
52
- result = tool_diary_write(
57
+ result = mcp_server.tool_diary_write(
53
58
  agent_name=payload["agent_name"],
54
59
  entry=payload["entry"],
55
60
  topic=payload.get("topic", "autosave"),
@@ -59,7 +64,7 @@ def main() -> int:
59
64
  extract_mode = payload.get("extract_mode", "general")
60
65
  with tempfile.TemporaryDirectory(prefix="mempalace-autosave-") as tmpdir:
61
66
  transcript_path = Path(tmpdir) / "session.txt"
62
- transcript_path.write_text(payload["transcript"], encoding="utf-8")
67
+ write_utf8_text(transcript_path, payload["transcript"])
63
68
  with redirect_stdout(StringIO()):
64
69
  mine_convos(
65
70
  convo_dir=tmpdir,
@@ -2,9 +2,11 @@ import { AutosaveReason, AutosaveStatus, buildMessageSnapshot, getMessageSnapsho
2
2
  import { executeAdapter } from "../lib/adapter";
3
3
  import { loadConfig } from "../lib/config";
4
4
  import { COMPACTION_CONTEXT_MESSAGE, DEFAULT_AGENT_NAME, LOG_MESSAGES } from "../lib/constants";
5
+ import { sanitizeText } from "../lib/derive";
5
6
  import { getProjectName, loadSessionMessages } from "../lib/opencode";
6
7
  import { redactSecrets } from "../lib/privacy";
7
8
  import { getProjectScope } from "../lib/scope";
9
+ import { recordAutosave } from "../lib/status";
8
10
  import { writeLog } from "../lib/log";
9
11
  import { SESSION_EVENT_TYPES } from "../lib/types";
10
12
  const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -65,6 +67,12 @@ export const eventHooks = (ctx) => {
65
67
  });
66
68
  if (event.type === "session.error") {
67
69
  const failed = markFailed(sessionId);
70
+ await recordAutosave({
71
+ sessionId,
72
+ outcome: "failed",
73
+ reason: toReason(event.type),
74
+ sourcePreview: getMessageSnapshot(sessionId)?.lastUserMessage,
75
+ });
68
76
  await writeLog("ERROR", LOG_MESSAGES.autosaveFailedOnSessionError, {
69
77
  sessionId,
70
78
  retryCount: failed.retryCount,
@@ -80,6 +88,12 @@ export const eventHooks = (ctx) => {
80
88
  }
81
89
  const { transcript, transcriptDigest, userDigest } = snapshot;
82
90
  if (!shouldScheduleAutosave(sessionId, userDigest, transcriptDigest)) {
91
+ await recordAutosave({
92
+ sessionId,
93
+ outcome: "skipped",
94
+ reason: toReason(event.type),
95
+ sourcePreview: snapshot.lastUserMessage,
96
+ });
83
97
  await writeLog("INFO", LOG_MESSAGES.skippingAutosaveState, {
84
98
  sessionId,
85
99
  reason: toReason(event.type),
@@ -89,22 +103,35 @@ export const eventHooks = (ctx) => {
89
103
  });
90
104
  return;
91
105
  }
92
- const redactedTranscript = redactSecrets(transcript);
93
- if (!redactedTranscript.trim()) {
106
+ const sanitizedTranscript = sanitizeText(redactSecrets(transcript));
107
+ if (!sanitizedTranscript.trim()) {
94
108
  markAutosaveComplete(sessionId, userDigest, transcriptDigest, AutosaveStatus.Noop);
109
+ await recordAutosave({
110
+ sessionId,
111
+ outcome: "skipped",
112
+ reason: toReason(event.type),
113
+ sourcePreview: snapshot.lastUserMessage,
114
+ });
95
115
  await writeLog("INFO", LOG_MESSAGES.autosaveSkippedEmptyTranscript, { sessionId });
96
116
  return;
97
117
  }
98
118
  const wing = getProjectScope(getProjectName(ctx.project), config.projectWingPrefix).wing;
99
119
  const result = await executeAdapter(ctx.$, {
100
120
  mode: "mine_messages",
101
- transcript: redactedTranscript,
121
+ transcript: sanitizedTranscript,
102
122
  wing,
103
123
  extract_mode: config.autoMineExtractMode,
104
124
  agent: DEFAULT_AGENT_NAME,
105
125
  });
106
126
  if (result?.success === false) {
107
127
  const failed = markFailed(sessionId);
128
+ await recordAutosave({
129
+ sessionId,
130
+ outcome: "failed",
131
+ reason: toReason(event.type),
132
+ wing,
133
+ sourcePreview: snapshot.lastUserMessage,
134
+ });
108
135
  await writeLog("ERROR", LOG_MESSAGES.autosaveMiningFailed, {
109
136
  sessionId,
110
137
  retryCount: failed.retryCount,
@@ -113,6 +140,13 @@ export const eventHooks = (ctx) => {
113
140
  return;
114
141
  }
115
142
  const completed = markAutosaveComplete(sessionId, userDigest, transcriptDigest, AutosaveStatus.Saved);
143
+ await recordAutosave({
144
+ sessionId,
145
+ outcome: "saved",
146
+ reason: toReason(event.type),
147
+ wing,
148
+ sourcePreview: snapshot.lastUserMessage,
149
+ });
116
150
  await writeLog("INFO", LOG_MESSAGES.autosaveMinedSessionContext, {
117
151
  sessionId,
118
152
  reason: toReason(event.type),
@@ -4,6 +4,7 @@ import { LOG_MESSAGES } from "../lib/constants";
4
4
  import { buildKeywordSaveInstruction, buildRetrievalInstruction } from "../lib/context";
5
5
  import { writeLog } from "../lib/log";
6
6
  import { getProjectName, loadSessionMessages } from "../lib/opencode";
7
+ import { recordRetrievalPrompt } from "../lib/status";
7
8
  export const systemHooks = (ctx) => {
8
9
  return {
9
10
  "experimental.chat.system.transform": async (input, output) => {
@@ -29,6 +30,7 @@ export const systemHooks = (ctx) => {
29
30
  lastUserMessage,
30
31
  }));
31
32
  markRetrievalInjected(sessionId);
33
+ await recordRetrievalPrompt({ sessionId, queryPreview: lastUserMessage });
32
34
  await writeLog("INFO", LOG_MESSAGES.injectedRetrievalInstruction, { sessionId });
33
35
  }
34
36
  if (state.keywordSavePending) {
@@ -3,6 +3,7 @@ import { systemHooks } from "./hooks/system";
3
3
  import { toolHooks } from "./hooks/tool";
4
4
  import { setLogger } from "./lib/log";
5
5
  import { mempalaceMemoryTool } from "./tools/mempalace-memory";
6
+ import { mempalaceStatusTool } from "./tools/mempalace-status";
6
7
  export const MempalaceAutosavePlugin = async (ctx) => {
7
8
  setLogger(ctx.client);
8
9
  return {
@@ -11,6 +12,7 @@ export const MempalaceAutosavePlugin = async (ctx) => {
11
12
  ...toolHooks(),
12
13
  tool: {
13
14
  mempalace_memory: mempalaceMemoryTool(ctx),
15
+ mempalace_status: mempalaceStatusTool(),
14
16
  },
15
17
  };
16
18
  };
@@ -17,12 +17,18 @@ const getAdapterTimeoutMs = () => {
17
17
  const isAdapterResponse = (value) => {
18
18
  return typeof value === "object" && value !== null && !Array.isArray(value);
19
19
  };
20
+ const getStderrSnippet = (value) => {
21
+ const normalized = value.trim();
22
+ if (!normalized)
23
+ return "";
24
+ return normalized.length > 240 ? `${normalized.slice(0, 240)}...` : normalized;
25
+ };
20
26
  export const executeAdapter = async (_shell, payload, retries = 3) => {
21
27
  let lastError;
22
28
  const timeoutMs = getAdapterTimeoutMs();
23
29
  for (let attempt = 0; attempt <= retries; attempt += 1) {
24
30
  try {
25
- const text = await new Promise((resolve, reject) => {
31
+ const { stderrText, stdoutText } = await new Promise((resolve, reject) => {
26
32
  const child = spawn(getPythonCommand(), [getAdapterPath()], {
27
33
  stdio: ["pipe", "pipe", "pipe"],
28
34
  });
@@ -48,17 +54,25 @@ export const executeAdapter = async (_shell, payload, retries = 3) => {
48
54
  child.on("error", (error) => finish(() => reject(error)));
49
55
  child.on("close", (code) => {
50
56
  finish(() => {
57
+ const stderrText = Buffer.concat(stderr).toString("utf8");
58
+ const stdoutText = Buffer.concat(stdout).toString("utf8");
51
59
  if (code === 0) {
52
- resolve(Buffer.concat(stdout).toString("utf8"));
60
+ resolve({ stderrText, stdoutText });
53
61
  return;
54
62
  }
55
- reject(new Error(Buffer.concat(stderr).toString("utf8") || `Adapter exited with code ${code}`));
63
+ reject(new Error(stderrText || `Adapter exited with code ${code}`));
56
64
  });
57
65
  });
58
66
  child.stdin.write(JSON.stringify(payload), "utf8");
59
67
  child.stdin.end();
60
68
  });
61
- const parsed = JSON.parse(text);
69
+ if (!stdoutText.trim()) {
70
+ const stderrSnippet = getStderrSnippet(stderrText);
71
+ throw new Error(stderrSnippet
72
+ ? `${TOOL_ERROR_MESSAGES.emptyAdapterStdout}: ${stderrSnippet}`
73
+ : TOOL_ERROR_MESSAGES.emptyAdapterStdout);
74
+ }
75
+ const parsed = JSON.parse(stdoutText);
62
76
  if (!isAdapterResponse(parsed)) {
63
77
  throw new Error(TOOL_ERROR_MESSAGES.invalidAdapterPayload);
64
78
  }
@@ -21,6 +21,7 @@ export declare const ENV_KEYS: {
21
21
  readonly userWingPrefix: "MEMPALACE_USER_WING_PREFIX";
22
22
  readonly projectWingPrefix: "MEMPALACE_PROJECT_WING_PREFIX";
23
23
  readonly autosaveLogFile: "MEMPALACE_AUTOSAVE_LOG_FILE";
24
+ readonly statusFile: "MEMPALACE_STATUS_FILE";
24
25
  readonly adapterPython: "MEMPALACE_ADAPTER_PYTHON";
25
26
  readonly adapterTimeoutMs: "MEMPALACE_ADAPTER_TIMEOUT_MS";
26
27
  };
@@ -29,14 +30,17 @@ export declare const MAX_TRANSCRIPT_MESSAGES_DIGEST: 50;
29
30
  export declare const DATE_ISO_SLICE: 10;
30
31
  export declare const SERVICE_NAME: "mempalace-autosave";
31
32
  export declare const LOG_FILE_NAME: "opencode_autosave.log";
33
+ export declare const STATUS_FILE_NAME: "opencode_status.json";
32
34
  export declare const DIRECT_MEMPALACE_MUTATION_TOOLS: readonly ["mempalace_add_drawer", "mempalace_kg_add", "mempalace_diary_write", "mcp-router_mempalace_add_drawer", "mcp-router_mempalace_kg_add", "mcp-router_mempalace_diary_write"];
33
35
  export declare const COMPACTION_CONTEXT_MESSAGE: "MemPalace retrieval may be useful after compaction. Search relevant project and user memory if needed before answering.";
34
36
  export declare const TOOL_DESCRIPTIONS: {
35
37
  readonly mempalaceMemory: "Save or search memory in MemPalace with scope/privacy enforcement";
38
+ readonly mempalaceStatus: "Show recent visible evidence that MemPalace retrieval or autosave helped";
36
39
  };
37
40
  export declare const TOOL_ERROR_MESSAGES: {
38
41
  readonly directMutationBlocked: "Use mempalace_memory instead of direct MemPalace mutation tools";
39
42
  readonly invalidAdapterPayload: "Adapter returned an invalid JSON payload";
43
+ readonly emptyAdapterStdout: "Adapter returned empty stdout";
40
44
  readonly adapterTimedOut: "Adapter execution timed out";
41
45
  };
42
46
  export declare const LOG_MESSAGES: {
@@ -21,6 +21,7 @@ export const ENV_KEYS = {
21
21
  userWingPrefix: "MEMPALACE_USER_WING_PREFIX",
22
22
  projectWingPrefix: "MEMPALACE_PROJECT_WING_PREFIX",
23
23
  autosaveLogFile: "MEMPALACE_AUTOSAVE_LOG_FILE",
24
+ statusFile: "MEMPALACE_STATUS_FILE",
24
25
  adapterPython: "MEMPALACE_ADAPTER_PYTHON",
25
26
  adapterTimeoutMs: "MEMPALACE_ADAPTER_TIMEOUT_MS",
26
27
  };
@@ -29,6 +30,7 @@ export const MAX_TRANSCRIPT_MESSAGES_DIGEST = 50;
29
30
  export const DATE_ISO_SLICE = 10;
30
31
  export const SERVICE_NAME = "mempalace-autosave";
31
32
  export const LOG_FILE_NAME = "opencode_autosave.log";
33
+ export const STATUS_FILE_NAME = "opencode_status.json";
32
34
  export const DIRECT_MEMPALACE_MUTATION_TOOLS = [
33
35
  "mempalace_add_drawer",
34
36
  "mempalace_kg_add",
@@ -40,10 +42,12 @@ export const DIRECT_MEMPALACE_MUTATION_TOOLS = [
40
42
  export const COMPACTION_CONTEXT_MESSAGE = "MemPalace retrieval may be useful after compaction. Search relevant project and user memory if needed before answering.";
41
43
  export const TOOL_DESCRIPTIONS = {
42
44
  mempalaceMemory: "Save or search memory in MemPalace with scope/privacy enforcement",
45
+ mempalaceStatus: "Show recent visible evidence that MemPalace retrieval or autosave helped",
43
46
  };
44
47
  export const TOOL_ERROR_MESSAGES = {
45
48
  directMutationBlocked: "Use mempalace_memory instead of direct MemPalace mutation tools",
46
49
  invalidAdapterPayload: "Adapter returned an invalid JSON payload",
50
+ emptyAdapterStdout: "Adapter returned empty stdout",
47
51
  adapterTimedOut: "Adapter execution timed out",
48
52
  };
49
53
  export const LOG_MESSAGES = {
@@ -0,0 +1,105 @@
1
+ import type { AdapterResponse, MemoryScope } from "./types";
2
+ export type StatusCounters = {
3
+ retrievalPrompts: number;
4
+ retrievalSearches: number;
5
+ retrievalHits: number;
6
+ autosavesCompleted: number;
7
+ autosavesSkipped: number;
8
+ autosavesFailed: number;
9
+ manualWrites: number;
10
+ };
11
+ export type StatusSessionCounters = {
12
+ retrievalSearches: number;
13
+ retrievalHits: number;
14
+ autosavesCompleted: number;
15
+ autosavesSkipped: number;
16
+ autosavesFailed: number;
17
+ manualWrites: number;
18
+ };
19
+ export type StatusRetrievalPrompt = {
20
+ sessionId?: string;
21
+ timestamp: string;
22
+ queryPreview: string;
23
+ };
24
+ export type StatusRetrieval = {
25
+ sessionId?: string;
26
+ timestamp: string;
27
+ scope: MemoryScope;
28
+ room?: string;
29
+ queryPreview: string;
30
+ resultCount?: number;
31
+ previews: string[];
32
+ };
33
+ export type StatusAutosaveOutcome = "saved" | "skipped" | "failed";
34
+ export type StatusAutosave = {
35
+ sessionId: string;
36
+ timestamp: string;
37
+ outcome: StatusAutosaveOutcome;
38
+ reason: string;
39
+ wing?: string;
40
+ sourcePreview?: string;
41
+ };
42
+ export type StatusWrite = {
43
+ timestamp: string;
44
+ mode: "save" | "kg_add" | "diary_write";
45
+ scope?: MemoryScope;
46
+ room?: string;
47
+ preview: string;
48
+ };
49
+ export type StatusSessionState = {
50
+ updatedAt: string;
51
+ counters: StatusSessionCounters;
52
+ lastRetrievalPrompt?: StatusRetrievalPrompt;
53
+ lastRetrieval?: StatusRetrieval;
54
+ lastAutosave?: StatusAutosave;
55
+ lastWrite?: StatusWrite;
56
+ };
57
+ export type StatusState = {
58
+ version: 2;
59
+ updatedAt: string;
60
+ counters: StatusCounters;
61
+ helpedSessionIds: string[];
62
+ sessions: Record<string, StatusSessionState>;
63
+ lastRetrievalPrompt?: StatusRetrievalPrompt;
64
+ lastRetrieval?: StatusRetrieval;
65
+ lastAutosave?: StatusAutosave;
66
+ lastWrite?: StatusWrite;
67
+ };
68
+ type SearchResultSummary = {
69
+ resultCount?: number;
70
+ previews: string[];
71
+ };
72
+ export declare const summarizeSearchResult: (result: AdapterResponse) => SearchResultSummary;
73
+ export declare const recordRetrievalPrompt: (input: {
74
+ sessionId?: string;
75
+ queryPreview: string;
76
+ }) => Promise<void>;
77
+ export declare const recordRetrievalSearch: (input: {
78
+ sessionId?: string;
79
+ scope: MemoryScope;
80
+ room?: string;
81
+ query: string;
82
+ result: AdapterResponse;
83
+ }) => Promise<void>;
84
+ export declare const recordMemoryWrite: (input: {
85
+ sessionId?: string;
86
+ mode: "save" | "kg_add" | "diary_write";
87
+ scope?: MemoryScope;
88
+ room?: string;
89
+ preview: string;
90
+ }) => Promise<void>;
91
+ export declare const recordAutosave: (input: {
92
+ sessionId: string;
93
+ outcome: StatusAutosaveOutcome;
94
+ reason: string;
95
+ wing?: string;
96
+ sourcePreview?: string;
97
+ }) => Promise<void>;
98
+ export declare const readStatusState: () => Promise<StatusState>;
99
+ export declare const resetStatusState: () => Promise<void>;
100
+ export declare const formatSessionHud: (state: StatusState, sessionId: string) => string;
101
+ export declare const formatStatusSummary: (state: StatusState, sessionId?: string, options?: {
102
+ verbose?: boolean;
103
+ compact?: boolean;
104
+ }) => string;
105
+ export {};