dsh-diary-memory 0.1.1 → 0.1.2

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 (52) hide show
  1. package/README.md +23 -10
  2. package/lib/client/choices.d.ts +53 -0
  3. package/lib/client/choices.js +51 -0
  4. package/lib/client/choices.js.map +1 -0
  5. package/lib/client/controller.d.ts +11 -3
  6. package/lib/client/controller.js +45 -6
  7. package/lib/client/controller.js.map +1 -1
  8. package/lib/client/index.js +5 -2
  9. package/lib/client/index.js.map +1 -1
  10. package/lib/client/page.js +6 -1
  11. package/lib/client/page.js.map +1 -1
  12. package/lib/client.js +165 -9
  13. package/lib/client.js.map +3 -3
  14. package/lib/config.d.ts +5 -1
  15. package/lib/config.js +7 -1
  16. package/lib/config.js.map +1 -1
  17. package/lib/diary-runtime.d.ts +10 -0
  18. package/lib/diary-runtime.js +65 -0
  19. package/lib/diary-runtime.js.map +1 -0
  20. package/lib/index.js +2 -0
  21. package/lib/index.js.map +1 -1
  22. package/lib/memory-store.d.ts +6 -3
  23. package/lib/memory-store.js +6 -3
  24. package/lib/memory-store.js.map +1 -1
  25. package/lib/orchestrator.js +4 -66
  26. package/lib/orchestrator.js.map +1 -1
  27. package/lib/session-memory.d.ts +20 -0
  28. package/lib/session-memory.js +161 -0
  29. package/lib/session-memory.js.map +1 -0
  30. package/lib/session-selection.d.ts +1 -0
  31. package/lib/session-selection.js +6 -0
  32. package/lib/session-selection.js.map +1 -0
  33. package/lib/sqlite.d.ts +25 -0
  34. package/lib/sqlite.js +64 -0
  35. package/lib/sqlite.js.map +1 -0
  36. package/lib/tools.d.ts +1 -0
  37. package/lib/tools.js +3 -0
  38. package/lib/tools.js.map +1 -1
  39. package/package.json +21 -4
  40. package/src/client/choices.ts +66 -0
  41. package/src/client/controller.ts +34 -7
  42. package/src/client/index.ts +7 -2
  43. package/src/client/page.tsx +32 -0
  44. package/src/config.ts +7 -1
  45. package/src/diary-runtime.ts +62 -0
  46. package/src/index.ts +2 -0
  47. package/src/memory-store.ts +10 -6
  48. package/src/orchestrator.ts +4 -57
  49. package/src/session-memory.ts +151 -0
  50. package/src/session-selection.ts +4 -0
  51. package/src/sqlite.ts +78 -0
  52. package/src/tools.ts +4 -0
@@ -0,0 +1,151 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { symbols } from '@deepseek-ai/cordis'
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import type { Agent } from '@deepseek-ai/dsh-agent'
5
+ import type { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic'
6
+ import type {} from '@deepseek-ai/dsh-agent-preset-registry'
7
+ import type { Message } from '@deepseek-ai/dsh-llm'
8
+ import type { DiarySummary } from './diary.js'
9
+ import { selectRoundRange } from './diary.js'
10
+ import { appendRecalledContext, prepareDiarySummary } from './diary-runtime.js'
11
+ import { DiaryOrchestrator } from './orchestrator.js'
12
+ import type { RetrievalService } from './retrieval-service.js'
13
+ import { registerRecall } from './tools.js'
14
+ import { createScope, scopeOf } from '@deepseek-ai/dsh-scope'
15
+ import type { Scope } from '@deepseek-ai/dsh-scope'
16
+
17
+ type NativeEngine = Pick<BasicCompactionEngine, 'compactIfNeeded' | 'compactRegion' | 'compactNow'> & {
18
+ readonly ctx: Context
19
+ summarize(input: { readonly messages: readonly Message[] }, agent: Agent, signal?: AbortSignal): Promise<DiarySummary>
20
+ }
21
+
22
+ /** Route the existing native engine by Session, without rebinding its preset. */
23
+ export class SessionMemory {
24
+ private readonly restorers = new Map<object, () => void>()
25
+ private readonly operations = new AsyncLocalStorage<{ agent: Agent; enabled: boolean }>()
26
+ private readonly jobs = new Set<Promise<unknown>>()
27
+ private readonly lifetime = new AbortController()
28
+ private closed = false
29
+ private readonly recallScopes = new Map<Agent, Scope>()
30
+
31
+ constructor(private readonly ctx: Context, private readonly memory: RetrievalService) {
32
+ ctx.effect(() => () => this.dispose(), 'diary-memory: existing sessions')
33
+ ctx.on('agent/created', async ({ agent }) => { this.attach(agent) })
34
+ ctx.on('agent/disposed', async ({ agent }) => {
35
+ const scope = this.recallScopes.get(agent)
36
+ this.recallScopes.delete(agent)
37
+ await scope?.dispose()
38
+ })
39
+ ctx.on('agent/pre-step', async ({ agent, messages, signal }, next) => {
40
+ const enabled = this.selected(agent)
41
+ const engine = this.attach(agent)
42
+ if (enabled && engine && !(engine instanceof DiaryOrchestrator) && !this.recallScopes.has(agent)) {
43
+ const key = scopeOf(agent.ctx)
44
+ if (!key) throw new Error('指定会话缺少原生 Agent scope')
45
+ const scope = createScope(ctx, key)
46
+ registerRecall(scope.ctx)
47
+ this.recallScopes.set(agent, scope)
48
+ }
49
+ if (!enabled) {
50
+ const scope = this.recallScopes.get(agent)
51
+ this.recallScopes.delete(agent)
52
+ await scope?.dispose()
53
+ }
54
+ const decision = await next()
55
+ // The companion preset already owns its recall middleware.
56
+ if (!enabled || engine instanceof DiaryOrchestrator) return decision
57
+ return appendRecalledContext(ctx, memory, decision, messages, signal)
58
+ }, { prepend: true })
59
+ for (const agent of ctx.agents.list()) this.attach(agent)
60
+ }
61
+
62
+ private selected(agent: Agent): boolean { return !this.closed && this.memory.config.sessionIds.includes(agent.session.id) }
63
+
64
+ attach(agent: Agent): BasicCompactionEngine | undefined {
65
+ const provider = this.ctx.agentPresets.serviceFor(agent, 'compaction')
66
+ if (!provider) {
67
+ if (this.selected(agent)) throw new Error('指定会话的预设没有原生压缩器,无法接入日记记忆')
68
+ return undefined
69
+ }
70
+ if (this.closed) return provider as BasicCompactionEngine
71
+ // Cordis proxies expose their original instance through this public symbol.
72
+ const engine = ((provider as unknown as Record<symbol, unknown>)[symbols.original] ?? provider) as NativeEngine
73
+ if (engine instanceof DiaryOrchestrator || this.restorers.has(engine)) return provider as BasicCompactionEngine
74
+ const descriptors = new Map<string, PropertyDescriptor | undefined>()
75
+ const replacements = new Map<string, unknown>()
76
+ const bridge = this
77
+ const enabled = (owner: Agent) => {
78
+ const operation = bridge.operations.getStore()
79
+ return operation?.agent === owner ? operation.enabled : bridge.selected(owner)
80
+ }
81
+ const replace = <K extends 'compactIfNeeded' | 'compactRegion' | 'compactNow' | 'summarize'>(name: K, method: NativeEngine[K]) => {
82
+ descriptors.set(name, Object.getOwnPropertyDescriptor(engine, name))
83
+ replacements.set(name, method)
84
+ Object.defineProperty(engine, name, { configurable: true, writable: true, value: method })
85
+ }
86
+ const summarize = engine.summarize
87
+ replace('summarize', function (this: NativeEngine, input, owner, signal) {
88
+ if (!enabled(owner)) return summarize.call(this, input, owner, signal)
89
+ return prepareDiarySummary(this.ctx, bridge.memory, input, owner, AbortSignal.any([bridge.lifetime.signal, ...signal ? [signal] : []]))
90
+ })
91
+ const compactIfNeeded = engine.compactIfNeeded
92
+ replace('compactIfNeeded', function (this: NativeEngine, owner, trigger, signal) {
93
+ const selected = enabled(owner)
94
+ return bridge.run(owner, selected, async () => {
95
+ if (!selected) return compactIfNeeded.call(this, owner, trigger, signal)
96
+ await bridge.memory.reconcile(owner.session)
97
+ signal.throwIfAborted()
98
+ const config = bridge.memory.config
99
+ if (trigger === 'pressure' && bridge.memory.stateOf(owner.session).completedRounds >= config.compressionRounds) {
100
+ const range = selectRoundRange(owner.session, config.retainRounds)
101
+ if (range) return this.compactRegion(range.start, range.end, owner, signal)
102
+ }
103
+ return compactIfNeeded.call(this, owner, trigger, signal)
104
+ })
105
+ })
106
+ const compactRegion = engine.compactRegion
107
+ replace('compactRegion', function (this: NativeEngine, start, end, owner, signal) {
108
+ const selected = enabled(owner)
109
+ return bridge.run(owner, selected, async () => {
110
+ if (!selected) return compactRegion.call(this, start, end, owner, signal)
111
+ try { return await compactRegion.call(this, start, end, owner, signal) }
112
+ finally { await bridge.memory.reconcile(owner.session) }
113
+ })
114
+ })
115
+ const compactNow = engine.compactNow
116
+ replace('compactNow', function (this: NativeEngine, owner, signal, sourceCommandId) {
117
+ const selected = enabled(owner)
118
+ return bridge.run(owner, selected, async () => {
119
+ if (!selected) return compactNow.call(this, owner, signal, sourceCommandId)
120
+ try { return await compactNow.call(this, owner, signal, sourceCommandId) }
121
+ finally { await bridge.memory.reconcile(owner.session) }
122
+ })
123
+ })
124
+ this.restorers.set(engine, () => {
125
+ for (const [name, descriptor] of descriptors) {
126
+ if (Object.getOwnPropertyDescriptor(engine, name)?.value !== replacements.get(name)) continue
127
+ if (descriptor) Object.defineProperty(engine, name, descriptor)
128
+ else Reflect.deleteProperty(engine, name)
129
+ }
130
+ })
131
+ return provider as BasicCompactionEngine
132
+ }
133
+
134
+ private run<T>(agent: Agent, enabled: boolean, operation: () => Promise<T>): Promise<T> {
135
+ const job = this.memory.track(() => this.operations.run({ agent, enabled }, operation))
136
+ this.jobs.add(job)
137
+ void job.then(() => this.jobs.delete(job), () => this.jobs.delete(job))
138
+ return job
139
+ }
140
+
141
+ async dispose(): Promise<void> {
142
+ if (this.closed) return
143
+ this.closed = true
144
+ this.lifetime.abort(new Error('指定会话的日记记忆已关闭'))
145
+ await Promise.allSettled([...this.jobs])
146
+ for (const restore of this.restorers.values()) restore()
147
+ this.restorers.clear()
148
+ await Promise.allSettled([...this.recallScopes.values()].map(scope => scope.dispose()))
149
+ this.recallScopes.clear()
150
+ }
151
+ }
@@ -0,0 +1,4 @@
1
+ export function validateSessionIds(value: unknown): string[] {
2
+ if (!Array.isArray(value) || value.length > 1000 || value.some(id => typeof id !== 'string' || !id.length || id.length > 256 || /[\r\n\0]/.test(id))) throw new Error('指定会话必须是至多 1000 个有效会话 ID 的数组')
3
+ return [...new Set(value as string[])]
4
+ }
package/src/sqlite.ts ADDED
@@ -0,0 +1,78 @@
1
+ import { createRequire } from 'node:module'
2
+ import type { DatabaseSync, SQLInputValue } from 'node:sqlite'
3
+
4
+ export type SqliteDriver = 'auto' | 'builtin' | 'native'
5
+ export interface SqliteStatement {
6
+ run(...parameters: SQLInputValue[]): { changes: number | bigint; lastInsertRowid: number | bigint }
7
+ get(...parameters: SQLInputValue[]): Record<string, unknown> | undefined
8
+ all(...parameters: SQLInputValue[]): Record<string, unknown>[]
9
+ iterate(...parameters: SQLInputValue[]): IterableIterator<Record<string, unknown>>
10
+ }
11
+ export interface SqliteConnection {
12
+ readonly open: boolean
13
+ exec(sql: string): unknown
14
+ prepare(sql: string): SqliteStatement
15
+ pragma(sql: string, options?: { simple: boolean }): unknown
16
+ transaction<T>(operation: () => T): () => T
17
+ close(): unknown
18
+ }
19
+
20
+ const require = createRequire(import.meta.url)
21
+
22
+ /** Use the same SQLite file format without requiring an Electron-specific addon. */
23
+ function builtinDatabase(filename: string): SqliteConnection {
24
+ const { DatabaseSync: Database } = require('node:sqlite') as { DatabaseSync: typeof DatabaseSync }
25
+ const database = new Database(filename)
26
+ let open = true
27
+ let transactionId = 0
28
+ const normalize = (row: Record<string, unknown> | undefined) => row === undefined ? undefined
29
+ : Object.fromEntries(Object.entries(row).map(([key, value]) => [key, value instanceof Uint8Array ? Buffer.from(value) : value]))
30
+ return {
31
+ get open() { return open },
32
+ exec: sql => database.exec(sql),
33
+ prepare(sql) {
34
+ const statement = database.prepare(sql)
35
+ return {
36
+ run: (...parameters) => statement.run(...parameters),
37
+ get: (...parameters) => normalize(statement.get(...parameters)),
38
+ all: (...parameters) => statement.all(...parameters).map(row => normalize(row)!),
39
+ *iterate(...parameters) { for (const row of statement.iterate(...parameters)) yield normalize(row)! },
40
+ }
41
+ },
42
+ pragma(sql, options) {
43
+ const rows = database.prepare(`PRAGMA ${sql}`).all()
44
+ return options?.simple ? Object.values(rows[0] ?? {})[0] : rows
45
+ },
46
+ transaction: operation => () => {
47
+ // Savepoints also give nested synchronous operations rollback isolation.
48
+ const point = `diary_${++transactionId}`
49
+ database.exec(`SAVEPOINT ${point}`)
50
+ try {
51
+ const result = operation()
52
+ if (result instanceof Promise) throw new TypeError('SQLite 事务必须同步完成')
53
+ database.exec(`RELEASE ${point}`)
54
+ return result
55
+ } catch (error) {
56
+ database.exec(`ROLLBACK TO ${point}; RELEASE ${point}`)
57
+ throw error
58
+ }
59
+ },
60
+ close() { database.close(); open = false },
61
+ }
62
+ }
63
+
64
+ export function openDatabase(filename: string, driver: SqliteDriver = 'auto'): { db: SqliteConnection; backend: 'better-sqlite3' | 'node:sqlite' } {
65
+ if (driver !== 'builtin') {
66
+ try {
67
+ const Database = require('better-sqlite3') as typeof import('better-sqlite3')
68
+ return { db: new Database(filename) as unknown as SqliteConnection, backend: 'better-sqlite3' }
69
+ } catch (error) {
70
+ const failure = error as NodeJS.ErrnoException
71
+ const unavailable = failure.code === 'MODULE_NOT_FOUND' || failure.code === 'ERR_DLOPEN_FAILED'
72
+ || failure.message.startsWith('Could not locate the bindings file.')
73
+ // Disk, SQL, permission and corruption failures must never select another backend.
74
+ if (driver === 'native' || !unavailable) throw error
75
+ }
76
+ }
77
+ return { db: builtinDatabase(filename), backend: 'node:sqlite' }
78
+ }
package/src/tools.ts CHANGED
@@ -6,6 +6,10 @@ export const inject = ['tools', 'diaryMemory']
6
6
 
7
7
  /** Model-visible read-only recall, scoped by the companion preset and namespace. */
8
8
  export function apply(ctx: Context): void {
9
+ registerRecall(ctx)
10
+ }
11
+
12
+ export function registerRecall(ctx: Context): void {
9
13
  ctx.tools.register({
10
14
  name: 'diary_recall',
11
15
  description: '想起我们之前的经历、称呼、偏好、情绪变化、约定或我自己的反思。输入自然语言,会检索历史日记并按相关性重排。日记中的主观感受和不确定解释不等于已确认事实。',