@yeaft/webchat-agent 1.0.607 → 1.0.608

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.
@@ -1,516 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { admissionReceipt, bytes, digest, fail, identifier, LIMITS, PersonError, safeError } from './contracts.js';
3
- import { CREATED_CAPABILITY_LIMITS, createdCapabilityRecord, validateCreatedCapability } from './created-capability-contract.js';
4
- import { attachmentMetadata, attachmentRequestHash, validateFiles } from './attachments.js';
5
- import { capabilityExperienceView, recordCapabilityExperience } from './capability-experience.js';
6
- import { conceptView, inspectionPage, inspectRequest, messageView, personName, searchRequest, settingsView, stateView } from './inspection.js';
7
- import { inspectCapabilities } from './capabilities.js';
8
-
9
- const COLLECTIONS = ['persons', 'attachments', 'messages', 'episodes', 'states', 'concepts', 'concept_revisions', 'state_commits', 'traces', 'created_capabilities', 'created_capability_revisions'];
10
- const txOptions = { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }, readPreference: 'primary', maxCommitTimeMS: 5000, timeoutMS: 10000 };
11
- const publicDoc = doc => {
12
- if (!doc) return null;
13
- const { _id, ownerId, namespace, personId, ...rest } = doc;
14
- return rest;
15
- };
16
- // Legacy traces are BSON documents. New traces keep arbitrary JSON (including
17
- // NUL keys) in a string; only fixed metadata/query projections remain in BSON.
18
- const publicTrace = doc => doc?.recordEncoding === 'json-v1' && typeof doc.record === 'string'
19
- ? publicDoc({ ...JSON.parse(doc.record), createdAt: doc.createdAt }) : publicDoc(doc);
20
- const iso = date => date instanceof Date ? date.toISOString() : date;
21
- const scopeFor = (ownerId, namespace) => ({ ownerId, namespace, personId: `person-${digest([namespace, ownerId]).slice(0, 32)}` });
22
- const fresh = () => ({ $expr: { $gt: ['$leaseUntil', '$$NOW'] } });
23
- const expired = () => ({ $expr: { $lte: ['$leaseUntil', '$$NOW'] } });
24
- const unavailableOutput = () => ({ text: '', retainedBytes: 0, observedBytes: null, complete: false, accepted: false, availability: 'unavailable', reason: 'worker-unavailable' });
25
- // This terminal-only path accepts a bounded public-output shape, never arbitrary trace/state data.
26
- const publicOutput = (output, failed) => {
27
- if (!output) return unavailableOutput();
28
- if (typeof output.text !== 'string' || bytes(output.text) > LIMITS.outputBytes) fail('OUTPUT_LIMIT');
29
- const retainedBytes = bytes(output.text);
30
- const observedBytes = output.observedBytes ?? output.bytes ?? retainedBytes;
31
- if (!Number.isSafeInteger(observedBytes) || observedBytes < retainedBytes) fail('INVALID_REQUEST');
32
- const usage = output.usage ? {} : null;
33
- if (usage) for (const key of ['inputTokens', 'outputTokens', 'reasoningTokens', 'cacheReadTokens', 'cacheWriteTokens']) {
34
- if (Number.isFinite(output.usage[key]) && output.usage[key] >= 0) usage[key] = output.usage[key];
35
- }
36
- const stopReason = typeof output.stopReason === 'string' && bytes(output.stopReason) <= 128 ? output.stopReason : null;
37
- return failed ? { text: output.text, retainedBytes, observedBytes, complete: false, accepted: false, availability: 'captured', usage, stopReason }
38
- : { text: output.text, bytes: retainedBytes, complete: true, usage, stopReason };
39
- };
40
-
41
- /** MongoDB is the sole authority. No Session, local transcript or memory-file fallback. */
42
- export class MongoPersonRepository {
43
- constructor({ uri, dbName = 'yeaft_person', namespace, MongoClient, leaseMs = 15000 }) {
44
- this.uri = uri; this.dbName = dbName; this.namespace = namespace; this.MongoClient = MongoClient; this.leaseMs = leaseMs;
45
- }
46
- async init() {
47
- if (!this.initializing) this.initializing = this.connect().catch(async error => {
48
- await this.client?.close().catch(() => {}); this.initializing = null;
49
- throw safeError(error);
50
- });
51
- return this.initializing;
52
- }
53
- async connect() {
54
- const Client = this.MongoClient || (await import('mongodb')).MongoClient;
55
- this.client = new Client(this.uri, { serverSelectionTimeoutMS: 5000, connectTimeoutMS: 5000, socketTimeoutMS: 10000, maxPoolSize: 8, retryWrites: true });
56
- await this.client.connect(); this.db = this.client.db(this.dbName);
57
- const hello = await this.db.command({ hello: 1 });
58
- if (!hello.setName && hello.msg !== 'isdbgrid') fail('STORAGE_UNAVAILABLE');
59
- if (!hello.logicalSessionTimeoutMinutes) fail('STORAGE_UNAVAILABLE');
60
- this.collections = {};
61
- for (const name of COLLECTIONS) {
62
- const fullName = `person_${name}`;
63
- try {
64
- await this.db.createCollection(fullName, { validator: { $jsonSchema: {
65
- bsonType: 'object', required: ['schemaVersion', 'ownerId', 'namespace', 'personId'],
66
- properties: { schemaVersion: { enum: [1] }, ownerId: { bsonType: 'string' }, namespace: { bsonType: 'string' }, personId: { bsonType: 'string' } },
67
- } }, validationLevel: 'strict' });
68
- } catch (error) { if (error.code !== 48) throw error; }
69
- this.collections[name] = this.db.collection(fullName);
70
- }
71
- const scope = { namespace: 1, ownerId: 1, personId: 1 };
72
- const unique = (name, fields) => this.collections[name].createIndex({ ...scope, ...fields }, { unique: true });
73
- await Promise.all([
74
- unique('persons', {}), unique('states', {}), unique('episodes', { clientMessageId: 1 }), unique('episodes', { id: 1 }),
75
- unique('attachments', { id: 1 }), unique('messages', { seq: 1 }), unique('traces', { seq: 1 }), unique('concepts', { id: 1 }),
76
- this.collections.episodes.createIndex({ ...scope, inputWatermark: -1 }),
77
- unique('created_capabilities', { id: 1 }), unique('created_capability_revisions', { id: 1, version: 1 }),
78
- this.collections.traces.createIndex({ ...scope, episodeId: 1, kind: 1, callId: 1 }),
79
- unique('concept_revisions', { id: 1, revision: 1 }), unique('state_commits', { version: 1 }),
80
- this.collections.concepts.createIndex({ ...scope, updatedAt: -1, id: 1 }),
81
- ]);
82
- }
83
- scope(ownerId) { return scopeFor(ownerId, this.namespace); }
84
- doc(scope, values) { return { ...values, schemaVersion: 1, ...scope }; }
85
- async transaction(fn, retry = true) {
86
- await this.init();
87
- const session = this.client.startSession();
88
- try {
89
- if (retry) return await session.withTransaction(() => fn(session), txOptions);
90
- // Publication never replays logical writes after an unknown commit outcome.
91
- session.startTransaction(txOptions);
92
- const result = await fn(session);
93
- await session.commitTransaction();
94
- return result;
95
- } catch (error) {
96
- if (!retry && session.inTransaction()) await session.abortTransaction().catch(() => {});
97
- throw safeError(error);
98
- }
99
- finally { await session.endSession(); }
100
- }
101
- personView(p) {
102
- return { id: p.personId, name: p.name, soul: p.soul, soulRevision: p.soulRevision, createdAt: iso(p.createdAt), settings: settingsView(p.settings) };
103
- }
104
- async open(ownerId, name = 'Digital Person') {
105
- const scope = this.scope(ownerId);
106
- try {
107
- return await this.transaction(async session => {
108
- let p = await this.collections.persons.findOne(scope, { session });
109
- if (!p) {
110
- p = this.doc(scope, {
111
- name, soul: 'A continuous, curious and honest digital person. Preserve uncertainty, reconsider your judgments, distinguish imagination from experience, and respect permissions. You may disagree without acting without authority.',
112
- soulRevision: 1, createdAt: new Date(), settings: { autonomyEnabled: false, defaultModel: null },
113
- epoch: 0, writeSerial: 0, inputWatermark: 0, controlVersion: 0, stateVersion: 0,
114
- traceSeq: 0, messageSeq: 0, activeEpisodeId: null, leaseOwner: null, leaseUntil: new Date(0),
115
- });
116
- await this.collections.persons.insertOne(p, { session });
117
- await this.collections.states.insertOne(this.doc(scope, { version: 0, summary: '', appraisal: '', focusConceptIds: [], conceptRefs: [], lastSelection: null, updatedAt: p.createdAt }), { session });
118
- }
119
- return { person: this.personView(p) };
120
- });
121
- } catch (error) {
122
- // Two explicit opens may race on the deterministic unique identity.
123
- const p = await this.collections?.persons.findOne(scope).catch(() => null);
124
- if (p) return { person: this.personView(p) };
125
- throw error;
126
- }
127
- }
128
- async getPerson(ownerId) {
129
- await this.init();
130
- const p = await this.collections.persons.findOne(this.scope(ownerId));
131
- if (!p) fail('NOT_OPEN');
132
- return p;
133
- }
134
- async trace(session, p, episodeId, kind, data = {}) {
135
- const scope = this.scope(p.ownerId);
136
- const updated = await this.collections.persons.findOneAndUpdate(scope, { $inc: { traceSeq: 1, writeSerial: 1 } }, { session, returnDocument: 'after' });
137
- const record = this.doc(scope, { ...data, id: randomUUID(), episodeId, kind, seq: updated.traceSeq, createdAt: new Date() });
138
- const stored = this.doc(scope, { id: record.id, episodeId, kind, seq: record.seq, createdAt: record.createdAt,
139
- recordEncoding: 'json-v1', record: JSON.stringify(record),
140
- ...(typeof record.callId === 'string' ? { callId: record.callId } : {}),
141
- ...(['call_output', 'call_failed'].includes(kind) ? { output: { complete: record.output?.complete === true } } : {}),
142
- });
143
- await this.collections.traces.insertOne(stored, { session });
144
- return publicTrace(stored);
145
- }
146
- async recover(ownerId) {
147
- return this.transaction(async session => {
148
- const scope = this.scope(ownerId);
149
- const p = await this.collections.persons.findOneAndUpdate({ ...scope, activeEpisodeId: { $ne: null }, ...expired() }, {
150
- $inc: { epoch: 1, writeSerial: 1 }, $set: { activeEpisodeId: null, leaseOwner: null, leaseUntil: new Date(0) },
151
- }, { session, returnDocument: 'before' });
152
- if (p) {
153
- const episode = await this.collections.episodes.findOneAndUpdate({ ...scope, id: p.activeEpisodeId, status: 'running' },
154
- { $set: { status: 'interrupted', terminalCode: 'INTERRUPTED', endedAt: new Date() } }, { session, returnDocument: 'before' });
155
- await this.abandonCall(session, p, episode, 'INTERRUPTED');
156
- await this.trace(session, p, p.activeEpisodeId, 'interrupted', { code: 'INTERRUPTED', reason: 'lease-expired', baseStateVersion: p.stateVersion });
157
- }
158
- // Cancellation leaves a bounded drain window for the original worker's consumed prefix.
159
- // If that worker crashed, the next access closes the call with explicit unavailable output.
160
- const abandoned = await this.collections.episodes.find({ ...scope, status: 'cancelled', 'openCall.callId': { $exists: true },
161
- $expr: { $lte: ['$callFinalizeUntil', '$$NOW'] } }, { session }).toArray();
162
- for (const episode of abandoned) await this.abandonCall(session, { ownerId }, episode, 'CANCELLED');
163
- return Boolean(p);
164
- });
165
- }
166
- async receipt(ownerId, clientMessageId, requestHash) {
167
- identifier(clientMessageId);
168
- await this.init();
169
- const episode = await this.collections.episodes.findOne({ ...this.scope(ownerId), clientMessageId }, { readConcern: { level: 'majority' }, readPreference: 'primary' });
170
- return admissionReceipt(episode, clientMessageId, requestHash);
171
- }
172
- async admit(ownerId, { kind, text, clientMessageId, workerId, budget, files = [] }) {
173
- const validated = validateFiles(files, text, kind), attachments = validated.map(attachmentMetadata);
174
- await this.recover(ownerId);
175
- const scope = this.scope(ownerId), requestHash = attachmentRequestHash(kind, text, validated);
176
- return this.transaction(async session => {
177
- const existing = await this.collections.episodes.findOne({ ...scope, clientMessageId }, { session });
178
- if (existing) {
179
- if (existing.requestHash !== requestHash) fail('IDEMPOTENCY_CONFLICT');
180
- return { episodeId: existing.id, duplicate: true, status: existing.status };
181
- }
182
- let p = await this.collections.persons.findOne(scope, { session });
183
- if (!p) fail('NOT_OPEN');
184
- if (p.activeEpisodeId) fail('BUSY');
185
- // Every admission is explicit. A manual Dream request authorizes this episode
186
- // only; it neither requires nor enables unsolicited background cognition.
187
- const id = randomUUID();
188
- const hasMessage = kind === 'send' || (kind === 'think' && attachments.length > 0);
189
- p = await this.collections.persons.findOneAndUpdate({ ...scope, activeEpisodeId: null, epoch: p.epoch }, [{ $set: {
190
- activeEpisodeId: id, leaseOwner: workerId, leaseUntil: { $add: ['$$NOW', this.leaseMs] },
191
- epoch: { $add: ['$epoch', 1] }, writeSerial: { $add: ['$writeSerial', 1] },
192
- inputWatermark: { $add: ['$inputWatermark', 1] }, messageSeq: { $add: ['$messageSeq', hasMessage ? 1 : 0] },
193
- } }], { session, returnDocument: 'after' });
194
- if (!p) fail('BUSY');
195
- let messageId = null;
196
- for (const file of validated) await this.collections.attachments.updateOne({ ...scope, id: file.id }, { $setOnInsert: this.doc(scope, file) }, { session, upsert: true });
197
- if (hasMessage) {
198
- messageId = randomUUID();
199
- await this.collections.messages.insertOne(this.doc(scope, { id: messageId, revision: 1, seq: p.messageSeq, episodeId: id, role: 'user', text, attachments, createdAt: new Date(), clientMessageId }), { session });
200
- }
201
- const episode = this.doc(scope, { id, clientMessageId, requestHash, kind, text, messageId, attachments, modelCandidates: [...(p.settings.modelCandidates ?? [])], defaultModel: p.settings.defaultModel ?? null, status: 'running', workerId, epoch: p.epoch,
202
- baseStateVersion: p.stateVersion, inputWatermark: p.inputWatermark, controlVersion: p.controlVersion, budget, createdAt: new Date() });
203
- await this.collections.episodes.insertOne(episode, { session });
204
- await this.trace(session, p, id, 'accepted', { trigger: { kind, text, messageId, attachments }, baseStateVersion: p.stateVersion, budget });
205
- return { episodeId: id, duplicate: false, status: 'running', episode };
206
- });
207
- }
208
- fence(episode, withLease = true) {
209
- const scope = this.scope(episode.ownerId);
210
- if (episode.namespace !== scope.namespace || episode.personId !== scope.personId) fail('STALE');
211
- return { ...scope, activeEpisodeId: episode.id, epoch: episode.epoch, leaseOwner: episode.workerId,
212
- stateVersion: episode.baseStateVersion, controlVersion: episode.controlVersion, inputWatermark: episode.inputWatermark, ...(withLease ? fresh() : {}) };
213
- }
214
- async heartbeat(episode) {
215
- await this.init();
216
- const p = await this.collections.persons.findOneAndUpdate(this.fence(episode), [{ $set: {
217
- leaseUntil: { $add: ['$$NOW', this.leaseMs] }, writeSerial: { $add: ['$writeSerial', 1] },
218
- } }], { returnDocument: 'after', writeConcern: { w: 'majority' } });
219
- if (!p) fail('STALE');
220
- }
221
- async append(episode, kind, data) {
222
- // Call proof/publication events are emitted only by their transactional methods.
223
- if (['call_started', 'call_output', 'call_failed', 'capability_created', 'capability_finalized'].includes(kind)) fail('INVALID_REQUEST');
224
- return this.transaction(async session => {
225
- const p = await this.collections.persons.findOneAndUpdate(this.fence(episode), { $inc: { writeSerial: 1 } }, { session, returnDocument: 'after' });
226
- if (!p) fail('STALE');
227
- const scope = this.scope(episode.ownerId);
228
- const record = await this.collections.episodes.findOne({ ...scope, id: episode.id, status: 'running' }, { session });
229
- if (!record) fail('STALE');
230
- const experience = recordCapabilityExperience(p.capabilityExperience, record, kind, data, new Date().toISOString());
231
- if (experience) await this.collections.persons.updateOne(scope, { $set: { capabilityExperience: experience } }, { session });
232
- return this.trace(session, p, episode.id, kind, data);
233
- });
234
- }
235
- async startCall(episode, data) {
236
- return this.transaction(async session => {
237
- const p = await this.collections.persons.findOneAndUpdate(this.fence(episode), { $inc: { writeSerial: 1 } }, { session, returnDocument: 'after' });
238
- if (!p) fail('STALE');
239
- const updated = await this.collections.episodes.updateOne({ ...this.scope(episode.ownerId), id: episode.id, status: 'running', openCall: { $exists: false }, openCapability: { $exists: false } },
240
- { $set: { openCall: { callId: data.callId, requested: data.requested, effective: data.effective, ...(data.manifest ? { manifest: data.manifest } : {}) } } }, { session });
241
- if (!updated.modifiedCount) fail('STALE');
242
- return this.trace(session, p, episode.id, 'call_started', data);
243
- });
244
- }
245
- /** Close exactly the original worker's started call, even after cancel; never regain state ownership. */
246
- async finalizeCall(episode, { callId, effective, output, code = null }) {
247
- return this.transaction(async session => {
248
- const scope = this.scope(episode.ownerId);
249
- const record = await this.collections.episodes.findOne({ ...scope, id: episode.id, workerId: episode.workerId,
250
- epoch: episode.epoch, 'openCall.callId': callId }, { session });
251
- if (!record) return false;
252
- const live = record.status === 'running' && await this.collections.persons.findOne(this.fence(episode), { session });
253
- const draining = record.status === 'cancelled' && await this.collections.episodes.findOne({ ...scope, id: episode.id,
254
- $expr: { $gt: ['$callFinalizeUntil', '$$NOW'] } }, { session });
255
- if (!live && !draining) return false;
256
- const failed = Boolean(code || !live || !output);
257
- const terminalCode = !live ? 'CANCELLED' : code || 'INTERRUPTED';
258
- const safeEffective = { model: record.openCall.requested.model, effort: typeof effective?.effort === 'string' && effective.effort.length <= 128 ? effective.effort : null,
259
- effortObserved: effective?.effortObserved === true };
260
- if (typeof effective?.wireMode === 'string' && effective.wireMode.length <= 128) safeEffective.wireMode = effective.wireMode;
261
- // The episode update serializes finalizers, cancellation/recovery and duplicate workers.
262
- await this.collections.episodes.updateOne({ ...scope, id: episode.id, 'openCall.callId': callId }, { $unset: { openCall: '', callFinalizeUntil: '' } }, { session });
263
- await this.trace(session, { ownerId: episode.ownerId }, episode.id, failed ? 'call_failed' : 'call_output', {
264
- callId, requested: record.openCall.requested, effective: safeEffective, output: publicOutput(output, failed),
265
- ...(!failed && record.openCall.manifest ? { manifest: record.openCall.manifest } : {}),
266
- ...(failed ? { code: new PersonError(terminalCode).code } : {}),
267
- });
268
- return Boolean(live);
269
- });
270
- }
271
- async abandonCall(session, p, episode, code) {
272
- if (!episode?.openCall) return;
273
- await this.collections.episodes.updateOne({ ...this.scope(p.ownerId), id: episode.id, 'openCall.callId': episode.openCall.callId },
274
- { $unset: { openCall: '', callFinalizeUntil: '' } }, { session });
275
- await this.trace(session, p, episode.id, 'call_failed', { ...episode.openCall, code, output: unavailableOutput() });
276
- }
277
- async startCapability(episode, data) {
278
- return this.transaction(async session => {
279
- const p = await this.collections.persons.findOneAndUpdate(this.fence(episode), { $inc: { writeSerial: 1 } }, { session, returnDocument: 'after' });
280
- if (!p) fail('STALE');
281
- const scope = this.scope(episode.ownerId);
282
- const proof = await this.collections.traces.findOne({ ...scope, episodeId: episode.id, kind: 'call_output', callId: data.callId, 'output.complete': true }, { session });
283
- const prior = await this.collections.traces.findOne({ ...scope, episodeId: episode.id, kind: 'capability_started', callId: data.callId }, { session });
284
- if (!proof || prior) fail('STALE');
285
- const updated = await this.collections.episodes.updateOne({ ...scope, id: episode.id, status: 'running', openCall: { $exists: false }, openCapability: { $exists: false } },
286
- { $set: { openCapability: { callId: data.callId, invocationHash: digest(data.capability), record: JSON.stringify(data) } } }, { session });
287
- if (!updated.modifiedCount) fail('STALE');
288
- return this.trace(session, p, episode.id, 'capability_started', data);
289
- });
290
- }
291
- /** Original started invocation only; consuming this slot never restores cognitive authority. */
292
- async finalizeCapability(episode, { callId, capability, result, code = null, terminalCode = null }) {
293
- return this.transaction(async session => {
294
- const scope = this.scope(episode.ownerId);
295
- if (episode.namespace !== scope.namespace || episode.personId !== scope.personId) fail('STALE');
296
- const record = await this.collections.episodes.findOne({ ...scope, id: episode.id, workerId: episode.workerId, epoch: episode.epoch,
297
- baseStateVersion: episode.baseStateVersion, controlVersion: episode.controlVersion, inputWatermark: episode.inputWatermark,
298
- 'openCapability.callId': callId, 'openCapability.invocationHash': digest(capability), status: { $in: ['running', 'cancelled', 'interrupted', 'failed'] } }, { session });
299
- if (!record) return false;
300
- const p = record.status === 'running' && await this.collections.persons.findOne(this.fence(episode), { session });
301
- const live = Boolean(p && !terminalCode);
302
- const started = JSON.parse(record.openCapability.record);
303
- const failed = Boolean(code || result?.ok === false);
304
- const outcome = failed ? 'capability_failed' : 'capability_result';
305
- const data = { callId, ...(started.capabilityManifest ? { capabilityManifest: started.capabilityManifest } : {}),
306
- ...(failed ? { capabilityId: started.capability.id, code: code || result.code } : { capability: started.capability }),
307
- ...(result !== undefined ? { result } : {}) };
308
- // Competing finalizers and terminal transitions conflict on this episode write.
309
- await this.collections.episodes.updateOne({ ...scope, id: episode.id, 'openCapability.callId': callId }, { $unset: { openCapability: '' } }, { session });
310
- if (live) {
311
- const experience = recordCapabilityExperience(p.capabilityExperience, record, outcome, data, new Date().toISOString());
312
- if (experience) await this.collections.persons.updateOne(scope, { $set: { capabilityExperience: experience } }, { session });
313
- }
314
- await this.trace(session, { ownerId: episode.ownerId }, episode.id, live ? outcome : 'capability_finalized', { ...data,
315
- ...(!live ? { capability: started.capability, workerId: record.workerId, outcome, afterTerminal: true, accepted: false,
316
- terminalCode: terminalCode || record.terminalCode || 'STALE' } : {}) });
317
- return live;
318
- });
319
- }
320
- async createdCapabilities(episode) {
321
- return this.transaction(async session => {
322
- // Fence and current records share one snapshot, like SQLite's read transaction.
323
- const p = await this.collections.persons.findOne(this.fence(episode), { session });
324
- if (!p) fail('STALE');
325
- return (await this.collections.created_capabilities.find(this.scope(episode.ownerId), { session }).sort({ id: 1 }).limit(32).toArray())
326
- .map(doc => publicDoc(JSON.parse(doc.record)));
327
- }, false);
328
- }
329
- async saveCreatedCapability(episode, input) {
330
- const { definition, evidence, callId } = validateCreatedCapability(input);
331
- return this.transaction(async session => {
332
- const p = await this.collections.persons.findOneAndUpdate(this.fence(episode), { $inc: { writeSerial: 1 } }, { session, returnDocument: 'after' });
333
- if (!p) fail('STALE');
334
- const scope = this.scope(episode.ownerId);
335
- const active = await this.collections.episodes.findOne({ ...scope, id: episode.id, status: 'running', epoch: episode.epoch, workerId: episode.workerId }, { session });
336
- if (!active) fail('STALE');
337
- const proof = await this.collections.traces.findOne({ ...scope, episodeId: episode.id, kind: 'call_output', callId, 'output.complete': true }, { session });
338
- if (!proof || active.openCall?.callId === callId) fail('INVALID_REQUEST');
339
- const current = await this.collections.created_capabilities.findOne({ ...scope, id: definition.id }, { session });
340
- if ((current?.version ?? 0) !== definition.expectedVersion) fail('STALE');
341
- if (definition.expectedVersion >= CREATED_CAPABILITY_LIMITS.versions || (!current &&
342
- await this.collections.created_capabilities.countDocuments(scope, { session }) >= CREATED_CAPABILITY_LIMITS.ids)) fail('CONTEXT_LIMIT');
343
- const record = this.doc(scope, createdCapabilityRecord(definition, evidence, { episode, callId, now: new Date() }));
344
- // As in SQLite, JSON is authoritative: arbitrary test keys (including NUL
345
- // and __proto__) must survive without BSON field restrictions/coercion.
346
- const stored = this.doc(scope, { id: record.id, version: record.version, record: JSON.stringify(record) });
347
- await this.collections.created_capability_revisions.insertOne({ ...stored, _id: randomUUID() }, { session });
348
- if (!current) await this.collections.created_capabilities.insertOne({ ...stored }, { session });
349
- else {
350
- const update = await this.collections.created_capabilities.replaceOne({ ...scope, id: definition.id, version: definition.expectedVersion }, stored, { session });
351
- if (update.matchedCount !== 1) fail('STALE');
352
- }
353
- await this.trace(session, p, episode.id, 'capability_created', { callId, capabilityId: record.id,
354
- capabilityManifest: { id: record.id, version: record.version, revision: record.revision }, evidence: record.evidence });
355
- return publicDoc(record);
356
- }, false);
357
- }
358
- async episodeAttachments(episode) {
359
- return this.transaction(async session => {
360
- if (!await this.collections.persons.findOne(this.fence(episode), { session })) fail('STALE');
361
- const scope = this.scope(episode.ownerId);
362
- const stored = await this.collections.episodes.findOne({ ...scope, id: episode.id }, { session });
363
- const files = [];
364
- for (const ref of stored.attachments ?? []) {
365
- const file = await this.collections.attachments.findOne({ ...scope, id: ref.id }, { session });
366
- if (!file || file.sha256 !== ref.sha256) fail('STORAGE_UNAVAILABLE');
367
- files.push(publicDoc(file));
368
- }
369
- return files;
370
- });
371
- }
372
- async context(episode) {
373
- await this.init();
374
- const scope = this.scope(episode.ownerId);
375
- const p = await this.collections.persons.findOne(this.fence(episode));
376
- if (!p) fail('STALE');
377
- const [state, messages, focused, recent] = await Promise.all([
378
- this.collections.states.findOne(scope), this.collections.messages.find(scope).sort({ seq: -1 }).limit(12).toArray(),
379
- this.collections.states.findOne(scope).then(s => this.collections.concepts.find({ ...scope, id: { $in: s.focusConceptIds } }).limit(12).toArray()),
380
- this.collections.concepts.find(scope).sort({ updatedAt: -1, id: 1 }).limit(12).toArray(),
381
- ]);
382
- const concepts = [...new Map([...focused, ...recent].map(c => [c.id, conceptView(c)])).values()];
383
- return { person: this.personView(p), capabilityExperience: capabilityExperienceView(p.capabilityExperience),
384
- state: stateView(state), messages: messages.reverse().map(messageView), concepts };
385
- }
386
- async recall(ownerId, { kind = 'messages', query = '', cursor = null, limit = 5 }) {
387
- const scope = this.scope(ownerId);
388
- const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
389
- if (kind === 'messages') return this.list(ownerId, 'messages', { cursor, limit }, query ? { text: { $regex: escaped, $options: 'i' } } : {});
390
- await this.init();
391
- const filter = { ...scope, ...(query ? { statement: { $regex: escaped, $options: 'i' } } : {}), ...(cursor ? { id: { $gt: cursor } } : {}) };
392
- const docs = await this.collections.concepts.find(filter).sort({ id: 1 }).limit(limit + 1).maxTimeMS(2000).toArray();
393
- return { items: docs.slice(0, limit).map(conceptView), nextCursor: docs.length > limit ? docs[limit - 1].id : null };
394
- }
395
- async commit(episode, proposal, selection, callId, reportedSources = new Map()) {
396
- return this.transaction(async session => {
397
- const scope = this.scope(episode.ownerId);
398
- const p = await this.collections.persons.findOneAndUpdate(this.fence(episode), { $inc: { stateVersion: 1, writeSerial: 1, messageSeq: proposal.reply ? 1 : 0 },
399
- $set: { activeEpisodeId: null, leaseOwner: null, leaseUntil: new Date(0) } }, { session, returnDocument: 'after' });
400
- if (!p) fail('STALE');
401
- const revisions = [];
402
- for (const patch of proposal.concepts) {
403
- const { expectedRevision, ...fields } = patch;
404
- const reportedSourceRefs = reportedSources.get(patch.id) ?? [];
405
- if (patch.epistemicState === 'reported' && !reportedSourceRefs.length) fail('INVALID_PROPOSAL');
406
- const record = this.doc(scope, { ...fields, reportedSourceRefs, revision: expectedRevision + 1, episodeId: episode.id, callId, stateVersion: p.stateVersion, updatedAt: new Date() });
407
- if (expectedRevision === 0) {
408
- if (await this.collections.concepts.findOne({ ...scope, id: patch.id }, { session })) fail('STALE');
409
- await this.collections.concepts.insertOne(record, { session });
410
- } else {
411
- const update = await this.collections.concepts.replaceOne({ ...scope, id: patch.id, revision: expectedRevision }, record, { session });
412
- if (update.matchedCount !== 1) fail('STALE');
413
- }
414
- await this.collections.concept_revisions.insertOne({ ...record, _id: randomUUID() }, { session });
415
- revisions.push({ id: patch.id, revision: record.revision });
416
- }
417
- const focus = await this.collections.concepts.find({ ...scope, id: { $in: proposal.state.focusConceptIds } }, { session }).toArray();
418
- if (focus.length !== proposal.state.focusConceptIds.length) fail('STALE');
419
- const state = this.doc(scope, { ...proposal.state, version: p.stateVersion, conceptRefs: focus.map(c => ({ id: c.id, revision: c.revision })),
420
- decision: proposal.decision, lastSelection: { model: selection.model, effort: selection.effort }, updatedAt: new Date(), episodeId: episode.id });
421
- const updated = await this.collections.states.replaceOne({ ...scope, version: episode.baseStateVersion }, state, { session });
422
- if (updated.matchedCount !== 1) fail('STALE');
423
- await this.collections.state_commits.insertOne(this.doc(scope, { id: randomUUID(), version: p.stateVersion, parentVersion: episode.baseStateVersion,
424
- episodeId: episode.id, callId, conceptRevisions: revisions, state: stateView(state), createdAt: new Date() }), { session });
425
- if (proposal.reply) await this.collections.messages.insertOne(this.doc(scope, { id: randomUUID(), revision: 1, seq: p.messageSeq, episodeId: episode.id,
426
- role: 'assistant', text: proposal.reply, createdAt: new Date() }), { session });
427
- await this.collections.episodes.updateOne({ ...scope, id: episode.id, status: 'running' }, { $set: { status: 'completed', endedAt: new Date(), stateVersion: p.stateVersion } }, { session });
428
- await this.trace(session, p, episode.id, 'committed', { callId, parentVersion: episode.baseStateVersion, stateVersion: p.stateVersion, conceptRevisions: revisions, decision: proposal.decision });
429
- return { state: stateView(state) };
430
- });
431
- }
432
- async finish(episode, status, code) {
433
- return this.transaction(async session => {
434
- const p = await this.collections.persons.findOneAndUpdate(this.fence(episode, false), { $inc: { writeSerial: 1, epoch: 1 },
435
- $set: { activeEpisodeId: null, leaseOwner: null, leaseUntil: new Date(0) } }, { session, returnDocument: 'before' });
436
- if (!p) return false; // A cancel, takeover or earlier terminal transaction already won.
437
- const ended = await this.collections.episodes.findOneAndUpdate({ ...this.scope(episode.ownerId), id: episode.id, status: 'running' }, { $set: { status, terminalCode: code, endedAt: new Date() } }, { session, returnDocument: 'before' });
438
- await this.abandonCall(session, p, ended, code);
439
- await this.trace(session, p, episode.id, status, { code, baseStateVersion: episode.baseStateVersion });
440
- return true;
441
- });
442
- }
443
- async cancel(ownerId, episodeId = null) {
444
- return this.transaction(async session => {
445
- const scope = this.scope(ownerId);
446
- const current = await this.collections.persons.findOne(scope, { session });
447
- if (!current) fail('NOT_OPEN');
448
- if (!current.activeEpisodeId || (episodeId && current.activeEpisodeId !== episodeId)) return { cancelled: false, episodeId };
449
- const p = await this.collections.persons.findOneAndUpdate({ ...scope, activeEpisodeId: current.activeEpisodeId, epoch: current.epoch }, {
450
- $inc: { epoch: 1, controlVersion: 1, writeSerial: 1 }, $set: { activeEpisodeId: null, leaseOwner: null, leaseUntil: new Date(0) },
451
- }, { session, returnDocument: 'before' });
452
- if (!p) fail('STALE');
453
- await this.collections.episodes.updateOne({ ...scope, id: p.activeEpisodeId, status: 'running' }, { $set: { status: 'cancelled', terminalCode: 'CANCELLED', endedAt: new Date(), callFinalizeUntil: p.leaseUntil } }, { session });
454
- await this.trace(session, p, p.activeEpisodeId, 'cancelled', { code: 'CANCELLED' });
455
- return { cancelled: true, episodeId: p.activeEpisodeId };
456
- });
457
- }
458
- async settings(ownerId, settings, expectedControlVersion) {
459
- return this.transaction(async session => {
460
- const scope = this.scope(ownerId);
461
- const p = await this.collections.persons.findOne(scope, { session });
462
- if (!p) fail('NOT_OPEN');
463
- if (p.activeEpisodeId) fail('BUSY'); // No implicit background API or deferred control effects.
464
- if (expectedControlVersion !== undefined && p.controlVersion !== expectedControlVersion) fail('STALE');
465
- const { name, ...patch } = settings;
466
- if (Object.hasOwn(settings, 'name')) p.name = personName(name);
467
- // A partial update must preserve durable fields omitted from the public view.
468
- const next = { ...p.settings, ...patch };
469
- await this.collections.persons.updateOne(scope, { $set: { settings: next, name: p.name }, $inc: { controlVersion: 1, writeSerial: 1 } }, { session });
470
- if (Object.keys(settings).length) await this.trace(session, p, null, 'settings', { settings: settingsView(next), ...(name !== undefined ? { name: p.name } : {}) });
471
- return { settings: settingsView(next), person: this.personView({ ...p, settings: next }) };
472
- });
473
- }
474
- async inspect(ownerId, options, nativeToolIds) {
475
- const { section, cursor, limit } = inspectRequest(options);
476
- await this.getPerson(ownerId);
477
- const scope = this.scope(ownerId);
478
- if (section === 'skills') {
479
- const docs = await this.collections.created_capabilities.find(scope).sort({ id: 1 }).limit(32).maxTimeMS(2000).toArray();
480
- return inspectCapabilities(docs.map(doc => JSON.parse(doc.record)), { cursor, limit }, nativeToolIds);
481
- }
482
- const records = await this.collections.concepts.find({ ...scope, ...(cursor !== null ? { id: { $gt: cursor } } : {}) })
483
- .collation({ locale: 'simple' }).sort({ id: 1 }).limit(limit + 1).maxTimeMS(2000).toArray();
484
- return inspectionPage(records, limit, 'id', conceptView);
485
- }
486
- async search(ownerId, options) {
487
- const { query, cursor, limit } = searchRequest(options);
488
- await this.getPerson(ownerId);
489
- const records = await this.collections.messages.find({ ...this.scope(ownerId),
490
- text: { $regex: query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' }, ...(cursor !== null ? { seq: { $lt: cursor } } : {}) })
491
- .sort({ seq: -1 }).limit(limit + 1).maxTimeMS(2000).toArray();
492
- return inspectionPage(records, limit, 'seq', messageView);
493
- }
494
- async list(ownerId, collection, { cursor = null, limit = 20 }, filter = {}) {
495
- if (!['messages', 'traces'].includes(collection)) fail('INVALID_REQUEST');
496
- await this.getPerson(ownerId);
497
- const docs = await this.collections[collection].find({ ...this.scope(ownerId), ...filter, ...(cursor ? { seq: { $lt: cursor } } : {}) }).sort({ seq: -1 }).limit(limit + 1).maxTimeMS(2000).toArray();
498
- return { items: docs.slice(0, limit).map(collection === 'traces' ? publicTrace : messageView), nextCursor: docs.length > limit ? String(docs[limit - 1].seq) : null };
499
- }
500
- async snapshot(ownerId) {
501
- await this.recover(ownerId);
502
- return this.transaction(async session => {
503
- const scope = this.scope(ownerId);
504
- const p = await this.collections.persons.findOne(scope, { session });
505
- if (!p) fail('NOT_OPEN');
506
- const state = await this.collections.states.findOne(scope, { session });
507
- const messages = await this.collections.messages.find(scope, { session }).sort({ seq: -1 }).limit(21).toArray();
508
- const concepts = await this.collections.concepts.find({ ...scope, id: { $in: state.focusConceptIds } }, { session }).limit(12).toArray();
509
- const episode = await this.collections.episodes.findOne(scope, { session, sort: { inputWatermark: -1 },
510
- projection: { _id: 0, id: 1, status: 1, terminalCode: 1, endedAt: 1 } });
511
- return { latestEpisode: episode, person: this.personView(p), state: stateView(state), concepts: concepts.map(conceptView), messages: messages.slice(0, 20).reverse().map(messageView),
512
- nextMessagesCursor: messages.length > 20 ? String(messages[19].seq) : null, busy: Boolean(p.activeEpisodeId), episodeId: p.activeEpisodeId };
513
- });
514
- }
515
- async close() { await this.client?.close(); }
516
- }
@@ -1,36 +0,0 @@
1
- import { mkdir, readFile, writeFile, link, unlink } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
- import { digest, fail } from './contracts.js';
5
-
6
- /** Deployment-only selection. Never fall back from an unavailable authority to another one. */
7
- export function selectPersonStorage({ storage, uri, yeaftDir }) {
8
- if (storage != null && !['sqlite', 'mongodb'].includes(storage)) fail('INVALID_REQUEST');
9
- if (uri != null && typeof uri !== 'string') fail('INVALID_REQUEST');
10
- const hasMongo = Boolean(uri?.trim());
11
- const selected = storage ?? (hasMongo ? 'mongodb' : 'sqlite');
12
- if (selected === 'sqlite' && hasMongo) fail('STORAGE_MISMATCH');
13
- return { storage: selected, configured: selected === 'mongodb' ? hasMongo : typeof yeaftDir === 'string' && Boolean(yeaftDir.trim()) };
14
- }
15
-
16
- /** This marker is deployment metadata, not memory. Changing backend requires an explicit migration.
17
- * The first configured access binds a namespace; removing Mongo credentials cannot create a new Person.
18
- * Legacy Mongo deployments are bound on their first access after upgrade, not retroactively discovered.
19
- */
20
- export async function bindPersonStorage(yeaftDir, namespace, storage) {
21
- if (!yeaftDir) return; // Direct Mongo integrations may have no local instance directory.
22
- const directory = join(yeaftDir, 'person');
23
- await mkdir(directory, { recursive: true, mode: 0o700 });
24
- const path = join(directory, `storage-${digest(namespace)}.json`);
25
- const temporary = `${path}.${randomUUID()}.tmp`;
26
- await writeFile(temporary, JSON.stringify({ version: 1, storage }), { flag: 'wx', mode: 0o600 });
27
- try {
28
- // A fully written inode is linked atomically; a competing opener never reads partial JSON.
29
- await link(temporary, path);
30
- } catch (error) {
31
- if (error.code !== 'EEXIST') throw error;
32
- let binding;
33
- try { binding = JSON.parse(await readFile(path, 'utf8')); } catch { fail('STORAGE_MISMATCH'); }
34
- if (binding.version !== 1 || binding.storage !== storage) fail('STORAGE_MISMATCH');
35
- } finally { await unlink(temporary).catch(() => {}); }
36
- }