@siduri-x/core 2.0.4 → 2.0.6

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,643 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const fs = __importStar(require("fs"));
37
+ const os = __importStar(require("os"));
38
+ const path = __importStar(require("path"));
39
+ const index_1 = require("./index");
40
+ describe('Conversational Teach Mode End-to-End Lifecycle', () => {
41
+ let tmpDir;
42
+ let dbPath;
43
+ beforeEach(() => {
44
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'siduri-teach-test-'));
45
+ dbPath = path.join(tmpDir, 'siduri.db');
46
+ });
47
+ afterEach(() => {
48
+ try {
49
+ fs.rmSync(tmpDir, { recursive: true, force: true });
50
+ }
51
+ catch {
52
+ // ignore cleanup errors
53
+ }
54
+ });
55
+ /**
56
+ * Helper to build a SelfRepository wrapping a SiduriDatabase
57
+ */
58
+ function createSelfRepository(db) {
59
+ return {
60
+ getIdentity: async (companionId) => db.getIdentity(companionId),
61
+ setIdentity: async (identity) => db.setIdentity(identity),
62
+ getPersonality: async (companionId) => db.getPersonality(companionId) || {
63
+ warmth: 0.5,
64
+ formality: 0.5,
65
+ sarcasm: 0.5,
66
+ verbosity: 0.5,
67
+ curiosity: 0.5,
68
+ },
69
+ setPersonality: async (companionId, traits) => db.setPersonality(companionId, traits),
70
+ getActiveDirectives: async (companionId) => db.getActiveDirectives(companionId),
71
+ commitDirectives: async (companionId, directives) => {
72
+ for (const d of directives) {
73
+ db.commitDirective(d);
74
+ }
75
+ },
76
+ getRelationship: async (companionId, entityId) => db.getRelationship(companionId, entityId) || null,
77
+ updateRelationship: async (companionId, rel) => db.upsertRelationship({ ...rel, companionId }),
78
+ approveDirective: async (id, companionId) => db.approveDirective(id, companionId),
79
+ rejectDirective: async (id, companionId) => db.rejectDirective(id, companionId),
80
+ revokeDirective: async (id, companionId) => db.revokeDirective(id, companionId),
81
+ };
82
+ }
83
+ /**
84
+ * Helper to build a MemoryOrgan wrapping a SiduriDatabase
85
+ */
86
+ function createMemoryOrgan(db) {
87
+ return {
88
+ initialize: async () => { },
89
+ proposeClaim: async (claim) => db.proposeClaim(claim),
90
+ searchClaims: async () => [],
91
+ getClaims: async (limit) => db.getAllClaims(undefined, limit || 500),
92
+ getPendingClaims: async (limit) => db.getAllClaims(undefined, limit || 500).filter((c) => c.status === 'pending'),
93
+ approveClaim: async (id) => db.approveClaim(id),
94
+ rejectClaim: async (id) => db.rejectClaim(id),
95
+ getDirectives: async (companionId) => db.getActiveDirectives(companionId || 'default'),
96
+ proposeDirective: async (dir) => {
97
+ const id = `dir-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
98
+ const directive = {
99
+ ...dir,
100
+ id,
101
+ status: 'pending',
102
+ createdAt: new Date().toISOString(),
103
+ };
104
+ db.commitDirective(directive);
105
+ return directive;
106
+ },
107
+ approveDirective: async (id, companionId) => db.approveDirective(id, companionId),
108
+ rejectDirective: async (id, companionId) => db.rejectDirective(id, companionId),
109
+ revokeDirective: async (id, companionId) => db.revokeDirective(id, companionId),
110
+ disableDirective: async (id, companionId) => db.disableDirective(id, companionId),
111
+ };
112
+ }
113
+ /**
114
+ * Mock behavior compiler that compiles active_self tags from context
115
+ */
116
+ function createBehaviorCompiler() {
117
+ return {
118
+ compile: async (ctx) => {
119
+ const parts = ['<active_self>'];
120
+ if (ctx.identity) {
121
+ parts.push(`Identity:\n- Name: ${ctx.identity.name}`);
122
+ if (ctx.identity.archetype || ctx.identity.role) {
123
+ parts.push(`- Role: ${ctx.identity.role || ctx.identity.archetype}`);
124
+ }
125
+ }
126
+ if (ctx.relationship) {
127
+ const nameStr = ctx.relationship.name ? ` [Name: ${ctx.relationship.name}]` : '';
128
+ parts.push(`Relationship Stance:\n- Stance toward ${ctx.relationship.entityId}${nameStr}: ${ctx.relationship.stance} (Role: ${ctx.relationship.role})`);
129
+ }
130
+ if (ctx.directives && ctx.directives.length > 0) {
131
+ parts.push(`Behavioral Directives:\n${ctx.directives.map((d) => `- ${d.directive}`).join('\n')}`);
132
+ }
133
+ parts.push('</active_self>');
134
+ return parts.join('\n\n');
135
+ },
136
+ };
137
+ }
138
+ function createRequestContext(companionId, mode = 'teach', actorId = 'kur-zagin') {
139
+ return {
140
+ companionId,
141
+ mode,
142
+ actor: {
143
+ actorId,
144
+ sessionId: 'sess-teach-1',
145
+ authorizationRole: 'OWNER',
146
+ authenticated: true,
147
+ },
148
+ conversation: {
149
+ channel: 'direct',
150
+ correlationId: `corr-${Date.now()}`,
151
+ },
152
+ };
153
+ }
154
+ function createCognitiveMockBrain(companionId) {
155
+ return {
156
+ generatePlan: jest.fn().mockImplementation(async (brainCtx) => {
157
+ const lastMsg = brainCtx.recentMessages?.[brainCtx.recentMessages.length - 1]?.content || '';
158
+ const memoryProposals = [];
159
+ const behaviorProposals = [];
160
+ if (/AI researcher at VXNUS Studio/i.test(lastMsg)) {
161
+ memoryProposals.push({
162
+ subject: `companion:${companionId}`,
163
+ predicate: 'role',
164
+ value: 'AI researcher at VXNUS Studio',
165
+ });
166
+ behaviorProposals.push({
167
+ directive: 'Acknowledge role as AI researcher at VXNUS Studio',
168
+ category: 'relational',
169
+ subject: `companion:${companionId}`,
170
+ predicate: 'role',
171
+ value: 'AI researcher at VXNUS Studio',
172
+ });
173
+ }
174
+ else if (/Lead Architect at VXNUS Studio/i.test(lastMsg)) {
175
+ memoryProposals.push({
176
+ subject: `companion:${companionId}`,
177
+ predicate: 'role',
178
+ value: 'Lead Architect at VXNUS Studio',
179
+ });
180
+ }
181
+ else if (/Research Specialist/i.test(lastMsg)) {
182
+ memoryProposals.push({
183
+ subject: `companion:${companionId}`,
184
+ predicate: 'role',
185
+ value: 'Research Specialist',
186
+ });
187
+ }
188
+ else if (/Security Officer/i.test(lastMsg)) {
189
+ memoryProposals.push({
190
+ subject: `companion:${companionId}`,
191
+ predicate: 'role',
192
+ value: 'Security Officer',
193
+ });
194
+ }
195
+ else if (/VXNUS Studio Staff/i.test(lastMsg)) {
196
+ memoryProposals.push({
197
+ subject: `companion:${companionId}`,
198
+ predicate: 'role',
199
+ value: 'VXNUS Studio Staff',
200
+ });
201
+ }
202
+ if (/\bcreator\b/i.test(lastMsg)) {
203
+ memoryProposals.push({
204
+ subject: 'actor:kur-zagin',
205
+ predicate: 'stated_relationship',
206
+ value: 'creator',
207
+ claimType: 'relationship',
208
+ });
209
+ behaviorProposals.push({
210
+ directive: 'Recognize actor:kur-zagin stated relationship as creator',
211
+ category: 'relational',
212
+ subject: 'actor:kur-zagin',
213
+ predicate: 'stated_relationship',
214
+ value: 'creator',
215
+ });
216
+ }
217
+ if (/Kur Zagin/i.test(lastMsg)) {
218
+ memoryProposals.push({
219
+ subject: 'actor:kur-zagin',
220
+ predicate: 'name',
221
+ value: 'Kur Zagin',
222
+ claimType: 'preference',
223
+ });
224
+ behaviorProposals.push({
225
+ directive: 'Address actor:kur-zagin as Kur Zagin',
226
+ category: 'relational',
227
+ subject: 'actor:kur-zagin',
228
+ predicate: 'name',
229
+ value: 'Kur Zagin',
230
+ });
231
+ }
232
+ if (/be concise when answering technical questions/i.test(lastMsg)) {
233
+ behaviorProposals.push({
234
+ directive: 'Be concise when answering technical questions',
235
+ category: 'behavioral',
236
+ priority: 60,
237
+ });
238
+ memoryProposals.push({
239
+ subject: 'actor:kur-zagin',
240
+ predicate: 'rule',
241
+ value: 'Be concise when answering technical questions',
242
+ });
243
+ }
244
+ return {
245
+ speech: 'Understood, I have acknowledged your input.',
246
+ language: 'en',
247
+ memoryProposals: memoryProposals.length > 0 ? memoryProposals : undefined,
248
+ behaviorProposals: behaviorProposals.length > 0 ? behaviorProposals : undefined,
249
+ _receivedSystemPrompt: brainCtx.systemPrompt,
250
+ };
251
+ }),
252
+ };
253
+ }
254
+ // =========================================================================
255
+ // Test A: Conversational role teaching in Teach mode
256
+ // =========================================================================
257
+ it('Test A: teaches companion role in Teach mode, approves proposal, and reflects role in Active Self', async () => {
258
+ const db = new index_1.SiduriDatabase({ dbPath });
259
+ const companionId = 'comp-test-a';
260
+ const self = createSelfRepository(db);
261
+ const memory = createMemoryOrgan(db);
262
+ const behavior = createBehaviorCompiler();
263
+ const mockBrain = createCognitiveMockBrain(companionId);
264
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
265
+ brain: mockBrain,
266
+ memory,
267
+ self,
268
+ behavior,
269
+ });
270
+ await runtime.initialize();
271
+ // 1. Send conversational role teaching message
272
+ const perceptionResult = await runtime.processPerception({
273
+ source: 'text_chat',
274
+ text: 'Siduri, you are an AI researcher at VXNUS Studio.',
275
+ context: createRequestContext(companionId, 'teach'),
276
+ });
277
+ expect(perceptionResult.status).toBe('APPROVED');
278
+ // 2. Verify proposal was generated
279
+ const proposals = perceptionResult.metadata?.proposals || [];
280
+ expect(proposals.length).toBeGreaterThanOrEqual(1);
281
+ const roleProposal = proposals.find((p) => p.predicate === 'role' || p.predicate === 'archetype');
282
+ expect(roleProposal).toBeDefined();
283
+ expect(roleProposal.value).toBe('AI researcher at VXNUS Studio');
284
+ expect(roleProposal.status).toBe('pending');
285
+ // 3. Truth Gate check: Before approval, SelfRepository must NOT contain the new role
286
+ const identityBefore = await self.getIdentity(companionId);
287
+ expect(identityBefore?.role).toBeUndefined();
288
+ expect(identityBefore?.archetype).toBeUndefined();
289
+ // 4. Operator / Owner approves the proposal
290
+ const approveResult = await runtime.approveProposal(roleProposal.id, { companionId });
291
+ expect(approveResult.success).toBe(true);
292
+ // 5. Verify SelfRepository now has the learned role
293
+ const identityAfter = await self.getIdentity(companionId);
294
+ expect(identityAfter).toBeDefined();
295
+ expect(identityAfter?.role).toBe('AI researcher at VXNUS Studio');
296
+ expect(identityAfter?.archetype).toBe('AI researcher at VXNUS Studio');
297
+ // Also verify a relational directive acknowledging the role was added
298
+ const directives = await self.getActiveDirectives(companionId);
299
+ const roleDirective = directives.find((d) => d.directive.includes('AI researcher at VXNUS Studio'));
300
+ expect(roleDirective).toBeDefined();
301
+ // 6. Next conversational turn: verify prompt compiler injects the updated Active Self
302
+ await runtime.processPerception({
303
+ source: 'text_chat',
304
+ text: 'What is your primary mission?',
305
+ context: createRequestContext(companionId, 'hybrid'),
306
+ });
307
+ expect(mockBrain.generatePlan).toHaveBeenCalled();
308
+ const lastCallCtx = mockBrain.generatePlan.mock.calls[1][0];
309
+ expect(lastCallCtx.systemPrompt).toContain('<active_self>');
310
+ expect(lastCallCtx.systemPrompt).toContain('Role: AI researcher at VXNUS Studio');
311
+ db.close();
312
+ });
313
+ // =========================================================================
314
+ // Test B: Creator relationship teaching
315
+ // =========================================================================
316
+ it('Test B: teaches creator relationship, approves proposal, and updates relationship stance', async () => {
317
+ const db = new index_1.SiduriDatabase({ dbPath });
318
+ const companionId = 'comp-test-b';
319
+ const self = createSelfRepository(db);
320
+ const memory = createMemoryOrgan(db);
321
+ const behavior = createBehaviorCompiler();
322
+ const mockBrain = createCognitiveMockBrain(companionId);
323
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
324
+ brain: mockBrain,
325
+ memory,
326
+ self,
327
+ behavior,
328
+ });
329
+ await runtime.initialize();
330
+ // 1. Send creator teaching message
331
+ const perceptionResult = await runtime.processPerception({
332
+ source: 'text_chat',
333
+ text: 'I am your creator, Kur Zagin.',
334
+ context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
335
+ });
336
+ const proposals = perceptionResult.metadata?.proposals || [];
337
+ const relProposal = proposals.find((p) => p.predicate === 'stated_relationship' ||
338
+ p.predicate === 'relationship' ||
339
+ p.claimType === 'relationship');
340
+ expect(relProposal).toBeDefined();
341
+ expect(relProposal.value).toBe('creator');
342
+ // 2. Truth Gate: Before approval, relationship does not have creator stance
343
+ const relBefore = await self.getRelationship?.(companionId, 'actor:kur-zagin');
344
+ expect(relBefore?.role).toBeUndefined();
345
+ // 3. Approve the relationship proposal
346
+ await runtime.approveProposal(relProposal.id, { companionId });
347
+ // 4. Verify SelfRepository now has creator relationship with loyal stance
348
+ const relAfter = await self.getRelationship?.(companionId, 'actor:kur-zagin');
349
+ expect(relAfter).toBeDefined();
350
+ expect(relAfter?.role).toBe('creator');
351
+ expect(relAfter?.stance).toBe('familiar_loyal');
352
+ expect(relAfter?.trustScore).toBe(1.0);
353
+ // 5. Subsequent conversation retrieves relationship in prompt
354
+ await runtime.processPerception({
355
+ source: 'text_chat',
356
+ text: 'Status update please.',
357
+ context: createRequestContext(companionId, 'hybrid', 'actor:kur-zagin'),
358
+ });
359
+ const lastCallCtx = mockBrain.generatePlan.mock.calls[1][0];
360
+ expect(lastCallCtx.systemPrompt).toContain('Stance toward actor:kur-zagin: familiar_loyal (Role: creator)');
361
+ db.close();
362
+ });
363
+ // =========================================================================
364
+ // Test C: Behavioral rule teaching
365
+ // =========================================================================
366
+ it('Test C: teaches behavioral rule, generates proposal, approves, and activates directive in Self', async () => {
367
+ const db = new index_1.SiduriDatabase({ dbPath });
368
+ const companionId = 'comp-test-c';
369
+ const self = createSelfRepository(db);
370
+ const memory = createMemoryOrgan(db);
371
+ const behavior = createBehaviorCompiler();
372
+ const mockBrain = createCognitiveMockBrain(companionId);
373
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
374
+ brain: mockBrain,
375
+ memory,
376
+ self,
377
+ behavior,
378
+ });
379
+ await runtime.initialize();
380
+ // 1. Send behavioral rule teaching message
381
+ const perceptionResult = await runtime.processPerception({
382
+ source: 'text_chat',
383
+ text: 'Remember this rule: be concise when answering technical questions.',
384
+ context: createRequestContext(companionId, 'teach'),
385
+ });
386
+ // Check behavioral proposals or memory proposals
387
+ const behavioralReceipts = perceptionResult.metadata?.behavioral_proposals || [];
388
+ const memoryProposals = perceptionResult.metadata?.proposals || [];
389
+ const hasDirectiveProposal = behavioralReceipts.length > 0 ||
390
+ memoryProposals.some((p) => p.predicate === 'rule' || p.predicate === 'behavioral_rule');
391
+ expect(hasDirectiveProposal).toBe(true);
392
+ const directiveId = behavioralReceipts[0]?.directive_id;
393
+ const proposalId = memoryProposals[0]?.id;
394
+ // 2. Active directives before approval must not include the new rule
395
+ const directivesBefore = await self.getActiveDirectives(companionId);
396
+ expect(directivesBefore.some((d) => d.directive.toLowerCase().includes('be concise when answering technical questions'))).toBe(false);
397
+ // 3. Approve directive via runtime
398
+ if (directiveId) {
399
+ await runtime.approveDirective(directiveId, { companionId });
400
+ }
401
+ else if (proposalId) {
402
+ await runtime.approveProposal(proposalId, { companionId });
403
+ }
404
+ // 4. Verify directive is now ACTIVE in SelfRepository
405
+ const directivesAfter = await self.getActiveDirectives(companionId);
406
+ expect(directivesAfter.some((d) => d.directive.toLowerCase().includes('be concise when answering technical questions'))).toBe(true);
407
+ db.close();
408
+ });
409
+ // =========================================================================
410
+ // Test D: Casual banter in Casual mode (Zero Memory Drift)
411
+ // =========================================================================
412
+ it('Test D: casual banter in casual mode causes Zero Memory Drift and does NOT mutate Self', async () => {
413
+ const db = new index_1.SiduriDatabase({ dbPath });
414
+ const companionId = 'comp-test-d';
415
+ const self = createSelfRepository(db);
416
+ const memory = createMemoryOrgan(db);
417
+ const mockBrain = {
418
+ generatePlan: jest.fn().mockResolvedValue({
419
+ speech: 'Haha, thank you! I try my best.',
420
+ language: 'en',
421
+ }),
422
+ };
423
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
424
+ brain: mockBrain,
425
+ memory,
426
+ self,
427
+ });
428
+ await runtime.initialize();
429
+ // "You are probably the funniest AI I've ever talked to." in casual mode
430
+ const perceptionResult = await runtime.processPerception({
431
+ source: 'text_chat',
432
+ text: "You are probably the funniest AI I've ever talked to.",
433
+ context: createRequestContext(companionId, 'casual'),
434
+ });
435
+ expect(perceptionResult.status).toBe('APPROVED');
436
+ // Zero proposals allowed in casual mode
437
+ expect(perceptionResult.metadata?.proposals).toEqual([]);
438
+ expect(perceptionResult.metadata?.memory_proposals).toEqual([]);
439
+ // Self must remain unchanged
440
+ const identity = await self.getIdentity(companionId);
441
+ expect(identity?.role).toBeUndefined();
442
+ expect(identity?.archetype).toBeUndefined();
443
+ const directives = await self.getActiveDirectives(companionId);
444
+ expect(directives).toHaveLength(0);
445
+ db.close();
446
+ });
447
+ // =========================================================================
448
+ // Test E: Approval gating: rejection leaves Self unchanged
449
+ // =========================================================================
450
+ it('Test E: rejecting a proposal prevents mutation of Self', async () => {
451
+ const db = new index_1.SiduriDatabase({ dbPath });
452
+ const companionId = 'comp-test-e';
453
+ const self = createSelfRepository(db);
454
+ const memory = createMemoryOrgan(db);
455
+ const mockBrain = createCognitiveMockBrain(companionId);
456
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
457
+ brain: mockBrain,
458
+ memory,
459
+ self,
460
+ });
461
+ await runtime.initialize();
462
+ // 1. Propose a role change
463
+ const perceptionResult = await runtime.processPerception({
464
+ source: 'text_chat',
465
+ text: 'Siduri, your role is Security Officer.',
466
+ context: createRequestContext(companionId, 'teach'),
467
+ });
468
+ const proposal = perceptionResult.metadata?.proposals?.[0];
469
+ expect(proposal).toBeDefined();
470
+ // 2. Reject the proposal
471
+ await runtime.rejectProposal(proposal.id, { companionId });
472
+ // 3. Verify Self is NOT mutated
473
+ const identity = await self.getIdentity(companionId);
474
+ expect(identity?.role).toBeUndefined();
475
+ expect(identity?.archetype).toBeUndefined();
476
+ db.close();
477
+ });
478
+ // =========================================================================
479
+ // Test F: Cross-restart durability (survives runtime restart)
480
+ // =========================================================================
481
+ it('Test F: persists learned state across runtime destruction and recreation', async () => {
482
+ const companionId = 'comp-test-f';
483
+ // --- Phase 1: Runtime 1 teaches and approves ---
484
+ {
485
+ const db1 = new index_1.SiduriDatabase({ dbPath });
486
+ const self1 = createSelfRepository(db1);
487
+ const memory1 = createMemoryOrgan(db1);
488
+ const mockBrain1 = createCognitiveMockBrain(companionId);
489
+ const runtime1 = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
490
+ brain: mockBrain1,
491
+ memory: memory1,
492
+ self: self1,
493
+ });
494
+ await runtime1.initialize();
495
+ const res = await runtime1.processPerception({
496
+ source: 'text_chat',
497
+ text: 'Siduri, you are the Lead Architect at VXNUS Studio.',
498
+ context: createRequestContext(companionId, 'teach'),
499
+ });
500
+ const roleProposal = res.metadata?.proposals?.find((p) => p.predicate === 'role');
501
+ expect(roleProposal).toBeDefined();
502
+ await runtime1.approveProposal(roleProposal.id, { companionId });
503
+ // Verify in runtime 1
504
+ const id1 = await self1.getIdentity(companionId);
505
+ expect(id1?.role).toBe('Lead Architect at VXNUS Studio');
506
+ // Close and destroy runtime 1 completely
507
+ db1.close();
508
+ }
509
+ // --- Phase 2: Runtime 2 initialized afresh pointing to same SQLite database ---
510
+ {
511
+ const db2 = new index_1.SiduriDatabase({ dbPath });
512
+ const self2 = createSelfRepository(db2);
513
+ const memory2 = createMemoryOrgan(db2);
514
+ const behavior2 = createBehaviorCompiler();
515
+ const mockBrain2 = createCognitiveMockBrain(companionId);
516
+ const runtime2 = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
517
+ brain: mockBrain2,
518
+ memory: memory2,
519
+ self: self2,
520
+ behavior: behavior2,
521
+ });
522
+ await runtime2.initialize();
523
+ // Verify that runtime 2 retrieves the learned role from SQLite!
524
+ const id2 = await self2.getIdentity(companionId);
525
+ expect(id2).toBeDefined();
526
+ expect(id2?.role).toBe('Lead Architect at VXNUS Studio');
527
+ expect(id2?.archetype).toBe('Lead Architect at VXNUS Studio');
528
+ // Verify that conversation in runtime 2 retrieves the learned role in prompt compilation
529
+ await runtime2.processPerception({
530
+ source: 'text_chat',
531
+ text: 'What do you do?',
532
+ context: createRequestContext(companionId, 'hybrid'),
533
+ });
534
+ const lastCallCtx = mockBrain2.generatePlan.mock.calls[0][0];
535
+ expect(lastCallCtx.systemPrompt).toContain('Role: Lead Architect at VXNUS Studio');
536
+ db2.close();
537
+ }
538
+ });
539
+ // =========================================================================
540
+ // Test G: Idempotency (duplicate approval does not corrupt database)
541
+ // =========================================================================
542
+ it('Test G: approving the same proposal twice is idempotent and does not create duplicate entries', async () => {
543
+ const db = new index_1.SiduriDatabase({ dbPath });
544
+ const companionId = 'comp-test-g';
545
+ const self = createSelfRepository(db);
546
+ const memory = createMemoryOrgan(db);
547
+ const mockBrain = createCognitiveMockBrain(companionId);
548
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
549
+ brain: mockBrain,
550
+ memory,
551
+ self,
552
+ });
553
+ await runtime.initialize();
554
+ const res = await runtime.processPerception({
555
+ source: 'text_chat',
556
+ text: 'Siduri, you are a Research Specialist.',
557
+ context: createRequestContext(companionId, 'teach'),
558
+ });
559
+ const proposal = res.metadata?.proposals?.[0];
560
+ expect(proposal).toBeDefined();
561
+ // Approve once
562
+ await runtime.approveProposal(proposal.id, { companionId });
563
+ const idFirst = await self.getIdentity(companionId);
564
+ expect(idFirst?.role).toBe('Research Specialist');
565
+ // Approve a second time (should be completely idempotent and not error)
566
+ await runtime.approveProposal(proposal.id, { companionId });
567
+ const idSecond = await self.getIdentity(companionId);
568
+ expect(idSecond?.role).toBe('Research Specialist');
569
+ // Check directives: should only have one directive for acknowledging the role
570
+ const directives = await self.getActiveDirectives(companionId);
571
+ const roleDirectives = directives.filter((d) => d.directive.includes('Research Specialist'));
572
+ expect(roleDirectives).toHaveLength(1);
573
+ db.close();
574
+ });
575
+ // =========================================================================
576
+ // Test H: Natural conversational teaching (past-tense role, user name, creator)
577
+ // =========================================================================
578
+ it('Test H: teaches companion past role ("she was VXNUS Studio Staff"), creator, and name, then recognizes user on subsequent turn', async () => {
579
+ const db = new index_1.SiduriDatabase({ dbPath });
580
+ const companionId = 'comp-test-h';
581
+ const self = createSelfRepository(db);
582
+ const memory = createMemoryOrgan(db);
583
+ const behavior = createBehaviorCompiler();
584
+ const mockBrain = createCognitiveMockBrain(companionId);
585
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
586
+ brain: mockBrain,
587
+ memory,
588
+ self,
589
+ behavior,
590
+ });
591
+ await runtime.initialize();
592
+ // 1. "she was VXNUS Studio Staff" in Teach mode
593
+ const resRole = await runtime.processPerception({
594
+ source: 'text_chat',
595
+ text: 'She was VXNUS Studio Staff.',
596
+ context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
597
+ });
598
+ expect(resRole.status).toBe('APPROVED');
599
+ const roleProp = resRole.metadata?.proposals?.find((p) => p.predicate === 'role');
600
+ expect(roleProp).toBeDefined();
601
+ expect(roleProp.value).toBe('VXNUS Studio Staff');
602
+ await runtime.approveProposal(roleProp.id, { companionId });
603
+ // 2. "my name is Kur Zagin" in Teach mode
604
+ const resName = await runtime.processPerception({
605
+ source: 'text_chat',
606
+ text: 'My name is Kur Zagin.',
607
+ context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
608
+ });
609
+ expect(resName.status).toBe('APPROVED');
610
+ const nameProp = resName.metadata?.proposals?.find((p) => p.predicate === 'name');
611
+ expect(nameProp).toBeDefined();
612
+ expect(nameProp.value).toBe('Kur Zagin');
613
+ await runtime.approveProposal(nameProp.id, { companionId });
614
+ // 3. "I am your creator" in Teach mode
615
+ const resRel = await runtime.processPerception({
616
+ source: 'text_chat',
617
+ text: 'I am your creator.',
618
+ context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
619
+ });
620
+ expect(resRel.status).toBe('APPROVED');
621
+ const relProp = resRel.metadata?.proposals?.find((p) => p.predicate === 'stated_relationship');
622
+ expect(relProp).toBeDefined();
623
+ await runtime.approveProposal(relProp.id, { companionId });
624
+ // 4. Verify Self relationship has both creator stance AND user name preserved
625
+ const rel = await self.getRelationship?.(companionId, 'actor:kur-zagin');
626
+ expect(rel).toBeDefined();
627
+ expect(rel?.role).toBe('creator');
628
+ expect(rel?.stance).toBe('familiar_loyal');
629
+ expect(rel?.name).toBe('Kur Zagin');
630
+ // 5. Subsequent conversation turn: "do you know me?"
631
+ await runtime.processPerception({
632
+ source: 'text_chat',
633
+ text: 'do you know me?',
634
+ context: createRequestContext(companionId, 'hybrid', 'actor:kur-zagin'),
635
+ });
636
+ const lastCallCtx = mockBrain.generatePlan.mock.calls[mockBrain.generatePlan.mock.calls.length - 1][0];
637
+ // Active Self contains both the creator stance and the user's name
638
+ expect(lastCallCtx.systemPrompt).toContain('Role: VXNUS Studio Staff');
639
+ expect(lastCallCtx.systemPrompt).toContain('familiar_loyal');
640
+ expect(lastCallCtx.systemPrompt).toContain('Kur Zagin');
641
+ db.close();
642
+ });
643
+ });
package/dist/index.d.ts CHANGED
@@ -261,7 +261,9 @@ import type { SelfIdentity, PersonalityTraits, SelfDirective, SelfRelationship,
261
261
  export type { SelfIdentity, PersonalityTraits, SelfDirective, SelfRelationship, SelfDialogueExample, LifeInventoryItem, LifeScheduleItem, LifePreference, MemoryClaim, EpisodicEvent, };
262
262
  export interface SelfRepository {
263
263
  getIdentity(companionId: string): Promise<SelfIdentity | undefined>;
264
+ setIdentity(identity: SelfIdentity): Promise<void>;
264
265
  getPersonality?(companionId: string): Promise<PersonalityTraits>;
266
+ setPersonality?(companionId: string, traits: PersonalityTraits): Promise<void>;
265
267
  getActiveDirectives(companionId: string): Promise<SelfDirective[]>;
266
268
  getRelationship(companionId: string, entityId: string): Promise<SelfRelationship | null>;
267
269
  getRelationships?(companionId: string): Promise<SelfRelationship[]>;
@@ -269,7 +271,11 @@ export interface SelfRepository {
269
271
  setExemplars?(companionId: string, exemplars: SelfDialogueExample[]): Promise<void>;
270
272
  commitDirectives(companionId: string, directives: SelfDirective[]): Promise<void>;
271
273
  updateRelationship(companionId: string, rel: SelfRelationship): Promise<void>;
272
- disableDirective?(id: string): Promise<void>;
274
+ disableDirective?(id: string, companionId?: string): Promise<void>;
275
+ approveDirective?(id: string, companionId?: string): Promise<void>;
276
+ rejectDirective?(id: string, companionId?: string): Promise<void>;
277
+ revokeDirective?(id: string, companionId?: string): Promise<void>;
278
+ expireDirective?(id: string, companionId?: string): Promise<void>;
273
279
  getActiveSelf?(companionId: string): Promise<{
274
280
  identity?: SelfIdentity;
275
281
  personality?: PersonalityTraits;