@rikaidev/yomi 0.5.0 → 0.6.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 (62) hide show
  1. package/README.md +28 -0
  2. package/dist/ambient/ambient-event-graph.js +172 -0
  3. package/dist/ambient/ambient-observer.js +71 -0
  4. package/dist/ambient/event-graph-reducer.js +300 -0
  5. package/dist/ambient/ingestion-adapter.js +203 -0
  6. package/dist/ambient/journal/crypto.js +58 -0
  7. package/dist/ambient/journal/journal-store.js +284 -0
  8. package/dist/ambient/journal/types.js +7 -0
  9. package/dist/ambient/summaries/retriever.js +55 -0
  10. package/dist/ambient/summaries/summary-engine.js +128 -0
  11. package/dist/ambient/summaries/types.js +4 -0
  12. package/dist/attention/policy.js +22 -16
  13. package/dist/auth/credential-store.js +15 -6
  14. package/dist/cli/query.js +276 -69
  15. package/dist/cli/work-item.js +24 -0
  16. package/dist/line/client/auth-service/client.js +7 -8
  17. package/dist/line/client/buddy-service/client.js +73 -0
  18. package/dist/line/client/index.js +28 -1
  19. package/dist/line/client/obs-media-client.js +2 -1
  20. package/dist/line/client/square-service/client.js +298 -0
  21. package/dist/line/client/talk-service/command-client.js +9 -1
  22. package/dist/line/client/talk-service/message-id-query.js +27 -3
  23. package/dist/line/client/talk-service/requests.js +9 -0
  24. package/dist/line/core/auth-session-runtime.js +1 -1
  25. package/dist/line/core/chat-event.js +2 -0
  26. package/dist/line/core/chat-runtime-service.js +18 -3
  27. package/dist/line/core/config.js +1 -0
  28. package/dist/line/core/flex-message.js +323 -0
  29. package/dist/line/core/service.js +0 -1
  30. package/dist/mcp/handlers/contacts.js +12 -6
  31. package/dist/mcp/handlers/conversations.js +54 -0
  32. package/dist/mcp/handlers/send.js +8 -11
  33. package/dist/mcp/media.js +101 -2
  34. package/dist/mcp/names.js +78 -6
  35. package/dist/search/default-embedder.js +12 -3
  36. package/dist/version.js +1 -1
  37. package/dist/work-item/adapter.js +1 -0
  38. package/dist/work-item/destination-contracts.js +1 -0
  39. package/dist/work-item/event-context.js +73 -0
  40. package/dist/work-item/gold-contract.js +101 -0
  41. package/dist/work-item/gold-coverage.js +96 -0
  42. package/dist/work-item/gold-derivation.js +67 -0
  43. package/dist/work-item/gold-split.js +143 -0
  44. package/dist/work-item/index.js +23 -0
  45. package/dist/work-item/line-adapter.js +111 -0
  46. package/dist/work-item/llama-cpp-work-event-adapter.js +239 -0
  47. package/dist/work-item/local-provider.js +38 -0
  48. package/dist/work-item/local-work-event-provider.js +56 -0
  49. package/dist/work-item/onnx-work-event-adapter.js +223 -0
  50. package/dist/work-item/provider.js +21 -0
  51. package/dist/work-item/registry.js +19 -0
  52. package/dist/work-item/service.js +85 -0
  53. package/dist/work-item/store.js +97 -0
  54. package/dist/work-item/types.js +79 -0
  55. package/dist/work-item/work-event-graph-engine.js +63 -0
  56. package/dist/work-item/work-event-inference.js +267 -0
  57. package/dist/work-item/work-event-matcher.js +105 -0
  58. package/dist/work-item/work-event-model-manifest.js +39 -0
  59. package/dist/work-item/work-event-store.js +125 -0
  60. package/dist/work-item/work-event.js +123 -0
  61. package/package.json +24 -3
  62. package/run.mjs +162 -9
@@ -0,0 +1,128 @@
1
+ import { createHash } from 'node:crypto';
2
+ export const CURRENT_SUMMARY_ENGINE_VERSION = 1;
3
+ const EPISODE_GAP_MS = 45 * 60 * 1000; // 45 minutes
4
+ const ROLLUP_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
5
+ function computeDigest(content) {
6
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
7
+ }
8
+ function inferDialogueActs(events) {
9
+ const acts = new Set();
10
+ for (const event of events) {
11
+ const text = event.text ?? '';
12
+ const isQuestion = /[??]/.test(text);
13
+ if (isQuestion) {
14
+ acts.add('ask');
15
+ }
16
+ if (['image', 'video', 'file'].includes(event.contentType)) {
17
+ acts.add('deliver');
18
+ }
19
+ if (event.mentionsSelf || (event.metadata.mentionMids?.length ?? 0) > 0) {
20
+ acts.add('directive');
21
+ }
22
+ if (event.contentType === 'sticker') {
23
+ acts.add('acknowledge');
24
+ }
25
+ if (text.length > 0 && !isQuestion) {
26
+ acts.add('inform');
27
+ }
28
+ }
29
+ return [...acts];
30
+ }
31
+ export function buildEpisodeSummaries(events, engineVersion = CURRENT_SUMMARY_ENGINE_VERSION) {
32
+ if (events.length === 0)
33
+ return [];
34
+ const byChat = new Map();
35
+ for (const event of events) {
36
+ const list = byChat.get(event.chatId) ?? [];
37
+ list.push(event);
38
+ byChat.set(event.chatId, list);
39
+ }
40
+ const summaries = [];
41
+ for (const [chatId, chatEvents] of byChat) {
42
+ const sorted = [...chatEvents].sort((a, b) => a.sentAt - b.sentAt);
43
+ let currentEpisode = [];
44
+ const flush = () => {
45
+ if (currentEpisode.length === 0)
46
+ return;
47
+ const minSentAt = currentEpisode[0].sentAt;
48
+ const maxSentAt = currentEpisode[currentEpisode.length - 1].sentAt;
49
+ const messageIds = currentEpisode.map((e) => e.messageId);
50
+ const participantNames = [
51
+ ...new Set(currentEpisode
52
+ .map((e) => e.senderName)
53
+ .filter((n) => Boolean(n))),
54
+ ];
55
+ const ownerHints = [
56
+ ...new Set(currentEpisode.flatMap((e) => e.metadata.mentionNames ?? [])),
57
+ ];
58
+ const hasMentionsSelf = currentEpisode.some((e) => e.mentionsSelf);
59
+ const hasQuestions = currentEpisode.some((e) => /[??]/.test(e.text ?? ''));
60
+ const hasMedia = currentEpisode.some((e) => ['image', 'video', 'file', 'audio'].includes(e.contentType));
61
+ const dialogueActs = inferDialogueActs(currentEpisode);
62
+ const hasSelfQuestion = currentEpisode.some((e) => e.isSelf && /[??]/.test(e.text ?? ''));
63
+ const selfQuestionIdx = currentEpisode.findIndex((e) => e.isSelf && /[??]/.test(e.text ?? ''));
64
+ const hasReplyAfterSelfQuestion = selfQuestionIdx >= 0 &&
65
+ currentEpisode.slice(selfQuestionIdx + 1).some((e) => !e.isSelf);
66
+ const responseState = hasReplyAfterSelfQuestion
67
+ ? 'received'
68
+ : hasSelfQuestion
69
+ ? 'waiting'
70
+ : 'observing';
71
+ const digest = computeDigest(`${chatId}:${minSentAt}:${messageIds.join(',')}`);
72
+ const episodeId = `ep-${chatId.slice(0, 8)}-${minSentAt}`;
73
+ summaries.push({
74
+ episodeId,
75
+ chatId,
76
+ minSentAt,
77
+ maxSentAt,
78
+ messageIds,
79
+ participantNames,
80
+ dialogueActs,
81
+ ownerHints,
82
+ responseState,
83
+ hasMentionsSelf,
84
+ hasQuestions,
85
+ hasMedia,
86
+ candidateCount: hasMentionsSelf || hasSelfQuestion ? 1 : 0,
87
+ digest,
88
+ engineVersion,
89
+ });
90
+ currentEpisode = [];
91
+ };
92
+ for (const event of sorted) {
93
+ if (currentEpisode.length > 0 &&
94
+ event.sentAt - currentEpisode[currentEpisode.length - 1].sentAt >
95
+ EPISODE_GAP_MS) {
96
+ flush();
97
+ }
98
+ currentEpisode.push(event);
99
+ }
100
+ flush();
101
+ }
102
+ return summaries.sort((a, b) => a.minSentAt - b.minSentAt);
103
+ }
104
+ export function buildRollupSummary(windowStart, windowEnd, episodes, engineVersion = CURRENT_SUMMARY_ENGINE_VERSION) {
105
+ const matching = episodes.filter((ep) => ep.maxSentAt >= windowStart && ep.minSentAt <= windowEnd);
106
+ const activeChatIds = [...new Set(matching.map((ep) => ep.chatId))];
107
+ const episodeIds = matching.map((ep) => ep.episodeId);
108
+ const totalEvents = matching.reduce((sum, ep) => sum + ep.messageIds.length, 0);
109
+ const unresolvedFrontierCount = matching.filter((ep) => ep.responseState === 'waiting' || ep.hasMentionsSelf).length;
110
+ const topics = matching
111
+ .filter((ep) => ep.dialogueActs.length > 0)
112
+ .map((ep) => `${ep.chatId}:${ep.dialogueActs.join('+')}`)
113
+ .slice(0, 10);
114
+ const rollupId = `rollup-${Math.floor(windowStart / ROLLUP_INTERVAL_MS)}`;
115
+ const digest = computeDigest(`${rollupId}:${episodeIds.join(',')}:${totalEvents}`);
116
+ return {
117
+ rollupId,
118
+ windowStart,
119
+ windowEnd,
120
+ activeChatIds,
121
+ episodeIds,
122
+ totalEvents,
123
+ unresolvedFrontierCount,
124
+ topics,
125
+ digest,
126
+ engineVersion,
127
+ };
128
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Types for Hierarchical Summaries (10-minute Episodes & 6-hour Rollups).
3
+ */
4
+ export {};
@@ -49,23 +49,29 @@ export async function scoreAttentionBatch(inputs, embedder = getDefaultEmbedder(
49
49
  const messageVectors = vectors.slice(anchors.length);
50
50
  return inputs.map((input, inputIndex) => {
51
51
  if (input.isMuted)
52
- return { score: -100, tier: 'filtered', reason: '已靜音' };
52
+ return { score: -100, tier: 'filtered', reason: 'muted' };
53
53
  if (input.explicitPriority)
54
- return { score: 70, tier: 'now', reason: '使用者標記優先' };
54
+ return { score: 70, tier: 'now', reason: 'manual_priority' };
55
55
  if (input.hasMention)
56
- return { score: 65, tier: 'now', reason: '直接提及你' };
56
+ return { score: 65, tier: 'now', reason: 'direct_mention' };
57
+ if (input.isOfficial &&
58
+ /^\[yomi:(?:image|rich|sticker|video|audio|file|extended_image)\]$/.test(input.text.trim())) {
59
+ return { score: 0, tier: 'filtered', reason: 'ordinary' };
60
+ }
57
61
  const similarities = new Map();
58
62
  anchors.forEach((anchor, anchorIndex) => {
59
63
  const similarity = dot(messageVectors[inputIndex], anchorVectors[anchorIndex]);
60
64
  similarities.set(anchor.intent, Math.max(similarities.get(anchor.intent) ?? -Infinity, similarity));
61
65
  });
62
66
  const ranked = [...intents].sort((a, b) => (similarities.get(b) ?? 0) - (similarities.get(a) ?? 0));
63
- let intent = ranked[0];
64
- // Broadcast accounts commonly announce dated events and use rhetorical
65
- // questions. Only a transaction/account/service state tied to the user may
66
- // elevate them; generic events are not the user's deadline.
67
- if (input.isOfficial && intent !== 'transaction')
68
- intent = 'promotion';
67
+ const intent = ranked[0];
68
+ if (input.isOfficial && intent !== 'transaction') {
69
+ return {
70
+ score: -40,
71
+ tier: 'filtered',
72
+ reason: intent === 'promotion' ? 'promotion' : 'ordinary',
73
+ };
74
+ }
69
75
  const scoreByIntent = {
70
76
  reply: 55,
71
77
  deadline: 60,
@@ -74,7 +80,7 @@ export async function scoreAttentionBatch(inputs, embedder = getDefaultEmbedder(
74
80
  ordinary: 0,
75
81
  promotion: -40,
76
82
  };
77
- const score = scoreByIntent[intent] + (input.unreadCount > 0 ? 5 : 0);
83
+ const score = scoreByIntent[intent];
78
84
  const tier = score >= 55
79
85
  ? 'now'
80
86
  : score >= 35
@@ -83,12 +89,12 @@ export async function scoreAttentionBatch(inputs, embedder = getDefaultEmbedder(
83
89
  ? 'know'
84
90
  : 'filtered';
85
91
  const reasons = {
86
- reply: '對方可能在等你回覆',
87
- deadline: '有期限、行程或緊急事項',
88
- transaction: '與你的交易或服務狀態有關',
89
- important: '重要資訊,但不需立即回覆',
90
- ordinary: '一般資訊,沒有明確下一步',
91
- promotion: '官方宣傳或與你較無關',
92
+ reply: 'reply_expected',
93
+ deadline: 'deadline',
94
+ transaction: 'transaction',
95
+ important: 'important',
96
+ ordinary: 'ordinary',
97
+ promotion: 'promotion',
92
98
  };
93
99
  return { score, tier, reason: reasons[intent] };
94
100
  });
@@ -86,7 +86,7 @@ class VolatileCacheStore {
86
86
  cache = new Map();
87
87
  loaded = false;
88
88
  dirty = false;
89
- writing = false;
89
+ flushPromise = null;
90
90
  constructor(filePath) {
91
91
  this.filePath = filePath;
92
92
  }
@@ -139,11 +139,10 @@ class VolatileCacheStore {
139
139
  /** Coalesce bursts of writes into a single atomic flush cycle. */
140
140
  scheduleFlush() {
141
141
  this.dirty = true;
142
- if (this.writing) {
142
+ if (this.flushPromise) {
143
143
  return;
144
144
  }
145
- this.writing = true;
146
- void (async () => {
145
+ this.flushPromise = (async () => {
147
146
  const fs = await import('node:fs/promises');
148
147
  while (this.dirty) {
149
148
  this.dirty = false;
@@ -169,8 +168,13 @@ class VolatileCacheStore {
169
168
  // Best-effort cache persistence; in-memory copy still serves reads.
170
169
  }
171
170
  }
172
- this.writing = false;
173
- })();
171
+ })().finally(() => {
172
+ this.flushPromise = null;
173
+ });
174
+ }
175
+ async flush() {
176
+ while (this.flushPromise)
177
+ await this.flushPromise;
174
178
  }
175
179
  }
176
180
  /**
@@ -361,6 +365,11 @@ export class CredentialStore {
361
365
  return persisted;
362
366
  });
363
367
  }
368
+ /** Await durable and volatile write queues, primarily for orderly shutdown. */
369
+ async flush() {
370
+ await this.persistQueue;
371
+ await this.volatile.flush();
372
+ }
364
373
  /**
365
374
  * Delete a credential and persist.
366
375
  * @param key - Credential key to delete.