@hybridaione/hybridclaw 0.2.3 → 0.2.7
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/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +57 -19
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +72 -7
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1405 -0
- package/container/src/browser-tools.ts +672 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +65 -16
- package/container/src/index.ts +463 -110
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +107 -18
- package/container/src/tools.ts +600 -225
- package/container/src/types.ts +49 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -4
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +24 -4
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +31 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +156 -6
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +7 -4
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +755 -150
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +19 -12
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +77 -23
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +113 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +59 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +624 -87
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +303 -26
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +62 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/hybridai-models.d.ts +8 -0
- package/dist/hybridai-models.d.ts.map +1 -0
- package/dist/hybridai-models.js +94 -0
- package/dist/hybridai-models.js.map +1 -0
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +11 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +104 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +503 -24
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +43 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +72 -13
- package/package.json +8 -2
- package/src/agent.ts +31 -4
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +65 -14
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +190 -8
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1161 -205
- package/src/channels/discord/stream.ts +85 -31
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +174 -57
- package/src/config.ts +208 -36
- package/src/container-runner.ts +134 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +996 -220
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +487 -52
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +99 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/hybridai-models.ts +158 -0
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +42 -17
- package/src/runtime-config.ts +1112 -122
- package/src/scheduled-task-runner.ts +77 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +199 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +335 -0
- package/tests/discord.basic.test.ts +213 -4
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/hybridai-client.test.ts +112 -0
- package/tests/hybridai-models.test.ts +46 -0
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/tests/token-usage.cache.test.ts +128 -0
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
package/src/ipc.ts
CHANGED
|
@@ -46,7 +46,11 @@ export function ensureAgentDirs(agentId: string): void {
|
|
|
46
46
|
* When omitApiKey is set, the apiKey field is excluded from the file on disk
|
|
47
47
|
* (the container already has the key in memory from the initial stdin payload).
|
|
48
48
|
*/
|
|
49
|
-
export function writeInput(
|
|
49
|
+
export function writeInput(
|
|
50
|
+
sessionId: string,
|
|
51
|
+
input: ContainerInput,
|
|
52
|
+
opts?: { omitApiKey?: boolean },
|
|
53
|
+
): string {
|
|
50
54
|
const dir = ipcDir(sessionId);
|
|
51
55
|
const inputPath = path.join(dir, 'input.json');
|
|
52
56
|
const toWrite = opts?.omitApiKey ? { ...input, apiKey: '' } : input;
|
|
@@ -67,7 +71,10 @@ function interruptedOutput(): ContainerOutput {
|
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
async function sleepWithAbort(
|
|
74
|
+
async function sleepWithAbort(
|
|
75
|
+
ms: number,
|
|
76
|
+
signal?: AbortSignal,
|
|
77
|
+
): Promise<boolean> {
|
|
71
78
|
if (!signal) {
|
|
72
79
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
80
|
return false;
|
|
@@ -109,8 +116,16 @@ export async function readOutput(
|
|
|
109
116
|
const stat = fs.statSync(outputPath);
|
|
110
117
|
if (stat.size > CONTAINER_MAX_OUTPUT_SIZE) {
|
|
111
118
|
fs.unlinkSync(outputPath);
|
|
112
|
-
logger.warn(
|
|
113
|
-
|
|
119
|
+
logger.warn(
|
|
120
|
+
{ sessionId, size: stat.size, limit: CONTAINER_MAX_OUTPUT_SIZE },
|
|
121
|
+
'Container output exceeded size limit',
|
|
122
|
+
);
|
|
123
|
+
return {
|
|
124
|
+
status: 'error',
|
|
125
|
+
result: null,
|
|
126
|
+
toolsUsed: [],
|
|
127
|
+
error: `Output too large (${stat.size} bytes, limit ${CONTAINER_MAX_OUTPUT_SIZE})`,
|
|
128
|
+
};
|
|
114
129
|
}
|
|
115
130
|
try {
|
|
116
131
|
const raw = fs.readFileSync(outputPath, 'utf-8');
|
|
@@ -152,7 +167,10 @@ export function cleanupIpc(sessionId: string): void {
|
|
|
152
167
|
/**
|
|
153
168
|
* Get host paths for container mounting.
|
|
154
169
|
*/
|
|
155
|
-
export function getSessionPaths(
|
|
170
|
+
export function getSessionPaths(
|
|
171
|
+
sessionId: string,
|
|
172
|
+
agentId: string,
|
|
173
|
+
): {
|
|
156
174
|
ipcPath: string;
|
|
157
175
|
workspacePath: string;
|
|
158
176
|
} {
|
package/src/logger.ts
CHANGED
|
@@ -12,7 +12,10 @@ export const logger = pino({
|
|
|
12
12
|
onRuntimeConfigChange((next, prev) => {
|
|
13
13
|
if (next.ops.logLevel !== prev.ops.logLevel) {
|
|
14
14
|
logger.level = next.ops.logLevel;
|
|
15
|
-
logger.info(
|
|
15
|
+
logger.info(
|
|
16
|
+
{ level: next.ops.logLevel },
|
|
17
|
+
'Logger level updated from config.json',
|
|
18
|
+
);
|
|
16
19
|
}
|
|
17
20
|
});
|
|
18
21
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MemoryBackend } from './memory-service.js';
|
|
2
|
+
|
|
3
|
+
export interface MemoryConsolidationConfig {
|
|
4
|
+
decayRate: number;
|
|
5
|
+
staleAfterDays: number;
|
|
6
|
+
minConfidence: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MemoryConsolidationReport {
|
|
10
|
+
memoriesDecayed: number;
|
|
11
|
+
durationMs: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class MemoryConsolidationEngine {
|
|
15
|
+
private readonly backend: MemoryBackend;
|
|
16
|
+
private readonly config: MemoryConsolidationConfig;
|
|
17
|
+
|
|
18
|
+
constructor(backend: MemoryBackend, config: MemoryConsolidationConfig) {
|
|
19
|
+
this.backend = backend;
|
|
20
|
+
this.config = config;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
consolidate(
|
|
24
|
+
overrides?: Partial<MemoryConsolidationConfig>,
|
|
25
|
+
): MemoryConsolidationReport {
|
|
26
|
+
const start = Date.now();
|
|
27
|
+
const config = {
|
|
28
|
+
...this.config,
|
|
29
|
+
...(overrides || {}),
|
|
30
|
+
};
|
|
31
|
+
const memoriesDecayed = this.backend.decaySemanticMemories({
|
|
32
|
+
decayRate: config.decayRate,
|
|
33
|
+
staleAfterDays: config.staleAfterDays,
|
|
34
|
+
minConfidence: config.minConfidence,
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
memoriesDecayed,
|
|
38
|
+
durationMs: Math.max(0, Date.now() - start),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addKnowledgeEntity as dbAddKnowledgeEntity,
|
|
3
|
+
addKnowledgeRelation as dbAddKnowledgeRelation,
|
|
4
|
+
appendCanonicalMessages as dbAppendCanonicalMessages,
|
|
5
|
+
clearSessionHistory as dbClearSessionHistory,
|
|
6
|
+
deleteMemoryValue as dbDeleteMemoryValue,
|
|
7
|
+
deleteMessagesBeforeId as dbDeleteMessagesBeforeId,
|
|
8
|
+
forgetSemanticMemory as dbForgetSemanticMemory,
|
|
9
|
+
getCanonicalContext as dbGetCanonicalContext,
|
|
10
|
+
getCompactionCandidateMessages as dbGetCompactionCandidateMessages,
|
|
11
|
+
getConversationHistory as dbGetConversationHistory,
|
|
12
|
+
getMemoryValue as dbGetMemoryValue,
|
|
13
|
+
getOrCreateSession as dbGetOrCreateSession,
|
|
14
|
+
getRecentMessages as dbGetRecentMessages,
|
|
15
|
+
getSessionById as dbGetSessionById,
|
|
16
|
+
listMemoryValues as dbListMemoryValues,
|
|
17
|
+
markSessionMemoryFlush as dbMarkSessionMemoryFlush,
|
|
18
|
+
queryKnowledgeGraph as dbQueryKnowledgeGraph,
|
|
19
|
+
recallSemanticMemories as dbRecallSemanticMemories,
|
|
20
|
+
setMemoryValue as dbSetMemoryValue,
|
|
21
|
+
storeMessage as dbStoreMessage,
|
|
22
|
+
storeSemanticMemory as dbStoreSemanticMemory,
|
|
23
|
+
updateSessionSummary as dbUpdateSessionSummary,
|
|
24
|
+
decaySemanticMemories,
|
|
25
|
+
type SemanticRecallFilter,
|
|
26
|
+
} from './db.js';
|
|
27
|
+
import {
|
|
28
|
+
type MemoryConsolidationConfig,
|
|
29
|
+
MemoryConsolidationEngine,
|
|
30
|
+
type MemoryConsolidationReport,
|
|
31
|
+
} from './memory-consolidation.js';
|
|
32
|
+
import type {
|
|
33
|
+
CanonicalSession,
|
|
34
|
+
CanonicalSessionContext,
|
|
35
|
+
KnowledgeEntityTypeValue,
|
|
36
|
+
KnowledgeGraphMatch,
|
|
37
|
+
KnowledgeGraphPattern,
|
|
38
|
+
KnowledgeRelationTypeValue,
|
|
39
|
+
SemanticMemoryEntry,
|
|
40
|
+
Session,
|
|
41
|
+
StoredMessage,
|
|
42
|
+
StructuredMemoryEntry,
|
|
43
|
+
} from './types.js';
|
|
44
|
+
|
|
45
|
+
export interface CompactionCandidate {
|
|
46
|
+
cutoffId: number;
|
|
47
|
+
olderMessages: StoredMessage[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MemoryBackend {
|
|
51
|
+
getOrCreateSession: (
|
|
52
|
+
sessionId: string,
|
|
53
|
+
guildId: string | null,
|
|
54
|
+
channelId: string,
|
|
55
|
+
) => Session;
|
|
56
|
+
getSessionById: (sessionId: string) => Session | undefined;
|
|
57
|
+
getConversationHistory: (
|
|
58
|
+
sessionId: string,
|
|
59
|
+
limit?: number,
|
|
60
|
+
) => StoredMessage[];
|
|
61
|
+
getRecentMessages: (sessionId: string, limit?: number) => StoredMessage[];
|
|
62
|
+
get: (sessionId: string, key: string) => unknown | null;
|
|
63
|
+
set: (sessionId: string, key: string, value: unknown) => void;
|
|
64
|
+
delete: (sessionId: string, key: string) => boolean;
|
|
65
|
+
list: (sessionId: string, prefix?: string) => StructuredMemoryEntry[];
|
|
66
|
+
appendCanonicalMessages: (params: {
|
|
67
|
+
agentId: string;
|
|
68
|
+
userId: string;
|
|
69
|
+
newMessages: Array<{
|
|
70
|
+
role: string;
|
|
71
|
+
content: string;
|
|
72
|
+
sessionId: string;
|
|
73
|
+
channelId?: string | null;
|
|
74
|
+
createdAt?: string | null;
|
|
75
|
+
}>;
|
|
76
|
+
windowSize?: number;
|
|
77
|
+
compactionThreshold?: number;
|
|
78
|
+
}) => CanonicalSession;
|
|
79
|
+
getCanonicalContext: (params: {
|
|
80
|
+
agentId: string;
|
|
81
|
+
userId: string;
|
|
82
|
+
windowSize?: number;
|
|
83
|
+
excludeSessionId?: string | null;
|
|
84
|
+
}) => CanonicalSessionContext;
|
|
85
|
+
addKnowledgeEntity: (params: {
|
|
86
|
+
id?: string | null;
|
|
87
|
+
name: string;
|
|
88
|
+
entityType: KnowledgeEntityTypeValue | string;
|
|
89
|
+
properties?: Record<string, unknown> | null;
|
|
90
|
+
}) => string;
|
|
91
|
+
addKnowledgeRelation: (params: {
|
|
92
|
+
source: string;
|
|
93
|
+
relation: KnowledgeRelationTypeValue | string;
|
|
94
|
+
target: string;
|
|
95
|
+
properties?: Record<string, unknown> | null;
|
|
96
|
+
confidence?: number;
|
|
97
|
+
}) => string;
|
|
98
|
+
queryKnowledgeGraph: (
|
|
99
|
+
pattern?: KnowledgeGraphPattern,
|
|
100
|
+
) => KnowledgeGraphMatch[];
|
|
101
|
+
getCompactionCandidateMessages: (
|
|
102
|
+
sessionId: string,
|
|
103
|
+
keepRecent: number,
|
|
104
|
+
) => CompactionCandidate | null;
|
|
105
|
+
storeMessage: (
|
|
106
|
+
sessionId: string,
|
|
107
|
+
userId: string,
|
|
108
|
+
username: string | null,
|
|
109
|
+
role: string,
|
|
110
|
+
content: string,
|
|
111
|
+
) => number;
|
|
112
|
+
storeSemanticMemory: (params: {
|
|
113
|
+
sessionId: string;
|
|
114
|
+
role: string;
|
|
115
|
+
source?: string | null;
|
|
116
|
+
scope?: string | null;
|
|
117
|
+
metadata?: Record<string, unknown> | string | null;
|
|
118
|
+
content: string;
|
|
119
|
+
confidence?: number;
|
|
120
|
+
embedding?: number[] | null;
|
|
121
|
+
sourceMessageId?: number | null;
|
|
122
|
+
}) => number;
|
|
123
|
+
recallSemanticMemories: (params: {
|
|
124
|
+
sessionId: string;
|
|
125
|
+
query: string;
|
|
126
|
+
limit?: number;
|
|
127
|
+
minConfidence?: number;
|
|
128
|
+
queryEmbedding?: number[] | null;
|
|
129
|
+
filter?: SemanticRecallFilter;
|
|
130
|
+
}) => SemanticMemoryEntry[];
|
|
131
|
+
forgetSemanticMemory: (id: number) => boolean;
|
|
132
|
+
decaySemanticMemories: (params?: {
|
|
133
|
+
decayRate?: number;
|
|
134
|
+
staleAfterDays?: number;
|
|
135
|
+
minConfidence?: number;
|
|
136
|
+
}) => number;
|
|
137
|
+
clearSessionHistory: (sessionId: string) => number;
|
|
138
|
+
deleteMessagesBeforeId: (sessionId: string, cutoffId: number) => number;
|
|
139
|
+
updateSessionSummary: (sessionId: string, summary: string) => void;
|
|
140
|
+
markSessionMemoryFlush: (sessionId: string) => void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface MemoryServiceConfig {
|
|
144
|
+
semanticRecallLimit: number;
|
|
145
|
+
semanticMinConfidence: number;
|
|
146
|
+
semanticMaxContentChars: number;
|
|
147
|
+
semanticDecayRate: number;
|
|
148
|
+
semanticDecayStaleAfterDays: number;
|
|
149
|
+
semanticDecayMinConfidence: number;
|
|
150
|
+
summaryDecayRate: number;
|
|
151
|
+
summaryMinConfidence: number;
|
|
152
|
+
summaryDiscardThreshold: number;
|
|
153
|
+
embeddingDimensions: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface EmbeddingProvider {
|
|
157
|
+
embed(text: string): number[] | null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface StoreTurnParams {
|
|
161
|
+
sessionId: string;
|
|
162
|
+
user: {
|
|
163
|
+
userId: string;
|
|
164
|
+
username: string | null;
|
|
165
|
+
content: string;
|
|
166
|
+
};
|
|
167
|
+
assistant: {
|
|
168
|
+
userId?: string;
|
|
169
|
+
username?: string | null;
|
|
170
|
+
content: string;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface BuildMemoryPromptParams {
|
|
175
|
+
session: Session;
|
|
176
|
+
query: string;
|
|
177
|
+
semanticLimit?: number;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface BuildMemoryPromptResult {
|
|
181
|
+
promptSummary: string | null;
|
|
182
|
+
summaryConfidence: number | null;
|
|
183
|
+
semanticMemories: SemanticMemoryEntry[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface RecallSemanticMemoriesParams {
|
|
187
|
+
sessionId: string;
|
|
188
|
+
query: string;
|
|
189
|
+
limit?: number;
|
|
190
|
+
minConfidence?: number;
|
|
191
|
+
filter?: SemanticRecallFilter;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const DEFAULT_CONFIG: MemoryServiceConfig = {
|
|
195
|
+
semanticRecallLimit: 5,
|
|
196
|
+
semanticMinConfidence: 0.2,
|
|
197
|
+
semanticMaxContentChars: 1_200,
|
|
198
|
+
semanticDecayRate: 0.1,
|
|
199
|
+
semanticDecayStaleAfterDays: 7,
|
|
200
|
+
semanticDecayMinConfidence: 0.1,
|
|
201
|
+
summaryDecayRate: 0.04,
|
|
202
|
+
summaryMinConfidence: 0.1,
|
|
203
|
+
summaryDiscardThreshold: 0.22,
|
|
204
|
+
embeddingDimensions: 128,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const DEFAULT_BACKEND: MemoryBackend = {
|
|
208
|
+
getOrCreateSession: dbGetOrCreateSession,
|
|
209
|
+
getSessionById: dbGetSessionById,
|
|
210
|
+
getConversationHistory: dbGetConversationHistory,
|
|
211
|
+
getRecentMessages: dbGetRecentMessages,
|
|
212
|
+
get: dbGetMemoryValue,
|
|
213
|
+
set: dbSetMemoryValue,
|
|
214
|
+
delete: dbDeleteMemoryValue,
|
|
215
|
+
list: dbListMemoryValues,
|
|
216
|
+
appendCanonicalMessages: dbAppendCanonicalMessages,
|
|
217
|
+
getCanonicalContext: dbGetCanonicalContext,
|
|
218
|
+
addKnowledgeEntity: dbAddKnowledgeEntity,
|
|
219
|
+
addKnowledgeRelation: dbAddKnowledgeRelation,
|
|
220
|
+
queryKnowledgeGraph: dbQueryKnowledgeGraph,
|
|
221
|
+
getCompactionCandidateMessages: dbGetCompactionCandidateMessages,
|
|
222
|
+
storeMessage: dbStoreMessage,
|
|
223
|
+
storeSemanticMemory: dbStoreSemanticMemory,
|
|
224
|
+
recallSemanticMemories: dbRecallSemanticMemories,
|
|
225
|
+
forgetSemanticMemory: dbForgetSemanticMemory,
|
|
226
|
+
decaySemanticMemories,
|
|
227
|
+
clearSessionHistory: dbClearSessionHistory,
|
|
228
|
+
deleteMessagesBeforeId: dbDeleteMessagesBeforeId,
|
|
229
|
+
updateSessionSummary: dbUpdateSessionSummary,
|
|
230
|
+
markSessionMemoryFlush: dbMarkSessionMemoryFlush,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
function parseTimestamp(raw: string | null | undefined): number | null {
|
|
234
|
+
const value = (raw || '').trim();
|
|
235
|
+
if (!value) return null;
|
|
236
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value)) {
|
|
237
|
+
const parsed = Date.parse(`${value.replace(' ', 'T')}Z`);
|
|
238
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
239
|
+
}
|
|
240
|
+
const parsed = Date.parse(value);
|
|
241
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function computeDecayedConfidence(params: {
|
|
245
|
+
updatedAt: string | null | undefined;
|
|
246
|
+
decayRate: number;
|
|
247
|
+
minConfidence: number;
|
|
248
|
+
nowMs?: number;
|
|
249
|
+
}): number {
|
|
250
|
+
const updatedMs = parseTimestamp(params.updatedAt);
|
|
251
|
+
if (updatedMs == null) return 1;
|
|
252
|
+
|
|
253
|
+
const nowMs =
|
|
254
|
+
typeof params.nowMs === 'number' && Number.isFinite(params.nowMs)
|
|
255
|
+
? params.nowMs
|
|
256
|
+
: Date.now();
|
|
257
|
+
const ageDays = Math.max(0, (nowMs - updatedMs) / 86_400_000);
|
|
258
|
+
const decayRate = Math.max(0, Math.min(0.95, params.decayRate));
|
|
259
|
+
const minConfidence = Math.max(0, Math.min(0.95, params.minConfidence));
|
|
260
|
+
const decayed = (1 - decayRate) ** ageDays;
|
|
261
|
+
return Math.max(minConfidence, Math.min(1, decayed));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function truncateInline(content: string, maxChars: number): string {
|
|
265
|
+
const compact = content.replace(/\s+/g, ' ').trim();
|
|
266
|
+
if (compact.length <= maxChars) return compact;
|
|
267
|
+
return `${compact.slice(0, maxChars)}...`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
class HashedTokenEmbeddingProvider implements EmbeddingProvider {
|
|
271
|
+
private readonly dimensions: number;
|
|
272
|
+
|
|
273
|
+
constructor(dimensions: number) {
|
|
274
|
+
this.dimensions = Math.max(16, Math.min(1024, Math.floor(dimensions)));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
embed(text: string): number[] | null {
|
|
278
|
+
const normalized = text
|
|
279
|
+
.toLowerCase()
|
|
280
|
+
.replace(/[^a-z0-9_\s-]/g, ' ')
|
|
281
|
+
.replace(/\s+/g, ' ')
|
|
282
|
+
.trim();
|
|
283
|
+
if (!normalized) return null;
|
|
284
|
+
|
|
285
|
+
const tokens = normalized
|
|
286
|
+
.split(' ')
|
|
287
|
+
.map((token) => token.trim())
|
|
288
|
+
.filter((token) => token.length > 1)
|
|
289
|
+
.slice(0, 256);
|
|
290
|
+
if (tokens.length === 0) return null;
|
|
291
|
+
|
|
292
|
+
const vector = new Float32Array(this.dimensions);
|
|
293
|
+
for (const token of tokens) {
|
|
294
|
+
const hash = this.hashToken(token);
|
|
295
|
+
const index = hash % this.dimensions;
|
|
296
|
+
const sign = (hash & 1) === 0 ? 1 : -1;
|
|
297
|
+
vector[index] += sign * Math.min(4, token.length);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let norm = 0;
|
|
301
|
+
for (let i = 0; i < vector.length; i += 1) {
|
|
302
|
+
norm += vector[i] * vector[i];
|
|
303
|
+
}
|
|
304
|
+
if (norm <= Number.EPSILON) return null;
|
|
305
|
+
const scale = 1 / Math.sqrt(norm);
|
|
306
|
+
return Array.from(vector, (value) => value * scale);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private hashToken(token: string): number {
|
|
310
|
+
let hash = 2166136261;
|
|
311
|
+
for (let i = 0; i < token.length; i += 1) {
|
|
312
|
+
hash ^= token.charCodeAt(i);
|
|
313
|
+
hash = Math.imul(hash, 16777619);
|
|
314
|
+
}
|
|
315
|
+
return hash >>> 0;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export class MemoryService {
|
|
320
|
+
private readonly backend: MemoryBackend;
|
|
321
|
+
private readonly config: MemoryServiceConfig;
|
|
322
|
+
private readonly embeddingProvider: EmbeddingProvider;
|
|
323
|
+
private readonly consolidationEngine: MemoryConsolidationEngine;
|
|
324
|
+
|
|
325
|
+
constructor(
|
|
326
|
+
backend: MemoryBackend = DEFAULT_BACKEND,
|
|
327
|
+
config?: Partial<MemoryServiceConfig>,
|
|
328
|
+
embeddingProvider?: EmbeddingProvider,
|
|
329
|
+
) {
|
|
330
|
+
this.backend = backend;
|
|
331
|
+
this.config = { ...DEFAULT_CONFIG, ...(config || {}) };
|
|
332
|
+
this.embeddingProvider =
|
|
333
|
+
embeddingProvider ||
|
|
334
|
+
new HashedTokenEmbeddingProvider(this.config.embeddingDimensions);
|
|
335
|
+
this.consolidationEngine = new MemoryConsolidationEngine(this.backend, {
|
|
336
|
+
decayRate: this.config.semanticDecayRate,
|
|
337
|
+
staleAfterDays: this.config.semanticDecayStaleAfterDays,
|
|
338
|
+
minConfidence: this.config.semanticDecayMinConfidence,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
getOrCreateSession(
|
|
343
|
+
sessionId: string,
|
|
344
|
+
guildId: string | null,
|
|
345
|
+
channelId: string,
|
|
346
|
+
): Session {
|
|
347
|
+
return this.backend.getOrCreateSession(sessionId, guildId, channelId);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
getSessionById(sessionId: string): Session | undefined {
|
|
351
|
+
return this.backend.getSessionById(sessionId);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
getConversationHistory(sessionId: string, limit = 50): StoredMessage[] {
|
|
355
|
+
return this.backend.getConversationHistory(sessionId, limit);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
getRecentMessages(sessionId: string, limit?: number): StoredMessage[] {
|
|
359
|
+
return this.backend.getRecentMessages(sessionId, limit);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
get(sessionId: string, key: string): unknown | null {
|
|
363
|
+
return this.backend.get(sessionId, key);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
set(sessionId: string, key: string, value: unknown): void {
|
|
367
|
+
this.backend.set(sessionId, key, value);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
delete(sessionId: string, key: string): boolean {
|
|
371
|
+
return this.backend.delete(sessionId, key);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
list(sessionId: string, prefix?: string): StructuredMemoryEntry[] {
|
|
375
|
+
return this.backend.list(sessionId, prefix);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
appendCanonicalMessages(params: {
|
|
379
|
+
agentId: string;
|
|
380
|
+
userId: string;
|
|
381
|
+
newMessages: Array<{
|
|
382
|
+
role: string;
|
|
383
|
+
content: string;
|
|
384
|
+
sessionId: string;
|
|
385
|
+
channelId?: string | null;
|
|
386
|
+
createdAt?: string | null;
|
|
387
|
+
}>;
|
|
388
|
+
windowSize?: number;
|
|
389
|
+
compactionThreshold?: number;
|
|
390
|
+
}): CanonicalSession {
|
|
391
|
+
return this.backend.appendCanonicalMessages(params);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
getCanonicalContext(params: {
|
|
395
|
+
agentId: string;
|
|
396
|
+
userId: string;
|
|
397
|
+
windowSize?: number;
|
|
398
|
+
excludeSessionId?: string | null;
|
|
399
|
+
}): CanonicalSessionContext {
|
|
400
|
+
return this.backend.getCanonicalContext(params);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
addKnowledgeEntity(params: {
|
|
404
|
+
id?: string | null;
|
|
405
|
+
name: string;
|
|
406
|
+
entityType: KnowledgeEntityTypeValue | string;
|
|
407
|
+
properties?: Record<string, unknown> | null;
|
|
408
|
+
}): string {
|
|
409
|
+
return this.backend.addKnowledgeEntity(params);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
addKnowledgeRelation(params: {
|
|
413
|
+
source: string;
|
|
414
|
+
relation: KnowledgeRelationTypeValue | string;
|
|
415
|
+
target: string;
|
|
416
|
+
properties?: Record<string, unknown> | null;
|
|
417
|
+
confidence?: number;
|
|
418
|
+
}): string {
|
|
419
|
+
return this.backend.addKnowledgeRelation(params);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
queryKnowledgeGraph(pattern?: KnowledgeGraphPattern): KnowledgeGraphMatch[] {
|
|
423
|
+
return this.backend.queryKnowledgeGraph(pattern);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
consolidateMemories(
|
|
427
|
+
overrides?: Partial<MemoryConsolidationConfig>,
|
|
428
|
+
): MemoryConsolidationReport {
|
|
429
|
+
return this.consolidationEngine.consolidate(overrides);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
recallSemanticMemories(
|
|
433
|
+
params: RecallSemanticMemoriesParams,
|
|
434
|
+
): SemanticMemoryEntry[] {
|
|
435
|
+
const limit = Math.max(
|
|
436
|
+
1,
|
|
437
|
+
Math.min(Math.floor(params.limit || this.config.semanticRecallLimit), 50),
|
|
438
|
+
);
|
|
439
|
+
const rawMinConfidence =
|
|
440
|
+
typeof params.minConfidence === 'number' &&
|
|
441
|
+
Number.isFinite(params.minConfidence)
|
|
442
|
+
? params.minConfidence
|
|
443
|
+
: this.config.semanticMinConfidence;
|
|
444
|
+
const minConfidence = Math.max(0, Math.min(1, rawMinConfidence));
|
|
445
|
+
const query = params.query.trim();
|
|
446
|
+
|
|
447
|
+
return this.backend.recallSemanticMemories({
|
|
448
|
+
sessionId: params.sessionId,
|
|
449
|
+
query,
|
|
450
|
+
limit,
|
|
451
|
+
minConfidence,
|
|
452
|
+
queryEmbedding: this.embeddingProvider.embed(query),
|
|
453
|
+
filter: params.filter,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
getCompactionCandidateMessages(
|
|
458
|
+
sessionId: string,
|
|
459
|
+
keepRecent: number,
|
|
460
|
+
): CompactionCandidate | null {
|
|
461
|
+
return this.backend.getCompactionCandidateMessages(sessionId, keepRecent);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
clearSessionHistory(sessionId: string): number {
|
|
465
|
+
return this.backend.clearSessionHistory(sessionId);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
deleteMessagesBeforeId(sessionId: string, cutoffId: number): number {
|
|
469
|
+
return this.backend.deleteMessagesBeforeId(sessionId, cutoffId);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
updateSessionSummary(sessionId: string, summary: string): void {
|
|
473
|
+
this.backend.updateSessionSummary(sessionId, summary);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
markSessionMemoryFlush(sessionId: string): void {
|
|
477
|
+
this.backend.markSessionMemoryFlush(sessionId);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
forgetSemanticMemory(id: number): boolean {
|
|
481
|
+
return this.backend.forgetSemanticMemory(id);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
storeMessage(params: {
|
|
485
|
+
sessionId: string;
|
|
486
|
+
userId: string;
|
|
487
|
+
username: string | null;
|
|
488
|
+
role: string;
|
|
489
|
+
content: string;
|
|
490
|
+
}): number {
|
|
491
|
+
return this.backend.storeMessage(
|
|
492
|
+
params.sessionId,
|
|
493
|
+
params.userId,
|
|
494
|
+
params.username,
|
|
495
|
+
params.role,
|
|
496
|
+
params.content,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
storeTurn(params: StoreTurnParams): void {
|
|
501
|
+
this.storeMessage({
|
|
502
|
+
sessionId: params.sessionId,
|
|
503
|
+
userId: params.user.userId,
|
|
504
|
+
username: params.user.username,
|
|
505
|
+
role: 'user',
|
|
506
|
+
content: params.user.content,
|
|
507
|
+
});
|
|
508
|
+
const assistantMessageId = this.storeMessage({
|
|
509
|
+
sessionId: params.sessionId,
|
|
510
|
+
userId: params.assistant.userId || 'assistant',
|
|
511
|
+
username: params.assistant.username || null,
|
|
512
|
+
role: 'assistant',
|
|
513
|
+
content: params.assistant.content,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const interactionText = this.normalizeSemanticContent(
|
|
517
|
+
`User asked: ${params.user.content.trim()}\nI responded: ${params.assistant.content.trim()}`,
|
|
518
|
+
);
|
|
519
|
+
if (!interactionText) return;
|
|
520
|
+
|
|
521
|
+
this.backend.storeSemanticMemory({
|
|
522
|
+
sessionId: params.sessionId,
|
|
523
|
+
role: 'assistant',
|
|
524
|
+
source: 'conversation',
|
|
525
|
+
scope: 'episodic',
|
|
526
|
+
metadata: {},
|
|
527
|
+
content: interactionText,
|
|
528
|
+
confidence: 1,
|
|
529
|
+
embedding: this.embeddingProvider.embed(interactionText),
|
|
530
|
+
sourceMessageId: assistantMessageId,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
buildPromptMemoryContext(
|
|
535
|
+
params: BuildMemoryPromptParams,
|
|
536
|
+
): BuildMemoryPromptResult {
|
|
537
|
+
const summaryText = (params.session.session_summary || '').trim();
|
|
538
|
+
const summaryConfidence = summaryText
|
|
539
|
+
? computeDecayedConfidence({
|
|
540
|
+
updatedAt: params.session.summary_updated_at,
|
|
541
|
+
decayRate: this.config.summaryDecayRate,
|
|
542
|
+
minConfidence: this.config.summaryMinConfidence,
|
|
543
|
+
})
|
|
544
|
+
: null;
|
|
545
|
+
|
|
546
|
+
const includeSummary =
|
|
547
|
+
summaryText &&
|
|
548
|
+
(summaryConfidence == null ||
|
|
549
|
+
summaryConfidence >= this.config.summaryDiscardThreshold);
|
|
550
|
+
|
|
551
|
+
const semanticLimit = Math.max(
|
|
552
|
+
1,
|
|
553
|
+
Math.min(
|
|
554
|
+
Math.floor(params.semanticLimit || this.config.semanticRecallLimit),
|
|
555
|
+
12,
|
|
556
|
+
),
|
|
557
|
+
);
|
|
558
|
+
const semanticMemories = this.recallSemanticMemories({
|
|
559
|
+
sessionId: params.session.id,
|
|
560
|
+
query: params.query,
|
|
561
|
+
limit: semanticLimit,
|
|
562
|
+
minConfidence: this.config.semanticMinConfidence,
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
const sections: string[] = [];
|
|
566
|
+
if (includeSummary) {
|
|
567
|
+
if (summaryConfidence != null && summaryConfidence < 0.999) {
|
|
568
|
+
sections.push(
|
|
569
|
+
`Summary confidence: ${Math.round(summaryConfidence * 100)}% (decayed by age).`,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
sections.push(summaryText);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (semanticMemories.length > 0) {
|
|
576
|
+
const lines = semanticMemories.map((memory) => {
|
|
577
|
+
const confidence = Math.round(
|
|
578
|
+
Math.max(0, Math.min(1, memory.confidence)) * 100,
|
|
579
|
+
);
|
|
580
|
+
return `- (${confidence}%) ${truncateInline(memory.content, 220)}`;
|
|
581
|
+
});
|
|
582
|
+
sections.push(
|
|
583
|
+
[
|
|
584
|
+
'### Relevant Memory Recall',
|
|
585
|
+
'Topic-matched context from older turns (vector cosine search):',
|
|
586
|
+
...lines,
|
|
587
|
+
].join('\n'),
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const promptSummary = sections.join('\n\n').trim();
|
|
592
|
+
return {
|
|
593
|
+
promptSummary: promptSummary || null,
|
|
594
|
+
summaryConfidence,
|
|
595
|
+
semanticMemories,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private normalizeSemanticContent(content: string): string {
|
|
600
|
+
const compact = content.replace(/\s+/g, ' ').trim();
|
|
601
|
+
if (compact.length <= this.config.semanticMaxContentChars) return compact;
|
|
602
|
+
return compact.slice(0, this.config.semanticMaxContentChars);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export const memoryService = new MemoryService();
|