@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 +3 -3
- package/dist/core/usecases/archive-cursor-update.d.ts +6 -0
- package/dist/core/usecases/archive-cursor-update.js +18 -0
- package/dist/core/usecases/archive-pending.d.ts +18 -1
- package/dist/core/usecases/archive-pending.js +37 -2
- package/dist/core/usecases/index.d.ts +3 -3
- package/dist/core/usecases/index.js +2 -2
- package/dist/core/usecases/memory-write.d.ts +5 -0
- package/dist/core/usecases/memory-write.js +26 -16
- package/dist/interfaces/cli/commands/config.js +1 -1
- package/dist/interfaces/mcp/index.js +109 -38
- package/dist/shared/config.d.ts +2 -2
- package/dist/shared/config.js +3 -3
- package/dist/shared/engram-composer.d.ts +1 -1
- package/dist/shared/engram-composer.js +9 -5
- package/package.json +1 -1
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
|
|
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:
|
|
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 =
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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:
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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,
|
package/dist/shared/config.d.ts
CHANGED
|
@@ -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:
|
|
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 >
|
|
83
|
+
* 優先順位: config.distillationBatchSize > 100 (デフォルト)
|
|
84
84
|
*/
|
|
85
85
|
export declare function resolveDistillationBatchSize(): Promise<number>;
|
|
86
86
|
/**
|
package/dist/shared/config.js
CHANGED
|
@@ -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:
|
|
28
|
-
distillationBatchSize: z.number().int().min(1).default(
|
|
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 >
|
|
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 エントリ数(デフォルト:
|
|
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 ??
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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`;
|