@yeaft/webchat-agent 1.0.591 → 1.0.593

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.
@@ -3,9 +3,10 @@ import { mkdirSync } from 'node:fs';
3
3
  import { dirname } from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { isMainThread } from 'node:worker_threads';
6
- import { bytes, digest, fail, identifier, LIMITS, PersonError, text } from './contracts.js';
6
+ import { admissionReceipt, bytes, digest, fail, identifier, LIMITS, PersonError, text } from './contracts.js';
7
7
  import { CREATED_CAPABILITY_LIMITS, createdCapabilityRecord, validateCreatedCapability } from './created-capability-contract.js';
8
8
  import { SCHEMA, TABLES } from './sqlite-schema.js';
9
+ import { attachmentMetadata, attachmentRequestHash, validateFiles } from './attachments.js';
9
10
  import { capabilityExperienceView, recordCapabilityExperience } from './capability-experience.js';
10
11
 
11
12
  const SCOPE = 'namespace = ? AND ownerId = ? AND personId = ?';
@@ -45,7 +46,7 @@ const publicOutput = (output, failed) => {
45
46
  return failed ? { text: output.text, retainedBytes, observedBytes, complete: false, accepted: false, availability: 'captured', usage, stopReason }
46
47
  : { text: output.text, bytes: retainedBytes, complete: true, usage, stopReason };
47
48
  };
48
- const READS = new Set(['getPerson', 'context', 'recall', 'list', 'searchChanges', 'resolveMemories', 'createdCapabilities']);
49
+ const READS = new Set(['receipt', 'getPerson', 'context', 'recall', 'list', 'searchChanges', 'resolveMemories', 'createdCapabilities', 'episodeAttachments']);
49
50
  const WRITES = new Set(['open', 'recover', 'admit', 'heartbeat', 'append', 'startCall', 'finalizeCall', 'commit', 'finish', 'cancel', 'settings', 'snapshot', 'saveCreatedCapability']);
50
51
  const memoryKind = kind => { if (!['messages', 'concepts'].includes(kind)) fail('INVALID_REQUEST'); return kind; };
51
52
  const boundedLimit = (limit, max = 100) => { if (!Number.isSafeInteger(limit) || limit < 1 || limit > max) fail('INVALID_REQUEST'); return limit; };
@@ -190,11 +191,17 @@ export class SqlitePersonStore {
190
191
  }
191
192
  return expired;
192
193
  }
193
- admit(ownerId, { kind, text: input, clientMessageId, workerId, budget }) {
194
+ receipt(ownerId, clientMessageId, requestHash) {
195
+ identifier(clientMessageId);
196
+ const episode = this.one('episodes', this.scope(ownerId), ' AND clientMessageId = ?', [clientMessageId]);
197
+ return admissionReceipt(episode, clientMessageId, requestHash);
198
+ }
199
+ admit(ownerId, { kind, text: input, clientMessageId, workerId, budget, files = [] }) {
194
200
  if (!['send', 'think', 'dream'].includes(kind)) fail('INVALID_REQUEST');
195
- text(input, LIMITS.inputBytes, kind !== 'send'); identifier(clientMessageId); text(workerId, 256);
201
+ const validated = validateFiles(files, input, kind), attachments = validated.map(attachmentMetadata);
202
+ identifier(clientMessageId); text(workerId, 256);
196
203
  this.recover(ownerId);
197
- const scope = this.scope(ownerId), requestHash = digest([kind, input]);
204
+ const scope = this.scope(ownerId), requestHash = attachmentRequestHash(kind, input, validated);
198
205
  const existing = this.one('episodes', scope, ' AND clientMessageId = ?', [clientMessageId]);
199
206
  if (existing) {
200
207
  if (existing.requestHash !== requestHash) fail('IDEMPOTENCY_CONFLICT');
@@ -203,21 +210,33 @@ export class SqlitePersonStore {
203
210
  const p = this.getPerson(ownerId);
204
211
  if (p.activeEpisodeId) fail('BUSY');
205
212
  const id = randomUUID();
213
+ const hasMessage = kind === 'send' || (kind === 'think' && attachments.length > 0);
206
214
  p.activeEpisodeId = id; p.leaseOwner = workerId; p.leaseUntil = new Date(this.now.getTime() + this.leaseMs);
207
- p.epoch++; p.writeSerial++; p.inputWatermark++; if (kind === 'send') p.messageSeq++;
215
+ p.epoch++; p.writeSerial++; p.inputWatermark++; if (hasMessage) p.messageSeq++;
208
216
  this.put('persons', p);
209
217
  let messageId = null;
210
- if (kind === 'send') {
218
+ for (const file of validated) this.put('attachments', this.doc(scope, file));
219
+ if (hasMessage) {
211
220
  messageId = randomUUID();
212
- const message = this.doc(scope, { id: messageId, revision: 1, seq: p.messageSeq, episodeId: id, role: 'user', text: input, createdAt: this.now, clientMessageId });
221
+ const message = this.doc(scope, { id: messageId, revision: 1, seq: p.messageSeq, episodeId: id, role: 'user', text: input, attachments, createdAt: this.now, clientMessageId });
213
222
  this.put('messages', message, true); this.journal(scope, 'messages', message);
214
223
  }
215
- const episode = this.doc(scope, { id, clientMessageId, requestHash, kind, text: input, messageId, status: 'running', workerId, epoch: p.epoch,
224
+ const episode = this.doc(scope, { id, clientMessageId, requestHash, kind, text: input, messageId, attachments, modelCandidates: [...(p.settings.modelCandidates ?? [])], status: 'running', workerId, epoch: p.epoch,
216
225
  baseStateVersion: p.stateVersion, inputWatermark: p.inputWatermark, controlVersion: p.controlVersion, budget, createdAt: this.now });
217
226
  this.put('episodes', episode, true);
218
- this.trace(ownerId, id, 'accepted', { trigger: { kind, text: input, messageId }, baseStateVersion: p.stateVersion, budget });
227
+ this.trace(ownerId, id, 'accepted', { trigger: { kind, text: input, messageId, attachments }, baseStateVersion: p.stateVersion, budget });
219
228
  return { episodeId: id, duplicate: false, status: 'running', episode };
220
229
  }
230
+ episodeAttachments(episode) {
231
+ this.own(episode);
232
+ const scope = this.episodeScope(episode);
233
+ const stored = this.one('episodes', scope, ' AND id = ?', [episode.id]);
234
+ return (stored.attachments ?? []).map(ref => {
235
+ const file = this.one('attachments', scope, ' AND id = ?', [ref.id]);
236
+ if (!file || file.sha256 !== ref.sha256) fail('STORAGE_UNAVAILABLE');
237
+ return publicDoc(file);
238
+ });
239
+ }
221
240
  episodeScope(episode) {
222
241
  const scope = this.scope(episode.ownerId);
223
242
  if (episode.namespace !== scope.namespace || episode.personId !== scope.personId) fail('STALE');