om-memory-system 3.1.0-next.4 → 3.2.0-next.10

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.
@@ -0,0 +1,172 @@
1
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, readdirSync, unlinkSync } from "fs";
2
+ import { join } from "path";
3
+ import { getLogDirPath } from "./log-path.js";
4
+ import { log } from "./logger.js";
5
+ import { stripPrivateContent } from "./privacy.js";
6
+ export const DEFAULT_CAPTURE_TRACE_RETENTION_DAYS = 7;
7
+ const TRACE_SCHEMA_VERSION = 1;
8
+ const TRACE_FILE = /^capture-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
9
+ const REDACTED = "[REDACTED]";
10
+ export function buildCaptureAttemptRecord(context, diagnostics, outcome, durationMs) {
11
+ const promptChars = diagnostics.systemPrompt === undefined && diagnostics.userPrompt === undefined
12
+ ? null
13
+ : (diagnostics.systemPrompt?.length ?? 0) + (diagnostics.userPrompt?.length ?? 0);
14
+ return {
15
+ host: context.host,
16
+ sourceType: context.sourceType,
17
+ sessionId: context.sessionId,
18
+ path: diagnostics.path ?? null,
19
+ provider: diagnostics.provider ?? null,
20
+ model: diagnostics.model ?? null,
21
+ stopReason: diagnostics.stopReason ?? null,
22
+ blockTypes: diagnostics.blockTypes ?? null,
23
+ promptChars,
24
+ replyChars: diagnostics.rawReply?.length ?? null,
25
+ durationMs,
26
+ outcome,
27
+ reason: outcome === "failed" ? (diagnostics.failureReason ?? "call-error") : null,
28
+ };
29
+ }
30
+ // Common key and token formats. Each match is replaced whole, except the
31
+ // Bearer rule, which keeps the scheme so the trace still reads naturally.
32
+ const SECRET_PATTERNS = [
33
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, REDACTED],
34
+ [/\bsk-ant-[A-Za-z0-9_-]{10,}/g, REDACTED],
35
+ [/\bsk-[A-Za-z0-9_-]{16,}/g, REDACTED],
36
+ [/\bgithub_pat_[A-Za-z0-9_]{20,}/g, REDACTED],
37
+ [/\bgh[pousr]_[A-Za-z0-9]{20,}/g, REDACTED],
38
+ [/\bAKIA[0-9A-Z]{16}\b/g, REDACTED],
39
+ [/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, REDACTED],
40
+ [/\bAIza[0-9A-Za-z_-]{35}/g, REDACTED],
41
+ [/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, REDACTED],
42
+ [/(\bBearer\s+)[A-Za-z0-9._~+/=-]{16,}/gi, `$1${REDACTED}`],
43
+ ];
44
+ function configuredSecrets(config) {
45
+ return [
46
+ config.memoryApiKey,
47
+ config.embeddingApiKey,
48
+ config.webServerApiToken,
49
+ config.webServerAuthPassword,
50
+ ].filter((value) => typeof value === "string" &&
51
+ // Short values would redact ordinary words; unresolved references are not secrets.
52
+ value.length >= 8 &&
53
+ !value.startsWith("env://") &&
54
+ !value.startsWith("file://"));
55
+ }
56
+ /**
57
+ * Prepare prompt or reply text for the trace file: drop `<private>` regions the
58
+ * same way memory storage does, then configured secrets, then common key and
59
+ * token formats.
60
+ */
61
+ export function redactTraceText(text, config) {
62
+ let result = stripPrivateContent(text);
63
+ for (const secret of configuredSecrets(config)) {
64
+ result = result.replaceAll(secret, REDACTED);
65
+ }
66
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
67
+ result = result.replace(pattern, replacement);
68
+ }
69
+ return result;
70
+ }
71
+ function formatDate(d) {
72
+ const pad = (n) => String(n).padStart(2, "0");
73
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
74
+ }
75
+ export function getTraceDirectory() {
76
+ return join(getLogDirPath(), "traces");
77
+ }
78
+ function retentionDays(config) {
79
+ const days = config.captureTraceRetentionDays;
80
+ if (typeof days !== "number" || !Number.isFinite(days)) {
81
+ return DEFAULT_CAPTURE_TRACE_RETENTION_DAYS;
82
+ }
83
+ return Math.max(1, Math.floor(days));
84
+ }
85
+ /**
86
+ * Delete trace files dated more than `captureTraceRetentionDays` before `now`.
87
+ * The date comes from the file name, not the modification time, and files
88
+ * that do not match the trace name pattern are never touched.
89
+ */
90
+ export function pruneTraces(config, now = new Date()) {
91
+ const dir = getTraceDirectory();
92
+ let removed = 0;
93
+ try {
94
+ if (!existsSync(dir))
95
+ return 0;
96
+ const cutoff = new Date(now.getFullYear(), now.getMonth(), now.getDate());
97
+ cutoff.setDate(cutoff.getDate() - retentionDays(config));
98
+ for (const file of readdirSync(dir)) {
99
+ const match = TRACE_FILE.exec(file);
100
+ if (!match)
101
+ continue;
102
+ const fileDate = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
103
+ if (fileDate < cutoff) {
104
+ unlinkSync(join(dir, file));
105
+ removed++;
106
+ }
107
+ }
108
+ }
109
+ catch (error) {
110
+ log("Capture trace cleanup failed", { code: errorCode(error) });
111
+ }
112
+ return removed;
113
+ }
114
+ let lastTraceDate = null;
115
+ /**
116
+ * Append one trace entry to the day's JSON Lines file. A single append per
117
+ * entry keeps lines whole when Pi and OpenCode write at the same time.
118
+ */
119
+ export function writeTraceEntry(record, diagnostics, config, now = new Date()) {
120
+ try {
121
+ const today = formatDate(now);
122
+ if (lastTraceDate !== today) {
123
+ lastTraceDate = today;
124
+ pruneTraces(config, now);
125
+ }
126
+ const dir = getTraceDirectory();
127
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
128
+ chmodSync(dir, 0o700);
129
+ const redact = (value) => value === undefined ? null : redactTraceText(value, config);
130
+ const entry = {
131
+ schemaVersion: TRACE_SCHEMA_VERSION,
132
+ timestamp: now.toISOString(),
133
+ ...record,
134
+ systemPrompt: redact(diagnostics.systemPrompt),
135
+ userPrompt: redact(diagnostics.userPrompt),
136
+ reply: redact(diagnostics.rawReply),
137
+ };
138
+ const file = join(dir, `capture-${today}.jsonl`);
139
+ appendFileSync(file, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
140
+ // The mode above applies only when the append creates the file; a file
141
+ // that already existed with wider permissions is narrowed here.
142
+ chmodSync(file, 0o600);
143
+ }
144
+ catch (error) {
145
+ log("Capture trace write failed", { code: errorCode(error) });
146
+ }
147
+ }
148
+ /**
149
+ * Write the attempt's metadata record to the log and, when tracing is on, its
150
+ * trace entry. Never throws: diagnostics must not change a capture outcome.
151
+ */
152
+ export function emitCaptureAttempt(record, diagnostics, config, now = new Date()) {
153
+ try {
154
+ log("Capture attempt", record);
155
+ }
156
+ catch {
157
+ // The log is best effort here, as everywhere else.
158
+ }
159
+ if (config.captureTrace === true) {
160
+ writeTraceEntry(record, diagnostics, config, now);
161
+ }
162
+ }
163
+ function errorCode(error) {
164
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
165
+ return error.code;
166
+ }
167
+ return error instanceof Error ? error.name : "unknown";
168
+ }
169
+ /** Test hook: forget which day the last trace was written. */
170
+ export function resetTraceStateForTests() {
171
+ lastTraceDate = null;
172
+ }
@@ -0,0 +1,2 @@
1
+ export declare function getLogFilePath(): string;
2
+ export declare function getLogDirPath(): string;
@@ -0,0 +1,15 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ // Kept apart from logger.ts: many tests replace logger.js with a stub that
4
+ // only has `log`, so modules that need the paths import them from here.
5
+ export function getLogFilePath() {
6
+ return (process.env.OMMS_LOG_FILE ||
7
+ // Legacy override, still honoured from the opencode-mem days.
8
+ process.env.OPENCODE_MEM_LOG_FILE ||
9
+ join(homedir(), ".omms", "omms.log"));
10
+ }
11
+ export function getLogDirPath() {
12
+ const logFile = getLogFilePath();
13
+ const lastSlash = Math.max(logFile.lastIndexOf("/"), logFile.lastIndexOf("\\"));
14
+ return lastSlash === -1 ? "." : logFile.slice(0, lastSlash);
15
+ }
@@ -1,17 +1,6 @@
1
1
  import { appendFileSync, writeFileSync, existsSync, mkdirSync, statSync, renameSync, unlinkSync, readdirSync, } from "fs";
2
- import { homedir } from "os";
3
2
  import { join } from "path";
4
- function getLogFilePath() {
5
- return (process.env.OMMS_LOG_FILE ||
6
- // Legacy override, still honoured from the opencode-mem days.
7
- process.env.OPENCODE_MEM_LOG_FILE ||
8
- join(homedir(), ".omms", "omms.log"));
9
- }
10
- function getLogDirPath() {
11
- const logFile = getLogFilePath();
12
- const lastSlash = Math.max(logFile.lastIndexOf("/"), logFile.lastIndexOf("\\"));
13
- return lastSlash === -1 ? "." : logFile.slice(0, lastSlash);
14
- }
3
+ import { getLogDirPath, getLogFilePath } from "./log-path.js";
15
4
  const MAX_LOG_SIZE = 5 * 1024 * 1024;
16
5
  const MAX_LOG_DAYS = 30;
17
6
  const GLOBAL_LOGGER_KEY = Symbol.for("omms.logger.initialized");
@@ -222,8 +222,17 @@ export function toLegacyEvent(raw) {
222
222
  const envelope = raw?.payload ?? raw;
223
223
  const source = envelope?.type === "sync" && envelope.syncEvent ? envelope.syncEvent : envelope;
224
224
  const rawType = typeof source?.type === "string" ? source.type.replace(/\.1$/, "") : source?.type;
225
- const type = rawType === "session.compaction.ended" ? "session.compacted" : rawType;
226
225
  const data = source?.data ?? {};
226
+ // OpenCode 2.0.14 ends a turn with `session.execution.succeeded` and emits no
227
+ // `session.idle`; some builds report idleness through `session.status`. The
228
+ // V1 capture path listens for `session.idle`, so both map onto it.
229
+ // Subscribed events carry their payload in `data`; bus-style ones in `properties`.
230
+ const payload = source?.data ?? source?.properties ?? {};
231
+ if (rawType === "session.execution.succeeded" ||
232
+ (rawType === "session.status" && payload?.status?.type === "idle")) {
233
+ return { type: "session.idle", properties: { sessionID: payload.sessionID } };
234
+ }
235
+ const type = rawType === "session.compaction.ended" ? "session.compacted" : rawType;
227
236
  if (source && typeof source === "object" && "properties" in source) {
228
237
  return { type, properties: source.properties };
229
238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "om-memory-system",
3
- "version": "3.1.0-next.4",
3
+ "version": "3.2.0-next.10",
4
4
  "description": "Opinionated modular memory system for coding agents (OpenCode + Pi)",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.js",