@rikaidev/yomi 0.4.2 → 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 (86) hide show
  1. package/README.md +46 -4
  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 +101 -0
  13. package/dist/auth/credential-store.js +47 -17
  14. package/dist/auth/keychain.js +7 -54
  15. package/dist/cli/query.js +875 -0
  16. package/dist/cli/work-item.js +24 -0
  17. package/dist/insight/relationships.js +1 -1
  18. package/dist/line/auth/protocol/pwless-login-flow.js +12 -0
  19. package/dist/line/auth/pwless/index.js +1 -1
  20. package/dist/line/client/auth-service/client.js +7 -8
  21. package/dist/line/client/buddy-service/client.js +73 -0
  22. package/dist/line/client/index.js +28 -3
  23. package/dist/line/client/obs-media-client.js +38 -0
  24. package/dist/line/client/parsers.js +1 -0
  25. package/dist/line/client/square-service/client.js +298 -0
  26. package/dist/line/client/sticker-meta.js +32 -0
  27. package/dist/line/client/sync-service/client.js +31 -3
  28. package/dist/line/client/talk-service/command-client.js +9 -1
  29. package/dist/line/client/talk-service/contact-query.js +8 -1
  30. package/dist/line/client/talk-service/message-box-query.js +17 -0
  31. package/dist/line/client/talk-service/message-id-query.js +27 -3
  32. package/dist/line/client/talk-service/query-client.js +32 -26
  33. package/dist/line/client/talk-service/requests.js +10 -11
  34. package/dist/line/core/auth-session-runtime.js +17 -2
  35. package/dist/line/core/chat-event.js +35 -0
  36. package/dist/line/core/chat-runtime-service.js +18 -23
  37. package/dist/line/core/config.js +1 -0
  38. package/dist/line/core/directory-runtime-service.js +10 -2
  39. package/dist/line/core/e2ee/recovery/bootstrap-keys.js +8 -16
  40. package/dist/line/core/flex-message.js +453 -0
  41. package/dist/line/core/mention.js +1 -1
  42. package/dist/line/core/message-preview.js +60 -0
  43. package/dist/line/core/message-query-service.js +0 -20
  44. package/dist/line/core/service.js +1 -3
  45. package/dist/mcp/handlers/contacts.js +12 -6
  46. package/dist/mcp/handlers/conversations.js +91 -4
  47. package/dist/mcp/handlers/login.js +1 -1
  48. package/dist/mcp/handlers/message-actions.js +1 -1
  49. package/dist/mcp/handlers/search.js +1 -1
  50. package/dist/mcp/handlers/send.js +8 -11
  51. package/dist/mcp/handlers/shared.js +1 -1
  52. package/dist/mcp/integrator.js +168 -0
  53. package/dist/mcp/media.js +135 -4
  54. package/dist/mcp/names.js +78 -6
  55. package/dist/mcp/phi-guard.js +2 -2
  56. package/dist/search/bigram.js +2 -2
  57. package/dist/search/collector.js +1 -1
  58. package/dist/search/default-embedder.js +12 -3
  59. package/dist/search/embedder.js +1 -1
  60. package/dist/version.js +1 -1
  61. package/dist/work-item/adapter.js +1 -0
  62. package/dist/work-item/destination-contracts.js +1 -0
  63. package/dist/work-item/event-context.js +73 -0
  64. package/dist/work-item/gold-contract.js +101 -0
  65. package/dist/work-item/gold-coverage.js +96 -0
  66. package/dist/work-item/gold-derivation.js +67 -0
  67. package/dist/work-item/gold-split.js +143 -0
  68. package/dist/work-item/index.js +23 -0
  69. package/dist/work-item/line-adapter.js +111 -0
  70. package/dist/work-item/llama-cpp-work-event-adapter.js +239 -0
  71. package/dist/work-item/local-provider.js +38 -0
  72. package/dist/work-item/local-work-event-provider.js +56 -0
  73. package/dist/work-item/onnx-work-event-adapter.js +223 -0
  74. package/dist/work-item/provider.js +21 -0
  75. package/dist/work-item/registry.js +19 -0
  76. package/dist/work-item/service.js +85 -0
  77. package/dist/work-item/store.js +97 -0
  78. package/dist/work-item/types.js +79 -0
  79. package/dist/work-item/work-event-graph-engine.js +63 -0
  80. package/dist/work-item/work-event-inference.js +267 -0
  81. package/dist/work-item/work-event-matcher.js +105 -0
  82. package/dist/work-item/work-event-model-manifest.js +39 -0
  83. package/dist/work-item/work-event-store.js +125 -0
  84. package/dist/work-item/work-event.js +123 -0
  85. package/package.json +26 -2
  86. package/run.mjs +371 -2
@@ -0,0 +1,267 @@
1
+ const DIALOGUE_ACTS = new Set([
2
+ 'inform',
3
+ 'ask',
4
+ 'answer',
5
+ 'directive',
6
+ 'commit',
7
+ 'delegate',
8
+ 'acknowledge',
9
+ 'deliver',
10
+ 'accept',
11
+ 'reject',
12
+ 'cancel',
13
+ ]);
14
+ const OWNERSHIP_STATES = new Set([
15
+ 'unassigned',
16
+ 'owned',
17
+ 'delegated',
18
+ 'unknown',
19
+ ]);
20
+ const PARTICIPANT_KINDS = new Set([
21
+ 'self',
22
+ 'conversation_member',
23
+ 'external_party',
24
+ 'unknown',
25
+ ]);
26
+ const DEPENDENCY_STATES = new Set([
27
+ 'none',
28
+ 'waiting_person',
29
+ 'waiting_external_system',
30
+ 'waiting_decision',
31
+ 'unknown',
32
+ ]);
33
+ const LIFECYCLES = new Set([
34
+ 'proposed',
35
+ 'committed',
36
+ 'in_progress',
37
+ 'blocked',
38
+ 'delivered',
39
+ 'accepted',
40
+ 'cancelled',
41
+ 'resolved',
42
+ 'uncertain',
43
+ ]);
44
+ const DEFAULT_THRESHOLDS = {
45
+ createsWork: 0.82,
46
+ action: 0.78,
47
+ ownership: 0.8,
48
+ dependency: 0.76,
49
+ lifecycle: 0.82,
50
+ completion: 0.86,
51
+ };
52
+ function isRecord(value) {
53
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
54
+ }
55
+ function validConfidence(value) {
56
+ return (typeof value === 'number' &&
57
+ Number.isFinite(value) &&
58
+ value >= 0 &&
59
+ value <= 1);
60
+ }
61
+ function validEvidenceArray(value) {
62
+ return (Array.isArray(value) &&
63
+ value.every((item) => typeof item === 'string' && item.length > 0));
64
+ }
65
+ /** Runtime boundary for untrusted JSON emitted by a local model. */
66
+ export function validateFactorizedGraphModelOutput(value) {
67
+ const errors = [];
68
+ if (!isRecord(value))
69
+ return ['model_output_not_object'];
70
+ if (typeof value.modelId !== 'string' || !value.modelId)
71
+ errors.push('model_id_invalid');
72
+ if (!Array.isArray(value.dialogue))
73
+ errors.push('dialogue_invalid');
74
+ else
75
+ for (const item of value.dialogue) {
76
+ if (!isRecord(item) ||
77
+ typeof item.messageId !== 'string' ||
78
+ !Array.isArray(item.acts) ||
79
+ item.acts.some((act) => !DIALOGUE_ACTS.has(act)) ||
80
+ !validConfidence(item.confidence))
81
+ errors.push('dialogue_item_invalid');
82
+ }
83
+ if (!Array.isArray(value.candidates))
84
+ errors.push('candidates_invalid');
85
+ else
86
+ for (const candidate of value.candidates) {
87
+ if (!isRecord(candidate) ||
88
+ typeof candidate.candidateKey !== 'string' ||
89
+ !candidate.candidateKey) {
90
+ errors.push('candidate_key_invalid');
91
+ continue;
92
+ }
93
+ for (const name of [
94
+ 'createsWork',
95
+ 'ownership',
96
+ 'dependency',
97
+ 'lifecycle',
98
+ 'completion',
99
+ ]) {
100
+ const field = candidate[name];
101
+ if (!isRecord(field) ||
102
+ !validConfidence(field.confidence) ||
103
+ !validEvidenceArray(field.evidenceMessageIds))
104
+ errors.push(`${name}_field_invalid`);
105
+ }
106
+ if (!isRecord(candidate.createsWork) ||
107
+ typeof candidate.createsWork.value !== 'boolean')
108
+ errors.push('creates_work_value_invalid');
109
+ if (candidate.action !== null) {
110
+ if (!isRecord(candidate.action) ||
111
+ typeof candidate.action.predicate !== 'string' ||
112
+ !candidate.action.predicate ||
113
+ !validEvidenceArray(candidate.action.sourceMessageIds) ||
114
+ candidate.action.sourceMessageIds.length === 0 ||
115
+ !validConfidence(candidate.action.confidence))
116
+ errors.push('action_invalid');
117
+ }
118
+ const ownership = candidate.ownership?.value;
119
+ if (!isRecord(ownership) ||
120
+ !OWNERSHIP_STATES.has(ownership.state) ||
121
+ !Array.isArray(ownership.owners) ||
122
+ ownership.owners.some((owner) => !isRecord(owner) || !PARTICIPANT_KINDS.has(owner.kind)))
123
+ errors.push('ownership_value_invalid');
124
+ const dependency = candidate.dependency?.value;
125
+ if (!isRecord(dependency) || !DEPENDENCY_STATES.has(dependency.state))
126
+ errors.push('dependency_value_invalid');
127
+ if (!isRecord(candidate.lifecycle) ||
128
+ !LIFECYCLES.has(candidate.lifecycle.value))
129
+ errors.push('lifecycle_value_invalid');
130
+ const completion = candidate.completion?.value;
131
+ if (!isRecord(completion) ||
132
+ ['delivered', 'accepted', 'cancelled'].some((key) => typeof completion[key] !== 'boolean'))
133
+ errors.push('completion_value_invalid');
134
+ }
135
+ return [...new Set(errors)];
136
+ }
137
+ function validEvidence(ids, available) {
138
+ return ids.length === 0 || ids.every((id) => available.has(id));
139
+ }
140
+ /** Validate, calibrate and project model output. It never converts uncertainty into no-work. */
141
+ export function projectFactorizedInference(context, output, thresholds = DEFAULT_THRESHOLDS, inferredAt = Date.now()) {
142
+ const reasons = [];
143
+ const available = new Set(context.messages.map((message) => message.messageId));
144
+ if (!context.complete)
145
+ reasons.push(`context_${context.boundary}`);
146
+ if (output.dialogue.some((item) => !available.has(item.messageId)))
147
+ reasons.push('dialogue_evidence_outside_context');
148
+ if (!validEvidence(output.createsWork.evidenceMessageIds, available))
149
+ reasons.push('work_evidence_outside_context');
150
+ if (output.createsWork.confidence < thresholds.createsWork)
151
+ reasons.push('work_confidence_low');
152
+ if (output.createsWork.value && !output.action)
153
+ reasons.push('work_action_missing');
154
+ if (output.action) {
155
+ if (output.action.sourceMessageIds.length === 0)
156
+ reasons.push('action_source_missing');
157
+ if (!validEvidence(output.action.sourceMessageIds, available))
158
+ reasons.push('action_source_outside_context');
159
+ if (output.action.confidence < thresholds.action)
160
+ reasons.push('action_confidence_low');
161
+ }
162
+ for (const [name, field, threshold] of [
163
+ ['ownership', output.ownership, thresholds.ownership],
164
+ ['dependency', output.dependency, thresholds.dependency],
165
+ ['lifecycle', output.lifecycle, thresholds.lifecycle],
166
+ ['completion', output.completion, thresholds.completion],
167
+ ]) {
168
+ if (field.confidence < threshold)
169
+ reasons.push(`${name}_confidence_low`);
170
+ if (!validEvidence(field.evidenceMessageIds, available))
171
+ reasons.push(`${name}_evidence_outside_context`);
172
+ }
173
+ if (output.completion.value.accepted && !output.completion.value.delivered)
174
+ reasons.push('acceptance_without_delivery');
175
+ if (output.lifecycle.value === 'accepted' &&
176
+ !output.completion.value.accepted)
177
+ reasons.push('accepted_lifecycle_without_evidence');
178
+ if (output.lifecycle.value === 'cancelled' &&
179
+ !output.completion.value.cancelled)
180
+ reasons.push('cancelled_lifecycle_without_evidence');
181
+ if (reasons.length > 0)
182
+ return { kind: 'abstain', reasons, modelId: output.modelId };
183
+ return {
184
+ kind: 'inference',
185
+ inference: {
186
+ eventId: context.anchorMessageIds.slice().sort().join(':'),
187
+ chatId: context.chatId,
188
+ evidence: context.messages.map((message) => ({
189
+ messageId: message.messageId,
190
+ chatId: message.chatId,
191
+ sentAt: message.sentAt,
192
+ speaker: message.isSelf ? 'self' : 'other',
193
+ })),
194
+ dialogue: output.dialogue,
195
+ createsWork: output.createsWork.value,
196
+ workConfidence: output.createsWork.confidence,
197
+ action: output.action
198
+ ? {
199
+ predicate: output.action.predicate,
200
+ object: output.action.object,
201
+ expectedOutcome: output.action.expectedOutcome,
202
+ dueAt: output.action.dueAt,
203
+ confidence: output.action.confidence,
204
+ evidenceMessageIds: output.action.sourceMessageIds,
205
+ }
206
+ : null,
207
+ ownership: {
208
+ ...output.ownership.value,
209
+ confidence: output.ownership.confidence,
210
+ evidenceMessageIds: output.ownership.evidenceMessageIds,
211
+ },
212
+ dependency: {
213
+ ...output.dependency.value,
214
+ confidence: output.dependency.confidence,
215
+ evidenceMessageIds: output.dependency.evidenceMessageIds,
216
+ },
217
+ lifecycle: output.lifecycle.value,
218
+ completion: {
219
+ ...output.completion.value,
220
+ confidence: output.completion.confidence,
221
+ evidenceMessageIds: output.completion.evidenceMessageIds,
222
+ },
223
+ sourceModel: output.modelId,
224
+ inferredAt,
225
+ },
226
+ };
227
+ }
228
+ /** Project every hypothesis independently; one utterance may support several events. */
229
+ export function projectFactorizedGraphInference(context, output, thresholds = DEFAULT_THRESHOLDS, inferredAt = Date.now()) {
230
+ const validationErrors = validateFactorizedGraphModelOutput(output);
231
+ if (validationErrors.length > 0) {
232
+ return {
233
+ dialogue: [],
234
+ candidates: [
235
+ {
236
+ candidateKey: '',
237
+ result: {
238
+ kind: 'abstain',
239
+ reasons: validationErrors.map((error) => `model_output_${error}`),
240
+ modelId: typeof output?.modelId === 'string' ? output.modelId : 'unknown',
241
+ },
242
+ },
243
+ ],
244
+ modelId: typeof output?.modelId === 'string' ? output.modelId : 'unknown',
245
+ };
246
+ }
247
+ const keys = new Set();
248
+ const candidates = output.candidates.map((candidate) => {
249
+ if (!candidate.candidateKey || keys.has(candidate.candidateKey)) {
250
+ return {
251
+ candidateKey: candidate.candidateKey,
252
+ result: {
253
+ kind: 'abstain',
254
+ reasons: ['candidate_key_invalid'],
255
+ modelId: output.modelId,
256
+ },
257
+ };
258
+ }
259
+ keys.add(candidate.candidateKey);
260
+ const result = projectFactorizedInference(context, { ...candidate, dialogue: output.dialogue, modelId: output.modelId }, thresholds, inferredAt);
261
+ if (result.kind === 'inference') {
262
+ result.inference.eventId = `${context.anchorMessageIds.slice().sort().join(':')}:${candidate.candidateKey}`;
263
+ }
264
+ return { candidateKey: candidate.candidateKey, result };
265
+ });
266
+ return { dialogue: output.dialogue, candidates, modelId: output.modelId };
267
+ }
@@ -0,0 +1,105 @@
1
+ function ngrams(value, size = 2) {
2
+ const normalized = value
3
+ .normalize('NFKC')
4
+ .toLocaleLowerCase()
5
+ .replace(/[\p{P}\p{S}\s]+/gu, '');
6
+ if (!normalized)
7
+ return new Set();
8
+ if (normalized.length <= size)
9
+ return new Set([normalized]);
10
+ return new Set(Array.from({ length: normalized.length - size + 1 }, (_, index) => normalized.slice(index, index + size)));
11
+ }
12
+ function jaccard(left, right) {
13
+ if (left.size === 0 && right.size === 0)
14
+ return 1;
15
+ if (left.size === 0 || right.size === 0)
16
+ return 0;
17
+ let intersection = 0;
18
+ for (const value of left)
19
+ if (right.has(value))
20
+ intersection += 1;
21
+ return intersection / (left.size + right.size - intersection);
22
+ }
23
+ function actionText(event) {
24
+ if (!event.action)
25
+ return '';
26
+ return [
27
+ event.action.predicate,
28
+ event.action.object,
29
+ event.action.expectedOutcome,
30
+ ]
31
+ .filter(Boolean)
32
+ .join(' ');
33
+ }
34
+ export function workEventSimilarity(previous, incoming) {
35
+ if (previous.chatId !== incoming.chatId ||
36
+ previous.createsWork !== incoming.createsWork)
37
+ return 0;
38
+ const evidence = jaccard(new Set(previous.evidence.map((item) => item.messageId)), new Set(incoming.evidence.map((item) => item.messageId)));
39
+ const action = jaccard(ngrams(actionText(previous)), ngrams(actionText(incoming)));
40
+ const previousOwners = new Set(previous.ownership.owners.map((owner) => owner.kind));
41
+ const incomingOwners = new Set(incoming.ownership.owners.map((owner) => owner.kind));
42
+ const ownership = jaccard(previousOwners, incomingOwners);
43
+ return Number((evidence * 0.45 + action * 0.45 + ownership * 0.1).toFixed(4));
44
+ }
45
+ /** Maximum-weight one-to-one matching keeps sibling events distinct across revisions. */
46
+ export function matchWorkEventRevisions(previous, incoming, minimumScore = 0.55) {
47
+ const candidates = incoming
48
+ .flatMap((next, incomingIndex) => previous.map((prior, previousIndex) => ({
49
+ incomingIndex,
50
+ previousIndex,
51
+ score: workEventSimilarity(prior, next),
52
+ })))
53
+ .filter((match) => match.score >= minimumScore)
54
+ .sort((left, right) => right.score - left.score ||
55
+ left.incomingIndex - right.incomingIndex ||
56
+ left.previousIndex - right.previousIndex);
57
+ if (previous.length <= 16 && incoming.length <= 16) {
58
+ const byIncoming = new Map();
59
+ for (const candidate of candidates) {
60
+ byIncoming.set(candidate.incomingIndex, [
61
+ ...(byIncoming.get(candidate.incomingIndex) ?? []),
62
+ candidate,
63
+ ]);
64
+ }
65
+ const memo = new Map();
66
+ const solve = (incomingIndex, claimedMask) => {
67
+ if (incomingIndex >= incoming.length)
68
+ return { score: 0, matches: [] };
69
+ const key = `${incomingIndex}:${claimedMask}`;
70
+ const cached = memo.get(key);
71
+ if (cached)
72
+ return cached;
73
+ let best = solve(incomingIndex + 1, claimedMask);
74
+ for (const candidate of byIncoming.get(incomingIndex) ?? []) {
75
+ const bit = 1 << candidate.previousIndex;
76
+ if ((claimedMask & bit) !== 0)
77
+ continue;
78
+ const rest = solve(incomingIndex + 1, claimedMask | bit);
79
+ const option = {
80
+ score: candidate.score + rest.score,
81
+ matches: [candidate, ...rest.matches],
82
+ };
83
+ if (option.score > best.score ||
84
+ (option.score === best.score &&
85
+ option.matches.length > best.matches.length))
86
+ best = option;
87
+ }
88
+ memo.set(key, best);
89
+ return best;
90
+ };
91
+ return solve(0, 0).matches.sort((left, right) => left.incomingIndex - right.incomingIndex);
92
+ }
93
+ const claimedIncoming = new Set();
94
+ const claimedPrevious = new Set();
95
+ const matches = [];
96
+ for (const candidate of candidates) {
97
+ if (claimedIncoming.has(candidate.incomingIndex) ||
98
+ claimedPrevious.has(candidate.previousIndex))
99
+ continue;
100
+ claimedIncoming.add(candidate.incomingIndex);
101
+ claimedPrevious.add(candidate.previousIndex);
102
+ matches.push(candidate);
103
+ }
104
+ return matches.sort((left, right) => left.incomingIndex - right.incomingIndex);
105
+ }
@@ -0,0 +1,39 @@
1
+ function validDigest(value) {
2
+ return /^[a-f0-9]{64}$/u.test(value);
3
+ }
4
+ export function productionEligibilityErrors(manifest) {
5
+ const errors = [];
6
+ if (manifest.schemaVersion !== 1)
7
+ errors.push('manifest_schema_unsupported');
8
+ if (!manifest.modelId)
9
+ errors.push('model_id_missing');
10
+ if (!validDigest(manifest.artifactSha256))
11
+ errors.push('artifact_digest_invalid');
12
+ if (!manifest.quantization)
13
+ errors.push('quantization_missing');
14
+ if (!manifest.sourceModel)
15
+ errors.push('source_model_missing');
16
+ if (!manifest.datasetRevision)
17
+ errors.push('dataset_revision_missing');
18
+ for (const [name, evidence] of Object.entries({
19
+ conversationSplit: manifest.conversationSplit,
20
+ factorizedCoverage: manifest.factorizedCoverage,
21
+ publicAcceptance: manifest.publicAcceptance,
22
+ privateAcceptance: manifest.privateAcceptance,
23
+ quantizationValidation: manifest.quantizationValidation,
24
+ targetHardwareBenchmark: manifest.targetHardwareBenchmark,
25
+ })) {
26
+ if (!evidence?.passed)
27
+ errors.push(`${name}_not_passed`);
28
+ if (!evidence || !validDigest(evidence.reportSha256))
29
+ errors.push(`${name}_report_digest_invalid`);
30
+ }
31
+ if (!manifest.modelOwnerApproved)
32
+ errors.push('model_owner_not_approved');
33
+ return errors;
34
+ }
35
+ export function assertProductionEligibleModel(manifest) {
36
+ const errors = productionEligibilityErrors(manifest);
37
+ if (errors.length > 0)
38
+ throw new Error(`Work-event model is not production eligible: ${errors.join(', ')}`);
39
+ }
@@ -0,0 +1,125 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ import { yomiDataPath } from '../util/data-dir.js';
5
+ function digest(value) {
6
+ return createHash('sha256').update(value).digest('hex');
7
+ }
8
+ function persist(revision) {
9
+ return {
10
+ eventIdHash: digest(revision.eventId),
11
+ chatIdHash: digest(revision.chatId),
12
+ schemaVersion: 1,
13
+ revision: revision.revision,
14
+ previousRevision: revision.previousRevision,
15
+ active: revision.active,
16
+ evidence: revision.evidence.map((item) => ({
17
+ messageIdHash: digest(item.messageId),
18
+ sentAt: item.sentAt,
19
+ speaker: item.speaker,
20
+ })),
21
+ dialogue: revision.dialogue.map((item) => ({
22
+ ...item,
23
+ messageId: digest(item.messageId),
24
+ })),
25
+ createsWork: revision.createsWork,
26
+ workConfidence: revision.workConfidence,
27
+ action: revision.action
28
+ ? {
29
+ predicateHash: digest(revision.action.predicate),
30
+ objectHash: revision.action.object
31
+ ? digest(revision.action.object)
32
+ : null,
33
+ expectedOutcomeHash: revision.action.expectedOutcome
34
+ ? digest(revision.action.expectedOutcome)
35
+ : null,
36
+ dueAt: revision.action.dueAt,
37
+ confidence: revision.action.confidence,
38
+ evidenceMessageIdHashes: revision.action.evidenceMessageIds.map(digest),
39
+ }
40
+ : null,
41
+ ownership: {
42
+ state: revision.ownership.state,
43
+ confidence: revision.ownership.confidence,
44
+ evidenceMessageIdHashes: revision.ownership.evidenceMessageIds.map(digest),
45
+ owners: revision.ownership.owners.map((owner) => ({
46
+ kind: owner.kind,
47
+ stableIdHash: owner.stableId ? digest(owner.stableId) : null,
48
+ displayNameHash: owner.displayName ? digest(owner.displayName) : null,
49
+ })),
50
+ },
51
+ dependency: {
52
+ state: revision.dependency.state,
53
+ confidence: revision.dependency.confidence,
54
+ evidenceMessageIdHashes: revision.dependency.evidenceMessageIds.map(digest),
55
+ on: revision.dependency.on
56
+ ? {
57
+ kind: revision.dependency.on.kind,
58
+ stableIdHash: revision.dependency.on.stableId
59
+ ? digest(revision.dependency.on.stableId)
60
+ : null,
61
+ displayNameHash: revision.dependency.on.displayName
62
+ ? digest(revision.dependency.on.displayName)
63
+ : null,
64
+ }
65
+ : null,
66
+ },
67
+ lifecycle: revision.lifecycle,
68
+ completion: {
69
+ ...revision.completion,
70
+ evidenceMessageIds: revision.completion.evidenceMessageIds.map(digest),
71
+ },
72
+ sourceModel: revision.sourceModel,
73
+ inferredAt: revision.inferredAt,
74
+ reasons: revision.reasons,
75
+ };
76
+ }
77
+ /** Metadata-only audit history; decrypted text remains in the secure UI cache. */
78
+ export class WorkEventRevisionStore {
79
+ path;
80
+ maxRevisions;
81
+ writeQueue = Promise.resolve();
82
+ constructor(path = yomiDataPath('work-event-revisions.json'), maxRevisions = 2000) {
83
+ this.path = path;
84
+ this.maxRevisions = maxRevisions;
85
+ }
86
+ async append(revision) {
87
+ await this.appendMany([revision]);
88
+ }
89
+ async appendMany(incoming) {
90
+ if (incoming.length === 0)
91
+ return;
92
+ const run = async () => {
93
+ const current = await this.read();
94
+ const rows = incoming.map(persist);
95
+ const incomingKeys = new Set(rows.map((row) => `${row.eventIdHash}:${row.revision}`));
96
+ const revisions = [
97
+ ...rows,
98
+ ...current.filter((item) => !incomingKeys.has(`${item.eventIdHash}:${item.revision}`)),
99
+ ]
100
+ .sort((left, right) => right.inferredAt - left.inferredAt)
101
+ .slice(0, this.maxRevisions);
102
+ await mkdir(dirname(this.path), { recursive: true });
103
+ const temporary = `${this.path}.tmp-${process.pid}`;
104
+ await writeFile(temporary, JSON.stringify({ schemaVersion: 1, revisions }), { mode: 0o600 });
105
+ await rename(temporary, this.path);
106
+ };
107
+ this.writeQueue = this.writeQueue.then(run, run);
108
+ await this.writeQueue;
109
+ }
110
+ async list() {
111
+ await this.writeQueue;
112
+ return this.read();
113
+ }
114
+ async read() {
115
+ try {
116
+ const value = JSON.parse(await readFile(this.path, 'utf8'));
117
+ return value.schemaVersion === 1 && Array.isArray(value.revisions)
118
+ ? value.revisions
119
+ : [];
120
+ }
121
+ catch {
122
+ return [];
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,123 @@
1
+ function sameValue(left, right) {
2
+ return JSON.stringify(left) === JSON.stringify(right);
3
+ }
4
+ function evidenceUnion(previous, incoming) {
5
+ return [
6
+ ...new Map([...previous, ...incoming].map((item) => [item.messageId, item])).values(),
7
+ ].sort((left, right) => left.sentAt - right.sentAt);
8
+ }
9
+ /**
10
+ * Append a new authoritative revision without making lifecycle monotonic.
11
+ * A later event window may disprove an earlier completion and reopen work.
12
+ */
13
+ export function reviseWorkEvent(previous, inference, explicitReasons = []) {
14
+ if (!previous) {
15
+ return {
16
+ ...inference,
17
+ schemaVersion: 1,
18
+ revision: 1,
19
+ previousRevision: null,
20
+ active: inference.createsWork,
21
+ reasons: ['created', ...explicitReasons],
22
+ };
23
+ }
24
+ if (previous.eventId !== inference.eventId) {
25
+ throw new Error('Cannot revise a different work event');
26
+ }
27
+ const evidence = evidenceUnion(previous.evidence, inference.evidence);
28
+ const evidenceAdded = evidence.length > previous.evidence.length;
29
+ const inferenceChanged = !sameValue({
30
+ dialogue: previous.dialogue,
31
+ createsWork: previous.createsWork,
32
+ action: previous.action,
33
+ ownership: previous.ownership,
34
+ dependency: previous.dependency,
35
+ lifecycle: previous.lifecycle,
36
+ completion: previous.completion,
37
+ }, {
38
+ dialogue: inference.dialogue,
39
+ createsWork: inference.createsWork,
40
+ action: inference.action,
41
+ ownership: inference.ownership,
42
+ dependency: inference.dependency,
43
+ lifecycle: inference.lifecycle,
44
+ completion: inference.completion,
45
+ });
46
+ const wasClosed = ['accepted', 'cancelled', 'resolved'].includes(previous.lifecycle);
47
+ const isOpen = ['proposed', 'committed', 'in_progress', 'blocked'].includes(inference.lifecycle);
48
+ const reasons = [
49
+ ...(evidenceAdded ? ['evidence_added'] : []),
50
+ ...(inferenceChanged ? ['inference_changed'] : []),
51
+ ...(wasClosed && isOpen ? ['reopened'] : []),
52
+ ...explicitReasons,
53
+ ];
54
+ return {
55
+ ...inference,
56
+ evidence,
57
+ schemaVersion: 1,
58
+ revision: previous.revision + 1,
59
+ previousRevision: previous.revision,
60
+ active: inference.createsWork &&
61
+ !['accepted', 'cancelled', 'resolved'].includes(inference.lifecycle),
62
+ reasons: [...new Set(reasons)],
63
+ };
64
+ }
65
+ export function excludeWorkEvent(previous, temporary = false) {
66
+ return {
67
+ ...previous,
68
+ revision: previous.revision + 1,
69
+ previousRevision: previous.revision,
70
+ active: false,
71
+ reasons: [temporary ? 'temporarily_excluded' : 'excluded'],
72
+ };
73
+ }
74
+ export function splitWorkEvent(previous, childInferences) {
75
+ if (childInferences.length < 2)
76
+ throw new Error('A split requires at least two child events');
77
+ const previousEvidence = new Set(previous.evidence.map((item) => item.messageId));
78
+ const childIds = new Set();
79
+ for (const child of childInferences) {
80
+ if (childIds.has(child.eventId))
81
+ throw new Error('Split children require distinct event ids');
82
+ childIds.add(child.eventId);
83
+ if (child.evidence.length === 0)
84
+ throw new Error('Split child requires evidence');
85
+ for (const evidence of child.evidence) {
86
+ if (!previousEvidence.has(evidence.messageId))
87
+ throw new Error('Split child contains evidence outside the parent event');
88
+ }
89
+ }
90
+ const superseded = {
91
+ ...previous,
92
+ revision: previous.revision + 1,
93
+ previousRevision: previous.revision,
94
+ active: false,
95
+ reasons: ['split'],
96
+ };
97
+ return {
98
+ superseded,
99
+ children: childInferences.map((child) => ({
100
+ ...reviseWorkEvent(null, child, ['split']),
101
+ previousRevision: previous.revision,
102
+ })),
103
+ };
104
+ }
105
+ export function mergeWorkEvents(previous, mergedInference) {
106
+ if (previous.length < 2)
107
+ throw new Error('A merge requires at least two source events');
108
+ const sourceEvidence = new Set(previous.flatMap((event) => event.evidence.map((item) => item.messageId)));
109
+ for (const evidence of mergedInference.evidence) {
110
+ if (!sourceEvidence.has(evidence.messageId))
111
+ throw new Error('Merged event contains unknown evidence');
112
+ }
113
+ return {
114
+ superseded: previous.map((event) => ({
115
+ ...event,
116
+ revision: event.revision + 1,
117
+ previousRevision: event.revision,
118
+ active: false,
119
+ reasons: ['merged'],
120
+ })),
121
+ merged: reviseWorkEvent(null, mergedInference, ['merged']),
122
+ };
123
+ }