@deepstrike/sdk 0.2.39 → 0.2.40

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.
Files changed (102) hide show
  1. package/README.md +61 -31
  2. package/dist/collaboration/contract.d.ts +2 -2
  3. package/dist/collaboration/contract.js +2 -2
  4. package/dist/collaboration/handoff.d.ts +2 -14
  5. package/dist/collaboration/handoff.js +1 -17
  6. package/dist/collaboration/harness.d.ts +16 -47
  7. package/dist/collaboration/harness.js +57 -158
  8. package/dist/collaboration/index.d.ts +2 -2
  9. package/dist/collaboration/index.js +1 -2
  10. package/dist/collaboration/modes/creator-verifier.js +42 -6
  11. package/dist/collaboration/pool.d.ts +8 -0
  12. package/dist/collaboration/pool.js +56 -3
  13. package/dist/harness/harness.d.ts +97 -97
  14. package/dist/harness/harness.js +190 -144
  15. package/dist/harness/judge.d.ts +3 -2
  16. package/dist/harness/judge.js +7 -3
  17. package/dist/harness/public.d.ts +5 -3
  18. package/dist/harness/public.js +3 -2
  19. package/dist/index.d.ts +22 -7
  20. package/dist/index.js +8 -1
  21. package/dist/kernel.d.ts +12 -39
  22. package/dist/memory/agent.d.ts +8 -41
  23. package/dist/memory/agent.js +34 -117
  24. package/dist/memory/extraction.d.ts +4 -0
  25. package/dist/memory/extraction.js +79 -0
  26. package/dist/memory/in-memory-store.d.ts +28 -15
  27. package/dist/memory/in-memory-store.js +74 -25
  28. package/dist/memory/protocols.d.ts +56 -56
  29. package/dist/memory/protocols.js +1 -0
  30. package/dist/memory/public.d.ts +3 -1
  31. package/dist/memory/public.js +1 -0
  32. package/dist/memory/ranking.d.ts +33 -0
  33. package/dist/memory/ranking.js +77 -0
  34. package/dist/memory/retention.d.ts +17 -0
  35. package/dist/memory/retention.js +54 -0
  36. package/dist/os/public.d.ts +3 -3
  37. package/dist/os/public.js +1 -1
  38. package/dist/providers/base.d.ts +8 -0
  39. package/dist/providers/base.js +0 -0
  40. package/dist/providers/gemini.js +12 -0
  41. package/dist/providers/ollama.js +3 -1
  42. package/dist/providers/openai-responses.js +9 -2
  43. package/dist/providers/profiles.d.ts +5 -3
  44. package/dist/providers/profiles.js +3 -3
  45. package/dist/runtime/context-policy.d.ts +35 -0
  46. package/dist/runtime/context-policy.js +66 -0
  47. package/dist/runtime/eval.d.ts +6 -2
  48. package/dist/runtime/eval.js +2 -2
  49. package/dist/runtime/event-stream.d.ts +9 -0
  50. package/dist/runtime/event-stream.js +25 -11
  51. package/dist/runtime/execution-plane.d.ts +5 -1
  52. package/dist/runtime/execution-plane.js +9 -3
  53. package/dist/runtime/facade.js +2 -1
  54. package/dist/runtime/kernel-event-log.d.ts +0 -2
  55. package/dist/runtime/kernel-event-log.js +61 -13
  56. package/dist/runtime/kernel-primitives-dashboard.js +1 -1
  57. package/dist/runtime/kernel-rebuild.d.ts +13 -0
  58. package/dist/runtime/kernel-rebuild.js +75 -0
  59. package/dist/runtime/kernel-step.d.ts +157 -8
  60. package/dist/runtime/kernel-step.js +220 -7
  61. package/dist/runtime/kernel-transaction-log.d.ts +61 -0
  62. package/dist/runtime/kernel-transaction-log.js +149 -0
  63. package/dist/runtime/large-result-spool.d.ts +3 -1
  64. package/dist/runtime/large-result-spool.js +24 -5
  65. package/dist/runtime/loop-driver.d.ts +1 -1
  66. package/dist/runtime/loop-driver.js +2 -7
  67. package/dist/runtime/mcp-proxy-plane.d.ts +1 -0
  68. package/dist/runtime/mcp-proxy-plane.js +23 -6
  69. package/dist/runtime/os-profile.d.ts +9 -10
  70. package/dist/runtime/os-profile.js +14 -10
  71. package/dist/runtime/os-snapshot.d.ts +19 -0
  72. package/dist/runtime/os-snapshot.js +33 -3
  73. package/dist/runtime/process-sandbox-plane.js +16 -11
  74. package/dist/runtime/reaction-checkpoint.d.ts +51 -0
  75. package/dist/runtime/reaction-checkpoint.js +83 -0
  76. package/dist/runtime/reactive-session.d.ts +9 -3
  77. package/dist/runtime/reactive-session.js +44 -14
  78. package/dist/runtime/reliability.d.ts +48 -0
  79. package/dist/runtime/reliability.js +86 -0
  80. package/dist/runtime/remote-vpc-plane.js +4 -3
  81. package/dist/runtime/run-group.d.ts +38 -35
  82. package/dist/runtime/run-group.js +97 -54
  83. package/dist/runtime/runner.d.ts +117 -78
  84. package/dist/runtime/runner.js +1516 -1197
  85. package/dist/runtime/session-log.d.ts +71 -12
  86. package/dist/runtime/session-log.js +192 -32
  87. package/dist/runtime/session-repair.d.ts +11 -7
  88. package/dist/runtime/session-repair.js +11 -8
  89. package/dist/runtime/sub-agent-orchestrator.d.ts +6 -2
  90. package/dist/runtime/sub-agent-orchestrator.js +45 -18
  91. package/dist/signals/gateway.d.ts +23 -10
  92. package/dist/signals/gateway.js +81 -17
  93. package/dist/signals/scheduled.js +0 -1
  94. package/dist/signals/types.d.ts +21 -13
  95. package/dist/skills/loader.js +12 -2
  96. package/dist/tools/index.d.ts +2 -0
  97. package/dist/types/agent.d.ts +40 -1
  98. package/dist/types/agent.js +61 -1
  99. package/dist/types.d.ts +6 -1
  100. package/dist/workflow/public.d.ts +2 -3
  101. package/dist/workflow/public.js +0 -1
  102. package/package.json +2 -2
@@ -1,66 +1,33 @@
1
1
  /**
2
2
  * Long-term memory agent implementation (Phase 7).
3
3
  *
4
- * Provides two main capabilities:
5
- * 1. Extract memories from conversation transcripts (后台代理)
6
- * 2. Select relevant memories for current context (LLM选择器)
4
+ * Provides deterministic selection and boundary validation for long-term memories.
7
5
  *
8
6
  * Design principles:
9
7
  * - Kernel defines memory types and validation rules
10
8
  * - SDK performs I/O and selection
11
- * - LLM (Sonnet) acts as selector, not vector similarity
9
+ * - Semantic reranking remains an optional DreamStore capability
12
10
  */
13
- import type { SessionData, MemoryEntry, MemoryKind, MemoryMetadata, MemoryWriteRequest, MemoryQuery, MemoryRetrieval } from "./protocols.js";
14
- export type { MemoryKind, MemoryMetadata, MemoryWriteRequest, MemoryQuery, MemoryRetrieval, } from "./protocols.js";
15
- /**
16
- * Memory index entry (from MEMORY.md).
17
- */
18
- export interface MemoryIndexEntry {
19
- name: string;
20
- description: string;
21
- kind?: MemoryKind;
22
- file: string;
23
- updated_at: number;
24
- }
25
- /**
26
- * Extract memories from a completed session.
27
- *
28
- * This is typically called by a background agent after a session completes.
29
- * It analyzes the conversation and generates new memory write requests.
30
- *
31
- * @param sessionData - The completed session data
32
- * @param existingMemories - Existing memories to avoid duplicates
33
- * @returns Array of memory write requests
34
- */
35
- export declare function extractMemories(sessionData: SessionData, existingMemories: MemoryEntry[]): Promise<MemoryWriteRequest[]>;
11
+ import type { MemoryQuery, MemoryRecall, MemoryRecord } from "./protocols.js";
12
+ export type { MemoryKind, MemoryQuery, MemoryRecall, MemoryRecord, } from "./protocols.js";
36
13
  /**
37
14
  * Select relevant memories for the current context.
38
15
  *
39
- * This uses an LLM (Sonnet) as a selector, not vector similarity.
40
- * The process:
41
- * 1. Read memory index (name + description for each memory)
42
- * 2. Filter out already_surfaced and recentTools
43
- * 3. Send the filtered list to LLM with current context
44
- * 4. LLM returns top-5 most relevant memory IDs
16
+ * The reference selector is deterministic and provider-independent: lexical overlap first,
17
+ * recency second. Semantic/embedding reranking belongs in a DreamStore plugin.
45
18
  *
46
19
  * @param query - Memory query from kernel
47
20
  * @param memoryIndex - Memory index entries
48
21
  * @param model - Optional model name (default: claude-sonnet-4-20250514)
49
22
  * @returns Memory retrieval result
50
23
  */
51
- export declare function selectMemories(query: MemoryQuery, memoryIndex: MemoryIndexEntry[], model?: string): Promise<MemoryRetrieval>;
24
+ export declare function selectMemories(query: MemoryQuery, records: MemoryRecord[]): Promise<MemoryRecall[]>;
52
25
  /**
53
26
  * Validate memory before writing (kernel-side validation mirror).
54
27
  *
55
28
  * This SDK-side validation provides early feedback before sending to kernel.
56
29
  */
57
- export declare function validateMemory(request: MemoryWriteRequest): {
30
+ export declare function validateMemory(record: MemoryRecord): {
58
31
  valid: boolean;
59
32
  error?: string;
60
33
  };
61
- /**
62
- * Infer memory kind from metadata (mirrors kernel logic).
63
- */
64
- export declare function inferMemoryKind(metadata: MemoryMetadata): MemoryKind;
65
- /** Build a memory index from DreamStore entries for {@link selectMemories}. */
66
- export declare function memoriesToIndex(entries: MemoryEntry[]): MemoryIndexEntry[];
@@ -1,151 +1,68 @@
1
1
  /**
2
2
  * Long-term memory agent implementation (Phase 7).
3
3
  *
4
- * Provides two main capabilities:
5
- * 1. Extract memories from conversation transcripts (后台代理)
6
- * 2. Select relevant memories for current context (LLM选择器)
4
+ * Provides deterministic selection and boundary validation for long-term memories.
7
5
  *
8
6
  * Design principles:
9
7
  * - Kernel defines memory types and validation rules
10
8
  * - SDK performs I/O and selection
11
- * - LLM (Sonnet) acts as selector, not vector similarity
9
+ * - Semantic reranking remains an optional DreamStore capability
12
10
  */
13
- /**
14
- * Extract memories from a completed session.
15
- *
16
- * This is typically called by a background agent after a session completes.
17
- * It analyzes the conversation and generates new memory write requests.
18
- *
19
- * @param sessionData - The completed session data
20
- * @param existingMemories - Existing memories to avoid duplicates
21
- * @returns Array of memory write requests
22
- */
23
- export async function extractMemories(sessionData, existingMemories) {
24
- // In a real implementation, this would:
25
- // 1. Use an LLM to analyze the session transcript
26
- // 2. Identify new insights, preferences, and context
27
- // 3. Generate memory write requests with proper classification
28
- // 4. Check for duplicates against existing memories
29
- // For now, return a stub implementation
30
- return [];
31
- }
11
+ import { rankMemories } from "./ranking.js";
32
12
  /**
33
13
  * Select relevant memories for the current context.
34
14
  *
35
- * This uses an LLM (Sonnet) as a selector, not vector similarity.
36
- * The process:
37
- * 1. Read memory index (name + description for each memory)
38
- * 2. Filter out already_surfaced and recentTools
39
- * 3. Send the filtered list to LLM with current context
40
- * 4. LLM returns top-5 most relevant memory IDs
15
+ * The reference selector is deterministic and provider-independent: lexical overlap first,
16
+ * recency second. Semantic/embedding reranking belongs in a DreamStore plugin.
41
17
  *
42
18
  * @param query - Memory query from kernel
43
19
  * @param memoryIndex - Memory index entries
44
20
  * @param model - Optional model name (default: claude-sonnet-4-20250514)
45
21
  * @returns Memory retrieval result
46
22
  */
47
- export async function selectMemories(query, memoryIndex, model = "claude-sonnet-4-20250514") {
48
- // 1. Filter out already surfaced and tool-related memories
49
- const filterOut = new Set([
50
- ...query.already_surfaced,
51
- ...query.active_tools,
52
- ]);
53
- const candidates = memoryIndex.filter((entry) => !filterOut.has(entry.name) && !isToolMemory(entry));
54
- // 2. If no candidates, return empty
55
- if (candidates.length === 0) {
56
- return {
57
- selected_memory_ids: [],
58
- selection_rationale: "No candidates after filtering",
59
- };
60
- }
61
- // 3. In a real implementation, this would:
62
- // - Construct a prompt with current context and memory descriptions
63
- // - Call the LLM API
64
- // - Parse the response to extract selected memory IDs
65
- // For now, return a stub implementation
66
- return {
67
- selected_memory_ids: candidates.slice(0, query.top_k).map((c) => c.name),
68
- selection_rationale: "Stub implementation",
69
- };
70
- }
71
- /**
72
- * Check if a memory is tool-related (should be filtered from recentTools).
73
- */
74
- function isToolMemory(entry) {
75
- const toolKeywords = ["usage", "how to use", "example", "syntax", "api"];
76
- const lowerDesc = entry.description.toLowerCase();
77
- const lowerName = entry.name.toLowerCase();
78
- // Filter out usage docs, but keep warnings/caveats
79
- const isUsage = toolKeywords.some((kw) => lowerDesc.includes(kw) || lowerName.includes(kw));
80
- const isWarning = lowerDesc.includes("warning") || lowerDesc.includes("caveat") || lowerDesc.includes("bug");
81
- return isUsage && !isWarning;
23
+ export async function selectMemories(query, records) {
24
+ const candidates = records.filter(record => record.scope.tenant_id === query.scope.tenant_id
25
+ && record.scope.namespace === query.scope.namespace
26
+ && (query.kinds.length === 0 || query.kinds.includes(record.kind)));
27
+ const ranked = rankMemories(query.query, candidates.map((record, insertionIndex) => ({
28
+ value: record,
29
+ searchableText: `${record.name} ${record.description} ${record.content}`,
30
+ updatedAt: Number.isFinite(record.updated_at) ? record.updated_at : 0,
31
+ recallCount: record.recall_count,
32
+ ttlDays: record.ttl_days,
33
+ insertionIndex,
34
+ })), query.top_k);
35
+ // score is relevance (from ranking), deliberately distinct from the record's stored confidence.
36
+ return ranked
37
+ .filter(hit => query.min_score === undefined || hit.score >= query.min_score)
38
+ .map(hit => ({ record: hit.value, score: hit.score, why: hit.why }));
82
39
  }
83
40
  /**
84
41
  * Validate memory before writing (kernel-side validation mirror).
85
42
  *
86
43
  * This SDK-side validation provides early feedback before sending to kernel.
87
44
  */
88
- export function validateMemory(request) {
45
+ export function validateMemory(record) {
89
46
  // Check required fields
90
- if (!request.metadata.name || request.metadata.name.trim().length === 0) {
47
+ if (!record.record_id || record.record_id.trim().length === 0) {
48
+ return { valid: false, error: "Missing required field: record_id" };
49
+ }
50
+ if (!record.scope.tenant_id || !record.scope.namespace) {
51
+ return { valid: false, error: "Missing required field: scope" };
52
+ }
53
+ if (!record.name || record.name.trim().length === 0) {
91
54
  return { valid: false, error: "Missing required field: name" };
92
55
  }
93
- if (!request.metadata.description || request.metadata.description.trim().length === 0) {
56
+ if (!record.description || record.description.trim().length === 0) {
94
57
  return { valid: false, error: "Missing required field: description" };
95
58
  }
96
59
  // Check name length
97
- if (request.metadata.name.length > 100) {
98
- return { valid: false, error: `Name too long: ${request.metadata.name.length} chars (limit: 100)` };
60
+ if (record.name.length > 100) {
61
+ return { valid: false, error: `Name too long: ${record.name.length} chars (limit: 100)` };
99
62
  }
100
63
  // Check content size
101
- if (request.content.length > 10_000) {
102
- return { valid: false, error: `Content too large: ${request.content.length} bytes (limit: 10000)` };
103
- }
104
- // Check forbidden patterns
105
- const forbiddenPatterns = [
106
- { pattern: "代码模式:", reason: "应从代码推,不应存储" },
107
- { pattern: "文件路径:", reason: "应从git推,不应存储" },
108
- { pattern: "架构:", reason: "应从实际代码推" },
109
- { pattern: "git历史:", reason: "git log是权威" },
110
- { pattern: "CLAUDE.md:", reason: "已在文档中" },
111
- { pattern: "TODO:", reason: "临时任务不应进记忆" },
112
- ];
113
- for (const { pattern, reason } of forbiddenPatterns) {
114
- if (request.content.includes(pattern)) {
115
- return { valid: false, error: `Forbidden pattern '${pattern}': ${reason}` };
116
- }
64
+ if (record.content.length > 10_000) {
65
+ return { valid: false, error: `Content too large: ${record.content.length} bytes (limit: 10000)` };
117
66
  }
118
67
  return { valid: true };
119
68
  }
120
- /**
121
- * Infer memory kind from metadata (mirrors kernel logic).
122
- */
123
- export function inferMemoryKind(metadata) {
124
- if (metadata.user_role || metadata.expertise_level) {
125
- return "user";
126
- }
127
- if (metadata.preference_rule || metadata.approved_pattern) {
128
- return "feedback";
129
- }
130
- if (metadata.project_phase || metadata.relative_date) {
131
- return "project";
132
- }
133
- if (metadata.external_url || metadata.ticket_ref) {
134
- return "reference";
135
- }
136
- // Default: feedback (most common)
137
- return "feedback";
138
- }
139
- /** Build a memory index from DreamStore entries for {@link selectMemories}. */
140
- export function memoriesToIndex(entries) {
141
- return entries.map(entry => {
142
- const meta = (entry.metadata ?? {});
143
- return {
144
- name: String(meta.name ?? entry.text.slice(0, 40)),
145
- description: String(meta.description ?? entry.text.slice(0, 120)),
146
- kind: meta.kind,
147
- file: String(meta.file ?? ""),
148
- updated_at: Number(meta.updated_at ?? 0),
149
- };
150
- });
151
- }
@@ -0,0 +1,4 @@
1
+ import type { LLMProvider } from "../types.js";
2
+ import type { MemoryRecord, MemoryScope, SessionData } from "./protocols.js";
3
+ export declare function extractSessionMemories(provider: LLMProvider, session: SessionData, scope: MemoryScope, systemPrompt?: string): Promise<MemoryRecord[]>;
4
+ export declare function parseExtractedMemories(output: string, session: SessionData, scope: MemoryScope): MemoryRecord[];
@@ -0,0 +1,79 @@
1
+ const KINDS = new Set(["user", "feedback", "project", "reference"]);
2
+ export async function extractSessionMemories(provider, session, scope, systemPrompt) {
3
+ const transcript = session.messages
4
+ .map(message => `[${message.role.toUpperCase()}] ${message.content}`)
5
+ .join("\n")
6
+ .slice(0, 8_000);
7
+ const context = {
8
+ systemText: [
9
+ systemPrompt,
10
+ "Extract durable, reusable facts from this completed session. Return only JSON; do not include transient progress or guesses.",
11
+ ].filter(Boolean).join("\n\n"),
12
+ turns: [{
13
+ role: "user",
14
+ content: `${transcript}\n\nReturn {"memories":[{"name":"stable-kebab-key","kind":"user|feedback|project|reference","content":"fact","description":"why durable","confidence":0.0,"links":[],"pinned":false,"ttl_days":null,"evidence_refs":[]}]} with at most 10 items. Return {"memories":[]} when nothing is durable.`,
15
+ toolCalls: [],
16
+ }],
17
+ };
18
+ let output = "";
19
+ const state = provider.createRunState?.();
20
+ for await (const event of provider.stream(context, [], undefined, state)) {
21
+ if (event.type === "text_delta" && "delta" in event)
22
+ output += event.delta;
23
+ }
24
+ return parseExtractedMemories(output, session, scope);
25
+ }
26
+ export function parseExtractedMemories(output, session, scope) {
27
+ let value;
28
+ try {
29
+ value = JSON.parse(output.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""));
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ if (!value || typeof value !== "object" || !Array.isArray(value.memories))
35
+ return [];
36
+ const now = session.updatedAtMs;
37
+ const records = [];
38
+ for (const raw of value.memories.slice(0, 10)) {
39
+ if (!raw || typeof raw !== "object")
40
+ continue;
41
+ const draft = raw;
42
+ const name = typeof draft.name === "string" ? draft.name.trim() : "";
43
+ const kind = typeof draft.kind === "string" && KINDS.has(draft.kind)
44
+ ? draft.kind
45
+ : undefined;
46
+ const content = typeof draft.content === "string" ? draft.content.trim() : "";
47
+ if (!name || !kind || !content)
48
+ continue;
49
+ const confidence = typeof draft.confidence === "number" && Number.isFinite(draft.confidence)
50
+ ? Math.max(0, Math.min(1, draft.confidence))
51
+ : 0.5;
52
+ records.push({
53
+ record_id: `${scope.tenant_id}:${scope.namespace}:${kind}:${name}`,
54
+ scope,
55
+ name,
56
+ kind,
57
+ content,
58
+ description: typeof draft.description === "string" ? draft.description.trim() : "",
59
+ provenance: {
60
+ session_id: session.sessionId,
61
+ author: "extraction",
62
+ trust: "untrusted",
63
+ evidence_refs: Array.isArray(draft.evidence_refs)
64
+ ? draft.evidence_refs.filter((ref) => typeof ref === "string")
65
+ : [],
66
+ },
67
+ created_at: now,
68
+ updated_at: now,
69
+ recall_count: 0,
70
+ confidence,
71
+ links: Array.isArray(draft.links) ? draft.links.filter((link) => typeof link === "string") : [],
72
+ pinned: draft.pinned === true,
73
+ ...(typeof draft.ttl_days === "number" && Number.isInteger(draft.ttl_days) && draft.ttl_days > 0
74
+ ? { ttl_days: draft.ttl_days }
75
+ : {}),
76
+ });
77
+ }
78
+ return records;
79
+ }
@@ -6,32 +6,45 @@
6
6
  *
7
7
  * Use cases:
8
8
  * - Benchmark A/B variants where memory is on/off (preload via constructor).
9
- * - Unit tests that exercise `Agent.dream()` or the `memory_query` path without disk I/O.
9
+ * - Unit tests that exercise session extraction or the `memory_query` path without disk I/O.
10
10
  * - Local development / CI where a persistent memory store isn't needed.
11
11
  *
12
- * The `search()` impl is intentionally trivial — it returns the first `topK` memories for the
13
- * agent regardless of `query`. The kernel ranks by score before deciding what to surface, so the
14
- * order memories were inserted is what callers see. For semantic search, plug in a real store.
12
+ * Search returns a genuine relevance score (distinct lexical overlap, lifted by recall history and
13
+ * lowered by TTL/staleness) — never the record's stored confidence. The store is the authority for
14
+ * the full record set, so it (M3) bounds itself by value-ordered retention eviction and (M3/M4)
15
+ * mirrors recall lifecycle and pin state. For semantic search, plug in a real store.
15
16
  */
16
- import type { CurationResult, DreamStore, MemoryEntry, SessionData } from "./protocols.js";
17
+ import type { DreamStore, MemoryQuery, MemoryRecall, MemoryRecallLifecycle, MemoryRecord, SessionData } from "./protocols.js";
18
+ export interface InMemoryDreamStoreOptions {
19
+ /** Cap the per-agent record set; a write past it evicts the lowest-value unpinned records (M3). */
20
+ maxRecords?: number;
21
+ /** Age (days) past which a record's recall relevance is discounted. Default 2. */
22
+ staleWarningDays?: number;
23
+ /** Wall-clock source for staleness scoring + recall stamps. Injectable for deterministic tests. */
24
+ now?: () => number;
25
+ }
17
26
  export declare class InMemoryDreamStore implements DreamStore {
18
27
  private readonly initialMemories;
19
- private sessions;
20
28
  private memories;
21
29
  /** Sessions persisted via `saveSession`; exposed for test assertions. */
22
30
  readonly savedSessions: SessionData[];
31
+ private readonly maxRecords?;
32
+ private readonly staleWarningDays;
33
+ private readonly now;
23
34
  /**
24
35
  * @param initialMemories Optional seed memories applied to every agent that asks for memories
25
36
  * for the first time. Useful for benchmark scenarios that preload a fact.
26
37
  */
27
- constructor(initialMemories?: MemoryEntry[]);
28
- /** Pre-populate sessions for a specific agent (test/benchmark setup). */
29
- addSession(agentId: string, session: SessionData): void;
30
- /** Pre-populate memories for a specific agent (test/benchmark setup). */
31
- addMemories(agentId: string, entries: MemoryEntry[]): void;
32
- loadSessions(agentId: string): Promise<SessionData[]>;
33
- loadMemories(agentId: string): Promise<MemoryEntry[]>;
34
- commit(agentId: string, result: CurationResult, existing: MemoryEntry[]): Promise<void>;
35
- search(agentId: string, _query: string, topK?: number): Promise<MemoryEntry[]>;
38
+ constructor(initialMemories?: MemoryRecord[], options?: InMemoryDreamStoreOptions);
39
+ private recordsFor;
40
+ upsert(agentId: string, incoming: MemoryRecord): Promise<void>;
41
+ /** M3: value-ordered retention eviction. Sheds the lowest-value unpinned records — the shared
42
+ * deterministic formula, never a blind tail-cut — until the set fits `maxRecords`. */
43
+ private evictToCapacity;
44
+ search(agentId: string, query: MemoryQuery): Promise<MemoryRecall[]>;
36
45
  saveSession(data: SessionData): Promise<void>;
46
+ /** M3: mirror the kernel's journaled recall lifecycle into the durable records. */
47
+ recordRecall(agentId: string, recalls: MemoryRecallLifecycle[]): Promise<void>;
48
+ /** M4: set a record's pin so retention eviction cannot shed it. */
49
+ setPinned(agentId: string, recordId: string, pinned: boolean): Promise<void>;
37
50
  }
@@ -1,30 +1,24 @@
1
+ import { rankMemories } from "./ranking.js";
2
+ import { memoryRetentionScore } from "./retention.js";
1
3
  export class InMemoryDreamStore {
2
4
  initialMemories;
3
- sessions = new Map();
4
5
  memories = new Map();
5
6
  /** Sessions persisted via `saveSession`; exposed for test assertions. */
6
7
  savedSessions = [];
8
+ maxRecords;
9
+ staleWarningDays;
10
+ now;
7
11
  /**
8
12
  * @param initialMemories Optional seed memories applied to every agent that asks for memories
9
13
  * for the first time. Useful for benchmark scenarios that preload a fact.
10
14
  */
11
- constructor(initialMemories = []) {
15
+ constructor(initialMemories = [], options = {}) {
12
16
  this.initialMemories = initialMemories;
17
+ this.maxRecords = options.maxRecords;
18
+ this.staleWarningDays = options.staleWarningDays ?? 2;
19
+ this.now = options.now ?? Date.now;
13
20
  }
14
- /** Pre-populate sessions for a specific agent (test/benchmark setup). */
15
- addSession(agentId, session) {
16
- const list = this.sessions.get(agentId) ?? [];
17
- list.push(session);
18
- this.sessions.set(agentId, list);
19
- }
20
- /** Pre-populate memories for a specific agent (test/benchmark setup). */
21
- addMemories(agentId, entries) {
22
- this.memories.set(agentId, [...(this.memories.get(agentId) ?? []), ...entries]);
23
- }
24
- async loadSessions(agentId) {
25
- return this.sessions.get(agentId) ?? [];
26
- }
27
- async loadMemories(agentId) {
21
+ recordsFor(agentId) {
28
22
  if (this.memories.has(agentId))
29
23
  return this.memories.get(agentId);
30
24
  if (this.initialMemories.length > 0) {
@@ -33,18 +27,73 @@ export class InMemoryDreamStore {
33
27
  }
34
28
  return [];
35
29
  }
36
- async commit(agentId, result, existing) {
37
- const kept = existing.filter((_, i) => !result.toRemoveIndices.includes(i));
38
- this.memories.set(agentId, [...kept, ...result.toAdd]);
30
+ async upsert(agentId, incoming) {
31
+ const kept = [...this.recordsFor(agentId)];
32
+ const index = kept.findIndex(record => record.scope.tenant_id === incoming.scope.tenant_id
33
+ && record.scope.namespace === incoming.scope.namespace
34
+ && record.kind === incoming.kind
35
+ && record.name === incoming.name);
36
+ if (index >= 0)
37
+ kept[index] = incoming;
38
+ else
39
+ kept.push(incoming);
40
+ this.memories.set(agentId, this.evictToCapacity(kept));
41
+ }
42
+ /** M3: value-ordered retention eviction. Sheds the lowest-value unpinned records — the shared
43
+ * deterministic formula, never a blind tail-cut — until the set fits `maxRecords`. */
44
+ evictToCapacity(records) {
45
+ if (this.maxRecords === undefined || records.length <= this.maxRecords)
46
+ return records;
47
+ const nowMs = this.now();
48
+ const scored = records.map((record, insertionIndex) => ({
49
+ record,
50
+ insertionIndex,
51
+ score: record.pinned ? Number.POSITIVE_INFINITY : memoryRetentionScore(record, nowMs, this.staleWarningDays),
52
+ }));
53
+ // Keep the highest-value maxRecords; ties break on insertion order (older first survives).
54
+ scored.sort((a, b) => b.score - a.score || a.insertionIndex - b.insertionIndex);
55
+ return scored.slice(0, this.maxRecords).map(entry => entry.record);
39
56
  }
40
- async search(agentId, _query, topK = 5) {
41
- const all = await this.loadMemories(agentId);
42
- return all.slice(0, topK);
57
+ async search(agentId, query) {
58
+ const all = this.recordsFor(agentId);
59
+ const candidates = all.filter(record => record.scope.tenant_id === query.scope.tenant_id
60
+ && record.scope.namespace === query.scope.namespace
61
+ && (query.kinds.length === 0 || query.kinds.includes(record.kind)));
62
+ const ranked = rankMemories(query.query, candidates.map((record, insertionIndex) => {
63
+ return {
64
+ value: record,
65
+ searchableText: `${record.name} ${record.description} ${record.content}`,
66
+ updatedAt: Number.isFinite(record.updated_at) ? record.updated_at : 0,
67
+ recallCount: record.recall_count,
68
+ ttlDays: record.ttl_days,
69
+ insertionIndex,
70
+ };
71
+ }), query.top_k, { nowMs: this.now(), staleWarningDays: this.staleWarningDays });
72
+ return ranked
73
+ .filter(hit => query.min_score === undefined || hit.score >= query.min_score)
74
+ .map(hit => ({ record: hit.value, score: hit.score, why: hit.why }));
43
75
  }
44
76
  async saveSession(data) {
45
77
  this.savedSessions.push(data);
46
- const list = this.sessions.get(data.agentId) ?? [];
47
- list.push(data);
48
- this.sessions.set(data.agentId, list);
78
+ }
79
+ /** M3: mirror the kernel's journaled recall lifecycle into the durable records. */
80
+ async recordRecall(agentId, recalls) {
81
+ const records = this.recordsFor(agentId);
82
+ for (const recall of recalls) {
83
+ const record = records.find(candidate => candidate.record_id === recall.record_id);
84
+ if (record) {
85
+ record.recall_count = recall.recall_count;
86
+ record.last_recalled_at = recall.last_recalled_at;
87
+ }
88
+ }
89
+ this.memories.set(agentId, records);
90
+ }
91
+ /** M4: set a record's pin so retention eviction cannot shed it. */
92
+ async setPinned(agentId, recordId, pinned) {
93
+ const records = this.recordsFor(agentId);
94
+ const record = records.find(candidate => candidate.record_id === recordId);
95
+ if (record)
96
+ record.pinned = pinned;
97
+ this.memories.set(agentId, records);
49
98
  }
50
99
  }