@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.
- package/CHANGELOG.md +124 -0
- package/LICENSE +21 -0
- package/README.md +61 -2
- package/cordis.patch.yml +14 -0
- package/docs/README.md +23 -0
- package/docs/architecture.md +154 -0
- package/docs/concepts.md +192 -0
- package/docs/configuration.md +154 -0
- package/docs/quickstart.md +103 -0
- package/docs/quickstart.zh-CN.md +86 -0
- package/docs/tools.md +607 -0
- package/docs/user-guide.zh-CN.md +132 -0
- package/docs/verification.md +69 -0
- package/examples/README.md +64 -0
- package/examples/demo-runtime.mjs +71 -0
- package/examples/learning-lab.mjs +263 -0
- package/examples/scenarios.mjs +45 -0
- package/lib/agent-preset.d.ts +20 -0
- package/lib/agent-preset.js +46 -0
- package/lib/agent-session-address.d.ts +14 -0
- package/lib/agent-session-address.js +15 -0
- package/lib/agent-session-coordinator.d.ts +24 -0
- package/lib/agent-session-coordinator.js +101 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +142 -0
- package/lib/client-types/agent-session-address.d.ts +13 -0
- package/lib/client-types/client/agent-mentions.d.ts +3 -0
- package/lib/client-types/client/api.d.ts +180 -0
- package/lib/client-types/client/components.d.ts +22 -0
- package/lib/client-types/client/controller.d.ts +23 -0
- package/lib/client-types/client/frame-participants.d.ts +5 -0
- package/lib/client-types/client/home-name.d.ts +9 -0
- package/lib/client-types/client/human-inbox.d.ts +8 -0
- package/lib/client-types/client/index.d.ts +13 -0
- package/lib/client-types/client/locales.d.ts +796 -0
- package/lib/client-types/client/markdown.d.ts +12 -0
- package/lib/client-types/client/project-artifact-link.d.ts +10 -0
- package/lib/client-types/client/project-outcome-review-inbox.d.ts +13 -0
- package/lib/client-types/client/projects.d.ts +11 -0
- package/lib/client-types/client/sample-guide.d.ts +9 -0
- package/lib/client-types/client/session-resource.d.ts +34 -0
- package/lib/client-types/client/session-surfaces.d.ts +24 -0
- package/lib/client-types/client/styles.d.ts +1 -0
- package/lib/client-types/config.d.ts +55 -0
- package/lib/client-types/cron.d.ts +8 -0
- package/lib/client-types/domain.d.ts +818 -0
- package/lib/client-types/frame-reference.d.ts +4 -0
- package/lib/client-types/home-directory.d.ts +5 -0
- package/lib/client-types/home-space.d.ts +49 -0
- package/lib/client-types/home-store.d.ts +125 -0
- package/lib/client-types/inbox-stalled-topic.d.ts +15 -0
- package/lib/client-types/inbox-wake-policy.d.ts +18 -0
- package/lib/client-types/known-format.d.ts +2 -0
- package/lib/client-types/learning-scheduler.d.ts +36 -0
- package/lib/client-types/learning-session-runner.d.ts +49 -0
- package/lib/client-types/learning-trigger.d.ts +17 -0
- package/lib/client-types/learning-usage.d.ts +17 -0
- package/lib/client-types/ledger.d.ts +190 -0
- package/lib/client-types/local-owner.d.ts +7 -0
- package/lib/client-types/message-source.d.ts +9 -0
- package/lib/client-types/organization-location.d.ts +19 -0
- package/lib/client-types/project-ledger.d.ts +182 -0
- package/lib/client-types/project-seat-context.d.ts +17 -0
- package/lib/client-types/safe-file.d.ts +23 -0
- package/lib/client-types/sample-organization.d.ts +9 -0
- package/lib/client-types/service.d.ts +596 -0
- package/lib/client-types/shared-knowledge.d.ts +116 -0
- package/lib/client-types/ui-contract.d.ts +276 -0
- package/lib/client-types/untrusted-text.d.ts +15 -0
- package/lib/client-types/work-ledger.d.ts +108 -0
- package/lib/client-types/work-output-eval.d.ts +15 -0
- package/lib/client-types/worklog-ledger.d.ts +88 -0
- package/lib/client-types/worklog-snapshot.d.ts +33 -0
- package/lib/client.js +5463 -0
- package/lib/config.d.ts +56 -0
- package/lib/config.js +115 -0
- package/lib/cron.d.ts +9 -0
- package/lib/cron.js +127 -0
- package/lib/domain.d.ts +819 -0
- package/lib/domain.js +662 -0
- package/lib/frame-admission.d.ts +6 -0
- package/lib/frame-admission.js +39 -0
- package/lib/frame-reference.d.ts +5 -0
- package/lib/frame-reference.js +12 -0
- package/lib/home-archive.d.ts +17 -0
- package/lib/home-archive.js +177 -0
- package/lib/home-directory.d.ts +6 -0
- package/lib/home-directory.js +38 -0
- package/lib/home-space.d.ts +50 -0
- package/lib/home-space.js +120 -0
- package/lib/home-store.d.ts +126 -0
- package/lib/home-store.js +837 -0
- package/lib/host.d.ts +22 -0
- package/lib/host.js +15 -0
- package/lib/human-inbox-transport.d.ts +23 -0
- package/lib/human-inbox-transport.js +105 -0
- package/lib/human-notification-runner.d.ts +83 -0
- package/lib/human-notification-runner.js +138 -0
- package/lib/human-notification-webhook.d.ts +18 -0
- package/lib/human-notification-webhook.js +102 -0
- package/lib/human-turn-completion.d.ts +61 -0
- package/lib/human-turn-completion.js +157 -0
- package/lib/inbox-return-notifier.d.ts +46 -0
- package/lib/inbox-return-notifier.js +279 -0
- package/lib/inbox-stalled-escalation-runner.d.ts +31 -0
- package/lib/inbox-stalled-escalation-runner.js +68 -0
- package/lib/inbox-stalled-topic.d.ts +16 -0
- package/lib/inbox-stalled-topic.js +44 -0
- package/lib/inbox-wake-policy.d.ts +19 -0
- package/lib/inbox-wake-policy.js +28 -0
- package/lib/inbox-wake-runner.d.ts +45 -0
- package/lib/inbox-wake-runner.js +481 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.js +480 -0
- package/lib/known-format.d.ts +3 -0
- package/lib/known-format.js +8 -0
- package/lib/learning-scheduler.d.ts +37 -0
- package/lib/learning-scheduler.js +182 -0
- package/lib/learning-session-runner.d.ts +50 -0
- package/lib/learning-session-runner.js +435 -0
- package/lib/learning-trigger.d.ts +18 -0
- package/lib/learning-trigger.js +92 -0
- package/lib/learning-usage.d.ts +18 -0
- package/lib/learning-usage.js +49 -0
- package/lib/ledger.d.ts +191 -0
- package/lib/ledger.js +752 -0
- package/lib/local-human-host.d.ts +11 -0
- package/lib/local-human-host.js +191 -0
- package/lib/local-human-token.d.ts +9 -0
- package/lib/local-human-token.js +32 -0
- package/lib/local-owner.d.ts +8 -0
- package/lib/local-owner.js +26 -0
- package/lib/message-source.d.ts +10 -0
- package/lib/message-source.js +2 -0
- package/lib/organization-location.d.ts +20 -0
- package/lib/organization-location.js +80 -0
- package/lib/organization-workspace.d.ts +22 -0
- package/lib/organization-workspace.js +32 -0
- package/lib/project-ledger.d.ts +183 -0
- package/lib/project-ledger.js +189 -0
- package/lib/project-review-transport.d.ts +23 -0
- package/lib/project-review-transport.js +155 -0
- package/lib/project-seat-context.d.ts +18 -0
- package/lib/project-seat-context.js +34 -0
- package/lib/proposal-review-transport.d.ts +26 -0
- package/lib/proposal-review-transport.js +104 -0
- package/lib/route-guard.d.ts +31 -0
- package/lib/route-guard.js +80 -0
- package/lib/safe-file.d.ts +24 -0
- package/lib/safe-file.js +93 -0
- package/lib/sample-organization.d.ts +10 -0
- package/lib/sample-organization.js +46 -0
- package/lib/service.d.ts +597 -0
- package/lib/service.js +4076 -0
- package/lib/session-surface.d.ts +11 -0
- package/lib/session-surface.js +109 -0
- package/lib/shared-knowledge-review-transport.d.ts +28 -0
- package/lib/shared-knowledge-review-transport.js +85 -0
- package/lib/shared-knowledge.d.ts +117 -0
- package/lib/shared-knowledge.js +240 -0
- package/lib/tools.d.ts +7 -0
- package/lib/tools.js +955 -0
- package/lib/ui-contract.d.ts +277 -0
- package/lib/ui-contract.js +12 -0
- package/lib/ui-host.d.ts +24 -0
- package/lib/ui-host.js +471 -0
- package/lib/untrusted-text.d.ts +16 -0
- package/lib/untrusted-text.js +50 -0
- package/lib/work-ledger.d.ts +109 -0
- package/lib/work-ledger.js +107 -0
- package/lib/work-output-eval.d.ts +16 -0
- package/lib/work-output-eval.js +58 -0
- package/lib/work-runner.d.ts +47 -0
- package/lib/work-runner.js +352 -0
- package/lib/worklog-ledger.d.ts +89 -0
- package/lib/worklog-ledger.js +91 -0
- package/lib/worklog-snapshot.d.ts +34 -0
- package/lib/worklog-snapshot.js +100 -0
- package/lib/writer-guard.d.ts +11 -0
- package/lib/writer-guard.js +207 -0
- 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
|