@ectplsm/relic 0.4.0 → 0.5.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 CHANGED
@@ -112,7 +112,7 @@ As you use a Construct, conversation logs are automatically saved to `archive.md
112
112
 
113
113
  > **"Organize my memories"**
114
114
 
115
- The Construct will review recent conversations, extract key facts and decisions into `memory/*.md`, promote important long-term insights to `MEMORY.md`, and update your preferences in `USER.md`. These distilled memories are then loaded into future sessions automatically.
115
+ The Construct will review recent conversations, group them by the actual dates recorded in `archive.md`, extract key facts and decisions into the matching `memory/*.md` files, promote important long-term insights to `MEMORY.md`, and update your preferences in `USER.md`. These distilled memories are then loaded into future sessions automatically.
116
116
 
117
117
  ### 5. Learn More
118
118
 
@@ -184,9 +184,9 @@ For Engram creation, the smoothest path is to use your LLM with the `relic_engra
184
184
 
185
185
  For LLM-assisted creation, persona authoring, template examples, and deletion rules, see [docs/engram-guide.md](docs/engram-guide.md).
186
186
 
187
- ## Cloud Storage and Sharing
187
+ ## Cloud Storage and Sharing (Mikoshi)
188
188
 
189
- Relic can push plaintext persona files and encrypted memory files to [Mikoshi](https://mikoshi.ectplsm.com), so you can keep Engrams in the cloud and move them across machines without turning Mikoshi into your authoring source of truth.
189
+ Relic can push plaintext persona files and (end-to-end) encrypted memory files to [Mikoshi](https://mikoshi.ectplsm.com), so you can keep Engrams in the cloud and move them across machines without turning Mikoshi into your authoring source of truth.
190
190
 
191
191
  For setup, API key configuration, persona push/pull, encrypted memory sync, and the recommended command flow, see [docs/mikoshi.md](docs/mikoshi.md).
192
192
 
@@ -16,6 +16,12 @@ export declare class ArchiveCursorUpdate {
16
16
  execute(engramId: string, count: number): Promise<ArchiveCursorUpdateResult>;
17
17
  private readCursor;
18
18
  }
19
+ export declare class ArchiveCursorAdvanceOverflowError extends Error {
20
+ readonly id: string;
21
+ readonly requestedCount: number;
22
+ readonly availablePending: number;
23
+ constructor(id: string, requestedCount: number, availablePending: number);
24
+ }
19
25
  export declare class ArchiveCursorUpdateEngramNotFoundError extends Error {
20
26
  constructor(id: string);
21
27
  }
@@ -1,6 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import { readFile, writeFile } from "node:fs/promises";
4
+ import { ArchivePending } from "./archive-pending.js";
4
5
  /**
5
6
  * ArchiveCursorUpdate — archive.cursor を指定件数分だけ進める
6
7
  *
@@ -19,6 +20,11 @@ export class ArchiveCursorUpdate {
19
20
  }
20
21
  const cursorPath = join(engramDir, "archive.cursor");
21
22
  const previousCursor = await this.readCursor(cursorPath);
23
+ const archivePending = new ArchivePending(this.engramsPath);
24
+ const pending = await archivePending.execute(engramId, Number.MAX_SAFE_INTEGER);
25
+ if (count > pending.entries.length) {
26
+ throw new ArchiveCursorAdvanceOverflowError(engramId, count, pending.entries.length);
27
+ }
22
28
  const newCursor = previousCursor + count;
23
29
  await writeFile(cursorPath, String(newCursor), "utf-8");
24
30
  return { previousCursor, newCursor };
@@ -36,6 +42,18 @@ export class ArchiveCursorUpdate {
36
42
  }
37
43
  }
38
44
  }
45
+ export class ArchiveCursorAdvanceOverflowError extends Error {
46
+ id;
47
+ requestedCount;
48
+ availablePending;
49
+ constructor(id, requestedCount, availablePending) {
50
+ super(`Engram "${id}" cursor advance overflow (${requestedCount} > ${availablePending}). Refusing to move archive.cursor.`);
51
+ this.id = id;
52
+ this.requestedCount = requestedCount;
53
+ this.availablePending = availablePending;
54
+ this.name = "ArchiveCursorAdvanceOverflowError";
55
+ }
56
+ }
39
57
  export class ArchiveCursorUpdateEngramNotFoundError extends Error {
40
58
  constructor(id) {
41
59
  super(`Engram "${id}" archive not found`);
@@ -1,6 +1,16 @@
1
+ export interface ArchivePendingEntry {
2
+ /** archive.md 先頭行から抽出した日付 */
3
+ date: string | null;
4
+ /** archive.md 先頭行の summary 部分 */
5
+ summary: string | null;
6
+ /** エントリ全文 */
7
+ raw: string;
8
+ /** 先頭行を除いた本文 */
9
+ body: string;
10
+ }
1
11
  export interface ArchivePendingResult {
2
12
  /** 未蒸留エントリ(cursor以降) */
3
- entries: string[];
13
+ entries: ArchivePendingEntry[];
4
14
  /** 現在のcursor位置 */
5
15
  cursor: number;
6
16
  /** archive内の総エントリ数 */
@@ -8,6 +18,12 @@ export interface ArchivePendingResult {
8
18
  /** 今回返さなかった残りの未蒸留エントリ数 */
9
19
  remaining: number;
10
20
  }
21
+ export declare class ArchiveCursorCorruptedError extends Error {
22
+ readonly id: string;
23
+ readonly cursor: number;
24
+ readonly total: number;
25
+ constructor(id: string, cursor: number, total: number);
26
+ }
11
27
  /**
12
28
  * ArchivePending — archive.md の未蒸留エントリを取得する
13
29
  *
@@ -20,6 +36,7 @@ export declare class ArchivePending {
20
36
  execute(engramId: string, limit?: number): Promise<ArchivePendingResult>;
21
37
  private readCursor;
22
38
  }
39
+ export declare function parseArchiveEntry(raw: string): ArchivePendingEntry;
23
40
  export declare class ArchivePendingEngramNotFoundError extends Error {
24
41
  constructor(id: string);
25
42
  }
@@ -2,7 +2,20 @@ import { join } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  const ENTRY_SEPARATOR = /\n---\n/;
5
- const DEFAULT_LIMIT = 30;
5
+ const DEFAULT_LIMIT = 100;
6
+ const ENTRY_HEADER_PATTERN = /^(\d{4}-\d{2}-\d{2}) \| ?(.*)$/;
7
+ export class ArchiveCursorCorruptedError extends Error {
8
+ id;
9
+ cursor;
10
+ total;
11
+ constructor(id, cursor, total) {
12
+ super(`Engram "${id}" archive cursor is invalid (${cursor} > ${total}). Repair archive.cursor before distilling.`);
13
+ this.id = id;
14
+ this.cursor = cursor;
15
+ this.total = total;
16
+ this.name = "ArchiveCursorCorruptedError";
17
+ }
18
+ }
6
19
  /**
7
20
  * ArchivePending — archive.md の未蒸留エントリを取得する
8
21
  *
@@ -29,11 +42,14 @@ export class ArchivePending {
29
42
  .map((e) => e.trim())
30
43
  .filter((e) => e.length > 0);
31
44
  const cursor = await this.readCursor(engramDir);
45
+ if (cursor > allEntries.length) {
46
+ throw new ArchiveCursorCorruptedError(engramId, cursor, allEntries.length);
47
+ }
32
48
  const pendingEntries = allEntries.slice(cursor);
33
49
  const effectiveLimit = limit ?? DEFAULT_LIMIT;
34
50
  const returned = pendingEntries.slice(0, effectiveLimit);
35
51
  return {
36
- entries: returned,
52
+ entries: returned.map(parseArchiveEntry),
37
53
  cursor,
38
54
  total: allEntries.length,
39
55
  remaining: pendingEntries.length - returned.length,
@@ -53,6 +69,25 @@ export class ArchivePending {
53
69
  }
54
70
  }
55
71
  }
72
+ export function parseArchiveEntry(raw) {
73
+ const trimmed = raw.trim();
74
+ const [header = "", ...rest] = trimmed.split("\n");
75
+ const match = header.match(ENTRY_HEADER_PATTERN);
76
+ if (!match) {
77
+ return {
78
+ date: null,
79
+ summary: null,
80
+ raw: trimmed,
81
+ body: trimmed,
82
+ };
83
+ }
84
+ return {
85
+ date: match[1],
86
+ summary: match[2] || null,
87
+ raw: trimmed,
88
+ body: rest.join("\n").trim(),
89
+ };
90
+ }
56
91
  export class ArchivePendingEngramNotFoundError extends Error {
57
92
  constructor(id) {
58
93
  super(`Engram "${id}" archive not found`);
@@ -5,11 +5,11 @@ export { MigrateEngrams, type MigrateEngramsResult, } from "./migrate-engrams.js
5
5
  export { RefreshSamples, type RefreshSamplesResult, } from "./refresh-samples.js";
6
6
  export { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, type InjectPersonaFileDiff, type InjectPersonaDiffResult, type InjectResult, } from "./inject.js";
7
7
  export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, type ExtractPersonaFileDiff, type ExtractPersonaDiffResult, type ExtractResult, } from "./extract.js";
8
- export { MemoryWrite, MemoryWriteEngramNotFoundError, type MemoryWriteResult, } from "./memory-write.js";
8
+ export { MemoryWrite, MemoryWriteEngramNotFoundError, type MemoryWriteBatchItem, type MemoryWriteResult, } from "./memory-write.js";
9
9
  export { ClawPull, ClawPullEngramNotFoundError, ClawPullWorkspaceNotFoundError, ClawPullPersonaMissingError, type ClawPullResult, type ClawPullDiff, } from "./claw-pull.js";
10
10
  export { Sync, SyncOpenclawDirNotFoundError, type SyncTarget, type SyncResult, type SyncInitialResult, } from "./sync.js";
11
- export { ArchivePending, ArchivePendingEngramNotFoundError, type ArchivePendingResult, } from "./archive-pending.js";
12
- export { ArchiveCursorUpdate, ArchiveCursorUpdateEngramNotFoundError, type ArchiveCursorUpdateResult, } from "./archive-cursor-update.js";
11
+ export { ArchivePending, ArchiveCursorCorruptedError, ArchivePendingEngramNotFoundError, type ArchivePendingEntry, type ArchivePendingResult, } from "./archive-pending.js";
12
+ export { ArchiveCursorUpdate, ArchiveCursorAdvanceOverflowError, ArchiveCursorUpdateEngramNotFoundError, type ArchiveCursorUpdateResult, } from "./archive-cursor-update.js";
13
13
  export { MikoshiStatus, MikoshiStatusEngramNotFoundError, MikoshiStatusCloudNotFoundError, type MikoshiStatusResult, } from "./mikoshi-status.js";
14
14
  export { MikoshiPush, MikoshiPushEngramNotFoundError, MikoshiPushPersonaHashError, MikoshiPushPersonaConflictError, type MikoshiPushResult, type MikoshiPushApplyResult, } from "./mikoshi-push.js";
15
15
  export { MikoshiDownload, MikoshiDownloadAlreadyExistsError, MikoshiDownloadCloudNotFoundError, MikoshiDownloadPersonaMissingError, type MikoshiDownloadResult, } from "./mikoshi-download.js";
@@ -8,8 +8,8 @@ export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedE
8
8
  export { MemoryWrite, MemoryWriteEngramNotFoundError, } from "./memory-write.js";
9
9
  export { ClawPull, ClawPullEngramNotFoundError, ClawPullWorkspaceNotFoundError, ClawPullPersonaMissingError, } from "./claw-pull.js";
10
10
  export { Sync, SyncOpenclawDirNotFoundError, } from "./sync.js";
11
- export { ArchivePending, ArchivePendingEngramNotFoundError, } from "./archive-pending.js";
12
- export { ArchiveCursorUpdate, ArchiveCursorUpdateEngramNotFoundError, } from "./archive-cursor-update.js";
11
+ export { ArchivePending, ArchiveCursorCorruptedError, ArchivePendingEngramNotFoundError, } from "./archive-pending.js";
12
+ export { ArchiveCursorUpdate, ArchiveCursorAdvanceOverflowError, ArchiveCursorUpdateEngramNotFoundError, } from "./archive-cursor-update.js";
13
13
  export { MikoshiStatus, MikoshiStatusEngramNotFoundError, MikoshiStatusCloudNotFoundError, } from "./mikoshi-status.js";
14
14
  export { MikoshiPush, MikoshiPushEngramNotFoundError, MikoshiPushPersonaHashError, MikoshiPushPersonaConflictError, } from "./mikoshi-push.js";
15
15
  export { MikoshiDownload, MikoshiDownloadAlreadyExistsError, MikoshiDownloadCloudNotFoundError, MikoshiDownloadPersonaMissingError, } from "./mikoshi-download.js";
@@ -4,6 +4,10 @@ export interface MemoryWriteResult {
4
4
  appended: boolean;
5
5
  path: string;
6
6
  }
7
+ export interface MemoryWriteBatchItem {
8
+ date: string;
9
+ content: string;
10
+ }
7
11
  /**
8
12
  * MemoryWrite — Engramのメモリエントリに追記する
9
13
  *
@@ -14,6 +18,7 @@ export declare class MemoryWrite {
14
18
  private readonly engramsPath;
15
19
  constructor(engramsPath: string);
16
20
  execute(engramId: string, content: string, date?: string): Promise<MemoryWriteResult>;
21
+ executeBatch(engramId: string, writes: MemoryWriteBatchItem[]): Promise<MemoryWriteResult[]>;
17
22
  }
18
23
  export declare class MemoryWriteEngramNotFoundError extends Error {
19
24
  constructor(id: string);
@@ -14,29 +14,39 @@ export class MemoryWrite {
14
14
  }
15
15
  async execute(engramId, content, date) {
16
16
  const resolvedDate = date ?? new Date().toISOString().split("T")[0];
17
+ const [result] = await this.executeBatch(engramId, [
18
+ { date: resolvedDate, content },
19
+ ]);
20
+ return result;
21
+ }
22
+ async executeBatch(engramId, writes) {
17
23
  const engramDir = join(this.engramsPath, engramId);
18
24
  if (!existsSync(engramDir)) {
19
25
  throw new MemoryWriteEngramNotFoundError(engramId);
20
26
  }
21
27
  const memoryDir = join(engramDir, "memory");
22
28
  await mkdir(memoryDir, { recursive: true });
23
- const filePath = join(memoryDir, `${resolvedDate}.md`);
24
- let appended = false;
25
- if (existsSync(filePath)) {
26
- const existing = await readFile(filePath, "utf-8");
27
- const separator = existing.endsWith("\n") ? "\n" : "\n\n";
28
- await writeFile(filePath, existing + separator + content + "\n", "utf-8");
29
- appended = true;
30
- }
31
- else {
32
- await writeFile(filePath, content + "\n", "utf-8");
29
+ const results = [];
30
+ for (const write of writes) {
31
+ const filePath = join(memoryDir, `${write.date}.md`);
32
+ let appended = false;
33
+ if (existsSync(filePath)) {
34
+ const existing = await readFile(filePath, "utf-8");
35
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
36
+ await writeFile(filePath, existing + separator + write.content + "\n", "utf-8");
37
+ appended = true;
38
+ }
39
+ else {
40
+ await writeFile(filePath, write.content + "\n", "utf-8");
41
+ }
42
+ results.push({
43
+ engramId,
44
+ date: write.date,
45
+ appended,
46
+ path: filePath,
47
+ });
33
48
  }
34
- return {
35
- engramId,
36
- date: resolvedDate,
37
- appended,
38
- path: filePath,
39
- };
49
+ return results;
40
50
  }
41
51
  }
42
52
  export class MemoryWriteEngramNotFoundError extends Error {
@@ -142,7 +142,7 @@ export function registerConfigCommand(program) {
142
142
  // relic config distillation-batch-size [n]
143
143
  config
144
144
  .command("distillation-batch-size [n]")
145
- .description("Get or set the number of archive entries to distill at once (default: 30)")
145
+ .description("Get or set the number of archive entries to distill at once (default: 100)")
146
146
  .action(async (n) => {
147
147
  await ensureInitialized();
148
148
  const cfg = await loadConfig();
@@ -3,8 +3,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { ArchiveSearch, ArchiveSearchEngramNotFoundError, } from "../../core/usecases/archive-search.js";
6
- import { ArchivePending, ArchivePendingEngramNotFoundError, } from "../../core/usecases/archive-pending.js";
7
- import { ArchiveCursorUpdate, } from "../../core/usecases/archive-cursor-update.js";
6
+ import { ArchivePending, ArchiveCursorCorruptedError, ArchivePendingEngramNotFoundError, } from "../../core/usecases/archive-pending.js";
7
+ import { ArchiveCursorUpdate, ArchiveCursorAdvanceOverflowError, } from "../../core/usecases/archive-cursor-update.js";
8
8
  import { MemoryWrite, MemoryWriteEngramNotFoundError, } from "../../core/usecases/memory-write.js";
9
9
  import { CreateEngram, EngramAlreadyExistsError, InvalidEngramIdError, } from "../../core/usecases/create-engram.js";
10
10
  import { DEFAULT_SOUL, DEFAULT_IDENTITY } from "../../shared/templates.js";
@@ -13,6 +13,63 @@ import { join } from "node:path";
13
13
  import { existsSync } from "node:fs";
14
14
  import { readFile, writeFile } from "node:fs/promises";
15
15
  import { resolveDistillationBatchSize, resolveEngramsPath, } from "../../shared/config.js";
16
+ async function appendLongTermAndUserProfile(engramsPath, id, long_term, user_profile) {
17
+ let longTermWritten = false;
18
+ if (long_term) {
19
+ const memoryMdPath = join(engramsPath, id, "MEMORY.md");
20
+ if (existsSync(memoryMdPath)) {
21
+ const existing = await readFile(memoryMdPath, "utf-8");
22
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
23
+ await writeFile(memoryMdPath, existing + separator + long_term + "\n", "utf-8");
24
+ }
25
+ else {
26
+ await writeFile(memoryMdPath, long_term + "\n", "utf-8");
27
+ }
28
+ longTermWritten = true;
29
+ }
30
+ let userProfileWritten = false;
31
+ if (user_profile) {
32
+ const userMdPath = join(engramsPath, id, "USER.md");
33
+ await writeFile(userMdPath, user_profile + "\n", "utf-8");
34
+ userProfileWritten = true;
35
+ }
36
+ return { longTermWritten, userProfileWritten };
37
+ }
38
+ function assertDateCoverage(writes, expectedDates, skippedDates) {
39
+ const writeDates = writes.map((write) => write.date);
40
+ const duplicateWriteDates = findDuplicates(writeDates);
41
+ if (duplicateWriteDates.length > 0) {
42
+ throw new Error(`Duplicate write dates are not allowed: ${duplicateWriteDates.join(", ")}`);
43
+ }
44
+ const duplicateSkippedDates = findDuplicates(skippedDates);
45
+ if (duplicateSkippedDates.length > 0) {
46
+ throw new Error(`Duplicate skipped_dates are not allowed: ${duplicateSkippedDates.join(", ")}`);
47
+ }
48
+ const overlappedDates = writeDates.filter((date) => skippedDates.includes(date));
49
+ if (overlappedDates.length > 0) {
50
+ throw new Error(`Dates cannot appear in both writes and skipped_dates: ${[...new Set(overlappedDates)].join(", ")}`);
51
+ }
52
+ const expectedSet = new Set(expectedDates);
53
+ const coveredDates = [...writeDates, ...skippedDates];
54
+ const unexpectedDates = coveredDates.filter((date) => !expectedSet.has(date));
55
+ if (unexpectedDates.length > 0) {
56
+ throw new Error(`Unexpected dates: ${[...new Set(unexpectedDates)].join(", ")}`);
57
+ }
58
+ const coveredSet = new Set(coveredDates);
59
+ const missingDates = expectedDates.filter((date) => !coveredSet.has(date));
60
+ if (missingDates.length > 0) {
61
+ throw new Error(`Missing dates: ${missingDates.join(", ")}`);
62
+ }
63
+ }
64
+ function findDuplicates(values) {
65
+ const counts = new Map();
66
+ for (const value of values) {
67
+ counts.set(value, (counts.get(value) ?? 0) + 1);
68
+ }
69
+ return [...counts.entries()]
70
+ .filter(([, count]) => count > 1)
71
+ .map(([value]) => value);
72
+ }
16
73
  const server = new McpServer({
17
74
  name: "relic",
18
75
  version: "0.1.0",
@@ -153,16 +210,34 @@ server.tool("relic_archive_pending", "Get un-distilled archive entries since the
153
210
  ],
154
211
  };
155
212
  }
156
- const header = `cursor: ${result.cursor} | total: ${result.total} | returned: ${result.entries.length} | remaining: ${result.remaining}`;
213
+ const dates = [...new Set(result.entries.map((entry) => entry.date).filter(Boolean))];
214
+ const header = [
215
+ `cursor: ${result.cursor}`,
216
+ `total: ${result.total}`,
217
+ `returned: ${result.entries.length}`,
218
+ `remaining: ${result.remaining}`,
219
+ `dates: ${dates.length > 0 ? dates.join(", ") : "unknown"}`,
220
+ ].join(" | ");
157
221
  const body = result.entries
158
- .map((e, i) => `[entry ${result.cursor + i + 1}]\n${e}`)
222
+ .map((entry, i) => {
223
+ const lines = [`[entry ${result.cursor + i + 1}]`];
224
+ if (entry.date) {
225
+ lines.push(`date: ${entry.date}`);
226
+ }
227
+ if (entry.summary) {
228
+ lines.push(`summary: ${entry.summary}`);
229
+ }
230
+ lines.push(entry.raw);
231
+ return lines.join("\n");
232
+ })
159
233
  .join("\n\n---\n\n");
160
234
  return {
161
235
  content: [{ type: "text", text: `${header}\n\n${body}` }],
162
236
  };
163
237
  }
164
238
  catch (err) {
165
- if (err instanceof ArchivePendingEngramNotFoundError) {
239
+ if (err instanceof ArchivePendingEngramNotFoundError ||
240
+ err instanceof ArchiveCursorCorruptedError) {
166
241
  return {
167
242
  content: [{ type: "text", text: err.message }],
168
243
  isError: true,
@@ -174,7 +249,19 @@ server.tool("relic_archive_pending", "Get un-distilled archive entries since the
174
249
  // --- relic_memory_write ---
175
250
  server.tool("relic_memory_write", "Write distilled memory to an Engram's memory file and advance the archive cursor. Call this after reviewing pending archive entries and distilling them into key insights.", {
176
251
  id: z.string().describe("Engram ID"),
177
- content: z.string().describe("Distilled memory content to write to memory/YYYY-MM-DD.md"),
252
+ writes: z
253
+ .array(z.object({
254
+ date: z.string().describe("Archive entry date for this memory file (YYYY-MM-DD)"),
255
+ content: z.string().describe("Distilled memory content for memory/YYYY-MM-DD.md"),
256
+ }))
257
+ .min(0)
258
+ .describe("Distilled memory writes grouped by actual archive dates"),
259
+ expected_dates: z
260
+ .array(z.string())
261
+ .describe("All archive dates that must be explicitly covered by writes or skipped_dates"),
262
+ skipped_dates: z
263
+ .array(z.string())
264
+ .describe("Archive dates intentionally skipped because they produced no durable memory"),
178
265
  count: z
179
266
  .number()
180
267
  .describe("Number of archive entries distilled (from relic_archive_pending returned count)"),
@@ -186,10 +273,6 @@ server.tool("relic_memory_write", "Write distilled memory to an Engram's memory
186
273
  .string()
187
274
  .optional()
188
275
  .describe("User profile updates to write to USER.md (preferences, tendencies, work style — about the human, not the project)"),
189
- date: z
190
- .string()
191
- .optional()
192
- .describe("Date for memory file (YYYY-MM-DD, default: today)"),
193
276
  path: z
194
277
  .string()
195
278
  .optional()
@@ -197,36 +280,21 @@ server.tool("relic_memory_write", "Write distilled memory to an Engram's memory
197
280
  }, async (args) => {
198
281
  const engramsPath = await resolveEngramsPath(args.path);
199
282
  try {
200
- // Step 1: Write daily memory
201
283
  const memoryWrite = new MemoryWrite(engramsPath);
202
- const writeResult = await memoryWrite.execute(args.id, args.content, args.date);
203
- // Step 2: Append to MEMORY.md if long_term is provided
204
- let longTermWritten = false;
205
- if (args.long_term) {
206
- const memoryMdPath = join(engramsPath, args.id, "MEMORY.md");
207
- if (existsSync(memoryMdPath)) {
208
- const existing = await readFile(memoryMdPath, "utf-8");
209
- const separator = existing.endsWith("\n") ? "\n" : "\n\n";
210
- await writeFile(memoryMdPath, existing + separator + args.long_term + "\n", "utf-8");
211
- }
212
- else {
213
- await writeFile(memoryMdPath, args.long_term + "\n", "utf-8");
214
- }
215
- longTermWritten = true;
216
- }
217
- // Step 2.5: Write USER.md if user_profile is provided
218
- let userProfileWritten = false;
219
- if (args.user_profile) {
220
- const userMdPath = join(engramsPath, args.id, "USER.md");
221
- await writeFile(userMdPath, args.user_profile + "\n", "utf-8");
222
- userProfileWritten = true;
223
- }
224
- // Step 3: Advance cursor by the number of distilled entries
284
+ assertDateCoverage(args.writes, args.expected_dates, args.skipped_dates);
285
+ const writeResults = await memoryWrite.executeBatch(args.id, args.writes);
286
+ const writeSummary = writeResults.length > 0
287
+ ? writeResults
288
+ .map((result) => `${result.date} (${result.appended ? "appended" : "created"})`)
289
+ .join(", ")
290
+ : "no files";
291
+ const { longTermWritten, userProfileWritten } = await appendLongTermAndUserProfile(engramsPath, args.id, args.long_term, args.user_profile);
225
292
  const cursorUpdate = new ArchiveCursorUpdate(engramsPath);
226
293
  const cursorResult = await cursorUpdate.execute(args.id, args.count);
227
- const parts = [
228
- `Memory written to ${writeResult.date} (${writeResult.appended ? "appended" : "created"}).`,
229
- ];
294
+ const parts = [`Memory written to ${writeSummary}.`];
295
+ if (args.skipped_dates.length > 0) {
296
+ parts.push(`Skipped dates: ${args.skipped_dates.join(", ")}.`);
297
+ }
230
298
  if (longTermWritten) {
231
299
  parts.push("Long-term memory (MEMORY.md) updated.");
232
300
  }
@@ -244,7 +312,10 @@ server.tool("relic_memory_write", "Write distilled memory to an Engram's memory
244
312
  };
245
313
  }
246
314
  catch (err) {
247
- if (err instanceof MemoryWriteEngramNotFoundError) {
315
+ if (err instanceof MemoryWriteEngramNotFoundError ||
316
+ err instanceof ArchiveCursorAdvanceOverflowError ||
317
+ err instanceof ArchiveCursorCorruptedError ||
318
+ err instanceof Error) {
248
319
  return {
249
320
  content: [{ type: "text", text: err.message }],
250
321
  isError: true,
@@ -17,7 +17,7 @@ export declare const RelicConfigSchema: z.ZodObject<{
17
17
  clawPath: z.ZodOptional<z.ZodString>;
18
18
  /** システムプロンプトに含める直近メモリエントリ数 (default: 2) */
19
19
  memoryWindowSize: z.ZodDefault<z.ZodNumber>;
20
- /** 一度の記憶蒸留で扱う archive エントリ数 (default: 30) */
20
+ /** 一度の記憶蒸留で扱う archive エントリ数 (default: 100) */
21
21
  distillationBatchSize: z.ZodDefault<z.ZodNumber>;
22
22
  }, "strip", z.ZodTypeAny, {
23
23
  memoryWindowSize: number;
@@ -80,7 +80,7 @@ export declare function resolveClawPath(cliOverride?: string): Promise<string |
80
80
  export declare function resolveMemoryWindowSize(): Promise<number>;
81
81
  /**
82
82
  * 記憶蒸留のバッチ件数を解決する。
83
- * 優先順位: config.distillationBatchSize > 30 (デフォルト)
83
+ * 優先順位: config.distillationBatchSize > 100 (デフォルト)
84
84
  */
85
85
  export declare function resolveDistillationBatchSize(): Promise<number>;
86
86
  /**
@@ -24,8 +24,8 @@ export const RelicConfigSchema = z.object({
24
24
  clawPath: z.string().optional(),
25
25
  /** システムプロンプトに含める直近メモリエントリ数 (default: 2) */
26
26
  memoryWindowSize: z.number().int().min(1).default(2),
27
- /** 一度の記憶蒸留で扱う archive エントリ数 (default: 30) */
28
- distillationBatchSize: z.number().int().min(1).default(30),
27
+ /** 一度の記憶蒸留で扱う archive エントリ数 (default: 100) */
28
+ distillationBatchSize: z.number().int().min(1).default(100),
29
29
  });
30
30
  /**
31
31
  * 設定ファイルを読み込む。存在しなければデフォルト値を返す。
@@ -163,7 +163,7 @@ export async function resolveMemoryWindowSize() {
163
163
  }
164
164
  /**
165
165
  * 記憶蒸留のバッチ件数を解決する。
166
- * 優先順位: config.distillationBatchSize > 30 (デフォルト)
166
+ * 優先順位: config.distillationBatchSize > 100 (デフォルト)
167
167
  */
168
168
  export async function resolveDistillationBatchSize() {
169
169
  await ensureInitialized();
@@ -9,7 +9,7 @@ export interface ComposeOptions {
9
9
  currentDate?: string;
10
10
  /** システムプロンプトに含める直近メモリエントリ数(デフォルト: 2) */
11
11
  memoryWindowSize?: number;
12
- /** 一度の記憶蒸留で扱う archive エントリ数(デフォルト: 30) */
12
+ /** 一度の記憶蒸留で扱う archive エントリ数(デフォルト: 100) */
13
13
  distillationBatchSize?: number;
14
14
  }
15
15
  /**
@@ -46,7 +46,7 @@ export function composeEngram(files, options) {
46
46
  */
47
47
  function composeRelicSection(meta, distillationBatchSize, currentDate) {
48
48
  const today = currentDate ?? new Date().toISOString().split("T")[0];
49
- const batchSize = distillationBatchSize ?? 30;
49
+ const batchSize = distillationBatchSize ?? 100;
50
50
  return `# Relic System
51
51
 
52
52
  - engramId: ${meta.id}
@@ -68,16 +68,20 @@ To recall past context, use \`relic_archive_search\` to search your archive by k
68
68
 
69
69
  When the user asks you to organize or distill memories:
70
70
  1. Call \`relic_archive_pending\` **once** to get un-distilled session entries (up to ${batchSize})
71
- 2. Review and distill them into:
72
- - **content**: key facts, decisions, and insights for \`memory/YYYY-MM-DD.md\`
71
+ 2. Review the archive entry dates and group the distilled memory by the **actual archive entry date**, not the distillation date
72
+ 3. Distill them into:
73
+ - **writes**: one item per date, each with \`date\` and \`content\`, for \`memory/YYYY-MM-DD.md\`
74
+ - **expected_dates**: every archive date present in the pending batch
75
+ - **skipped_dates**: dates that had pending entries but produced no durable memory worth saving
73
76
  - **long_term** (optional): only the most important, enduring facts for \`MEMORY.md\` (e.g. project architecture decisions, key constraints, hard rules)
74
77
  - **user_profile** (optional): full updated user profile for \`USER.md\` (preferences, tendencies, work style — about the human, not the project)
75
- 3. Call \`relic_memory_write\` with \`content\`, \`count\`, and optionally \`long_term\` and/or \`user_profile\`
76
- 4. If \`remaining > 0\`, inform the user how many entries are still pending — do NOT fetch more automatically
78
+ 4. Call \`relic_memory_write\` with \`writes\`, \`expected_dates\`, \`skipped_dates\`, \`count\`, and optionally \`long_term\` and/or \`user_profile\`
79
+ 5. If \`remaining > 0\`, inform the user how many entries are still pending — do NOT fetch more automatically
77
80
 
78
81
  **Important:**
79
82
  - Write distilled memories in the **same language the user is using** in the current conversation
80
83
  - Do NOT loop or repeat the distillation process — one round per user request
84
+ - Every archive date in the pending batch must appear in exactly one of \`writes.date\` or \`skipped_dates\`
81
85
  - \`long_term\` should be highly selective — only facts that matter across all future sessions (objective facts and rules)
82
86
  - \`user_profile\` is a complete replacement, not an append — write the full profile each time you update it (user tendencies, preferences, work style)
83
87
  - Distilled memories are loaded into your prompt on future sessions`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ectplsm/relic",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "PROJECT RELIC — Engram injection system for AI constructs",
5
5
  "license": "MIT",
6
6
  "repository": {