@lemoncat7/dsh-partner 1.1.6 → 1.4.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.
Files changed (88) hide show
  1. package/README.md +21 -6
  2. package/lib/agent-runtime.d.ts +46 -3
  3. package/lib/agent-runtime.d.ts.map +1 -1
  4. package/lib/agent-runtime.js +599 -50
  5. package/lib/agent-runtime.js.map +1 -1
  6. package/lib/api.d.ts +2 -0
  7. package/lib/api.d.ts.map +1 -1
  8. package/lib/api.js +44 -4
  9. package/lib/api.js.map +1 -1
  10. package/lib/autonomy.d.ts +2 -7
  11. package/lib/autonomy.d.ts.map +1 -1
  12. package/lib/autonomy.js +40 -28
  13. package/lib/autonomy.js.map +1 -1
  14. package/lib/channels/manager.d.ts +1 -0
  15. package/lib/channels/manager.d.ts.map +1 -1
  16. package/lib/channels/manager.js +11 -3
  17. package/lib/channels/manager.js.map +1 -1
  18. package/lib/client-api.d.ts +60 -0
  19. package/lib/client-api.d.ts.map +1 -1
  20. package/lib/client-api.js.map +1 -1
  21. package/lib/client.d.ts.map +1 -1
  22. package/lib/client.js +1880 -620
  23. package/lib/client.js.map +4 -4
  24. package/lib/concern-domain.d.ts +132 -0
  25. package/lib/concern-domain.d.ts.map +1 -0
  26. package/lib/concern-domain.js +212 -0
  27. package/lib/concern-domain.js.map +1 -0
  28. package/lib/concern-notification.d.ts +6 -0
  29. package/lib/concern-notification.d.ts.map +1 -0
  30. package/lib/concern-notification.js +27 -0
  31. package/lib/concern-notification.js.map +1 -0
  32. package/lib/concern-sources.d.ts +10 -0
  33. package/lib/concern-sources.d.ts.map +1 -0
  34. package/lib/concern-sources.js +81 -0
  35. package/lib/concern-sources.js.map +1 -0
  36. package/lib/concern-store.d.ts +68 -0
  37. package/lib/concern-store.d.ts.map +1 -0
  38. package/lib/concern-store.js +611 -0
  39. package/lib/concern-store.js.map +1 -0
  40. package/lib/concern-tool.d.ts +25 -0
  41. package/lib/concern-tool.d.ts.map +1 -0
  42. package/lib/concern-tool.js +145 -0
  43. package/lib/concern-tool.js.map +1 -0
  44. package/lib/daily-review.d.ts +3 -1
  45. package/lib/daily-review.d.ts.map +1 -1
  46. package/lib/daily-review.js +8 -2
  47. package/lib/daily-review.js.map +1 -1
  48. package/lib/domain.d.ts +4 -1
  49. package/lib/domain.d.ts.map +1 -1
  50. package/lib/domain.js +23 -0
  51. package/lib/domain.js.map +1 -1
  52. package/lib/glass-surface.d.ts +20 -0
  53. package/lib/glass-surface.d.ts.map +1 -0
  54. package/lib/glass-surface.js +119 -0
  55. package/lib/glass-surface.js.map +1 -0
  56. package/lib/heartbeat.d.ts +10 -2
  57. package/lib/heartbeat.d.ts.map +1 -1
  58. package/lib/heartbeat.js +81 -17
  59. package/lib/heartbeat.js.map +1 -1
  60. package/lib/index.d.ts.map +1 -1
  61. package/lib/index.js +36 -5
  62. package/lib/index.js.map +1 -1
  63. package/lib/memory-domain.d.ts +39 -7
  64. package/lib/memory-domain.d.ts.map +1 -1
  65. package/lib/memory-prompt-context.d.ts +20 -0
  66. package/lib/memory-prompt-context.d.ts.map +1 -0
  67. package/lib/memory-prompt-context.js +138 -0
  68. package/lib/memory-prompt-context.js.map +1 -0
  69. package/lib/memory-reflection.d.ts +8 -3
  70. package/lib/memory-reflection.d.ts.map +1 -1
  71. package/lib/memory-reflection.js +80 -19
  72. package/lib/memory-reflection.js.map +1 -1
  73. package/lib/memory-store.d.ts +13 -1
  74. package/lib/memory-store.d.ts.map +1 -1
  75. package/lib/memory-store.js +182 -8
  76. package/lib/memory-store.js.map +1 -1
  77. package/lib/profile-domain.d.ts +7 -0
  78. package/lib/profile-domain.d.ts.map +1 -0
  79. package/lib/profile-domain.js +56 -0
  80. package/lib/profile-domain.js.map +1 -0
  81. package/lib/store.d.ts.map +1 -1
  82. package/lib/store.js +39 -3
  83. package/lib/store.js.map +1 -1
  84. package/lib/time-format.d.ts +2 -0
  85. package/lib/time-format.d.ts.map +1 -0
  86. package/lib/time-format.js +20 -0
  87. package/lib/time-format.js.map +1 -0
  88. package/package.json +3 -1
@@ -0,0 +1,611 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ import { boundedConcernCheckMinutes, clamp, concernDecay, concernInterval, extractConcernResources, focusedConcernQuery, implicitConcernRejection, interruptDecision, MAX_IMPLICIT_CONCERNS_PER_BATCH, normalizeConcernSubject, concernLifecycleRequest, selectConcernLifecycleTarget, } from './concern-domain.js';
6
+ export class PartnerConcernStore {
7
+ root;
8
+ writes = new Map();
9
+ constructor(root) {
10
+ this.root = root;
11
+ }
12
+ async migrateLegacy(companionId, seeds) {
13
+ await this.serial(this.path(companionId), async () => {
14
+ const database = await this.open(companionId);
15
+ try {
16
+ if (database.prepare("SELECT value FROM concern_meta WHERE key = 'legacy-focus-migrated'").get())
17
+ return;
18
+ database.exec('BEGIN IMMEDIATE');
19
+ const now = Date.now();
20
+ for (const item of seeds)
21
+ this.upsert(database, companionId, item.scopeId, {
22
+ subject: item.subject, reason: item.reason, operation: 'upsert', priority: item.origin === 'explicit' ? .9 : .58,
23
+ confidence: item.confidence, watchKind: 'auto', watchQuery: item.subject,
24
+ }, item.origin, now);
25
+ database.prepare("INSERT INTO concern_meta (key, value) VALUES ('legacy-focus-migrated', ?)").run(String(now));
26
+ database.exec('COMMIT');
27
+ }
28
+ catch (error) {
29
+ rollback(database);
30
+ throw error;
31
+ }
32
+ finally {
33
+ database.close();
34
+ }
35
+ });
36
+ return true;
37
+ }
38
+ async applyCandidates(companionId, scopeId, candidates, origin, at = Date.now(), options = {}) {
39
+ return (await this.ingestCandidates(companionId, scopeId, candidates, origin, at, options)).created;
40
+ }
41
+ async ingestCandidates(companionId, scopeId, candidates, origin, at = Date.now(), options = {}) {
42
+ if (candidates.length === 0)
43
+ return { created: [], entries: [] };
44
+ return this.serialValue(this.path(companionId), async () => {
45
+ const database = await this.open(companionId);
46
+ const created = [];
47
+ const entries = [];
48
+ const source = options.source ?? (origin === 'explicit' ? 'ui' : 'reflection');
49
+ const maxImplicitCreates = Math.min(MAX_IMPLICIT_CONCERNS_PER_BATCH, Math.max(0, options.maxImplicitCreates ?? MAX_IMPLICIT_CONCERNS_PER_BATCH));
50
+ try {
51
+ database.exec('BEGIN IMMEDIATE');
52
+ for (const candidate of candidates.slice(0, 12)) {
53
+ const normalized = normalizeConcernSubject(compact(candidate.subject, 300));
54
+ const existing = normalized ? this.existing(database, companionId, scopeId, normalized) : undefined;
55
+ const rejection = origin === 'implicit' ? implicitConcernRejection(candidate) : undefined;
56
+ if (rejection !== undefined || (origin === 'implicit' && candidate.operation === 'upsert' && existing === undefined && created.length >= maxImplicitCreates)) {
57
+ const reason = rejection ?? `每批最多新增 ${maxImplicitCreates} 条隐式关注`;
58
+ const entry = { subject: compact(candidate.subject, 300), decision: 'rejected', reason };
59
+ entries.push(entry);
60
+ this.audit(database, companionId, scopeId, origin, candidate, entry, at, options);
61
+ continue;
62
+ }
63
+ const concern = this.upsert(database, companionId, scopeId, candidate, origin, at);
64
+ if (concern !== undefined)
65
+ created.push(concern);
66
+ const current = concern ?? existing;
67
+ const decision = candidate.operation === 'resolve'
68
+ ? existing === undefined ? 'ignored' : 'resolved'
69
+ : candidate.operation === 'dismiss'
70
+ ? existing === undefined ? 'ignored' : 'dismissed'
71
+ : existing === undefined ? 'created' : 'updated';
72
+ const reason = decision === 'created' ? origin === 'implicit' ? '通过隐式关注策略并创建' : '根据用户明确要求创建'
73
+ : decision === 'updated' ? '与已有关注同主题,已合并更新'
74
+ : decision === 'ignored' ? '没有找到可关闭的同主题关注'
75
+ : decision === 'resolved' ? '已有关注已闭环'
76
+ : '已有关注已归档';
77
+ const entry = { subject: compact(candidate.subject, 300), decision, reason, ...(current === undefined ? {} : { concern: current }) };
78
+ entries.push(entry);
79
+ this.audit(database, companionId, scopeId, origin, candidate, entry, at, options);
80
+ }
81
+ database.prepare('DELETE FROM concern_audit WHERE companion_id = ? AND created_at < ?').run(companionId, at - 90 * 86_400_000);
82
+ database.exec('COMMIT');
83
+ }
84
+ catch (error) {
85
+ rollback(database);
86
+ throw error;
87
+ }
88
+ finally {
89
+ database.close();
90
+ }
91
+ return { created, entries };
92
+ });
93
+ }
94
+ async createExplicit(companionId, scopeId, subject, reason = '') {
95
+ const descriptor = explicitConcernDescriptor(subject);
96
+ await this.applyCandidates(companionId, scopeId, [{
97
+ subject: descriptor.subject, reason: reason || '用户明确要求伙伴留意', operation: 'upsert', priority: .9,
98
+ confidence: 1, watchKind: descriptor.watchKind, watchQuery: descriptor.watchQuery, resources: descriptor.resources,
99
+ }], 'explicit', Date.now(), { source: 'ui' });
100
+ const concern = (await this.list(companionId, scopeId)).find(item => normalizeConcernSubject(item.subject) === normalizeConcernSubject(descriptor.subject));
101
+ if (!concern)
102
+ throw new Error('concern could not be created');
103
+ return concern;
104
+ }
105
+ async applyUserDirective(companionId, scopeId, value, now = Date.now()) {
106
+ const request = concernLifecycleRequest(value);
107
+ if (request === undefined)
108
+ return undefined;
109
+ return this.serialValue(this.path(companionId), async () => {
110
+ const database = await this.open(companionId);
111
+ try {
112
+ const rows = database.prepare(`SELECT * FROM concerns WHERE companion_id = ? AND scope_id IN (?, '*')
113
+ AND state != 'archived' ORDER BY score DESC, updated_at DESC LIMIT 80`).all(companionId, scopeId);
114
+ const target = selectConcernLifecycleTarget(request, rows.map(concernFromRow));
115
+ if (target === undefined)
116
+ return undefined;
117
+ if (request.action === 'ignore')
118
+ database.prepare("UPDATE concerns SET state = 'archived', updated_at = ? WHERE id = ?").run(now, target.id);
119
+ else
120
+ database.prepare("UPDATE concerns SET state = 'resolved', resolved_at = ?, updated_at = ? WHERE id = ?").run(now, now, target.id);
121
+ return { ...request, concernId: target.id, subject: target.subject };
122
+ }
123
+ finally {
124
+ database.close();
125
+ }
126
+ });
127
+ }
128
+ async list(companionId, scopeId, includeArchived = false, limit = 100) {
129
+ const database = await this.open(companionId);
130
+ try {
131
+ const states = includeArchived ? '' : "AND state NOT IN ('archived')";
132
+ const rows = scopeId === undefined
133
+ ? database.prepare(`SELECT * FROM concerns WHERE companion_id = ? ${states} ORDER BY score DESC, updated_at DESC LIMIT ?`).all(companionId, limit)
134
+ : database.prepare(`SELECT * FROM concerns WHERE companion_id = ? AND scope_id = ? ${states} ORDER BY score DESC, updated_at DESC LIMIT ?`).all(companionId, scopeId, limit);
135
+ return rows.map(concernFromRow);
136
+ }
137
+ finally {
138
+ database.close();
139
+ }
140
+ }
141
+ async due(companionId, scopeId, options = {}) {
142
+ const now = options.now ?? Date.now();
143
+ const limit = options.limit ?? 12;
144
+ return this.serialValue(this.path(companionId), async () => {
145
+ const database = await this.open(companionId);
146
+ try {
147
+ database.exec('BEGIN IMMEDIATE');
148
+ const rows = database.prepare(`SELECT * FROM concerns WHERE companion_id = ? AND scope_id IN (?, '*')
149
+ AND state IN ('active', 'watching') AND (? = 1 OR next_check_at <= ?)
150
+ AND (? IS NULL OR id = ?)
151
+ ORDER BY priority DESC, next_check_at ASC LIMIT ?`).all(companionId, scopeId, options.includeFuture ? 1 : 0, now, options.concernId ?? null, options.concernId ?? null, Math.max(limit, 24));
152
+ const due = [];
153
+ const update = database.prepare('UPDATE concerns SET score = ?, state = ? WHERE id = ?');
154
+ for (const row of rows) {
155
+ const item = concernFromRow(row);
156
+ const score = concernDecay(item.score, item.lastActivityAt, now, item.origin);
157
+ if (item.origin === 'implicit' && score < .18) {
158
+ update.run(score, 'archived', item.id);
159
+ continue;
160
+ }
161
+ if (score !== item.score)
162
+ update.run(score, item.state, item.id);
163
+ if (due.length < limit)
164
+ due.push({ ...item, score });
165
+ }
166
+ database.exec('COMMIT');
167
+ return due;
168
+ }
169
+ catch (error) {
170
+ rollback(database);
171
+ throw error;
172
+ }
173
+ finally {
174
+ database.close();
175
+ }
176
+ });
177
+ }
178
+ async recordObservations(concerns, candidates, now = Date.now(), userRecentlyActive = false) {
179
+ if (concerns.length === 0)
180
+ return { observations: [], notifications: [] };
181
+ const companionId = concerns[0].companionId;
182
+ return this.serialValue(this.path(companionId), async () => {
183
+ const database = await this.open(companionId);
184
+ const observations = [];
185
+ try {
186
+ database.exec('BEGIN IMMEDIATE');
187
+ const byConcern = new Map(candidates.map(item => [item.concernId, item]));
188
+ for (const concern of concerns) {
189
+ const candidate = byConcern.get(concern.id);
190
+ const chosenMinutes = boundedConcernCheckMinutes(candidate?.nextCheckInMinutes);
191
+ const nextCheckAt = now + (chosenMinutes === undefined ? concernInterval(concern.priority, concern.origin) : chosenMinutes * 60_000);
192
+ if (!candidate?.changed || !candidate.event.trim()) {
193
+ database.prepare('UPDATE concerns SET last_checked_at = ?, next_check_at = ?, score = ? WHERE id = ?').run(now, nextCheckAt, concern.score, concern.id);
194
+ continue;
195
+ }
196
+ const fingerprint = observationFingerprint(candidate);
197
+ const duplicateRow = database.prepare('SELECT * FROM concern_observations WHERE concern_id = ? AND fingerprint = ?').get(concern.id, fingerprint);
198
+ const previousCount = number(database.prepare('SELECT COUNT(*) AS count FROM concern_observations WHERE concern_id = ?').get(concern.id)?.count);
199
+ const recentlyMentioned = concern.lastMentionedAt !== undefined && now - concern.lastMentionedAt < 72 * 3_600_000;
200
+ const novelty = duplicateRow ? number(duplicateRow.novelty) : previousCount === 0 ? .5 : 1;
201
+ const ruleReason = compact(candidate.notificationRuleReason ?? '', 800);
202
+ const knowledgeLinked = concern.resources.some(resource => resource.kind === 'knowledge');
203
+ const notificationRuleEffect = knowledgeLinked && ruleReason
204
+ ? candidate.notificationRuleEffect ?? 'auto'
205
+ : 'auto';
206
+ const decision = interruptDecision({
207
+ priority: concern.priority, concernConfidence: concern.confidence,
208
+ observationConfidence: clamp(candidate.confidence), relevance: clamp(candidate.relevance), novelty,
209
+ actionability: clamp(candidate.actionability), recentlyMentioned,
210
+ firstObservation: duplicateRow ? novelty < 1 : previousCount === 0, userRecentlyActive,
211
+ notificationRuleEffect,
212
+ });
213
+ const observation = {
214
+ id: duplicateRow ? string(duplicateRow.id) : `observation-${randomUUID()}`,
215
+ concernId: concern.id, companionId: concern.companionId, scopeId: concern.scopeId, fingerprint,
216
+ event: duplicateRow ? string(duplicateRow.event) : compact(candidate.event, 800),
217
+ evidence: duplicateRow ? string(duplicateRow.evidence) : compact(candidate.evidence, 2_000),
218
+ source: duplicateRow ? string(duplicateRow.source) : compact(candidate.source, 240),
219
+ novelty, relevance: clamp(candidate.relevance), confidence: clamp(candidate.confidence), actionability: clamp(candidate.actionability),
220
+ interruptScore: decision.score, decision: decision.decision, notificationRuleEffect,
221
+ notificationRuleReason: ruleReason, decisionReason: decision.reason,
222
+ createdAt: duplicateRow ? number(duplicateRow.created_at) : now,
223
+ ...(duplicateRow?.mentioned_at === null || duplicateRow?.mentioned_at === undefined ? {} : { mentionedAt: number(duplicateRow.mentioned_at) }),
224
+ };
225
+ if (duplicateRow) {
226
+ const previousDecision = string(duplicateRow.decision);
227
+ const preserveNotification = previousDecision === 'notify'
228
+ && (observation.mentionedAt !== undefined || observation.notificationRuleEffect !== 'suppress');
229
+ if (preserveNotification) {
230
+ observation.decision = 'notify';
231
+ observation.interruptScore = number(duplicateRow.interrupt_score);
232
+ observation.notificationRuleEffect = string(duplicateRow.rule_effect || 'auto');
233
+ observation.notificationRuleReason = string(duplicateRow.rule_reason);
234
+ observation.decisionReason = string(duplicateRow.decision_reason) || '此变化已进入待提醒队列';
235
+ }
236
+ else {
237
+ database.prepare(`UPDATE concern_observations SET relevance = ?, confidence = ?, actionability = ?,
238
+ interrupt_score = ?, decision = ?, rule_effect = ?, rule_reason = ?, decision_reason = ? WHERE id = ?`).run(observation.relevance, observation.confidence, observation.actionability, observation.interruptScore, observation.decision, observation.notificationRuleEffect, observation.notificationRuleReason, observation.decisionReason, observation.id);
239
+ }
240
+ if (previousDecision !== 'notify' && observation.decision === 'notify' && observation.mentionedAt === undefined)
241
+ observations.push(observation);
242
+ }
243
+ else {
244
+ insertObservation(database, observation);
245
+ observations.push(observation);
246
+ }
247
+ const nextState = observation.decision === 'notify' || observation.decision === 'feed' ? 'active' : 'watching';
248
+ database.prepare(`UPDATE concerns SET state = ?, last_checked_at = ?, next_check_at = ?, score = ?, updated_at = ? WHERE id = ?`).run(nextState, now, nextCheckAt, Math.max(concern.score, observation.interruptScore), now, concern.id);
249
+ }
250
+ database.exec('COMMIT');
251
+ }
252
+ catch (error) {
253
+ rollback(database);
254
+ throw error;
255
+ }
256
+ finally {
257
+ database.close();
258
+ }
259
+ return { observations, notifications: observations.filter(item => item.decision === 'notify') };
260
+ });
261
+ }
262
+ async activity(companionId, limit = 60) {
263
+ const database = await this.open(companionId);
264
+ try {
265
+ const concerns = database.prepare("SELECT * FROM concerns WHERE companion_id = ? AND state != 'archived' ORDER BY score DESC, updated_at DESC LIMIT 200").all(companionId).map(concernFromRow);
266
+ const observations = database.prepare("SELECT * FROM concern_observations WHERE companion_id = ? AND decision IN ('defer', 'feed', 'notify') ORDER BY created_at DESC LIMIT ?").all(companionId, limit).map(observationFromRow);
267
+ return { concerns, observations };
268
+ }
269
+ finally {
270
+ database.close();
271
+ }
272
+ }
273
+ async pendingToolCreationNotices(companionId, scopeId, sessionId, limit = 4) {
274
+ const database = await this.open(companionId);
275
+ try {
276
+ const rows = database.prepare(`SELECT audit.id AS audit_id, concerns.* FROM concern_audit AS audit
277
+ JOIN concerns ON concerns.id = audit.concern_id
278
+ WHERE audit.companion_id = ? AND audit.scope_id = ? AND audit.session_id = ?
279
+ AND audit.source = 'tool' AND audit.origin = 'implicit' AND audit.decision = 'created' AND audit.notified_at IS NULL
280
+ AND concerns.state NOT IN ('resolved', 'archived')
281
+ ORDER BY audit.created_at ASC LIMIT ?`).all(companionId, scopeId, sessionId, Math.max(1, Math.min(12, limit)));
282
+ return { auditIds: rows.map(row => string(row.audit_id)), concerns: rows.map(concernFromRow) };
283
+ }
284
+ finally {
285
+ database.close();
286
+ }
287
+ }
288
+ async markCreationAuditsNotified(companionId, auditIds, at = Date.now()) {
289
+ if (auditIds.length === 0)
290
+ return;
291
+ await this.serial(this.path(companionId), async () => {
292
+ const database = await this.open(companionId);
293
+ try {
294
+ database.exec('BEGIN IMMEDIATE');
295
+ const update = database.prepare('UPDATE concern_audit SET notified_at = ? WHERE companion_id = ? AND id = ? AND notified_at IS NULL');
296
+ for (const id of [...new Set(auditIds)].slice(0, 20))
297
+ update.run(at, companionId, id);
298
+ database.exec('COMMIT');
299
+ }
300
+ catch (error) {
301
+ rollback(database);
302
+ throw error;
303
+ }
304
+ finally {
305
+ database.close();
306
+ }
307
+ });
308
+ }
309
+ async deferred(companionId, scopeId, query, limit = 3) {
310
+ const database = await this.open(companionId);
311
+ try {
312
+ const rows = database.prepare(`SELECT * FROM concern_observations WHERE companion_id = ? AND scope_id IN (?, '*')
313
+ AND decision = 'defer' AND mentioned_at IS NULL ORDER BY interrupt_score DESC, created_at DESC LIMIT 30`).all(companionId, scopeId);
314
+ const terms = tokens(query);
315
+ return rows.map(observationFromRow).map(item => ({ item, score: lexicalScore(`${item.event} ${item.evidence}`, terms) + item.interruptScore }))
316
+ .filter(item => terms.length === 0 || item.score > .45).sort((a, b) => b.score - a.score).slice(0, limit).map(item => item.item);
317
+ }
318
+ finally {
319
+ database.close();
320
+ }
321
+ }
322
+ async pendingNotifications(companionId, scopeId, limit = 4) {
323
+ const database = await this.open(companionId);
324
+ try {
325
+ return database.prepare(`SELECT * FROM concern_observations WHERE companion_id = ? AND scope_id IN (?, '*')
326
+ AND decision = 'notify' AND mentioned_at IS NULL ORDER BY interrupt_score DESC, created_at ASC LIMIT ?`).all(companionId, scopeId, limit).map(observationFromRow);
327
+ }
328
+ finally {
329
+ database.close();
330
+ }
331
+ }
332
+ async markMentioned(companionId, observationIds, now = Date.now()) {
333
+ if (observationIds.length === 0)
334
+ return;
335
+ await this.serial(this.path(companionId), async () => {
336
+ const database = await this.open(companionId);
337
+ try {
338
+ database.exec('BEGIN IMMEDIATE');
339
+ const updateObservation = database.prepare('UPDATE concern_observations SET mentioned_at = ? WHERE companion_id = ? AND id = ?');
340
+ const updateConcern = database.prepare('UPDATE concerns SET last_mentioned_at = ?, updated_at = ? WHERE companion_id = ? AND id = (SELECT concern_id FROM concern_observations WHERE id = ?)');
341
+ for (const id of observationIds) {
342
+ updateObservation.run(now, companionId, id);
343
+ updateConcern.run(now, now, companionId, id);
344
+ }
345
+ database.exec('COMMIT');
346
+ }
347
+ catch (error) {
348
+ rollback(database);
349
+ throw error;
350
+ }
351
+ finally {
352
+ database.close();
353
+ }
354
+ });
355
+ }
356
+ async act(companionId, concernId, action, now = Date.now()) {
357
+ await this.serial(this.path(companionId), async () => {
358
+ const database = await this.open(companionId);
359
+ try {
360
+ const concern = database.prepare('SELECT * FROM concerns WHERE companion_id = ? AND id = ?').get(companionId, concernId);
361
+ if (!concern)
362
+ throw new Error('concern was not found');
363
+ if (action === 'ignore')
364
+ database.prepare("UPDATE concerns SET state = 'archived', updated_at = ? WHERE id = ?").run(now, concernId);
365
+ if (action === 'resolve')
366
+ database.prepare("UPDATE concerns SET state = 'resolved', resolved_at = ?, updated_at = ? WHERE id = ?").run(now, now, concernId);
367
+ if (action === 'watch')
368
+ database.prepare("UPDATE concerns SET state = 'watching', score = MAX(score, .72), last_activity_at = ?, next_check_at = ?, updated_at = ? WHERE id = ?").run(now, now, now, concernId);
369
+ if (action === 'prioritize')
370
+ database.prepare("UPDATE concerns SET state = 'active', priority = 1, score = 1, last_activity_at = ?, next_check_at = ?, updated_at = ? WHERE id = ?").run(now, now, now, concernId);
371
+ }
372
+ finally {
373
+ database.close();
374
+ }
375
+ });
376
+ }
377
+ async clear(companionId) {
378
+ await this.serial(this.path(companionId), () => rm(join(this.root, 'partners', companionId, 'concerns'), { recursive: true, force: true }));
379
+ }
380
+ upsert(database, companionId, scopeId, candidate, origin, at) {
381
+ const subject = compact(candidate.subject, 300);
382
+ const normalized = normalizeConcernSubject(subject);
383
+ if (!normalized)
384
+ return undefined;
385
+ const existing = database.prepare(`SELECT * FROM concerns WHERE companion_id = ? AND normalized_subject = ? AND scope_id IN (?, '*')
386
+ ORDER BY CASE WHEN origin = 'explicit' THEN 0 ELSE 1 END LIMIT 1`).get(companionId, normalized, scopeId);
387
+ if (candidate.operation === 'resolve' || candidate.operation === 'dismiss') {
388
+ if (!existing)
389
+ return undefined;
390
+ const state = candidate.operation === 'resolve' ? 'resolved' : 'archived';
391
+ database.prepare('UPDATE concerns SET state = ?, resolved_at = ?, updated_at = ? WHERE id = ?').run(state, state === 'resolved' ? at : null, at, string(existing.id));
392
+ return undefined;
393
+ }
394
+ const priority = clamp(candidate.priority);
395
+ const confidence = clamp(candidate.confidence);
396
+ const score = clamp(priority * .55 + confidence * .45);
397
+ const nextCheckAt = at + concernInterval(priority, origin);
398
+ if (existing && string(existing.scope_id) !== scopeId) {
399
+ const resources = resourcesJson(candidate.resources);
400
+ database.prepare(`UPDATE concerns SET subject = ?, reason = ?, state = 'watching', priority = MAX(priority, ?),
401
+ confidence = MAX(confidence, ?), score = MAX(score, ?), watch_kind = ?, watch_query = ?,
402
+ resources_json = CASE WHEN ? = '[]' THEN resources_json ELSE ? END, updated_at = ?,
403
+ last_activity_at = ?, next_check_at = MIN(next_check_at, ?), resolved_at = NULL WHERE id = ?`).run(subject, compact(candidate.reason, 800), priority, confidence, score, candidate.watchKind, compact(candidate.watchQuery || subject, 500), resources, resources, at, at, nextCheckAt, string(existing.id));
404
+ return undefined;
405
+ }
406
+ const id = `concern-${randomUUID()}`;
407
+ database.prepare(`INSERT INTO concerns
408
+ (id, companion_id, scope_id, normalized_subject, subject, reason, origin, state, priority, confidence, score,
409
+ watch_kind, watch_query, resources_json, created_at, updated_at, last_activity_at, next_check_at)
410
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'watching', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
411
+ ON CONFLICT(companion_id, scope_id, normalized_subject) DO UPDATE SET
412
+ subject=excluded.subject, reason=excluded.reason, origin=CASE WHEN concerns.origin='explicit' THEN 'explicit' ELSE excluded.origin END,
413
+ state='watching', priority=MAX(concerns.priority, excluded.priority), confidence=MAX(concerns.confidence, excluded.confidence),
414
+ score=MAX(concerns.score, excluded.score), watch_kind=excluded.watch_kind, watch_query=excluded.watch_query,
415
+ resources_json=CASE WHEN excluded.resources_json='[]' THEN concerns.resources_json ELSE excluded.resources_json END,
416
+ updated_at=excluded.updated_at, last_activity_at=excluded.last_activity_at, next_check_at=MIN(concerns.next_check_at, excluded.next_check_at), resolved_at=NULL`).run(id, companionId, scopeId, normalized, subject, compact(candidate.reason, 800), origin, priority, confidence, score, candidate.watchKind, compact(candidate.watchQuery || subject, 500), resourcesJson(candidate.resources), at, at, at, nextCheckAt);
417
+ const created = database.prepare('SELECT * FROM concerns WHERE id = ?').get(id);
418
+ return created === undefined ? undefined : concernFromRow(created);
419
+ }
420
+ existing(database, companionId, scopeId, normalized) {
421
+ const row = database.prepare(`SELECT * FROM concerns WHERE companion_id = ? AND normalized_subject = ? AND scope_id IN (?, '*')
422
+ ORDER BY CASE WHEN origin = 'explicit' THEN 0 ELSE 1 END LIMIT 1`).get(companionId, normalized, scopeId);
423
+ return row === undefined ? undefined : concernFromRow(row);
424
+ }
425
+ audit(database, companionId, scopeId, origin, candidate, entry, at, options) {
426
+ database.prepare(`INSERT INTO concern_audit
427
+ (id, concern_id, companion_id, scope_id, session_id, source, origin, operation, subject, reason, evidence,
428
+ decision, decision_reason, created_at, notified_at)
429
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`).run(`concern-audit-${randomUUID()}`, entry.concern?.id ?? null, companionId, scopeId, options.sessionId ?? null, options.source ?? (origin === 'explicit' ? 'ui' : 'reflection'), origin, candidate.operation, compact(candidate.subject, 300), compact(candidate.reason, 800), compact(options.evidence ?? '', 1_200), entry.decision, compact(entry.reason, 400), at);
430
+ }
431
+ async open(companionId) {
432
+ const directory = join(this.root, 'partners', companionId, 'concerns');
433
+ await mkdir(directory, { recursive: true, mode: 0o700 });
434
+ const database = new DatabaseSync(this.path(companionId));
435
+ database.exec(`PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;
436
+ CREATE TABLE IF NOT EXISTS concern_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
437
+ CREATE TABLE IF NOT EXISTS concerns (
438
+ id TEXT PRIMARY KEY, companion_id TEXT NOT NULL, scope_id TEXT NOT NULL, normalized_subject TEXT NOT NULL,
439
+ subject TEXT NOT NULL, reason TEXT NOT NULL, origin TEXT NOT NULL, state TEXT NOT NULL,
440
+ priority REAL NOT NULL, confidence REAL NOT NULL, score REAL NOT NULL,
441
+ watch_kind TEXT NOT NULL, watch_query TEXT NOT NULL, resources_json TEXT NOT NULL DEFAULT '[]',
442
+ created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, last_activity_at INTEGER NOT NULL,
443
+ next_check_at INTEGER NOT NULL, last_checked_at INTEGER, last_mentioned_at INTEGER, resolved_at INTEGER,
444
+ UNIQUE(companion_id, scope_id, normalized_subject)
445
+ );
446
+ CREATE INDEX IF NOT EXISTS concerns_due ON concerns(companion_id, scope_id, state, next_check_at);
447
+ CREATE INDEX IF NOT EXISTS concerns_score ON concerns(companion_id, score DESC, updated_at DESC);
448
+ CREATE TABLE IF NOT EXISTS concern_observations (
449
+ id TEXT PRIMARY KEY, concern_id TEXT NOT NULL, companion_id TEXT NOT NULL, scope_id TEXT NOT NULL,
450
+ fingerprint TEXT NOT NULL, event TEXT NOT NULL, evidence TEXT NOT NULL, source TEXT NOT NULL,
451
+ novelty REAL NOT NULL, relevance REAL NOT NULL, confidence REAL NOT NULL, actionability REAL NOT NULL,
452
+ interrupt_score REAL NOT NULL, decision TEXT NOT NULL, rule_effect TEXT NOT NULL DEFAULT 'auto',
453
+ rule_reason TEXT NOT NULL DEFAULT '', decision_reason TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL, mentioned_at INTEGER,
454
+ UNIQUE(concern_id, fingerprint), FOREIGN KEY(concern_id) REFERENCES concerns(id) ON DELETE CASCADE
455
+ );
456
+ CREATE INDEX IF NOT EXISTS observations_feed ON concern_observations(companion_id, decision, created_at DESC);
457
+ CREATE INDEX IF NOT EXISTS observations_deferred ON concern_observations(companion_id, scope_id, decision, mentioned_at, created_at DESC);
458
+ CREATE TABLE IF NOT EXISTS concern_audit (
459
+ id TEXT PRIMARY KEY, concern_id TEXT, companion_id TEXT NOT NULL, scope_id TEXT NOT NULL, session_id TEXT,
460
+ source TEXT NOT NULL, origin TEXT NOT NULL, operation TEXT NOT NULL, subject TEXT NOT NULL, reason TEXT NOT NULL,
461
+ evidence TEXT NOT NULL, decision TEXT NOT NULL, decision_reason TEXT NOT NULL, created_at INTEGER NOT NULL, notified_at INTEGER
462
+ );
463
+ CREATE INDEX IF NOT EXISTS concern_audit_session ON concern_audit(companion_id, scope_id, session_id, source, decision, notified_at, created_at);
464
+ PRAGMA user_version = 4;`);
465
+ ensureColumn(database, 'concerns', 'resources_json', "TEXT NOT NULL DEFAULT '[]'");
466
+ ensureColumn(database, 'concern_observations', 'rule_effect', "TEXT NOT NULL DEFAULT 'auto'");
467
+ ensureColumn(database, 'concern_observations', 'rule_reason', "TEXT NOT NULL DEFAULT ''");
468
+ ensureColumn(database, 'concern_observations', 'decision_reason', "TEXT NOT NULL DEFAULT ''");
469
+ migrateReferenceOnlyConcerns(database);
470
+ return database;
471
+ }
472
+ path(companionId) { return join(this.root, 'partners', companionId, 'concerns', 'concerns.sqlite'); }
473
+ async serial(key, task) { await this.serialValue(key, task); }
474
+ async serialValue(key, task) {
475
+ const previous = this.writes.get(key) ?? Promise.resolve();
476
+ let value;
477
+ const current = previous.catch(() => { }).then(async () => { value = await task(); });
478
+ this.writes.set(key, current);
479
+ try {
480
+ await current;
481
+ return value;
482
+ }
483
+ finally {
484
+ if (this.writes.get(key) === current)
485
+ this.writes.delete(key);
486
+ }
487
+ }
488
+ }
489
+ function concernFromRow(row) {
490
+ return {
491
+ id: string(row.id), companionId: string(row.companion_id), scopeId: string(row.scope_id), subject: string(row.subject), reason: string(row.reason),
492
+ origin: string(row.origin), state: string(row.state),
493
+ priority: number(row.priority), confidence: number(row.confidence), score: number(row.score),
494
+ watchKind: string(row.watch_kind), watchQuery: string(row.watch_query), resources: parseResources(row.resources_json),
495
+ createdAt: number(row.created_at), updatedAt: number(row.updated_at), lastActivityAt: number(row.last_activity_at), nextCheckAt: number(row.next_check_at),
496
+ ...(row.last_checked_at === null ? {} : { lastCheckedAt: number(row.last_checked_at) }),
497
+ ...(row.last_mentioned_at === null ? {} : { lastMentionedAt: number(row.last_mentioned_at) }),
498
+ ...(row.resolved_at === null ? {} : { resolvedAt: number(row.resolved_at) }),
499
+ };
500
+ }
501
+ function observationFromRow(row) {
502
+ return {
503
+ id: string(row.id), concernId: string(row.concern_id), companionId: string(row.companion_id), scopeId: string(row.scope_id),
504
+ fingerprint: string(row.fingerprint), event: string(row.event), evidence: string(row.evidence), source: string(row.source),
505
+ novelty: number(row.novelty), relevance: number(row.relevance), confidence: number(row.confidence), actionability: number(row.actionability),
506
+ interruptScore: number(row.interrupt_score), decision: string(row.decision),
507
+ notificationRuleEffect: string(row.rule_effect || 'auto'),
508
+ notificationRuleReason: string(row.rule_reason), decisionReason: string(row.decision_reason), createdAt: number(row.created_at),
509
+ ...(row.mentioned_at === null ? {} : { mentionedAt: number(row.mentioned_at) }),
510
+ };
511
+ }
512
+ function insertObservation(database, item) {
513
+ database.prepare(`INSERT INTO concern_observations
514
+ (id, concern_id, companion_id, scope_id, fingerprint, event, evidence, source, novelty, relevance, confidence,
515
+ actionability, interrupt_score, decision, rule_effect, rule_reason, decision_reason, created_at, mentioned_at)
516
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(item.id, item.concernId, item.companionId, item.scopeId, item.fingerprint, item.event, item.evidence, item.source, item.novelty, item.relevance, item.confidence, item.actionability, item.interruptScore, item.decision, item.notificationRuleEffect, item.notificationRuleReason, item.decisionReason, item.createdAt, item.mentionedAt ?? null);
517
+ }
518
+ function observationFingerprint(value) {
519
+ return createHash('sha256').update(`${value.concernId}\n${normalizeConcernSubject(value.event)}\n${normalizeConcernSubject(value.evidence)}\n${value.source}`).digest('hex');
520
+ }
521
+ function tokens(value) { return [...new Set(value.toLocaleLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [])].slice(0, 40); }
522
+ function lexicalScore(value, terms) {
523
+ if (terms.length === 0)
524
+ return 0;
525
+ const text = value.toLocaleLowerCase();
526
+ return terms.filter(term => text.includes(term)).length / terms.length;
527
+ }
528
+ function compact(value, max) { const text = value.replace(/\s+/g, ' ').trim(); return text.length <= max ? text : `${text.slice(0, max - 1)}…`; }
529
+ function explicitConcernDescriptor(value) {
530
+ const resources = extractConcernResources(value);
531
+ const remainder = compact(value
532
+ .replace(/@知识库\[[^\]]+\]/gu, ' ')
533
+ .replace(/(?:^|\s)@(?:"[^"]+"|[^\s]+)/gu, ' ')
534
+ .replace(/^(?:(?:请|麻烦|帮我|替我|让(?:伙伴|你)?)[,,::\s]*)?(?:关注|留意|盯着|跟进|惦记|记着)[,,::\s]*/u, '')
535
+ .replace(/(?:一下|这件事|这个|这些)[。!!??\s]*$/u, ''), 300);
536
+ const resourceSubjects = resources.map(resource => {
537
+ if (resource.kind === 'file')
538
+ return resource.locator;
539
+ const slash = resource.locator.indexOf('/');
540
+ return slash < 0 ? resource.label : resource.locator.slice(slash + 1).trim();
541
+ }).filter(Boolean);
542
+ const primary = resourceSubjects[0] ?? '';
543
+ const subject = remainder
544
+ ? primary && !remainder.toLocaleLowerCase('zh-CN').includes(primary.toLocaleLowerCase('zh-CN')) ? `${primary} · ${remainder}` : remainder
545
+ : primary || compact(value, 300);
546
+ const watchQuery = compact(focusedConcernQuery([...resourceSubjects, remainder].filter((item, index, all) => item && all.indexOf(item) === index).join(' ')), 500) || subject;
547
+ const kinds = new Set(resources.map(item => item.kind));
548
+ const watchKind = kinds.size === 1 && kinds.has('knowledge') ? 'knowledge' : kinds.size === 1 && kinds.has('file') ? 'workspace' : 'auto';
549
+ return { subject, watchKind, watchQuery, resources };
550
+ }
551
+ function migrateReferenceOnlyConcerns(database) {
552
+ if (database.prepare("SELECT value FROM concern_meta WHERE key = 'reference-subject-normalized-v1'").get())
553
+ return;
554
+ database.exec('BEGIN IMMEDIATE');
555
+ try {
556
+ const rows = database.prepare("SELECT id, companion_id, scope_id, subject, resources_json FROM concerns WHERE origin = 'explicit' AND resources_json != '[]'").all();
557
+ const update = database.prepare('UPDATE concerns SET normalized_subject = ?, subject = ?, watch_kind = ?, watch_query = ? WHERE id = ?');
558
+ for (const row of rows) {
559
+ const raw = string(row.subject);
560
+ const descriptor = explicitConcernDescriptor(raw);
561
+ if (descriptor.subject === raw || descriptor.resources.length === 0)
562
+ continue;
563
+ const normalized = normalizeConcernSubject(descriptor.subject);
564
+ const duplicate = database.prepare('SELECT id FROM concerns WHERE companion_id = ? AND scope_id = ? AND normalized_subject = ? AND id != ?').get(string(row.companion_id), string(row.scope_id), normalized, string(row.id));
565
+ if (!duplicate)
566
+ update.run(normalized, descriptor.subject, descriptor.watchKind, descriptor.watchQuery, string(row.id));
567
+ }
568
+ database.prepare("INSERT INTO concern_meta (key, value) VALUES ('reference-subject-normalized-v1', ?)").run(String(Date.now()));
569
+ database.exec('COMMIT');
570
+ }
571
+ catch (error) {
572
+ rollback(database);
573
+ throw error;
574
+ }
575
+ }
576
+ function resourcesJson(value) {
577
+ return JSON.stringify((value ?? []).slice(0, 8).map(item => ({ kind: item.kind, locator: compact(item.locator, 500), label: compact(item.label, 240) }))
578
+ .filter(item => (item.kind === 'file' || item.kind === 'knowledge') && item.locator));
579
+ }
580
+ function parseResources(value) {
581
+ if (typeof value !== 'string')
582
+ return [];
583
+ try {
584
+ const parsed = JSON.parse(value);
585
+ if (!Array.isArray(parsed))
586
+ return [];
587
+ return parsed.flatMap(item => {
588
+ if (typeof item !== 'object' || item === null)
589
+ return [];
590
+ const resource = item;
591
+ if ((resource.kind !== 'file' && resource.kind !== 'knowledge') || typeof resource.locator !== 'string' || typeof resource.label !== 'string')
592
+ return [];
593
+ return [{ kind: resource.kind, locator: compact(resource.locator, 500), label: compact(resource.label, 240) }];
594
+ }).slice(0, 8);
595
+ }
596
+ catch {
597
+ return [];
598
+ }
599
+ }
600
+ function ensureColumn(database, table, column, definition) {
601
+ const columns = database.prepare(`PRAGMA table_info(${table})`).all();
602
+ if (!columns.some(item => item.name === column))
603
+ database.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
604
+ }
605
+ function string(value) { return typeof value === 'string' ? value : String(value ?? ''); }
606
+ function number(value) { return typeof value === 'bigint' ? Number(value) : typeof value === 'number' ? value : Number(value ?? 0); }
607
+ function rollback(database) { try {
608
+ database.exec('ROLLBACK');
609
+ }
610
+ catch { /* no active transaction */ } }
611
+ //# sourceMappingURL=concern-store.js.map