@bizagent/dsh 0.0.0-stage → 1.0.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 (181) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/LICENSE +21 -0
  3. package/README.md +61 -2
  4. package/cordis.patch.yml +14 -0
  5. package/docs/README.md +23 -0
  6. package/docs/architecture.md +154 -0
  7. package/docs/concepts.md +192 -0
  8. package/docs/configuration.md +154 -0
  9. package/docs/quickstart.md +103 -0
  10. package/docs/quickstart.zh-CN.md +86 -0
  11. package/docs/tools.md +607 -0
  12. package/docs/user-guide.zh-CN.md +132 -0
  13. package/docs/verification.md +69 -0
  14. package/examples/README.md +64 -0
  15. package/examples/demo-runtime.mjs +71 -0
  16. package/examples/learning-lab.mjs +263 -0
  17. package/examples/scenarios.mjs +45 -0
  18. package/lib/agent-preset.d.ts +20 -0
  19. package/lib/agent-preset.js +46 -0
  20. package/lib/agent-session-address.d.ts +14 -0
  21. package/lib/agent-session-address.js +15 -0
  22. package/lib/agent-session-coordinator.d.ts +24 -0
  23. package/lib/agent-session-coordinator.js +101 -0
  24. package/lib/cli.d.ts +3 -0
  25. package/lib/cli.js +142 -0
  26. package/lib/client-types/agent-session-address.d.ts +13 -0
  27. package/lib/client-types/client/agent-mentions.d.ts +3 -0
  28. package/lib/client-types/client/api.d.ts +180 -0
  29. package/lib/client-types/client/components.d.ts +22 -0
  30. package/lib/client-types/client/controller.d.ts +23 -0
  31. package/lib/client-types/client/frame-participants.d.ts +5 -0
  32. package/lib/client-types/client/home-name.d.ts +9 -0
  33. package/lib/client-types/client/human-inbox.d.ts +8 -0
  34. package/lib/client-types/client/index.d.ts +13 -0
  35. package/lib/client-types/client/locales.d.ts +796 -0
  36. package/lib/client-types/client/markdown.d.ts +12 -0
  37. package/lib/client-types/client/project-artifact-link.d.ts +10 -0
  38. package/lib/client-types/client/project-outcome-review-inbox.d.ts +13 -0
  39. package/lib/client-types/client/projects.d.ts +11 -0
  40. package/lib/client-types/client/sample-guide.d.ts +9 -0
  41. package/lib/client-types/client/session-resource.d.ts +34 -0
  42. package/lib/client-types/client/session-surfaces.d.ts +24 -0
  43. package/lib/client-types/client/styles.d.ts +1 -0
  44. package/lib/client-types/config.d.ts +55 -0
  45. package/lib/client-types/cron.d.ts +8 -0
  46. package/lib/client-types/domain.d.ts +818 -0
  47. package/lib/client-types/frame-reference.d.ts +4 -0
  48. package/lib/client-types/home-directory.d.ts +5 -0
  49. package/lib/client-types/home-space.d.ts +49 -0
  50. package/lib/client-types/home-store.d.ts +125 -0
  51. package/lib/client-types/inbox-stalled-topic.d.ts +15 -0
  52. package/lib/client-types/inbox-wake-policy.d.ts +18 -0
  53. package/lib/client-types/known-format.d.ts +2 -0
  54. package/lib/client-types/learning-scheduler.d.ts +36 -0
  55. package/lib/client-types/learning-session-runner.d.ts +49 -0
  56. package/lib/client-types/learning-trigger.d.ts +17 -0
  57. package/lib/client-types/learning-usage.d.ts +17 -0
  58. package/lib/client-types/ledger.d.ts +190 -0
  59. package/lib/client-types/local-owner.d.ts +7 -0
  60. package/lib/client-types/message-source.d.ts +9 -0
  61. package/lib/client-types/organization-location.d.ts +19 -0
  62. package/lib/client-types/project-ledger.d.ts +182 -0
  63. package/lib/client-types/project-seat-context.d.ts +17 -0
  64. package/lib/client-types/safe-file.d.ts +23 -0
  65. package/lib/client-types/sample-organization.d.ts +9 -0
  66. package/lib/client-types/service.d.ts +596 -0
  67. package/lib/client-types/shared-knowledge.d.ts +116 -0
  68. package/lib/client-types/ui-contract.d.ts +276 -0
  69. package/lib/client-types/untrusted-text.d.ts +15 -0
  70. package/lib/client-types/work-ledger.d.ts +108 -0
  71. package/lib/client-types/work-output-eval.d.ts +15 -0
  72. package/lib/client-types/worklog-ledger.d.ts +88 -0
  73. package/lib/client-types/worklog-snapshot.d.ts +33 -0
  74. package/lib/client.js +5463 -0
  75. package/lib/config.d.ts +56 -0
  76. package/lib/config.js +115 -0
  77. package/lib/cron.d.ts +9 -0
  78. package/lib/cron.js +127 -0
  79. package/lib/domain.d.ts +819 -0
  80. package/lib/domain.js +662 -0
  81. package/lib/frame-admission.d.ts +6 -0
  82. package/lib/frame-admission.js +39 -0
  83. package/lib/frame-reference.d.ts +5 -0
  84. package/lib/frame-reference.js +12 -0
  85. package/lib/home-archive.d.ts +17 -0
  86. package/lib/home-archive.js +177 -0
  87. package/lib/home-directory.d.ts +6 -0
  88. package/lib/home-directory.js +38 -0
  89. package/lib/home-space.d.ts +50 -0
  90. package/lib/home-space.js +120 -0
  91. package/lib/home-store.d.ts +126 -0
  92. package/lib/home-store.js +837 -0
  93. package/lib/host.d.ts +22 -0
  94. package/lib/host.js +15 -0
  95. package/lib/human-inbox-transport.d.ts +23 -0
  96. package/lib/human-inbox-transport.js +105 -0
  97. package/lib/human-notification-runner.d.ts +83 -0
  98. package/lib/human-notification-runner.js +138 -0
  99. package/lib/human-notification-webhook.d.ts +18 -0
  100. package/lib/human-notification-webhook.js +102 -0
  101. package/lib/human-turn-completion.d.ts +61 -0
  102. package/lib/human-turn-completion.js +157 -0
  103. package/lib/inbox-return-notifier.d.ts +46 -0
  104. package/lib/inbox-return-notifier.js +279 -0
  105. package/lib/inbox-stalled-escalation-runner.d.ts +31 -0
  106. package/lib/inbox-stalled-escalation-runner.js +68 -0
  107. package/lib/inbox-stalled-topic.d.ts +16 -0
  108. package/lib/inbox-stalled-topic.js +44 -0
  109. package/lib/inbox-wake-policy.d.ts +19 -0
  110. package/lib/inbox-wake-policy.js +28 -0
  111. package/lib/inbox-wake-runner.d.ts +45 -0
  112. package/lib/inbox-wake-runner.js +481 -0
  113. package/lib/index.d.ts +29 -0
  114. package/lib/index.js +480 -0
  115. package/lib/known-format.d.ts +3 -0
  116. package/lib/known-format.js +8 -0
  117. package/lib/learning-scheduler.d.ts +37 -0
  118. package/lib/learning-scheduler.js +182 -0
  119. package/lib/learning-session-runner.d.ts +50 -0
  120. package/lib/learning-session-runner.js +435 -0
  121. package/lib/learning-trigger.d.ts +18 -0
  122. package/lib/learning-trigger.js +92 -0
  123. package/lib/learning-usage.d.ts +18 -0
  124. package/lib/learning-usage.js +49 -0
  125. package/lib/ledger.d.ts +191 -0
  126. package/lib/ledger.js +752 -0
  127. package/lib/local-human-host.d.ts +11 -0
  128. package/lib/local-human-host.js +191 -0
  129. package/lib/local-human-token.d.ts +9 -0
  130. package/lib/local-human-token.js +32 -0
  131. package/lib/local-owner.d.ts +8 -0
  132. package/lib/local-owner.js +26 -0
  133. package/lib/message-source.d.ts +10 -0
  134. package/lib/message-source.js +2 -0
  135. package/lib/organization-location.d.ts +20 -0
  136. package/lib/organization-location.js +80 -0
  137. package/lib/organization-workspace.d.ts +22 -0
  138. package/lib/organization-workspace.js +32 -0
  139. package/lib/project-ledger.d.ts +183 -0
  140. package/lib/project-ledger.js +189 -0
  141. package/lib/project-review-transport.d.ts +23 -0
  142. package/lib/project-review-transport.js +155 -0
  143. package/lib/project-seat-context.d.ts +18 -0
  144. package/lib/project-seat-context.js +34 -0
  145. package/lib/proposal-review-transport.d.ts +26 -0
  146. package/lib/proposal-review-transport.js +104 -0
  147. package/lib/route-guard.d.ts +31 -0
  148. package/lib/route-guard.js +80 -0
  149. package/lib/safe-file.d.ts +24 -0
  150. package/lib/safe-file.js +93 -0
  151. package/lib/sample-organization.d.ts +10 -0
  152. package/lib/sample-organization.js +46 -0
  153. package/lib/service.d.ts +597 -0
  154. package/lib/service.js +4076 -0
  155. package/lib/session-surface.d.ts +11 -0
  156. package/lib/session-surface.js +109 -0
  157. package/lib/shared-knowledge-review-transport.d.ts +28 -0
  158. package/lib/shared-knowledge-review-transport.js +85 -0
  159. package/lib/shared-knowledge.d.ts +117 -0
  160. package/lib/shared-knowledge.js +240 -0
  161. package/lib/tools.d.ts +7 -0
  162. package/lib/tools.js +955 -0
  163. package/lib/ui-contract.d.ts +277 -0
  164. package/lib/ui-contract.js +12 -0
  165. package/lib/ui-host.d.ts +24 -0
  166. package/lib/ui-host.js +471 -0
  167. package/lib/untrusted-text.d.ts +16 -0
  168. package/lib/untrusted-text.js +50 -0
  169. package/lib/work-ledger.d.ts +109 -0
  170. package/lib/work-ledger.js +107 -0
  171. package/lib/work-output-eval.d.ts +16 -0
  172. package/lib/work-output-eval.js +58 -0
  173. package/lib/work-runner.d.ts +47 -0
  174. package/lib/work-runner.js +352 -0
  175. package/lib/worklog-ledger.d.ts +89 -0
  176. package/lib/worklog-ledger.js +91 -0
  177. package/lib/worklog-snapshot.d.ts +34 -0
  178. package/lib/worklog-snapshot.js +100 -0
  179. package/lib/writer-guard.d.ts +11 -0
  180. package/lib/writer-guard.js +207 -0
  181. package/package.json +172 -4
package/lib/ledger.js ADDED
@@ -0,0 +1,752 @@
1
+ import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import { z } from 'zod';
4
+ import { SharedKnowledgeProposalSchema } from './shared-knowledge.js';
5
+ import { AgentFrameStateSchema, InboxDeliverySchema, InboxHumanNotificationSchema, HumanNotificationPreferenceSchema, InboxMessageSchema, InboxWakeOriginSchema, InboxWakePlanSchema, InboxReturnRouteSchema, FitnessReceiptSchema, LearningCheckpointSchema, LearningMaterialObservationSchema, LearningMaterialReviewSchema, LearningBatchRunSchema, LearningLaunchSchema, LearningScheduleCursorSchema, LearningSchedulePolicySchema, MemoryProposalSchema, ProjectTurnParticipationSchema, SessionBindingSchema, isUserAddress, UserAddressSchema, nowIso, stableId, } from './domain.js';
6
+ export const BIZAGENT_DOMAIN = defineDomain({
7
+ name: 'bizagent_learning',
8
+ version: 0,
9
+ tables: {
10
+ bindings: domainTable(SessionBindingSchema),
11
+ proposals: domainTable(MemoryProposalSchema),
12
+ receipts: domainTable(FitnessReceiptSchema),
13
+ metadata: domainTable(z.object({
14
+ key: z.string().min(1),
15
+ value: z.string(),
16
+ updatedAt: z.string().min(1),
17
+ })),
18
+ },
19
+ });
20
+ const CHECKPOINT_PREFIX = 'learning-checkpoint:';
21
+ const MATERIAL_OBSERVATION_PREFIX = 'learning-material-observation:';
22
+ const MATERIAL_REVIEW_PREFIX = 'learning-material-review:';
23
+ const LEARNING_BATCH_PREFIX = 'learning-batch:';
24
+ const LEARNING_LAUNCH_PREFIX = 'learning-launch:';
25
+ const LEARNING_SCHEDULE_PREFIX = 'learning-schedule:';
26
+ const LEARNING_POLICY_PREFIX = 'learning-policy:';
27
+ const FRAME_STATE_PREFIX = 'agent-frame-state:';
28
+ const INBOX_MESSAGE_PREFIX = 'inbox-message:';
29
+ const INBOX_DELIVERY_PREFIX = 'inbox-delivery:';
30
+ const INBOX_WAKE_ORIGIN_PREFIX = 'inbox-wake-origin:';
31
+ const INBOX_WAKE_PLAN_PREFIX = 'inbox-wake-plan:';
32
+ const INBOX_RETURN_ROUTE_PREFIX = 'inbox-return-route:';
33
+ const INBOX_HUMAN_NOTIFICATION_PREFIX = 'inbox-human-notification:';
34
+ const HUMAN_NOTIFICATION_PREFERENCE_PREFIX = 'human-notification-preference:';
35
+ const PROJECT_TURN_PREFIX = 'project-turn:';
36
+ const SHARED_KNOWLEDGE_PROPOSAL_PREFIX = 'shared-knowledge-proposal:';
37
+ export class LearningLedger {
38
+ domain;
39
+ receiptTail = Promise.resolve();
40
+ learningBatchTail = Promise.resolve();
41
+ materialReviewWrites = new Map();
42
+ frameMutationTail = new Map();
43
+ inboxMutationTail = Promise.resolve();
44
+ projectTurnTail = Promise.resolve();
45
+ proposalTail = Promise.resolve();
46
+ constructor(domain) {
47
+ this.domain = domain;
48
+ }
49
+ static async open(facility) {
50
+ return new LearningLedger(await facility.open(BIZAGENT_DOMAIN));
51
+ }
52
+ getBinding(sessionId) {
53
+ return this.domain.table('bindings').get(sessionId);
54
+ }
55
+ async putBinding(binding) {
56
+ const existing = this.getBinding(binding.sessionId);
57
+ if (existing !== undefined && existing.homeAddress !== binding.homeAddress) {
58
+ throw new Error(`session ${binding.sessionId} is already bound to ${existing.homeAddress}; cannot rebind to ${binding.homeAddress}`);
59
+ }
60
+ await this.domain.table('bindings').put(binding.sessionId, SessionBindingSchema.parse(binding));
61
+ }
62
+ /** Roll back a pre-creation reservation only after the host has verified that no Session materialized. */
63
+ async removeUnmaterializedBinding(binding) {
64
+ const current = this.getBinding(binding.sessionId);
65
+ if (current?.homeAddress !== binding.homeAddress || current.cwd !== binding.cwd
66
+ || current.boundAt !== binding.boundAt)
67
+ return false;
68
+ return this.domain.table('bindings').delete(binding.sessionId);
69
+ }
70
+ /** Forget a binding the current rules no longer make, so its Session binds afresh on its next turn. */
71
+ async releaseBinding(binding) {
72
+ const current = this.getBinding(binding.sessionId);
73
+ if (current?.homeAddress !== binding.homeAddress || current.cwd !== binding.cwd
74
+ || current.boundAt !== binding.boundAt)
75
+ return false;
76
+ return this.domain.table('bindings').delete(binding.sessionId);
77
+ }
78
+ listBindings(homeAddress) {
79
+ return [...this.domain.table('bindings').entries()]
80
+ .map(([, binding]) => binding)
81
+ .filter(binding => homeAddress === undefined || binding.homeAddress === homeAddress)
82
+ .sort((a, b) => a.boundAt.localeCompare(b.boundAt));
83
+ }
84
+ listProjectTurns(projectId) {
85
+ return [...this.domain.table('metadata').entries()]
86
+ .filter(([key]) => key.startsWith(PROJECT_TURN_PREFIX))
87
+ .map(([, record]) => ProjectTurnParticipationSchema.parse(JSON.parse(record.value)))
88
+ .filter(item => projectId === undefined || item.projectId === projectId)
89
+ .sort((a, b) => a.recordedAt.localeCompare(b.recordedAt)
90
+ || a.sessionId.localeCompare(b.sessionId) || a.turn - b.turn);
91
+ }
92
+ async putProjectTurn(input) {
93
+ const task = this.projectTurnTail.catch(() => undefined).then(async () => {
94
+ const key = `${PROJECT_TURN_PREFIX}${stableId('project-turn', JSON.stringify([
95
+ input.projectId, input.sessionId, input.turn,
96
+ ]))}`;
97
+ const previous = this.domain.table('metadata').get(key);
98
+ if (previous !== undefined) {
99
+ const existing = ProjectTurnParticipationSchema.parse(JSON.parse(previous.value));
100
+ if (existing.projectId !== input.projectId || existing.sessionId !== input.sessionId
101
+ || existing.turn !== input.turn || existing.homeAddress !== input.homeAddress) {
102
+ throw new Error('Project turn participation conflicts with its prior binding');
103
+ }
104
+ return existing;
105
+ }
106
+ const binding = this.getBinding(input.sessionId);
107
+ if (binding?.homeAddress !== input.homeAddress) {
108
+ throw new Error('Project turn requires a durable matching Home Session binding');
109
+ }
110
+ const participation = ProjectTurnParticipationSchema.parse({
111
+ schemaVersion: 1, ...input, recordedAt: nowIso(),
112
+ });
113
+ await this.domain.table('metadata').put(key, {
114
+ key, value: JSON.stringify(participation), updatedAt: participation.recordedAt,
115
+ });
116
+ return participation;
117
+ });
118
+ this.projectTurnTail = task;
119
+ return task;
120
+ }
121
+ getFrameState(sessionId) {
122
+ const record = this.domain.table('metadata').get(`${FRAME_STATE_PREFIX}${sessionId}`);
123
+ return record === undefined
124
+ ? undefined
125
+ : AgentFrameStateSchema.parse(JSON.parse(record.value));
126
+ }
127
+ async updateFrameState(sessionId, update) {
128
+ const previous = this.frameMutationTail.get(sessionId);
129
+ const task = (previous?.catch(() => undefined) ?? Promise.resolve()).then(async () => {
130
+ const state = AgentFrameStateSchema.parse(update(this.getFrameState(sessionId)));
131
+ if (state.sessionId !== sessionId)
132
+ throw new Error('Frame state Session mismatch');
133
+ const key = `${FRAME_STATE_PREFIX}${sessionId}`;
134
+ await this.domain.table('metadata').put(key, {
135
+ key,
136
+ value: JSON.stringify(state),
137
+ updatedAt: state.updatedAt,
138
+ });
139
+ return state;
140
+ });
141
+ this.frameMutationTail.set(sessionId, task);
142
+ try {
143
+ return await task;
144
+ }
145
+ finally {
146
+ if (this.frameMutationTail.get(sessionId) === task)
147
+ this.frameMutationTail.delete(sessionId);
148
+ }
149
+ }
150
+ async withInboxWrite(update) {
151
+ const task = this.inboxMutationTail.catch(() => undefined).then(update);
152
+ this.inboxMutationTail = task;
153
+ return task;
154
+ }
155
+ getInboxMessage(id) {
156
+ const record = this.domain.table('metadata').get(`${INBOX_MESSAGE_PREFIX}${id}`);
157
+ return record === undefined ? undefined
158
+ : InboxMessageSchema.parse(JSON.parse(record.value));
159
+ }
160
+ listInboxMessages() {
161
+ return [...this.domain.table('metadata').entries()]
162
+ .filter(([key]) => key.startsWith(INBOX_MESSAGE_PREFIX))
163
+ .map(([, record]) => InboxMessageSchema.parse(JSON.parse(record.value)))
164
+ .sort((a, b) => b.sentAt.localeCompare(a.sentAt) || b.id.localeCompare(a.id));
165
+ }
166
+ async putInboxMessage(message) {
167
+ const parsed = InboxMessageSchema.parse(message);
168
+ const key = `${INBOX_MESSAGE_PREFIX}${parsed.id}`;
169
+ if (this.getInboxMessage(parsed.id) !== undefined)
170
+ throw new Error(`Inbox message already exists: ${parsed.id}`);
171
+ await this.domain.table('metadata').put(key, {
172
+ key, value: JSON.stringify(parsed), updatedAt: parsed.sentAt,
173
+ });
174
+ }
175
+ getInboxDelivery(messageId, recipient) {
176
+ const record = this.domain.table('metadata').get(`${INBOX_DELIVERY_PREFIX}${messageId}:${recipient}`);
177
+ return record === undefined ? undefined
178
+ : InboxDeliverySchema.parse(JSON.parse(record.value));
179
+ }
180
+ async putInboxDelivery(delivery) {
181
+ const parsed = InboxDeliverySchema.parse(delivery);
182
+ const key = `${INBOX_DELIVERY_PREFIX}${parsed.messageId}:${parsed.recipient}`;
183
+ await this.domain.table('metadata').put(key, {
184
+ key, value: JSON.stringify(parsed), updatedAt: parsed.updatedAt,
185
+ });
186
+ }
187
+ getInboxHumanNotification(messageId, recipient) {
188
+ const record = this.domain.table('metadata').get(`${INBOX_HUMAN_NOTIFICATION_PREFIX}${messageId}:${recipient}`);
189
+ return record === undefined ? undefined
190
+ : InboxHumanNotificationSchema.parse(JSON.parse(record.value));
191
+ }
192
+ getHumanNotificationPreference(recipient) {
193
+ const record = this.domain.table('metadata').get(`${HUMAN_NOTIFICATION_PREFERENCE_PREFIX}${recipient}`);
194
+ return record === undefined ? undefined
195
+ : HumanNotificationPreferenceSchema.parse(JSON.parse(record.value));
196
+ }
197
+ async setHumanNotificationPreference(recipient, enabled, expectedRevision) {
198
+ const address = UserAddressSchema.parse(recipient);
199
+ if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) {
200
+ throw new Error('human notification preference revision is invalid');
201
+ }
202
+ return this.withInboxWrite(async () => {
203
+ const prior = this.getHumanNotificationPreference(address);
204
+ if ((prior?.revision ?? 0) !== expectedRevision) {
205
+ throw new Error('human notification preference changed before update');
206
+ }
207
+ const updatedAt = nowIso();
208
+ const preference = HumanNotificationPreferenceSchema.parse({ schemaVersion: 1,
209
+ recipient: address, enabled, revision: expectedRevision + 1, updatedAt });
210
+ const key = `${HUMAN_NOTIFICATION_PREFERENCE_PREFIX}${address}`;
211
+ await this.domain.table('metadata').put(key, {
212
+ key, value: JSON.stringify(preference), updatedAt,
213
+ });
214
+ return preference;
215
+ });
216
+ }
217
+ /** The external provider was never called; this terminal record survives later opt-in. */
218
+ async suppressInboxHumanNotification(messageId, recipient, reason) {
219
+ const address = UserAddressSchema.parse(recipient);
220
+ if (!reason.trim() || reason.length > 500)
221
+ throw new Error('human notification suppression needs a reason');
222
+ return this.withInboxWrite(async () => {
223
+ const prior = this.getInboxHumanNotification(messageId, address);
224
+ if (prior) {
225
+ if (prior.status !== 'suppressed' || prior.reason !== reason) {
226
+ throw new Error('human notification already has a channel outcome');
227
+ }
228
+ return prior;
229
+ }
230
+ const message = this.getInboxMessage(messageId);
231
+ const delivery = this.getInboxDelivery(messageId, address);
232
+ if (!message || ![...message.to, ...message.cc].includes(address)
233
+ || delivery?.status !== 'delivered' || delivery.handledAt !== undefined) {
234
+ throw new Error('human notification source is not pending in the recipient inbox');
235
+ }
236
+ return this.putSuppressedHumanNotification(messageId, address, reason);
237
+ });
238
+ }
239
+ async putSuppressedHumanNotification(messageId, recipient, reason) {
240
+ const suppressedAt = nowIso();
241
+ const notification = InboxHumanNotificationSchema.parse({ schemaVersion: 1,
242
+ id: stableId('inbox-human-suppressed', `${messageId}:${recipient}`),
243
+ messageId, recipient, attempt: 0, status: 'suppressed',
244
+ suppressedAt, resolvedAt: suppressedAt, reason,
245
+ });
246
+ const key = `${INBOX_HUMAN_NOTIFICATION_PREFIX}${messageId}:${recipient}`;
247
+ await this.domain.table('metadata').put(key, {
248
+ key, value: JSON.stringify(notification), updatedAt: suppressedAt,
249
+ });
250
+ return notification;
251
+ }
252
+ /** Persist an opt-out while holding the same lock as preference updates and channel reservations. */
253
+ async suppressInboxHumanNotificationIfOptedOut(messageId, recipient) {
254
+ return this.withInboxWrite(async () => {
255
+ if (this.getHumanNotificationPreference(recipient)?.enabled !== false)
256
+ return undefined;
257
+ const existing = this.getInboxHumanNotification(messageId, recipient);
258
+ if (existing)
259
+ return existing.status === 'suppressed' ? existing : undefined;
260
+ const message = this.getInboxMessage(messageId);
261
+ const delivery = this.getInboxDelivery(messageId, recipient);
262
+ if (!message || ![...message.to, ...message.cc].includes(recipient)
263
+ || delivery?.status !== 'delivered' || delivery.handledAt !== undefined) {
264
+ throw new Error('human notification source is not pending in the recipient inbox');
265
+ }
266
+ return this.putSuppressedHumanNotification(messageId, recipient, 'recipient-opted-out');
267
+ });
268
+ }
269
+ /** Reserve before calling the channel; a crash leaves unknown and never causes an automatic resend. */
270
+ async reserveInboxHumanNotification(messageId, recipient) {
271
+ if (!isUserAddress(recipient))
272
+ throw new Error('human notification requires a user recipient');
273
+ return this.withInboxWrite(async () => {
274
+ const existing = this.getInboxHumanNotification(messageId, recipient);
275
+ if (existing !== undefined)
276
+ return { notification: existing, reserved: false };
277
+ const message = this.getInboxMessage(messageId);
278
+ const delivery = this.getInboxDelivery(messageId, recipient);
279
+ if (message === undefined || ![...message.to, ...message.cc].includes(recipient)
280
+ || delivery?.status !== 'delivered' || delivery.handledAt !== undefined) {
281
+ throw new Error('human notification source is not pending in the recipient inbox');
282
+ }
283
+ if (this.getHumanNotificationPreference(recipient)?.enabled === false) {
284
+ return { notification: await this.putSuppressedHumanNotification(messageId, recipient, 'recipient-opted-out'), reserved: false };
285
+ }
286
+ const attemptedAt = nowIso();
287
+ const notification = InboxHumanNotificationSchema.parse({
288
+ schemaVersion: 1, id: stableId('inbox-human-notice', `${messageId}:${recipient}`),
289
+ messageId, recipient, status: 'unknown', attemptedAt,
290
+ });
291
+ const key = `${INBOX_HUMAN_NOTIFICATION_PREFIX}${messageId}:${recipient}`;
292
+ await this.domain.table('metadata').put(key, {
293
+ key, value: JSON.stringify(notification), updatedAt: attemptedAt,
294
+ });
295
+ return { notification, reserved: true };
296
+ });
297
+ }
298
+ async settleInboxHumanNotification(messageId, recipient, outcome) {
299
+ return this.withInboxWrite(async () => {
300
+ const prior = this.getInboxHumanNotification(messageId, recipient);
301
+ if (prior === undefined)
302
+ throw new Error('human notification was not reserved');
303
+ if (prior.status !== 'unknown') {
304
+ if (prior.status !== outcome.status
305
+ || (outcome.status === 'sent' && prior.providerReceipt !== outcome.providerReceipt)
306
+ || (outcome.status === 'rejected' && prior.reason !== outcome.reason)) {
307
+ throw new Error('human notification is already settled differently');
308
+ }
309
+ return prior;
310
+ }
311
+ const resolvedAt = nowIso();
312
+ const notification = InboxHumanNotificationSchema.parse({
313
+ ...prior, ...outcome, resolvedAt,
314
+ });
315
+ const key = `${INBOX_HUMAN_NOTIFICATION_PREFIX}${messageId}:${recipient}`;
316
+ await this.domain.table('metadata').put(key, {
317
+ key, value: JSON.stringify(notification), updatedAt: resolvedAt,
318
+ });
319
+ return notification;
320
+ });
321
+ }
322
+ /** A definite rejection permits one explicit new attempt; unknown and sent never do. */
323
+ async retryRejectedInboxHumanNotification(messageId, recipient) {
324
+ return this.withInboxWrite(async () => {
325
+ const prior = this.getInboxHumanNotification(messageId, recipient);
326
+ const delivery = this.getInboxDelivery(messageId, recipient);
327
+ if (prior?.status !== 'rejected' || prior.resolvedAt === undefined || prior.reason === undefined
328
+ || delivery?.status !== 'delivered' || delivery.handledAt !== undefined) {
329
+ throw new Error('human notification is not a pending, definitely rejected recipient');
330
+ }
331
+ if (this.getHumanNotificationPreference(recipient)?.enabled === false) {
332
+ throw new Error('human notification recipient has opted out');
333
+ }
334
+ const attemptedAt = nowIso();
335
+ const notification = InboxHumanNotificationSchema.parse({
336
+ schemaVersion: 1, id: stableId('inbox-human-notice', `${messageId}:${recipient}:${prior.attempt + 1}`),
337
+ messageId, recipient, attempt: prior.attempt + 1,
338
+ priorRejections: [...prior.priorRejections, {
339
+ id: prior.id, attemptedAt: prior.attemptedAt,
340
+ resolvedAt: prior.resolvedAt, reason: prior.reason,
341
+ }],
342
+ status: 'unknown', attemptedAt,
343
+ });
344
+ const key = `${INBOX_HUMAN_NOTIFICATION_PREFIX}${messageId}:${recipient}`;
345
+ await this.domain.table('metadata').put(key, {
346
+ key, value: JSON.stringify(notification), updatedAt: attemptedAt,
347
+ });
348
+ return notification;
349
+ });
350
+ }
351
+ getInboxWakeOrigin(sessionId) {
352
+ const record = this.domain.table('metadata').get(`${INBOX_WAKE_ORIGIN_PREFIX}${sessionId}`);
353
+ return record === undefined ? undefined
354
+ : InboxWakeOriginSchema.parse(JSON.parse(record.value));
355
+ }
356
+ async putInboxWakeOrigin(origin) {
357
+ const parsed = InboxWakeOriginSchema.parse(origin);
358
+ return this.withInboxWrite(async () => {
359
+ const existing = this.getInboxWakeOrigin(parsed.sessionId);
360
+ if (existing !== undefined) {
361
+ if (JSON.stringify(existing) !== JSON.stringify(parsed)) {
362
+ throw new Error('Inbox wake Session already has another origin');
363
+ }
364
+ return existing;
365
+ }
366
+ const key = `${INBOX_WAKE_ORIGIN_PREFIX}${parsed.sessionId}`;
367
+ await this.domain.table('metadata').put(key, {
368
+ key, value: JSON.stringify(parsed), updatedAt: parsed.createdAt,
369
+ });
370
+ return parsed;
371
+ });
372
+ }
373
+ getInboxWakePlan(id) {
374
+ const record = this.domain.table('metadata').get(`${INBOX_WAKE_PLAN_PREFIX}${id}`);
375
+ return record === undefined ? undefined
376
+ : InboxWakePlanSchema.parse(JSON.parse(record.value));
377
+ }
378
+ listInboxWakePlans() {
379
+ return [...this.domain.table('metadata').entries()]
380
+ .filter(([key]) => key.startsWith(INBOX_WAKE_PLAN_PREFIX))
381
+ .map(([, record]) => InboxWakePlanSchema.parse(JSON.parse(record.value)))
382
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
383
+ }
384
+ /** Caller holds withInboxWrite so plan selection and reservation share one local writer. */
385
+ async putInboxWakePlan(plan) {
386
+ const parsed = InboxWakePlanSchema.parse(plan);
387
+ const existing = this.getInboxWakePlan(parsed.id);
388
+ if (existing !== undefined) {
389
+ if (JSON.stringify(existing) !== JSON.stringify(parsed))
390
+ throw new Error('Inbox wake plan ID conflicts');
391
+ return existing;
392
+ }
393
+ const key = `${INBOX_WAKE_PLAN_PREFIX}${parsed.id}`;
394
+ await this.domain.table('metadata').put(key, {
395
+ key, value: JSON.stringify(parsed), updatedAt: parsed.createdAt,
396
+ });
397
+ return parsed;
398
+ }
399
+ async markInboxWakeDispatched(id, dispatchedAt, retiredReason) {
400
+ return this.withInboxWrite(async () => {
401
+ const plan = this.getInboxWakePlan(id);
402
+ if (plan === undefined)
403
+ throw new Error(`Inbox wake plan not found: ${id}`);
404
+ if (plan.dispatchedAt !== undefined)
405
+ return plan;
406
+ const updated = InboxWakePlanSchema.parse({ ...plan, dispatchedAt,
407
+ ...(retiredReason !== undefined ? { retiredReason: retiredReason.slice(0, 500) } : {}) });
408
+ const key = `${INBOX_WAKE_PLAN_PREFIX}${id}`;
409
+ await this.domain.table('metadata').put(key, {
410
+ key, value: JSON.stringify(updated), updatedAt: dispatchedAt,
411
+ });
412
+ return updated;
413
+ });
414
+ }
415
+ getInboxReturnRoute(messageId, recipient) {
416
+ const record = this.domain.table('metadata').get(`${INBOX_RETURN_ROUTE_PREFIX}${messageId}:${recipient}`);
417
+ return record === undefined ? undefined
418
+ : InboxReturnRouteSchema.parse(JSON.parse(record.value));
419
+ }
420
+ async putInboxReturnRoute(route) {
421
+ const parsed = InboxReturnRouteSchema.parse(route);
422
+ return this.withInboxWrite(async () => {
423
+ const existing = this.getInboxReturnRoute(parsed.messageId, parsed.recipient);
424
+ if (existing !== undefined) {
425
+ if (existing.sessionId !== parsed.sessionId)
426
+ throw new Error('Inbox return route Session changed');
427
+ if (existing.status === 'fallback' || existing.status === parsed.status)
428
+ return existing;
429
+ }
430
+ const key = `${INBOX_RETURN_ROUTE_PREFIX}${parsed.messageId}:${parsed.recipient}`;
431
+ await this.domain.table('metadata').put(key, {
432
+ key, value: JSON.stringify(parsed), updatedAt: parsed.updatedAt,
433
+ });
434
+ return parsed;
435
+ });
436
+ }
437
+ getMaterialObservation(id) {
438
+ const record = this.domain.table('metadata').get(`${MATERIAL_OBSERVATION_PREFIX}${id}`);
439
+ return record === undefined
440
+ ? undefined
441
+ : LearningMaterialObservationSchema.parse(JSON.parse(record.value));
442
+ }
443
+ async putMaterialObservation(observation) {
444
+ const parsed = LearningMaterialObservationSchema.parse(observation);
445
+ const existing = this.getMaterialObservation(parsed.id);
446
+ if (existing !== undefined) {
447
+ if (existing.homeAddress !== parsed.homeAddress || existing.sessionId !== parsed.sessionId
448
+ || existing.fingerprint !== parsed.fingerprint || existing.sourceKind !== parsed.sourceKind
449
+ || existing.worklogRevisionId !== parsed.worklogRevisionId
450
+ || existing.worklogRevision !== parsed.worklogRevision) {
451
+ throw new Error(`learning material observation ${parsed.id} conflicts with an existing source`);
452
+ }
453
+ return existing;
454
+ }
455
+ const key = `${MATERIAL_OBSERVATION_PREFIX}${parsed.id}`;
456
+ await this.domain.table('metadata').put(key, {
457
+ key,
458
+ value: JSON.stringify(parsed),
459
+ updatedAt: parsed.observedAt,
460
+ });
461
+ return parsed;
462
+ }
463
+ getMaterialReview(materialId) {
464
+ const record = this.domain.table('metadata').get(`${MATERIAL_REVIEW_PREFIX}${materialId}`);
465
+ return record === undefined
466
+ ? undefined
467
+ : LearningMaterialReviewSchema.parse(JSON.parse(record.value));
468
+ }
469
+ async putMaterialReview(review) {
470
+ const parsed = LearningMaterialReviewSchema.parse(review);
471
+ const pending = this.materialReviewWrites.get(parsed.materialId);
472
+ if (pending !== undefined)
473
+ return pending;
474
+ const write = (async () => {
475
+ const existing = this.getMaterialReview(parsed.materialId);
476
+ if (existing !== undefined)
477
+ return existing;
478
+ const key = `${MATERIAL_REVIEW_PREFIX}${parsed.materialId}`;
479
+ await this.domain.table('metadata').put(key, {
480
+ key,
481
+ value: JSON.stringify(parsed),
482
+ updatedAt: parsed.reviewedAt,
483
+ });
484
+ return parsed;
485
+ })();
486
+ this.materialReviewWrites.set(parsed.materialId, write);
487
+ try {
488
+ return await write;
489
+ }
490
+ finally {
491
+ this.materialReviewWrites.delete(parsed.materialId);
492
+ }
493
+ }
494
+ async withLearningBatchWrite(update) {
495
+ const task = this.learningBatchTail.catch(() => undefined).then(update);
496
+ this.learningBatchTail = task.then(() => undefined, () => undefined);
497
+ return task;
498
+ }
499
+ getLearningBatch(id) {
500
+ const record = this.domain.table('metadata').get(`${LEARNING_BATCH_PREFIX}${id}`);
501
+ return record === undefined ? undefined
502
+ : LearningBatchRunSchema.parse(JSON.parse(record.value));
503
+ }
504
+ listLearningBatches(homeAddress) {
505
+ return [...this.domain.table('metadata').entries()]
506
+ .filter(([key]) => key.startsWith(LEARNING_BATCH_PREFIX))
507
+ .map(([, record]) => LearningBatchRunSchema.parse(JSON.parse(record.value)))
508
+ .filter(run => run.homeAddress === homeAddress)
509
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id));
510
+ }
511
+ async putLearningBatch(run) {
512
+ const parsed = LearningBatchRunSchema.parse(run);
513
+ const previous = this.getLearningBatch(parsed.id);
514
+ if (previous !== undefined) {
515
+ if (previous.homeAddress !== parsed.homeAddress || previous.reviewerSessionId !== parsed.reviewerSessionId
516
+ || JSON.stringify(previous.materialIds) !== JSON.stringify(parsed.materialIds)
517
+ || previous.status !== 'open' || parsed.status === 'open') {
518
+ throw new Error(`learning batch ${parsed.id} cannot be replaced`);
519
+ }
520
+ }
521
+ const key = `${LEARNING_BATCH_PREFIX}${parsed.id}`;
522
+ await this.domain.table('metadata').put(key, {
523
+ key, value: JSON.stringify(parsed), updatedAt: parsed.updatedAt,
524
+ });
525
+ return parsed;
526
+ }
527
+ getLearningLaunch(id) {
528
+ const record = this.domain.table('metadata').get(`${LEARNING_LAUNCH_PREFIX}${id}`);
529
+ return record === undefined ? undefined
530
+ : LearningLaunchSchema.parse(JSON.parse(record.value));
531
+ }
532
+ listLearningLaunches(homeAddress) {
533
+ return [...this.domain.table('metadata').entries()]
534
+ .filter(([key]) => key.startsWith(LEARNING_LAUNCH_PREFIX))
535
+ .map(([, record]) => LearningLaunchSchema.parse(JSON.parse(record.value)))
536
+ .filter(item => homeAddress === undefined || item.homeAddress === homeAddress)
537
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id));
538
+ }
539
+ async putLearningLaunch(launch) {
540
+ const parsed = LearningLaunchSchema.parse(launch);
541
+ const prior = this.getLearningLaunch(parsed.id);
542
+ if (prior !== undefined && (prior.homeAddress !== parsed.homeAddress
543
+ || prior.requestedBySessionId !== parsed.requestedBySessionId
544
+ || prior.workerSessionId !== parsed.workerSessionId
545
+ || prior.workspace !== parsed.workspace
546
+ || prior.promptMessageId !== parsed.promptMessageId
547
+ || prior.provider !== parsed.provider || prior.model !== parsed.model
548
+ || prior.maxTokens !== parsed.maxTokens || prior.trigger !== parsed.trigger
549
+ || prior.scheduledFor !== parsed.scheduledFor)) {
550
+ throw new Error(`learning launch ${parsed.id} immutable fields changed`);
551
+ }
552
+ if (prior !== undefined && !{
553
+ reserved: ['reserved', 'active', 'needs_review', 'completed'],
554
+ active: ['active', 'completed', 'needs_review'],
555
+ completed: ['completed'],
556
+ needs_review: ['needs_review', 'abandoned', 'completed'],
557
+ abandoned: ['abandoned'],
558
+ }[prior.status].includes(parsed.status)) {
559
+ throw new Error(`learning launch ${parsed.id} cannot move from ${prior.status} to ${parsed.status}`);
560
+ }
561
+ const key = `${LEARNING_LAUNCH_PREFIX}${parsed.id}`;
562
+ await this.domain.table('metadata').put(key, {
563
+ key, value: JSON.stringify(parsed), updatedAt: parsed.updatedAt,
564
+ });
565
+ return parsed;
566
+ }
567
+ getLearningScheduleCursor(homeAddress) {
568
+ const record = this.domain.table('metadata').get(`${LEARNING_SCHEDULE_PREFIX}${homeAddress}`);
569
+ return record === undefined ? undefined
570
+ : LearningScheduleCursorSchema.parse(JSON.parse(record.value));
571
+ }
572
+ async putLearningScheduleCursor(cursor) {
573
+ const parsed = LearningScheduleCursorSchema.parse(cursor);
574
+ const key = `${LEARNING_SCHEDULE_PREFIX}${parsed.homeAddress}`;
575
+ const previous = this.getLearningScheduleCursor(parsed.homeAddress);
576
+ if (previous?.scheduleFingerprint === parsed.scheduleFingerprint
577
+ && (parsed.nextDueAt < previous.nextDueAt
578
+ || (parsed.lastDueAt !== undefined && previous.lastDueAt !== undefined
579
+ && parsed.lastDueAt < previous.lastDueAt))) {
580
+ throw new Error('learning schedule cursor cannot move backward');
581
+ }
582
+ await this.domain.table('metadata').put(key, {
583
+ key, value: JSON.stringify(parsed), updatedAt: parsed.updatedAt,
584
+ });
585
+ return parsed;
586
+ }
587
+ getLearningSchedulePolicy(homeAddress) {
588
+ const record = this.domain.table('metadata').get(`${LEARNING_POLICY_PREFIX}${homeAddress}`);
589
+ return record === undefined ? undefined
590
+ : LearningSchedulePolicySchema.parse(JSON.parse(record.value));
591
+ }
592
+ /** Caller holds the learning batch write lock and checks the prior revision. */
593
+ async putLearningSchedulePolicy(policy) {
594
+ const parsed = LearningSchedulePolicySchema.parse(policy);
595
+ const prior = this.getLearningSchedulePolicy(parsed.homeAddress);
596
+ if (parsed.revision !== (prior?.revision ?? 0) + 1) {
597
+ throw new Error('learning schedule policy revision conflict');
598
+ }
599
+ const key = `${LEARNING_POLICY_PREFIX}${parsed.homeAddress}`;
600
+ await this.domain.table('metadata').put(key, {
601
+ key, value: JSON.stringify(parsed), updatedAt: parsed.updatedAt,
602
+ });
603
+ return parsed;
604
+ }
605
+ getProposal(id) {
606
+ const value = this.domain.table('proposals').get(id);
607
+ return value === undefined ? undefined : MemoryProposalSchema.parse(value);
608
+ }
609
+ async putProposal(proposal) {
610
+ const parsed = MemoryProposalSchema.parse(proposal);
611
+ const task = this.proposalTail.catch(() => undefined).then(async () => {
612
+ const prior = this.getProposal(parsed.id);
613
+ if (prior) {
614
+ const identity = (item) => ({ id: item.id, fromAddress: item.fromAddress,
615
+ toAddress: item.toAddress, proposedKind: item.proposedKind, description: item.description,
616
+ body: item.body, tags: item.tags, sourceRefs: item.sourceRefs,
617
+ baseAsset: item.baseAsset, createdAt: item.createdAt });
618
+ if (!isDeepStrictEqual(identity(prior), identity(parsed))) {
619
+ throw new Error(`memory proposal identity changed: ${parsed.id}`);
620
+ }
621
+ if (prior.status !== 'pending' && !isDeepStrictEqual(prior, parsed)) {
622
+ throw new Error(`memory proposal decision is immutable: ${parsed.id}`);
623
+ }
624
+ if (prior.humanReview && !isDeepStrictEqual(prior.humanReview, parsed.humanReview)) {
625
+ throw new Error(`memory proposal human review is immutable: ${parsed.id}`);
626
+ }
627
+ if (prior.status === 'pending' && parsed.status === 'pending'
628
+ && !isDeepStrictEqual(prior, parsed)) {
629
+ const { humanReview: _before, ...before } = prior;
630
+ const { humanReview: _after, ...after } = parsed;
631
+ if (prior.humanReview || !parsed.humanReview || !isDeepStrictEqual(before, after)) {
632
+ throw new Error(`pending memory proposal changed: ${parsed.id}`);
633
+ }
634
+ }
635
+ if (isDeepStrictEqual(prior, parsed))
636
+ return;
637
+ }
638
+ await this.domain.table('proposals').put(parsed.id, parsed);
639
+ });
640
+ this.proposalTail = task;
641
+ await task;
642
+ }
643
+ listProposals(options = {}) {
644
+ return [...this.domain.table('proposals').entries()]
645
+ .map(([, proposal]) => MemoryProposalSchema.parse(proposal))
646
+ .filter(proposal => options.fromAddress === undefined || proposal.fromAddress === options.fromAddress)
647
+ .filter(proposal => options.toAddress === undefined || proposal.toAddress === options.toAddress)
648
+ .filter(proposal => options.status === undefined || proposal.status === options.status)
649
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
650
+ }
651
+ getSharedKnowledgeProposal(id) {
652
+ const value = this.domain.table('metadata').get(`${SHARED_KNOWLEDGE_PROPOSAL_PREFIX}${id}`);
653
+ return value === undefined ? undefined : SharedKnowledgeProposalSchema.parse(JSON.parse(value.value));
654
+ }
655
+ listSharedKnowledgeProposals(status) {
656
+ return [...this.domain.table('metadata').entries()]
657
+ .filter(([key]) => key.startsWith(SHARED_KNOWLEDGE_PROPOSAL_PREFIX))
658
+ .map(([, entry]) => SharedKnowledgeProposalSchema.parse(JSON.parse(entry.value)))
659
+ .filter(proposal => status === undefined || proposal.status === status)
660
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
661
+ }
662
+ async putSharedKnowledgeProposal(input) {
663
+ const proposal = SharedKnowledgeProposalSchema.parse(input);
664
+ const task = this.proposalTail.catch(() => undefined).then(async () => {
665
+ const prior = this.getSharedKnowledgeProposal(proposal.id);
666
+ if (prior) {
667
+ const identity = (item) => ({ id: item.id, fromAddress: item.fromAddress,
668
+ target: item.target, description: item.description, body: item.body,
669
+ sourceRefs: item.sourceRefs, createdAt: item.createdAt });
670
+ if (!isDeepStrictEqual(identity(prior), identity(proposal))) {
671
+ throw new Error(`shared Knowledge proposal identity changed: ${proposal.id}`);
672
+ }
673
+ if (prior.status !== 'pending' && !isDeepStrictEqual(prior, proposal)) {
674
+ throw new Error(`shared Knowledge decision is immutable: ${proposal.id}`);
675
+ }
676
+ if (prior.humanReview && !isDeepStrictEqual(prior.humanReview, proposal.humanReview)) {
677
+ throw new Error(`shared Knowledge human review is immutable: ${proposal.id}`);
678
+ }
679
+ if (prior.status === 'pending' && proposal.status === 'pending'
680
+ && (!proposal.humanReview || prior.humanReview)) {
681
+ if (!isDeepStrictEqual(prior, proposal)) {
682
+ throw new Error(`pending shared Knowledge proposal changed: ${proposal.id}`);
683
+ }
684
+ }
685
+ if (isDeepStrictEqual(prior, proposal))
686
+ return proposal;
687
+ }
688
+ const key = `${SHARED_KNOWLEDGE_PROPOSAL_PREFIX}${proposal.id}`;
689
+ await this.domain.table('metadata').put(key, {
690
+ key, value: JSON.stringify(proposal), updatedAt: nowIso(),
691
+ });
692
+ return proposal;
693
+ });
694
+ this.proposalTail = task;
695
+ return await task;
696
+ }
697
+ async putReceipt(receipt) {
698
+ const parsed = FitnessReceiptSchema.parse(receipt);
699
+ const task = this.receiptTail.catch(() => undefined).then(async () => {
700
+ const existing = this.domain.table('receipts').get(parsed.id);
701
+ if (existing !== undefined) {
702
+ const { createdAt: _previousTime, ...previous } = existing;
703
+ const { createdAt: _nextTime, ...next } = parsed;
704
+ if (JSON.stringify(previous) !== JSON.stringify(next)) {
705
+ throw new Error(`fitness receipt conflicts with its prior value: ${parsed.id}`);
706
+ }
707
+ return false;
708
+ }
709
+ await this.domain.table('receipts').put(parsed.id, parsed);
710
+ return true;
711
+ });
712
+ this.receiptTail = task;
713
+ return task;
714
+ }
715
+ listReceipts(assetId) {
716
+ return [...this.domain.table('receipts').entries()]
717
+ .map(([, receipt]) => receipt)
718
+ .filter(receipt => assetId === undefined || receipt.assetId === assetId)
719
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
720
+ }
721
+ getCheckpoint(id) {
722
+ const record = this.domain.table('metadata').get(checkpointKey(id));
723
+ if (record === undefined)
724
+ return undefined;
725
+ return LearningCheckpointSchema.parse(JSON.parse(record.value));
726
+ }
727
+ async putCheckpoint(checkpoint) {
728
+ const parsed = LearningCheckpointSchema.parse(checkpoint);
729
+ const key = checkpointKey(parsed.id);
730
+ await this.domain.table('metadata').put(key, {
731
+ key,
732
+ value: JSON.stringify(parsed),
733
+ updatedAt: parsed.settledAt ?? parsed.createdAt,
734
+ });
735
+ }
736
+ listCheckpoints(options = {}) {
737
+ return [...this.domain.table('metadata').entries()]
738
+ .filter(([key]) => key.startsWith(CHECKPOINT_PREFIX))
739
+ .map(([, record]) => LearningCheckpointSchema.parse(JSON.parse(record.value)))
740
+ .filter(item => options.sessionId === undefined || item.sessionId === options.sessionId)
741
+ .filter(item => options.homeAddress === undefined || item.homeAddress === options.homeAddress)
742
+ .filter(item => options.status === undefined || item.status === options.status)
743
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
744
+ }
745
+ async close() {
746
+ await this.domain.close();
747
+ }
748
+ }
749
+ function checkpointKey(id) {
750
+ return `${CHECKPOINT_PREFIX}${id}`;
751
+ }
752
+ //# sourceMappingURL=ledger.js.map