@yeaft/webchat-agent 1.0.595 → 1.0.597

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.595",
3
+ "version": "1.0.597",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,6 +1,7 @@
1
1
  import { bytes, digest, fail, identifier, object, page, text } from './contracts.js';
2
2
  import { createdCapabilityRecord, validateCreatedDefinition } from './created-capability-contract.js';
3
3
  import { runPersonScript, scriptInput, testPersonScript } from './script-executor.js';
4
+ import { createdSkillView, inspectionPage } from './inspection.js';
4
5
 
5
6
  // Built-in cognition plus bounded pure-script creation, not the Agent's full registry.
6
7
  // No shell/filesystem/network/VP authority is exposed to generated code.
@@ -65,6 +66,24 @@ function scriptManifest(record) {
65
66
  origin: 'person-created', evidence: record.evidence };
66
67
  }
67
68
 
69
+ /** Read the real catalog without preparing or executing any capability. No host
70
+ * registry, file lookup, model configuration or script VM participates. */
71
+ export function inspectCapabilities(created, { cursor, limit }) {
72
+ const entries = manifests.map(contract => ({ id: contract.id, domain: contract.domain, description: contract.description,
73
+ version: contract.version, revision: contract.revision, contract: copy(contract), source: { kind: 'builtin' } }));
74
+ for (const record of created) {
75
+ const contract = scriptManifest(record);
76
+ // Evidence is a fixed public shape even when reading older raw records.
77
+ const item = createdSkillView(record, contract);
78
+ contract.evidence = item.evidence;
79
+ entries.push(item);
80
+ }
81
+ // IDs are ASCII identifiers. Binary ordering matches SQLite and Mongo simple collation.
82
+ const matches = entries.filter(entry => cursor === null || entry.id > cursor)
83
+ .sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
84
+ return inspectionPage(matches, limit, 'id');
85
+ }
86
+
68
87
  export class PersonCapabilities {
69
88
  constructor(repository, ownerId, { experience = [], triggerKind = 'think', now = Date.now(), created = [], episode } = {}) {
70
89
  this.repository = repository; this.ownerId = ownerId; this.episode = episode;
@@ -0,0 +1,60 @@
1
+ import { bytes, fail, identifier, object, page, text } from './contracts.js';
2
+
3
+ /** Public read APIs use keyset cursors, never offsets or model-ranked recall. */
4
+ export function inspectRequest(payload = {}) {
5
+ object(payload, ['section', 'cursor', 'limit'], ['section']);
6
+ if (!['memory', 'skills'].includes(payload.section)) fail('INVALID_REQUEST');
7
+ const { limit } = page({ limit: payload.limit });
8
+ const cursor = payload.cursor ?? null;
9
+ if (cursor !== null) identifier(cursor);
10
+ return { section: payload.section, cursor, limit };
11
+ }
12
+ export function searchRequest(payload = {}) {
13
+ object(payload, ['query', 'cursor', 'limit'], ['query']);
14
+ return { query: text(payload.query, 512), ...page({ cursor: payload.cursor, limit: payload.limit }) };
15
+ }
16
+ export function personName(value) {
17
+ if (typeof value !== 'string') fail('INVALID_REQUEST');
18
+ return text(value.trim(), 160);
19
+ }
20
+ const pick = (value, keys) => value == null ? null : Object.fromEntries(keys.filter(key => Object.hasOwn(value, key)).map(key => [key, value[key]]));
21
+ export const settingsView = value => pick(value, ['autonomyEnabled', 'modelCandidates']);
22
+ export function messageView(value) {
23
+ const result = pick(value, ['id', 'schemaVersion', 'revision', 'seq', 'episodeId', 'clientMessageId', 'role', 'text', 'createdAt']);
24
+ if (result && Array.isArray(value.attachments)) result.attachments = value.attachments.slice(0, 4)
25
+ .map(file => pick(file, ['id', 'name', 'mimeType', 'size', 'sha256', 'kind']));
26
+ return result;
27
+ }
28
+ export function conceptView(value) {
29
+ const result = pick(value, ['id', 'schemaVersion', 'kind', 'statement', 'epistemicState', 'sourceRefs', 'reportedSourceRefs', 'revision', 'episodeId', 'callId', 'stateVersion', 'updatedAt']);
30
+ if (result && Array.isArray(value.associations)) result.associations = value.associations.map(a => pick(a, ['targetId', 'relation']));
31
+ return result;
32
+ }
33
+ export function stateView(value) {
34
+ const result = pick(value, ['schemaVersion', 'version', 'summary', 'appraisal', 'focusConceptIds', 'updatedAt', 'episodeId']);
35
+ if (!result) return null;
36
+ if (Array.isArray(value.conceptRefs)) result.conceptRefs = value.conceptRefs.map(ref => pick(ref, ['id', 'revision']));
37
+ if (Object.hasOwn(value, 'decision')) result.decision = pick(value.decision, ['summary', 'uncertainties', 'selfCheck']);
38
+ if (Object.hasOwn(value, 'lastSelection')) result.lastSelection = pick(value.lastSelection, ['model', 'effort']);
39
+ return result;
40
+ }
41
+ export function createdSkillView(record, contract) {
42
+ return { ...pick(record, ['id', 'description', 'version', 'revision', 'code', 'inputDescription', 'outputDescription', 'createdAt', 'updatedAt']),
43
+ tests: Array.isArray(record.tests) ? record.tests.map(test => pick(test, ['input', 'expected'])) : [],
44
+ domain: 'script', contract,
45
+ evidence: pick(record.evidence, ['engine', 'testsPassed', 'testedAt']),
46
+ source: { kind: 'person-created', episodeId: record.createdEpisodeId, callId: record.createdCallId } };
47
+ }
48
+ /** Complete records only. Continuation follows the last returned key even when the
49
+ * byte budget, rather than the requested count, ends a page. No attachment bytes. */
50
+ export function inspectionPage(records, limit, key, view = value => value) {
51
+ const items = []; let size = 0;
52
+ for (const record of records) {
53
+ if (items.length === limit) break;
54
+ const item = view(record), cost = bytes(item);
55
+ if (cost > 256 * 1024) fail('OUTPUT_LIMIT');
56
+ if (items.length && size + cost > 256 * 1024) break;
57
+ items.push(item); size += cost;
58
+ }
59
+ return { items, nextCursor: records.length > items.length ? String(items.at(-1)[key]) : null };
60
+ }
@@ -3,6 +3,8 @@ import { admissionReceipt, bytes, digest, fail, identifier, LIMITS, PersonError,
3
3
  import { CREATED_CAPABILITY_LIMITS, createdCapabilityRecord, validateCreatedCapability } from './created-capability-contract.js';
4
4
  import { attachmentMetadata, attachmentRequestHash, validateFiles } from './attachments.js';
5
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';
6
8
 
7
9
  const COLLECTIONS = ['persons', 'attachments', 'messages', 'episodes', 'states', 'concepts', 'concept_revisions', 'state_commits', 'traces', 'created_capabilities', 'created_capability_revisions'];
8
10
  const txOptions = { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }, readPreference: 'primary', maxCommitTimeMS: 5000, timeoutMS: 10000 };
@@ -97,7 +99,7 @@ export class MongoPersonRepository {
97
99
  finally { await session.endSession(); }
98
100
  }
99
101
  personView(p) {
100
- return { id: p.personId, name: p.name, soul: p.soul, soulRevision: p.soulRevision, createdAt: iso(p.createdAt), settings: p.settings };
102
+ return { id: p.personId, name: p.name, soul: p.soul, soulRevision: p.soulRevision, createdAt: iso(p.createdAt), settings: settingsView(p.settings) };
101
103
  }
102
104
  async open(ownerId, name = 'Digital Person') {
103
105
  const scope = this.scope(ownerId);
@@ -334,9 +336,9 @@ export class MongoPersonRepository {
334
336
  this.collections.states.findOne(scope).then(s => this.collections.concepts.find({ ...scope, id: { $in: s.focusConceptIds } }).limit(12).toArray()),
335
337
  this.collections.concepts.find(scope).sort({ updatedAt: -1, id: 1 }).limit(12).toArray(),
336
338
  ]);
337
- const concepts = [...new Map([...focused, ...recent].map(c => [c.id, publicDoc(c)])).values()];
339
+ const concepts = [...new Map([...focused, ...recent].map(c => [c.id, conceptView(c)])).values()];
338
340
  return { person: this.personView(p), capabilityExperience: capabilityExperienceView(p.capabilityExperience),
339
- state: publicDoc(state), messages: messages.reverse().map(publicDoc), concepts };
341
+ state: stateView(state), messages: messages.reverse().map(messageView), concepts };
340
342
  }
341
343
  async recall(ownerId, { kind = 'messages', query = '', cursor = null, limit = 5 }) {
342
344
  const scope = this.scope(ownerId);
@@ -345,7 +347,7 @@ export class MongoPersonRepository {
345
347
  await this.init();
346
348
  const filter = { ...scope, ...(query ? { statement: { $regex: escaped, $options: 'i' } } : {}), ...(cursor ? { id: { $gt: cursor } } : {}) };
347
349
  const docs = await this.collections.concepts.find(filter).sort({ id: 1 }).limit(limit + 1).maxTimeMS(2000).toArray();
348
- return { items: docs.slice(0, limit).map(publicDoc), nextCursor: docs.length > limit ? docs[limit - 1].id : null };
350
+ return { items: docs.slice(0, limit).map(conceptView), nextCursor: docs.length > limit ? docs[limit - 1].id : null };
349
351
  }
350
352
  async commit(episode, proposal, selection, callId, reportedSources = new Map()) {
351
353
  return this.transaction(async session => {
@@ -376,12 +378,12 @@ export class MongoPersonRepository {
376
378
  const updated = await this.collections.states.replaceOne({ ...scope, version: episode.baseStateVersion }, state, { session });
377
379
  if (updated.matchedCount !== 1) fail('STALE');
378
380
  await this.collections.state_commits.insertOne(this.doc(scope, { id: randomUUID(), version: p.stateVersion, parentVersion: episode.baseStateVersion,
379
- episodeId: episode.id, callId, conceptRevisions: revisions, state: publicDoc(state), createdAt: new Date() }), { session });
381
+ episodeId: episode.id, callId, conceptRevisions: revisions, state: stateView(state), createdAt: new Date() }), { session });
380
382
  if (proposal.reply) await this.collections.messages.insertOne(this.doc(scope, { id: randomUUID(), revision: 1, seq: p.messageSeq, episodeId: episode.id,
381
383
  role: 'assistant', text: proposal.reply, createdAt: new Date() }), { session });
382
384
  await this.collections.episodes.updateOne({ ...scope, id: episode.id, status: 'running' }, { $set: { status: 'completed', endedAt: new Date(), stateVersion: p.stateVersion } }, { session });
383
385
  await this.trace(session, p, episode.id, 'committed', { callId, parentVersion: episode.baseStateVersion, stateVersion: p.stateVersion, conceptRevisions: revisions, decision: proposal.decision });
384
- return { state: publicDoc(state) };
386
+ return { state: stateView(state) };
385
387
  });
386
388
  }
387
389
  async finish(episode, status, code) {
@@ -416,16 +418,39 @@ export class MongoPersonRepository {
416
418
  const p = await this.collections.persons.findOne(scope, { session });
417
419
  if (!p) fail('NOT_OPEN');
418
420
  if (p.activeEpisodeId) fail('BUSY'); // No implicit background API or deferred control effects.
419
- const next = { ...p.settings, ...settings };
420
- await this.collections.persons.updateOne(scope, { $set: { settings: next }, $inc: { controlVersion: 1, writeSerial: 1 } }, { session });
421
- if (Object.keys(settings).length) await this.trace(session, p, null, 'settings', { settings: next });
422
- return { settings: next };
421
+ const { name, ...patch } = settings;
422
+ if (Object.hasOwn(settings, 'name')) p.name = personName(name);
423
+ const next = { ...settingsView(p.settings), ...patch };
424
+ await this.collections.persons.updateOne(scope, { $set: { settings: next, name: p.name }, $inc: { controlVersion: 1, writeSerial: 1 } }, { session });
425
+ if (Object.keys(settings).length) await this.trace(session, p, null, 'settings', { settings: next, ...(name !== undefined ? { name: p.name } : {}) });
426
+ return { settings: next, person: this.personView({ ...p, settings: next }) };
423
427
  });
424
428
  }
429
+ async inspect(ownerId, options) {
430
+ const { section, cursor, limit } = inspectRequest(options);
431
+ await this.getPerson(ownerId);
432
+ const scope = this.scope(ownerId);
433
+ if (section === 'skills') {
434
+ const docs = await this.collections.created_capabilities.find(scope).sort({ id: 1 }).limit(32).maxTimeMS(2000).toArray();
435
+ return inspectCapabilities(docs.map(doc => JSON.parse(doc.record)), { cursor, limit });
436
+ }
437
+ const records = await this.collections.concepts.find({ ...scope, ...(cursor !== null ? { id: { $gt: cursor } } : {}) })
438
+ .collation({ locale: 'simple' }).sort({ id: 1 }).limit(limit + 1).maxTimeMS(2000).toArray();
439
+ return inspectionPage(records, limit, 'id', conceptView);
440
+ }
441
+ async search(ownerId, options) {
442
+ const { query, cursor, limit } = searchRequest(options);
443
+ await this.getPerson(ownerId);
444
+ const records = await this.collections.messages.find({ ...this.scope(ownerId),
445
+ text: { $regex: query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' }, ...(cursor !== null ? { seq: { $lt: cursor } } : {}) })
446
+ .sort({ seq: -1 }).limit(limit + 1).maxTimeMS(2000).toArray();
447
+ return inspectionPage(records, limit, 'seq', messageView);
448
+ }
425
449
  async list(ownerId, collection, { cursor = null, limit = 20 }, filter = {}) {
450
+ if (!['messages', 'traces'].includes(collection)) fail('INVALID_REQUEST');
426
451
  await this.getPerson(ownerId);
427
452
  const docs = await this.collections[collection].find({ ...this.scope(ownerId), ...filter, ...(cursor ? { seq: { $lt: cursor } } : {}) }).sort({ seq: -1 }).limit(limit + 1).maxTimeMS(2000).toArray();
428
- return { items: docs.slice(0, limit).map(collection === 'traces' ? publicTrace : publicDoc), nextCursor: docs.length > limit ? String(docs[limit - 1].seq) : null };
453
+ return { items: docs.slice(0, limit).map(collection === 'traces' ? publicTrace : messageView), nextCursor: docs.length > limit ? String(docs[limit - 1].seq) : null };
429
454
  }
430
455
  async snapshot(ownerId) {
431
456
  await this.recover(ownerId);
@@ -438,7 +463,7 @@ export class MongoPersonRepository {
438
463
  const concepts = await this.collections.concepts.find({ ...scope, id: { $in: state.focusConceptIds } }, { session }).limit(12).toArray();
439
464
  const episode = await this.collections.episodes.findOne(scope, { session, sort: { inputWatermark: -1 },
440
465
  projection: { _id: 0, id: 1, status: 1, terminalCode: 1, endedAt: 1 } });
441
- return { latestEpisode: episode, person: this.personView(p), state: publicDoc(state), concepts: concepts.map(publicDoc), messages: messages.slice(0, 20).reverse().map(publicDoc),
466
+ return { latestEpisode: episode, person: this.personView(p), state: stateView(state), concepts: concepts.map(conceptView), messages: messages.slice(0, 20).reverse().map(messageView),
442
467
  nextMessagesCursor: messages.length > 20 ? String(messages[19].seq) : null, busy: Boolean(p.activeEpisodeId), episodeId: p.activeEpisodeId };
443
468
  });
444
469
  }
@@ -6,6 +6,7 @@ import { PersonRuntime } from './runtime.js';
6
6
  import { validateFiles } from './attachments.js';
7
7
  import { createPersonProvider, validateModelCandidates } from './provider.js';
8
8
  import { fail, identifier, LIMITS, object, page, safeError, text } from './contracts.js';
9
+ import { inspectRequest, personName, searchRequest } from './inspection.js';
9
10
 
10
11
  /**
11
12
  * One durable Person per (namespace, authenticated ownerId). The transport MUST
@@ -102,6 +103,12 @@ export function createPersonService(options = {}) {
102
103
  }
103
104
  case 'traces': await repository.recover(ownerId); return repository.list(ownerId, 'traces', page(payload));
104
105
  case 'messages': return repository.list(ownerId, 'messages', page(payload));
106
+ // These reads deliberately bypass recover(), Recall and the runtime/provider.
107
+ case 'inspect': return repository.inspect(ownerId, inspectRequest(payload));
108
+ case 'search': {
109
+ searchRequest(payload);
110
+ return repository.search(ownerId, payload);
111
+ }
105
112
  case 'cancel': {
106
113
  object(payload, ['episodeId'], []);
107
114
  if (payload.episodeId != null) identifier(payload.episodeId);
@@ -110,7 +117,9 @@ export function createPersonService(options = {}) {
110
117
  return result;
111
118
  }
112
119
  case 'settings': {
113
- object(payload, ['autonomyEnabled', 'modelCandidates'], []);
120
+ object(payload, ['name', 'autonomyEnabled', 'modelCandidates'], []);
121
+ const patch = { ...payload };
122
+ if (Object.hasOwn(patch, 'name')) patch.name = personName(patch.name);
114
123
  if (Object.hasOwn(payload, 'autonomyEnabled') && typeof payload.autonomyEnabled !== 'boolean') fail('INVALID_REQUEST');
115
124
  if (Object.hasOwn(payload, 'modelCandidates')) {
116
125
  const refs = payload.modelCandidates;
@@ -122,7 +131,7 @@ export function createPersonService(options = {}) {
122
131
  }
123
132
  // Timer-driven autonomy is deliberately not claimed or silently enabled.
124
133
  if (payload.autonomyEnabled === true) fail('UNSUPPORTED');
125
- return repository.settings(ownerId, payload);
134
+ return repository.settings(ownerId, patch);
126
135
  }
127
136
  default: fail('INVALID_REQUEST');
128
137
  }
@@ -104,6 +104,8 @@ export class SqlitePersonRepository {
104
104
  async settings(ownerId, settings) { return this.request('settings', [ownerId, settings]); }
105
105
  async list(ownerId, collection, options, filter) { return this.request('list', [ownerId, collection, options, filter]); }
106
106
  async snapshot(ownerId) { return this.request('snapshot', [ownerId]); }
107
+ async inspect(ownerId, options) { return this.request('inspect', [ownerId, options]); }
108
+ async search(ownerId, options) { return this.request('search', [ownerId, options]); }
107
109
  /** Ascending durable journal, scoped before paging. Empty pages retain after. */
108
110
  async searchChanges(ownerId, options) { return this.request('searchChanges', [ownerId, options]); }
109
111
  /** kind is 'messages'|'concepts'; returns a deduplicated array of current public records. */
@@ -8,6 +8,8 @@ import { CREATED_CAPABILITY_LIMITS, createdCapabilityRecord, validateCreatedCapa
8
8
  import { SCHEMA, TABLES } from './sqlite-schema.js';
9
9
  import { attachmentMetadata, attachmentRequestHash, validateFiles } from './attachments.js';
10
10
  import { capabilityExperienceView, recordCapabilityExperience } from './capability-experience.js';
11
+ import { conceptView, inspectionPage, inspectRequest, messageView, personName, searchRequest, settingsView, stateView } from './inspection.js';
12
+ import { inspectCapabilities } from './capabilities.js';
11
13
 
12
14
  const SCOPE = 'namespace = ? AND ownerId = ? AND personId = ?';
13
15
  const scopeValues = s => [s.namespace, s.ownerId, s.personId];
@@ -46,7 +48,7 @@ const publicOutput = (output, failed) => {
46
48
  return failed ? { text: output.text, retainedBytes, observedBytes, complete: false, accepted: false, availability: 'captured', usage, stopReason }
47
49
  : { text: output.text, bytes: retainedBytes, complete: true, usage, stopReason };
48
50
  };
49
- const READS = new Set(['receipt', 'getPerson', 'context', 'recall', 'list', 'searchChanges', 'resolveMemories', 'createdCapabilities', 'episodeAttachments']);
51
+ const READS = new Set(['receipt', 'inspect', 'search', 'getPerson', 'context', 'recall', 'list', 'searchChanges', 'resolveMemories', 'createdCapabilities', 'episodeAttachments']);
50
52
  const WRITES = new Set(['open', 'recover', 'admit', 'heartbeat', 'append', 'startCall', 'finalizeCall', 'commit', 'finish', 'cancel', 'settings', 'snapshot', 'saveCreatedCapability']);
51
53
  const memoryKind = kind => { if (!['messages', 'concepts'].includes(kind)) fail('INVALID_REQUEST'); return kind; };
52
54
  const boundedLimit = (limit, max = 100) => { if (!Number.isSafeInteger(limit) || limit < 1 || limit > max) fail('INVALID_REQUEST'); return limit; };
@@ -135,7 +137,7 @@ export class SqlitePersonStore {
135
137
  .run(...scopeValues(scope), kind, record.id, record.revision, JSON.stringify(publicDoc(record)));
136
138
  }
137
139
  personView(p) {
138
- return { id: p.personId, name: p.name, soul: p.soul, soulRevision: p.soulRevision, createdAt: p.createdAt.toISOString(), settings: p.settings };
140
+ return { id: p.personId, name: p.name, soul: p.soul, soulRevision: p.soulRevision, createdAt: p.createdAt.toISOString(), settings: settingsView(p.settings) };
139
141
  }
140
142
  getPerson(ownerId) {
141
143
  const p = this.one('persons', this.scope(ownerId));
@@ -320,8 +322,8 @@ export class SqlitePersonStore {
320
322
  const focused = this.focused(scope, state.focusConceptIds).slice(0, 12);
321
323
  const recent = this.rows('concepts', scope, ' ORDER BY updatedAt DESC, id ASC LIMIT 12');
322
324
  return { person: this.personView(p), capabilityExperience: capabilityExperienceView(p.capabilityExperience),
323
- state: publicDoc(state), messages: this.rows('messages', scope, ' ORDER BY seq DESC LIMIT 12').reverse().map(publicDoc),
324
- concepts: [...new Map([...focused, ...recent].map(c => [c.id, publicDoc(c)])).values()] };
325
+ state: stateView(state), messages: this.rows('messages', scope, ' ORDER BY seq DESC LIMIT 12').reverse().map(messageView),
326
+ concepts: [...new Map([...focused, ...recent].map(c => [c.id, conceptView(c)])).values()] };
325
327
  }
326
328
  commit(episode, proposal, selection, callId, reportedSources = new Map()) {
327
329
  const p = this.own(episode), scope = this.episodeScope(episode);
@@ -346,7 +348,7 @@ export class SqlitePersonStore {
346
348
  if (this.one('states', scope)?.version !== episode.baseStateVersion) fail('STALE');
347
349
  this.put('states', state);
348
350
  this.put('state_commits', this.doc(scope, { id: randomUUID(), version: p.stateVersion, parentVersion: episode.baseStateVersion,
349
- episodeId: episode.id, callId, conceptRevisions: revisions, state: publicDoc(state), createdAt: this.now }), true);
351
+ episodeId: episode.id, callId, conceptRevisions: revisions, state: stateView(state), createdAt: this.now }), true);
350
352
  if (proposal.reply) {
351
353
  const message = this.doc(scope, { id: randomUUID(), revision: 1, seq: p.messageSeq, episodeId: episode.id, role: 'assistant', text: proposal.reply, createdAt: this.now });
352
354
  this.put('messages', message, true); this.journal(scope, 'messages', message);
@@ -355,7 +357,7 @@ export class SqlitePersonStore {
355
357
  if (!record || record.status !== 'running') fail('STALE');
356
358
  Object.assign(record, { status: 'completed', endedAt: this.now, stateVersion: p.stateVersion }); this.put('episodes', record);
357
359
  this.trace(episode.ownerId, episode.id, 'committed', { callId, parentVersion: episode.baseStateVersion, stateVersion: p.stateVersion, conceptRevisions: revisions, decision: proposal.decision });
358
- return { state: publicDoc(state) };
360
+ return { state: stateView(state) };
359
361
  }
360
362
  finish(episode, status, code) {
361
363
  const p = this.fenced(episode, false);
@@ -383,9 +385,26 @@ export class SqlitePersonStore {
383
385
  settings(ownerId, settings) {
384
386
  const p = this.getPerson(ownerId);
385
387
  if (p.activeEpisodeId) fail('BUSY');
386
- p.settings = { ...p.settings, ...settings }; p.controlVersion++; p.writeSerial++; this.put('persons', p);
387
- if (Object.keys(settings).length) this.trace(ownerId, null, 'settings', { settings: p.settings });
388
- return { settings: p.settings };
388
+ const { name, ...patch } = settings;
389
+ if (Object.hasOwn(settings, 'name')) p.name = personName(name);
390
+ p.settings = { ...settingsView(p.settings), ...patch }; p.controlVersion++; p.writeSerial++; this.put('persons', p);
391
+ if (Object.keys(settings).length) this.trace(ownerId, null, 'settings', { settings: p.settings, ...(name !== undefined ? { name: p.name } : {}) });
392
+ return { settings: settingsView(p.settings), person: this.personView(p) };
393
+ }
394
+ inspect(ownerId, options) {
395
+ const { section, cursor, limit } = inspectRequest(options);
396
+ this.getPerson(ownerId);
397
+ const scope = this.scope(ownerId);
398
+ if (section === 'skills') return inspectCapabilities(this.rows('created_capabilities', scope, ' ORDER BY id ASC LIMIT 32'), { cursor, limit });
399
+ const records = this.rows('concepts', scope, ' AND (? IS NULL OR id > ?) ORDER BY id ASC LIMIT ?', [cursor, cursor, limit + 1]);
400
+ return inspectionPage(records, limit, 'id', conceptView);
401
+ }
402
+ search(ownerId, options) {
403
+ const { query, cursor, limit } = searchRequest(options);
404
+ this.getPerson(ownerId);
405
+ const records = this.rows('messages', this.scope(ownerId),
406
+ ' AND (? IS NULL OR seq < ?) AND instr(text, ?) > 0 ORDER BY seq DESC LIMIT ?', [cursor, cursor, query.toLowerCase(), limit + 1]);
407
+ return inspectionPage(records, limit, 'seq', messageView);
389
408
  }
390
409
  list(ownerId, collection, { cursor = null, limit = 20 } = {}, filter = {}) {
391
410
  if (!['messages', 'traces'].includes(collection)) fail('INVALID_REQUEST');
@@ -398,7 +417,7 @@ export class SqlitePersonStore {
398
417
  if (cursor != null) { clause += ' AND seq < ?'; params.push(sequence(cursor)); }
399
418
  if (query) { clause += ' AND instr(text, ?) > 0'; params.push(query.toLowerCase()); }
400
419
  const docs = this.rows(collection, this.scope(ownerId), `${clause} ORDER BY seq DESC LIMIT ?`, [...params, limit + 1]);
401
- return { items: docs.slice(0, limit).map(publicDoc), nextCursor: docs.length > limit ? String(docs[limit - 1].seq) : null };
420
+ return { items: docs.slice(0, limit).map(collection === 'messages' ? messageView : publicDoc), nextCursor: docs.length > limit ? String(docs[limit - 1].seq) : null };
402
421
  }
403
422
  recall(ownerId, { kind = 'messages', query = '', cursor = null, limit = 5 } = {}) {
404
423
  memoryKind(kind); text(query, LIMITS.inputBytes, true); boundedLimit(limit);
@@ -406,7 +425,7 @@ export class SqlitePersonStore {
406
425
  this.getPerson(ownerId);
407
426
  if (cursor != null) text(cursor, 128);
408
427
  const docs = this.rows('concepts', this.scope(ownerId), ' AND (? IS NULL OR id > ?) AND instr(statement, ?) > 0 ORDER BY id ASC LIMIT ?', [cursor, cursor, query.toLowerCase(), limit + 1]);
409
- return { items: docs.slice(0, limit).map(publicDoc), nextCursor: docs.length > limit ? docs[limit - 1].id : null };
428
+ return { items: docs.slice(0, limit).map(conceptView), nextCursor: docs.length > limit ? docs[limit - 1].id : null };
410
429
  }
411
430
  snapshot(ownerId) {
412
431
  this.recover(ownerId);
@@ -415,15 +434,16 @@ export class SqlitePersonStore {
415
434
  const episode = this.one('episodes', scope, ' ORDER BY inputWatermark DESC');
416
435
  const latestEpisode = episode ? { id: episode.id, status: episode.status,
417
436
  ...(episode.terminalCode ? { terminalCode: episode.terminalCode } : {}), ...(episode.endedAt ? { endedAt: episode.endedAt } : {}) } : null;
418
- return { latestEpisode, person: this.personView(p), state: publicDoc(state), concepts: this.focused(scope, state.focusConceptIds).slice(0, 12).map(publicDoc),
419
- messages: messages.slice(0, 20).reverse().map(publicDoc), nextMessagesCursor: messages.length > 20 ? String(messages[19].seq) : null,
437
+ return { latestEpisode, person: this.personView(p), state: stateView(state), concepts: this.focused(scope, state.focusConceptIds).slice(0, 12).map(conceptView),
438
+ messages: messages.slice(0, 20).reverse().map(messageView), nextMessagesCursor: messages.length > 20 ? String(messages[19].seq) : null,
420
439
  busy: Boolean(p.activeEpisodeId), episodeId: p.activeEpisodeId };
421
440
  }
422
441
  searchChanges(ownerId, { after = 0, limit = 100 } = {}) {
423
442
  this.getPerson(ownerId); after = sequence(after); boundedLimit(limit, 1000);
424
443
  const rows = this.sql(`SELECT seq, kind, id, revision, record FROM memory_changes WHERE ${SCOPE} AND seq > ? ORDER BY seq ASC LIMIT ?`)
425
444
  .all(...scopeValues(this.scope(ownerId)), after, limit + 1);
426
- const items = rows.slice(0, limit).map(row => ({ ...row, record: decode(row, row.kind) }));
445
+ const items = rows.slice(0, limit).map(row => ({ ...row,
446
+ record: (row.kind === 'messages' ? messageView : conceptView)(decode(row, row.kind)) }));
427
447
  return { items, lastSeq: items.at(-1)?.seq ?? after, hasMore: rows.length > limit };
428
448
  }
429
449
  resolveMemories(ownerId, kind, refs) {
@@ -433,7 +453,7 @@ export class SqlitePersonStore {
433
453
  for (const ref of refs) {
434
454
  if (!ref || typeof ref.id !== 'string' || !Number.isSafeInteger(ref.revision) || ref.revision < 1) fail('INVALID_REQUEST');
435
455
  const record = this.one(kind, scope, ' AND id = ? AND revision = ?', [ref.id, ref.revision]);
436
- if (record && !seen.has(record.id)) { seen.add(record.id); records.push(publicDoc(record)); }
456
+ if (record && !seen.has(record.id)) { seen.add(record.id); records.push((kind === 'messages' ? messageView : conceptView)(record)); }
437
457
  }
438
458
  return records;
439
459
  }