@yeaft/webchat-agent 0.1.656 → 0.1.658

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/unify/vp/index.js CHANGED
@@ -3,12 +3,5 @@
3
3
  */
4
4
 
5
5
  export { parseRoleMd, loadVpFromDir, scanVpLibrary, count, DEFAULT_VP_LIB_DIR } from './vp-store.js';
6
- export { RoleInstance } from './role-instance.js';
7
6
  export { Registry, defaultRegistry } from './registry.js';
8
7
  export { VpLoader } from './vp-loader.js';
9
-
10
- // task-334c
11
- export { buildSystemPrompt } from './system-prompt.js';
12
- export { recallCoreMemory, searchCoreMemory } from './core-memory-recall.js';
13
- export { createEngineBinder } from './engine-binding.js';
14
- export { createTurnRunner } from './run-turn.js';
@@ -1,35 +1,19 @@
1
1
  /**
2
- * registry.js — In-memory VP + RoleInstance registry.
2
+ * registry.js — In-memory VP registry.
3
3
  *
4
- * Two maps:
5
- * - vpMap: vpId VP
6
- * - instanceMap: "<groupId>::<vpId>" → RoleInstance
4
+ * Holds the process-wide VP map (vpId → VP). VpLoader writes to it on
5
+ * startup + filesystem rescans; vp-bridge.js reads it to serve the
6
+ * `vp_snapshot` and live-diff WS events.
7
7
  *
8
- * RoleInstance creation is idempotent: `getOrCreateRoleInstance(vpId, groupId)`
9
- * returns the same instance for the same (vpId, groupId) pair.
10
- *
11
- * LRU eviction (per acceptance #3): when active instance count exceeds
12
- * `softLimit.maxActiveRoleInstances` (default 40), the least-recently-used
13
- * idle instance is evicted. Instances with `state !== 'idle'` are skipped
14
- * over during eviction; if no idle candidate exists, the new instance is
15
- * still created (soft limit — we do not hard-reject).
8
+ * The previous RoleInstance map (per (vpId, groupId) pair, with LRU
9
+ * eviction) was removed in GC.2 production fans out per-VP via
10
+ * `handleUnifyChat` directly and never instantiated RoleInstance.
16
11
  */
17
12
 
18
- import { RoleInstance } from './role-instance.js';
19
-
20
- const DEFAULT_MAX_ACTIVE_ROLE_INSTANCES = 40;
21
-
22
13
  export class Registry {
23
- constructor(options = {}) {
14
+ constructor() {
24
15
  /** @type {Map<string, import('./vp-store.js').VP>} */
25
16
  this.vpMap = new Map();
26
- /** @type {Map<string, RoleInstance>} */
27
- this.instanceMap = new Map();
28
- this.softLimit = {
29
- maxActiveRoleInstances: options.maxActiveRoleInstances ?? DEFAULT_MAX_ACTIVE_ROLE_INSTANCES,
30
- };
31
- /** Listeners for eviction / persona-refresh (optional). */
32
- this._evictListeners = new Set();
33
17
  }
34
18
 
35
19
  // ─── VP map ────────────────────────────────────────────────────
@@ -41,8 +25,8 @@ export class Registry {
41
25
 
42
26
  /**
43
27
  * Replace a VP's persona fields in-place, preserving identity so any
44
- * RoleInstance with `.vp === vp` keeps its reference stable across
45
- * hot-reload. Fields copied: name, role, traits, modelHint, persona,
28
+ * downstream reference keeps its handle stable across hot-reload.
29
+ * Fields copied: name, role, traits, modelHint, persona, personaHash,
46
30
  * mtimeMs.
47
31
  */
48
32
  updateVpInPlace(next) {
@@ -56,10 +40,6 @@ export class Registry {
56
40
  cur.traits = next.traits;
57
41
  cur.modelHint = next.modelHint;
58
42
  cur.persona = next.persona;
59
- // task-334c: personaHash must track persona body. 334a-followup added
60
- // the field; this is the in-place update mirror. (Originally recorded
61
- // as a 334h nit, but 334c's system-prompt block names the hash in
62
- // STATIC — carrying a stale hash would bleed into the prompt.)
63
43
  cur.personaHash = next.personaHash;
64
44
  cur.mtimeMs = next.mtimeMs;
65
45
  // dir / memoryDir / id stable
@@ -68,12 +48,6 @@ export class Registry {
68
48
 
69
49
  removeVp(vpId) {
70
50
  this.vpMap.delete(vpId);
71
- // Also drop any RoleInstances bound to a vanished VP.
72
- for (const [key, ri] of this.instanceMap) {
73
- if (ri.vpId === vpId) {
74
- this.instanceMap.delete(key);
75
- }
76
- }
77
51
  }
78
52
 
79
53
  getVp(vpId) {
@@ -87,87 +61,6 @@ export class Registry {
87
61
  vpCount() {
88
62
  return this.vpMap.size;
89
63
  }
90
-
91
- // ─── RoleInstance map ─────────────────────────────────────────
92
-
93
- _key(groupId, vpId) {
94
- return `${groupId}::${vpId}`;
95
- }
96
-
97
- /**
98
- * Idempotent create: same (vpId, groupId) returns same instance.
99
- * Triggers LRU eviction when the soft limit is exceeded.
100
- *
101
- * @param {string} vpId
102
- * @param {string} groupId
103
- * @returns {RoleInstance}
104
- */
105
- getOrCreateRoleInstance(vpId, groupId) {
106
- const vp = this.vpMap.get(vpId);
107
- if (!vp) throw new Error(`unknown vpId: ${vpId}`);
108
-
109
- const key = this._key(groupId, vpId);
110
- const existing = this.instanceMap.get(key);
111
- if (existing) {
112
- existing.touch();
113
- return existing;
114
- }
115
-
116
- const ri = new RoleInstance({ vp, groupId });
117
- this.instanceMap.set(key, ri);
118
- this._maybeEvict(ri);
119
- return ri;
120
- }
121
-
122
- getRoleInstance(vpId, groupId) {
123
- return this.instanceMap.get(this._key(groupId, vpId));
124
- }
125
-
126
- dropRoleInstance(vpId, groupId) {
127
- const key = this._key(groupId, vpId);
128
- const ri = this.instanceMap.get(key);
129
- if (!ri) return false;
130
- this.instanceMap.delete(key);
131
- return true;
132
- }
133
-
134
- activeRoleInstanceCount() {
135
- return this.instanceMap.size;
136
- }
137
-
138
- listRoleInstances() {
139
- return Array.from(this.instanceMap.values());
140
- }
141
-
142
- onEvict(listener) {
143
- this._evictListeners.add(listener);
144
- return () => this._evictListeners.delete(listener);
145
- }
146
-
147
- _maybeEvict(exclude) {
148
- const limit = this.softLimit.maxActiveRoleInstances;
149
- if (this.instanceMap.size <= limit) return;
150
-
151
- // Collect idle instances (excluding the just-created one), sort by
152
- // lastActivityAt ascending.
153
- const idle = [];
154
- for (const ri of this.instanceMap.values()) {
155
- if (ri === exclude) continue;
156
- if (ri.state === 'idle') idle.push(ri);
157
- }
158
- idle.sort((a, b) => a.lastActivityAt - b.lastActivityAt);
159
-
160
- while (this.instanceMap.size > limit && idle.length > 0) {
161
- const victim = idle.shift();
162
- const key = this._key(victim.groupId, victim.vpId);
163
- this.instanceMap.delete(key);
164
- for (const l of this._evictListeners) {
165
- try { l(victim); } catch { /* ignore */ }
166
- }
167
- }
168
- // If still over limit because no idle candidates exist, we accept the
169
- // soft-limit breach — per spec, softLimit is a target, not a hard cap.
170
- }
171
64
  }
172
65
 
173
66
  /** Module-level default registry (convenience). */
@@ -736,7 +736,17 @@ export async function handleUnifyGroupChat(msg) {
736
736
  ts: Date.now(),
737
737
  });
738
738
  } catch { /* never crash WS pipeline */ }
739
+ }
739
740
 
741
+ // GC.1 Commit C: VP-level parallelism. Each selected VP runs its
742
+ // turn concurrently via Promise.all. Intra-VP loops (LLM → tool →
743
+ // LLM) stay serial inside each handleUnifyChat call.
744
+ //
745
+ // Side-effect: VP-B's transcript no longer contains VP-A's reply
746
+ // (they're concurrent). Cross-VP visibility moves to the explicit
747
+ // route_forward tool. The group jsonl log remains the source of
748
+ // truth for full-fidelity replay.
749
+ await Promise.all(captured.map(async ({ vpId }) => {
740
750
  try {
741
751
  await handleUnifyChat({
742
752
  ...msg,
@@ -758,7 +768,7 @@ export async function handleUnifyGroupChat(msg) {
758
768
  });
759
769
  } catch { /* never crash WS pipeline */ }
760
770
  }
761
- }
771
+ }));
762
772
  }
763
773
 
764
774
  /**
@@ -1,75 +0,0 @@
1
- /**
2
- * core-memory-recall.js — single-dimension VP memory recall.
3
- *
4
- * Per R3 §Δ2.3: recall is scoped to the CURRENT VP only. The `scope`
5
- * dimension from R2 was deleted; VP memory is a single store per VP and
6
- * the recall filter is `{ vp: currentVpId, ... }` — no $or across local /
7
- * global.
8
- *
9
- * This module is a thin shim over whatever memory store RoleInstance has
10
- * (334f wraps 334o's shard-store). It duck-types the store so 334c does
11
- * not hard-depend on 334f's final shape:
12
- *
13
- * store.query({ vp, shard?, kind?, tags?, limit?, recency? })
14
- * store.search({ vp, query, limit? }) // optional
15
- *
16
- * Return shape: array of entries with at minimum `{ body, shard }`.
17
- * Additional fields are passed through.
18
- */
19
-
20
- const DEFAULT_LIMIT = 7;
21
-
22
- /**
23
- * Recall top-K core memory for a VP.
24
- *
25
- * @param {object|null} store — memoryStore (may be null)
26
- * @param {{ vp: string, limit?: number, shard?: string, kind?: string,
27
- * tags?: string[] }} opts
28
- * @returns {Promise<Array<{body:string, shard?:string, [k:string]:any}>>}
29
- */
30
- export async function recallCoreMemory(store, opts = {}) {
31
- if (!store || typeof store.query !== 'function') return [];
32
- if (!opts.vp || typeof opts.vp !== 'string') {
33
- throw new Error('recallCoreMemory: opts.vp (vpId) is required');
34
- }
35
- const limit = Number.isInteger(opts.limit) && opts.limit > 0
36
- ? opts.limit
37
- : DEFAULT_LIMIT;
38
-
39
- const filter = { vp: opts.vp, limit };
40
- if (opts.shard) filter.shard = opts.shard;
41
- if (opts.kind) filter.kind = opts.kind;
42
- if (opts.tags && opts.tags.length) filter.tags = opts.tags;
43
-
44
- // Support both sync and async stores.
45
- const out = await Promise.resolve(store.query(filter));
46
- if (!Array.isArray(out)) return [];
47
- return out.slice(0, limit);
48
- }
49
-
50
- /**
51
- * Optional keyword-search variant — used by the `memory_search` tool in
52
- * 334d. Also VP-scoped.
53
- *
54
- * @param {object|null} store
55
- * @param {{ vp: string, query: string, limit?: number }} opts
56
- */
57
- export async function searchCoreMemory(store, opts = {}) {
58
- if (!store) return [];
59
- if (!opts.vp || !opts.query) return [];
60
- const limit = opts.limit || DEFAULT_LIMIT;
61
- if (typeof store.search === 'function') {
62
- const out = await Promise.resolve(store.search({ vp: opts.vp, query: opts.query, limit }));
63
- return Array.isArray(out) ? out.slice(0, limit) : [];
64
- }
65
- // Fallback: query + naive substring filter.
66
- if (typeof store.query === 'function') {
67
- const all = await Promise.resolve(store.query({ vp: opts.vp, limit: 200 }));
68
- if (!Array.isArray(all)) return [];
69
- const needle = opts.query.toLowerCase();
70
- return all
71
- .filter((e) => (e.body || '').toLowerCase().includes(needle))
72
- .slice(0, limit);
73
- }
74
- return [];
75
- }
@@ -1,96 +0,0 @@
1
- /**
2
- * engine-binding.js — bind a single Engine instance to a VP.
3
- *
4
- * Per §5.1, Engine is a stateless library: `engine.query({prompt, messages,
5
- * signal, ...})`. Per §5.2, each RoleInstance runs ITS VP's Engine. We keep
6
- * one Engine per (vpId) — NOT per RoleInstance — because the Engine itself
7
- * holds no conversation state; messages live on the RoleInstance. Sharing
8
- * across groups for the same VP is correct and avoids adapter duplication.
9
- *
10
- * When a VP is hot-reloaded (persona body changed → personaHash changed),
11
- * the cached Engine is still valid — the persona is injected via system
12
- * prompt, not Engine constructor. We keep the cache stable unless the
13
- * caller passes a new adapter (e.g. modelHint switched).
14
- *
15
- * Hard constraint: 334c does NOT import ../engine.js directly, because
16
- * that pulls in adapter/config/tool machinery that's owned by the caller
17
- * (web-bridge / eval runner). Instead this module takes a factory:
18
- *
19
- * createEngine(vp) → Engine
20
- *
21
- * which the caller supplies when they wire up a Registry.
22
- */
23
-
24
- const DEFAULT_SCOPE = Symbol.for('yeaft.334c.default-engine-scope');
25
-
26
- /**
27
- * Build an engine binder. Stateless helper — state lives on the registry-
28
- * like cache the caller provides (defaults to an internal WeakMap-style).
29
- *
30
- * @param {{
31
- * createEngine: (vp: import('./vp-store.js').VP) => object,
32
- * cache?: Map<string, object>, // vpId → Engine
33
- * }} deps
34
- */
35
- export function createEngineBinder({ createEngine, cache } = {}) {
36
- if (typeof createEngine !== 'function') {
37
- throw new Error('createEngineBinder: createEngine(vp) factory is required');
38
- }
39
- const store = cache instanceof Map ? cache : new Map();
40
- // Track personaHash at bind time so the caller can decide whether a
41
- // later hot-reload warrants re-binding (MVP: we just stash it; engine
42
- // itself is persona-agnostic).
43
- const meta = new Map(); // vpId → { personaHash, modelHint }
44
-
45
- return {
46
- /**
47
- * Resolve (or lazily create) the Engine for a RoleInstance's VP and
48
- * attach it onto `ri.engine`. Idempotent.
49
- *
50
- * @param {import('./role-instance.js').RoleInstance} ri
51
- * @returns {object} Engine
52
- */
53
- bind(ri) {
54
- if (!ri || !ri.vp) throw new Error('bind: role instance required');
55
- const vp = ri.vp;
56
- let engine = store.get(vp.id);
57
- if (!engine) {
58
- engine = createEngine(vp);
59
- if (!engine || typeof engine.query !== 'function') {
60
- throw new Error(`bind: createEngine(${vp.id}) did not return an Engine-like object (missing query)`);
61
- }
62
- store.set(vp.id, engine);
63
- meta.set(vp.id, { personaHash: vp.personaHash, modelHint: vp.modelHint });
64
- }
65
- ri.engine = engine;
66
- return engine;
67
- },
68
-
69
- /**
70
- * Mark a VP as "needs re-bind" — drop the cache entry. The next bind()
71
- * call will invoke createEngine again. Useful when the caller knows
72
- * the adapter or tool inventory has changed.
73
- */
74
- invalidate(vpId) {
75
- const engine = store.get(vpId);
76
- store.delete(vpId);
77
- meta.delete(vpId);
78
- // Best-effort: if the engine exposed a dispose, call it.
79
- if (engine && typeof engine.dispose === 'function') {
80
- try { engine.dispose(); } catch { /* ignore */ }
81
- }
82
- },
83
-
84
- /** Drop every cached engine (e.g. on session teardown). */
85
- clear() {
86
- for (const vpId of Array.from(store.keys())) this.invalidate(vpId);
87
- },
88
-
89
- /** Diagnostics. */
90
- size() { return store.size; },
91
- has(vpId) { return store.has(vpId); },
92
- getMeta(vpId) { return meta.get(vpId) || null; },
93
- };
94
-
95
- void DEFAULT_SCOPE; // reserved for cross-module sharing; not used in MVP
96
- }
@@ -1,216 +0,0 @@
1
- /**
2
- * role-instance.js — RoleInstance class (per-group per-VP runtime handle).
3
- *
4
- * task-334 §5 RoleInstance. 334a defined the shape; task-334c adds the
5
- * lifecycle surface:
6
- * - state machine: idle → queued → running → (idle | error) + standby
7
- * - inputQueue: enqueue(envelope) + drain(runner) loop
8
- * - abort: per-instance AbortController, exposed via abort()
9
- *
10
- * Hard constraints (inherited from 334a):
11
- * - does NOT import 334o shard-store directly (memoryStore stays nullable)
12
- * - does NOT import 334d routing (inputQueue accepts whatever coordinator
13
- * hands it; runner is the sole consumer)
14
- *
15
- * Hard constraint (334c-specific):
16
- * - the runner callback is injected — RoleInstance does not know about
17
- * Engine internals. See run-turn.js for the canonical runner.
18
- */
19
-
20
- /**
21
- * @typedef {'idle'|'queued'|'running'|'standby'|'error'} RoleInstanceState
22
- *
23
- * - idle: no work, ready to accept
24
- * - queued: inputQueue has items, drain scheduled/running but this
25
- * iteration is not yet inside the runner (brief transition)
26
- * - running: inside runner for one envelope
27
- * - standby: explicit pause via setState('standby'); enqueue is allowed
28
- * (buffers) but drain is gated until setState('idle')
29
- * - error: last turn threw; next enqueue re-enters drain and clears
30
- */
31
-
32
- let _seq = 0;
33
- function nextInstanceId(vpId, groupId) {
34
- _seq = (_seq + 1) >>> 0;
35
- return `ri_${groupId}_${vpId}_${Date.now().toString(36)}_${_seq.toString(36)}`;
36
- }
37
-
38
- export class RoleInstance {
39
- /**
40
- * @param {{ vp: import('./vp-store.js').VP, groupId: string }} params
41
- */
42
- constructor({ vp, groupId }) {
43
- if (!vp) throw new Error('RoleInstance requires a vp');
44
- if (!groupId) throw new Error('RoleInstance requires a groupId');
45
-
46
- this.id = nextInstanceId(vp.id, groupId);
47
- this.vpId = vp.id;
48
- this.groupId = groupId;
49
- /** Live VP reference. Persona hot-reload mutates fields in place. */
50
- this.vp = vp;
51
-
52
- // ── Runtime state (preserved across persona hot-reload) ─────
53
- /** @type {RoleInstanceState} */
54
- this.state = 'idle';
55
- /** Conversation messages appended by run-turn (user + assistant pairs). */
56
- this.messages = [];
57
- /** Pending input queue (envelopes from coordinator). */
58
- this.inputQueue = [];
59
- /** Abort controller for in-flight run. Created per-turn. */
60
- this.abortController = null;
61
-
62
- // ── Nullable sub-system handles (attached by later slices) ──
63
- /** threadStore — optional (334h). */
64
- this.threadStore = null;
65
- /** subagentPool — optional. */
66
- this.subagentPool = null;
67
- /** memoryStore — 334f wraps 334o; NOT created here. */
68
- this.memoryStore = null;
69
- /** Cached Engine for this VP; bound lazily by engine-binding.js. */
70
- this.engine = null;
71
- /** Cached systemPrompt string; rebuilt when vp.mtimeMs changes. */
72
- this.systemPrompt = null;
73
- /** mtimeMs seen when systemPrompt was last built — invalidation key. */
74
- this._promptBuiltForMtime = null;
75
-
76
- // ── Telemetry ───────────────────────────────────────────────
77
- this.createdAt = Date.now();
78
- this.lastActivityAt = this.createdAt;
79
- /** Total turns executed via drain (error turns included). */
80
- this.turnCount = 0;
81
- /** Last error thrown by runner; cleared on next successful turn. */
82
- this.lastError = null;
83
-
84
- // ── Drain internals ────────────────────────────────────────
85
- /** Set while a drain loop is active. Prevents re-entry. */
86
- this._draining = false;
87
- /** Promise that resolves when the current drain loop exits. */
88
- this._drainPromise = null;
89
- }
90
-
91
- /** Update last-activity timestamp (used by LRU eviction). */
92
- touch() {
93
- this.lastActivityAt = Date.now();
94
- }
95
-
96
- /** Transition helper. Emits a state change to optional listeners. */
97
- setState(next) {
98
- this.state = next;
99
- this.touch();
100
- }
101
-
102
- /**
103
- * Enqueue an envelope for processing. Triggers a drain if one isn't
104
- * already running.
105
- *
106
- * Envelope shape (from coordinator.js makeEnvelope): { groupId, taskId,
107
- * msg, trigger }. Opaque to RoleInstance — handed verbatim to runner.
108
- *
109
- * @param {object} envelope
110
- * @returns {number} new inputQueue length
111
- */
112
- enqueue(envelope) {
113
- if (!envelope) throw new Error('enqueue: envelope required');
114
- this.inputQueue.push(envelope);
115
- this.touch();
116
- if (this.state === 'idle') this.setState('queued');
117
- return this.inputQueue.length;
118
- }
119
-
120
- /**
121
- * Abort the in-flight run (if any) and clear the pending queue's
122
- * ability to advance further by signalling abort. The queue is NOT
123
- * cleared — that's policy left to the caller (e.g. GroupCoordinator
124
- * may reshedule on next user message).
125
- *
126
- * @param {string} [reason]
127
- */
128
- abort(reason = 'user') {
129
- if (this.abortController && !this.abortController.signal.aborted) {
130
- try { this.abortController.abort(reason); } catch { /* ignore */ }
131
- }
132
- this.touch();
133
- }
134
-
135
- /**
136
- * Drain the inputQueue by invoking `runner(envelope)` for each. One
137
- * envelope at a time, FIFO. Re-entry safe: if a drain is already in
138
- * progress, return the in-flight promise instead of starting a second
139
- * loop. Standby gates the loop (standby → no runner calls until a
140
- * subsequent `setState('idle')` + enqueue).
141
- *
142
- * The runner owns state transitions into 'running' and back to 'idle'
143
- * for the iteration it's handling; drain only sets the initial queued
144
- * / final idle around the whole loop.
145
- *
146
- * @param {(envelope: object, ri: RoleInstance) => Promise<void>} runner
147
- * @returns {Promise<{turns:number, errors:number}>}
148
- */
149
- drain(runner) {
150
- if (typeof runner !== 'function') {
151
- return Promise.reject(new Error('drain: runner must be a function'));
152
- }
153
- if (this._draining) return this._drainPromise;
154
-
155
- this._draining = true;
156
- this._drainPromise = (async () => {
157
- let turns = 0;
158
- let errors = 0;
159
- try {
160
- while (this.inputQueue.length > 0) {
161
- // Standby blocks the drain — buffer until caller reactivates.
162
- if (this.state === 'standby') break;
163
- const envelope = this.inputQueue.shift();
164
- this.turnCount += 1;
165
- turns += 1;
166
- // Fresh controller per turn so abort(reason) only kills one iter
167
- // and the next envelope starts with a clean signal.
168
- this.abortController = new AbortController();
169
- try {
170
- this.setState('running');
171
- await runner(envelope, this);
172
- this.lastError = null;
173
- } catch (err) {
174
- errors += 1;
175
- this.lastError = err;
176
- this.setState('error');
177
- // Propagate abort — don't swallow it; but do stop draining so
178
- // the caller can decide to requeue / resume.
179
- if (err && err.name === 'AbortError') break;
180
- // Non-abort errors: continue draining. The next turn gets a
181
- // fresh state via setState('running') at the top of the loop.
182
- }
183
- }
184
- // Settle terminal state only if we didn't bail in 'error' or
185
- // 'standby'. Keep 'standby' sticky so caller must reactivate.
186
- if (this.state !== 'standby' && this.state !== 'error') {
187
- this.setState('idle');
188
- }
189
- } finally {
190
- this._draining = false;
191
- this._drainPromise = null;
192
- this.abortController = null;
193
- }
194
- return { turns, errors };
195
- })();
196
- return this._drainPromise;
197
- }
198
-
199
- /**
200
- * Snapshot for debug / telemetry. Excludes bulky messages.
201
- */
202
- snapshot() {
203
- return {
204
- id: this.id,
205
- vpId: this.vpId,
206
- groupId: this.groupId,
207
- state: this.state,
208
- messageCount: this.messages.length,
209
- queueDepth: this.inputQueue.length,
210
- turnCount: this.turnCount,
211
- hasError: Boolean(this.lastError),
212
- lastActivityAt: this.lastActivityAt,
213
- createdAt: this.createdAt,
214
- };
215
- }
216
- }