@yeaft/webchat-agent 1.0.589 → 1.0.590

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.
@@ -0,0 +1,77 @@
1
+ import { parentPort, workerData } from 'node:worker_threads';
2
+ import { getQuickJS } from 'quickjs-emscripten';
3
+
4
+ // Separate WASM interpreter, NOT node:vm. No host functions/modules are bound.
5
+ // Parent termination bounds startup, computation, output extraction and cleanup.
6
+ let runtime, context;
7
+ try {
8
+ const engine = await getQuickJS();
9
+ runtime = engine.newRuntime();
10
+ runtime.setMemoryLimit(16 * 1024 * 1024);
11
+ runtime.setMaxStackSize(256 * 1024);
12
+ const deadline = Date.now() + 200;
13
+ runtime.setInterruptHandler(() => Date.now() > deadline);
14
+ context = runtime.newContext();
15
+ const { code, input } = workerData;
16
+ // Capture intrinsics before executing generated code; Function compiles ONLY a
17
+ // function body and has no access to this validation closure. Neither raw code
18
+ // interpolation nor mutable guest serializers can redefine the output boundary.
19
+ const source = `(() => {
20
+ 'use strict';
21
+ const compile = Function, parse = JSON.parse, stringify = JSON.stringify;
22
+ const isArray = Array.isArray, finite = Number.isFinite;
23
+ const proto = Object.getPrototypeOf, objectProto = Object.prototype;
24
+ const descriptor = Object.getOwnPropertyDescriptor, keys = Reflect.ownKeys;
25
+ const create = Object.create, setProto = Object.setPrototypeOf, ErrorType = Error;
26
+ const input = parse(${JSON.stringify(JSON.stringify(input))});
27
+ const fn = compile('input', ${JSON.stringify(`'use strict';\n${code}`)});
28
+ const ancestors = create(null);
29
+ let nodes = 0;
30
+ function snapshot(v, depth) {
31
+ if (++nodes > 8192 || depth > 32) throw new ErrorType('JSON limit');
32
+ if (v === null || typeof v === 'string' || typeof v === 'boolean') return v;
33
+ if (typeof v === 'number' && finite(v)) return v;
34
+ if (typeof v !== 'object') throw new ErrorType('JSON only');
35
+ for (let i = 0; i < depth; i++) if (ancestors[i] === v) throw new ErrorType('JSON cycle');
36
+ const array = isArray(v), p = proto(v);
37
+ if (!array && p !== objectProto && p !== null) throw new ErrorType('JSON object only');
38
+ const names = keys(v);
39
+ const length = array ? descriptor(v, 'length').value : names.length;
40
+ if (length > 8192 || names.length !== length + (array ? 1 : 0)) throw new ErrorType('JSON keys');
41
+ const out = array ? setProto([], null) : create(null);
42
+ ancestors[depth] = v;
43
+ for (let i = 0; i < length; i++) {
44
+ const key = array ? '' + i : names[i];
45
+ if (typeof key !== 'string') throw new ErrorType('JSON key');
46
+ const d = descriptor(v, key);
47
+ if (!d || !d.enumerable || !descriptor(d, 'value')) throw new ErrorType('JSON data only');
48
+ out[key] = snapshot(d.value, depth + 1);
49
+ }
50
+ delete ancestors[depth];
51
+ return out;
52
+ }
53
+ const value = snapshot(fn(input), 0);
54
+ // Snapshot has null prototypes and data properties only: no guest getters or
55
+ // inherited toJSON are run during serialization, even after global mutation.
56
+ const json = stringify(value);
57
+ if (typeof json !== 'string' || json.length > 8192) throw new ErrorType('JSON output limit');
58
+ return json;
59
+ })()`;
60
+ const result = context.evalCode(source, 'created-capability.js');
61
+ if (result.error) {
62
+ result.error.dispose();
63
+ parentPort.postMessage({ ok: false, code: 'SCRIPT_EXECUTION' });
64
+ } else {
65
+ const json = context.getString(result.value);
66
+ result.value.dispose();
67
+ if (Buffer.byteLength(json, 'utf8') > 8192) parentPort.postMessage({ ok: false, code: 'SCRIPT_OUTPUT' });
68
+ else parentPort.postMessage({ ok: true, output: JSON.parse(json) });
69
+ }
70
+ } catch {
71
+ // Guest error strings may contain arbitrary input; fixed codes only cross out.
72
+ parentPort.postMessage({ ok: false, code: 'SCRIPT_EXECUTION' });
73
+ } finally {
74
+ context?.dispose();
75
+ runtime?.dispose();
76
+ parentPort.close();
77
+ }
@@ -1,6 +1,7 @@
1
1
  import { Worker } from 'node:worker_threads';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { digest, fail, identifier, PersonError, text } from './contracts.js';
4
+ import { validateCreatedCapability } from './created-capability-contract.js';
4
5
 
5
6
  const storageError = code => {
6
7
  const error = new PersonError(code);
@@ -91,6 +92,8 @@ export class SqlitePersonRepository {
91
92
  async append(episode, kind, data) { return this.request('append', [episode, kind, data]); }
92
93
  async startCall(episode, data) { return this.request('startCall', [episode, data]); }
93
94
  async finalizeCall(episode, data) { return this.request('finalizeCall', [episode, data]); }
95
+ async createdCapabilities(episode) { return this.request('createdCapabilities', [episode]); }
96
+ async saveCreatedCapability(episode, input) { return this.request('saveCreatedCapability', [episode, validateCreatedCapability(input)]); }
94
97
  async context(episode) { return this.request('context', [episode]); }
95
98
  async recall(ownerId, options) { return this.request('recall', [ownerId, options]); }
96
99
  async commit(episode, proposal, selection, callId, reportedSources = new Map()) { return this.request('commit', [episode, proposal, selection, callId, reportedSources]); }
@@ -8,6 +8,8 @@ export const TABLES = Object.freeze({
8
8
  concept_revisions: { keys: ['id', 'revision'], columns: [] },
9
9
  state_commits: { keys: ['version'], columns: ['id'] },
10
10
  traces: { keys: ['id'], columns: ['seq'] },
11
+ created_capabilities: { keys: ['id'], columns: ['version'] },
12
+ created_capability_revisions: { keys: ['id', 'version'], columns: [] },
11
13
  });
12
14
 
13
15
  export const SCHEMA = `
@@ -66,6 +68,21 @@ CREATE TABLE IF NOT EXISTS traces (
66
68
  PRIMARY KEY(namespace, ownerId, personId, id), UNIQUE(namespace, ownerId, personId, seq),
67
69
  FOREIGN KEY(namespace, ownerId, personId) REFERENCES persons(namespace, ownerId, personId)
68
70
  ) STRICT;
71
+ CREATE INDEX IF NOT EXISTS traces_call_output ON traces(namespace, ownerId, personId,
72
+ json_extract(record, '$.episodeId'), json_extract(record, '$.callId'))
73
+ WHERE json_extract(record, '$.kind') = 'call_output';
74
+ CREATE TABLE IF NOT EXISTS created_capabilities (
75
+ namespace TEXT NOT NULL, ownerId TEXT NOT NULL, personId TEXT NOT NULL,
76
+ id TEXT NOT NULL, version INTEGER NOT NULL CHECK(version BETWEEN 1 AND 32),
77
+ record TEXT NOT NULL CHECK(json_valid(record)), PRIMARY KEY(namespace, ownerId, personId, id),
78
+ FOREIGN KEY(namespace, ownerId, personId) REFERENCES persons(namespace, ownerId, personId)
79
+ ) STRICT;
80
+ CREATE TABLE IF NOT EXISTS created_capability_revisions (
81
+ namespace TEXT NOT NULL, ownerId TEXT NOT NULL, personId TEXT NOT NULL,
82
+ id TEXT NOT NULL, version INTEGER NOT NULL CHECK(version BETWEEN 1 AND 32),
83
+ record TEXT NOT NULL CHECK(json_valid(record)), PRIMARY KEY(namespace, ownerId, personId, id, version),
84
+ FOREIGN KEY(namespace, ownerId, personId) REFERENCES persons(namespace, ownerId, personId)
85
+ ) STRICT;
69
86
  -- This journal is authority data, not a disposable search index. A committed seq is
70
87
  -- never reused, including after reopening; gaps between owners are intentional.
71
88
  CREATE TABLE IF NOT EXISTS memory_changes (
@@ -4,6 +4,7 @@ import { dirname } from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { isMainThread } from 'node:worker_threads';
6
6
  import { bytes, digest, fail, identifier, LIMITS, PersonError, text } from './contracts.js';
7
+ import { CREATED_CAPABILITY_LIMITS, createdCapabilityRecord, validateCreatedCapability } from './created-capability-contract.js';
7
8
  import { SCHEMA, TABLES } from './sqlite-schema.js';
8
9
  import { capabilityExperienceView, recordCapabilityExperience } from './capability-experience.js';
9
10
 
@@ -11,8 +12,20 @@ const SCOPE = 'namespace = ? AND ownerId = ? AND personId = ?';
11
12
  const scopeValues = s => [s.namespace, s.ownerId, s.personId];
12
13
  // Query-only projections; record retains the original public Unicode text.
13
14
  const SEARCH_COLUMNS = Object.freeze({ messages: 'text', concepts: 'statement' });
14
- const dates = new Set(['createdAt', 'updatedAt', 'endedAt', 'leaseUntil', 'callFinalizeUntil']);
15
- const decode = row => row ? JSON.parse(row.record, (key, value) => dates.has(key) && typeof value === 'string' ? new Date(value) : value) : null;
15
+ const metadataDates = Object.freeze({
16
+ persons: ['createdAt', 'leaseUntil'], episodes: ['createdAt', 'endedAt', 'callFinalizeUntil'],
17
+ messages: ['createdAt'], states: ['updatedAt'], concepts: ['updatedAt'], concept_revisions: ['updatedAt'],
18
+ state_commits: ['createdAt'], traces: ['createdAt'],
19
+ });
20
+ const decode = (row, table) => {
21
+ if (!row) return null;
22
+ const record = JSON.parse(row.record);
23
+ // Only known storage metadata is a Date. Trace payloads and created-definition
24
+ // JSON may use these names at any depth without changing their string values.
25
+ for (const key of metadataDates[table] ?? []) if (typeof record[key] === 'string') record[key] = new Date(record[key]);
26
+ if (table === 'state_commits' && typeof record.state?.updatedAt === 'string') record.state.updatedAt = new Date(record.state.updatedAt);
27
+ return record;
28
+ };
16
29
  const publicDoc = doc => {
17
30
  if (!doc) return null;
18
31
  const { _id, ownerId, namespace, personId, ...rest } = doc;
@@ -32,8 +45,8 @@ const publicOutput = (output, failed) => {
32
45
  return failed ? { text: output.text, retainedBytes, observedBytes, complete: false, accepted: false, availability: 'captured', usage, stopReason }
33
46
  : { text: output.text, bytes: retainedBytes, complete: true, usage, stopReason };
34
47
  };
35
- const READS = new Set(['getPerson', 'context', 'recall', 'list', 'searchChanges', 'resolveMemories']);
36
- const WRITES = new Set(['open', 'recover', 'admit', 'heartbeat', 'append', 'startCall', 'finalizeCall', 'commit', 'finish', 'cancel', 'settings', 'snapshot']);
48
+ const READS = new Set(['getPerson', 'context', 'recall', 'list', 'searchChanges', 'resolveMemories', 'createdCapabilities']);
49
+ const WRITES = new Set(['open', 'recover', 'admit', 'heartbeat', 'append', 'startCall', 'finalizeCall', 'commit', 'finish', 'cancel', 'settings', 'snapshot', 'saveCreatedCapability']);
37
50
  const memoryKind = kind => { if (!['messages', 'concepts'].includes(kind)) fail('INVALID_REQUEST'); return kind; };
38
51
  const boundedLimit = (limit, max = 100) => { if (!Number.isSafeInteger(limit) || limit < 1 || limit > max) fail('INVALID_REQUEST'); return limit; };
39
52
  const sequence = value => {
@@ -101,7 +114,8 @@ export class SqlitePersonStore {
101
114
  // All clauses at call sites below are fixed application strings; values are bound.
102
115
  rows(table, scope, clause = '', params = []) {
103
116
  this.table(table);
104
- return this.sql(`SELECT record FROM ${table} WHERE ${SCOPE}${clause}`).all(...scopeValues(scope), ...params).map(decode);
117
+ return this.sql(`SELECT record FROM ${table} WHERE ${SCOPE}${clause}`).all(...scopeValues(scope), ...params)
118
+ .map(row => decode(row, table));
105
119
  }
106
120
  one(table, scope, clause = '', params = []) { return this.rows(table, scope, `${clause} LIMIT 1`, params)[0] ?? null; }
107
121
  put(table, record, insert = false) {
@@ -220,6 +234,8 @@ export class SqlitePersonStore {
220
234
  const p = this.own(episode); p.leaseUntil = new Date(this.now.getTime() + this.leaseMs); p.writeSerial++; this.put('persons', p);
221
235
  }
222
236
  append(episode, kind, data) {
237
+ // Call proof/publication events are emitted only by their transactional methods.
238
+ if (['call_started', 'call_output', 'call_failed', 'capability_created'].includes(kind)) fail('INVALID_REQUEST');
223
239
  const p = this.own(episode);
224
240
  const record = this.one('episodes', this.episodeScope(episode), ' AND id = ?', [episode.id]);
225
241
  if (!record || record.status !== 'running') fail('STALE');
@@ -255,6 +271,28 @@ export class SqlitePersonStore {
255
271
  });
256
272
  return live;
257
273
  }
274
+ createdCapabilities(episode) {
275
+ this.own(episode);
276
+ return this.rows('created_capabilities', this.episodeScope(episode), ' ORDER BY id ASC LIMIT 32').map(publicDoc);
277
+ }
278
+ saveCreatedCapability(episode, input) {
279
+ const { definition, evidence, callId } = validateCreatedCapability(input);
280
+ this.own(episode);
281
+ const scope = this.episodeScope(episode), active = this.one('episodes', scope, ' AND id = ?', [episode.id]);
282
+ if (!active || active.status !== 'running' || active.epoch !== episode.epoch || active.workerId !== episode.workerId) fail('STALE');
283
+ const proof = this.one('traces', scope, " AND json_extract(record, '$.episodeId') = ? AND json_extract(record, '$.kind') = 'call_output' AND json_extract(record, '$.callId') = ? AND json_extract(record, '$.output.complete') = 1", [episode.id, callId]);
284
+ if (!proof || active.openCall?.callId === callId) fail('INVALID_REQUEST');
285
+ const current = this.one('created_capabilities', scope, ' AND id = ?', [definition.id]);
286
+ if ((current?.version ?? 0) !== definition.expectedVersion) fail('STALE');
287
+ if (definition.expectedVersion >= CREATED_CAPABILITY_LIMITS.versions || (!current &&
288
+ this.sql(`SELECT count(*) AS n FROM created_capabilities WHERE ${SCOPE}`).get(...scopeValues(scope)).n >= CREATED_CAPABILITY_LIMITS.ids)) fail('CONTEXT_LIMIT');
289
+ const record = this.doc(scope, createdCapabilityRecord(definition, evidence, { episode, callId, now: this.now }));
290
+ this.put('created_capability_revisions', record, true);
291
+ this.put('created_capabilities', record);
292
+ this.trace(episode.ownerId, episode.id, 'capability_created', { callId, capabilityId: record.id,
293
+ capabilityManifest: { id: record.id, version: record.version, revision: record.revision }, evidence: record.evidence });
294
+ return publicDoc(record);
295
+ }
258
296
  focused(scope, ids) {
259
297
  return ids.map(id => this.one('concepts', scope, ' AND id = ?', [id])).filter(Boolean);
260
298
  }
@@ -366,7 +404,7 @@ export class SqlitePersonStore {
366
404
  this.getPerson(ownerId); after = sequence(after); boundedLimit(limit, 1000);
367
405
  const rows = this.sql(`SELECT seq, kind, id, revision, record FROM memory_changes WHERE ${SCOPE} AND seq > ? ORDER BY seq ASC LIMIT ?`)
368
406
  .all(...scopeValues(this.scope(ownerId)), after, limit + 1);
369
- const items = rows.slice(0, limit).map(row => ({ ...row, record: decode(row) }));
407
+ const items = rows.slice(0, limit).map(row => ({ ...row, record: decode(row, row.kind) }));
370
408
  return { items, lastSeq: items.at(-1)?.seq ?? after, hasMore: rows.length > limit };
371
409
  }
372
410
  resolveMemories(ownerId, kind, refs) {