@siduri-x/core 2.0.3 → 2.0.5

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.
@@ -13,6 +13,7 @@ export interface ChatRequest {
13
13
  history?: Message[];
14
14
  medium?: MouthMedium;
15
15
  signal?: AbortSignal;
16
+ subtitleLanguage?: string;
16
17
  [key: string]: any;
17
18
  }
18
19
  export interface ChatResponseMetadataEvent {
@@ -30,6 +31,9 @@ export interface ChatResponsePlan {
30
31
  subtitle_ja: string;
31
32
  subtitle_en: string;
32
33
  spoken_ja?: string;
34
+ subtitle?: string;
35
+ subtitle_language?: string;
36
+ subtitles?: Record<string, string>;
33
37
  evidence_ids?: string[];
34
38
  }
35
39
  export interface ChatResponseMetadata {
@@ -21,8 +21,9 @@ async function dispatchCompanionChat(runtime, payload) {
21
21
  else {
22
22
  roleOrContext = 'OWNER';
23
23
  }
24
- const runtimeResult = (payload.medium || payload.signal)
25
- ? await runner.handleUserMessage(userMessage, roleOrContext, history, payload.medium, payload.signal)
24
+ const requestedSubtitleLang = payload.subtitleLanguage || payload.subtitle_language;
25
+ const runtimeResult = (payload.medium || payload.signal || requestedSubtitleLang)
26
+ ? await runner.handleUserMessage(userMessage, roleOrContext, history, payload.medium, payload.signal, requestedSubtitleLang)
26
27
  : await runner.handleUserMessage(userMessage, roleOrContext, history);
27
28
  const delivery = runtimeResult?.delivery;
28
29
  // Normalize response plan
@@ -41,6 +42,17 @@ async function dispatchCompanionChat(runtime, payload) {
41
42
  expression = avatarEvent.expression;
42
43
  }
43
44
  }
45
+ const resolvedSubtitles = {
46
+ ...(runtimeResult?.response?.subtitles || {}),
47
+ ...(delivery?.subtitles || {}),
48
+ };
49
+ const subtitle = (requestedSubtitleLang && resolvedSubtitles[requestedSubtitleLang]) ||
50
+ runtimeResult?.response?.subtitle ||
51
+ (requestedSubtitleLang === 'ja' ? (delivery?.subtitles?.ja ?? runtimeResult?.response?.subtitle_ja) : undefined) ||
52
+ (requestedSubtitleLang === 'en' ? (delivery?.subtitles?.en ?? runtimeResult?.response?.subtitle_en) : undefined);
53
+ if (subtitle && requestedSubtitleLang) {
54
+ resolvedSubtitles[requestedSubtitleLang] = subtitle;
55
+ }
44
56
  // Ensure both spoken_ja and subtitle_en are accessible alongside speech_id and evidence_ids
45
57
  const responsePlan = {
46
58
  speech_id: runtimeResult?.response?.speech_id,
@@ -48,6 +60,9 @@ async function dispatchCompanionChat(runtime, payload) {
48
60
  subtitle_ja: delivery?.subtitles?.ja ?? runtimeResult?.response?.subtitle_ja ?? speech,
49
61
  subtitle_en: delivery?.subtitles?.en ?? runtimeResult?.response?.subtitle_en ?? speech,
50
62
  spoken_ja: delivery?.subtitles?.spoken ?? runtimeResult?.response?.spoken_ja ?? runtimeResult?.response?.subtitle_ja ?? speech,
63
+ subtitle,
64
+ subtitle_language: requestedSubtitleLang,
65
+ subtitles: resolvedSubtitles,
51
66
  evidence_ids: runtimeResult?.metadata?.evidence_ids ?? runtimeResult?.response?.evidence_ids ?? [],
52
67
  };
53
68
  const metadata = {
@@ -19,6 +19,9 @@ export interface RetrievedContext {
19
19
  collectedEvidence: EvidenceRecord[];
20
20
  citations: ResponseCitation[];
21
21
  lifeContext?: string[];
22
+ selfIdentity?: any;
23
+ selfRelationship?: any;
24
+ personality?: any;
22
25
  }
23
26
  /**
24
27
  * Concurrently queries Self, Knowledge (Life DB), External Knowledge, and Memory
@@ -15,8 +15,8 @@ async function retrieveRuntimeContext(params) {
15
15
  const subsystemDiagnostics = {};
16
16
  // 1. Resolve External Knowledge organ (either explicitly passed or from legacy knowledge with .search)
17
17
  const extKnowledge = externalKnowledge || (knowledge && typeof knowledge.search === 'function' ? knowledge : undefined);
18
- // 2. Query all 4 streams in parallel
19
- const [knowledgeData, memoryData, selfOrMemoryDirectives, lifeContext] = await Promise.all([
18
+ // 2. Query streams in parallel
19
+ const [knowledgeData, memoryData, selfOrMemoryDirectives, lifeContext, selfIdentity, selfRelationship, personality,] = await Promise.all([
20
20
  // Stream A: External Cited Lore / Documentation
21
21
  extKnowledge && shouldQueryKnowledge && typeof extKnowledge.search === 'function'
22
22
  ? extKnowledge.search(perceivedText).catch((e) => {
@@ -62,6 +62,24 @@ async function retrieveRuntimeContext(params) {
62
62
  return [];
63
63
  })
64
64
  : Promise.resolve([]),
65
+ // Stream E: Self Identity
66
+ self && typeof self.getIdentity === 'function'
67
+ ? self.getIdentity(companionId).catch((e) => {
68
+ console.error('[SiduriRuntime] Self identity failed:', e.message);
69
+ return undefined;
70
+ })
71
+ : Promise.resolve(undefined),
72
+ // Stream F: Self Relationship toward interacting actor
73
+ self && typeof self.getRelationship === 'function' && requestContext.actor?.actorId
74
+ ? self.getRelationship(companionId, requestContext.actor.actorId).catch((e) => {
75
+ console.error('[SiduriRuntime] Self relationship failed:', e.message);
76
+ return null;
77
+ })
78
+ : Promise.resolve(null),
79
+ // Stream G: Self Personality
80
+ self && typeof self.getPersonality === 'function'
81
+ ? self.getPersonality(companionId).catch(() => undefined)
82
+ : Promise.resolve(undefined),
65
83
  ]);
66
84
  const activeDirectives = (selfOrMemoryDirectives || []);
67
85
  // Build evidence records from retrieved external knowledge context
@@ -115,5 +133,8 @@ async function retrieveRuntimeContext(params) {
115
133
  collectedEvidence,
116
134
  citations,
117
135
  lifeContext,
136
+ selfIdentity,
137
+ selfRelationship,
138
+ personality,
118
139
  };
119
140
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,513 @@
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
+ parts.push(`Relationship Stance:\n- Stance toward ${ctx.relationship.entityId}: ${ctx.relationship.stance} (Role: ${ctx.relationship.role})`);
128
+ }
129
+ if (ctx.directives && ctx.directives.length > 0) {
130
+ parts.push(`Behavioral Directives:\n${ctx.directives.map((d) => `- ${d.directive}`).join('\n')}`);
131
+ }
132
+ parts.push('</active_self>');
133
+ return parts.join('\n\n');
134
+ },
135
+ };
136
+ }
137
+ function createRequestContext(companionId, mode = 'teach', actorId = 'kur-zagin') {
138
+ return {
139
+ companionId,
140
+ mode,
141
+ actor: {
142
+ actorId,
143
+ sessionId: 'sess-teach-1',
144
+ authorizationRole: 'OWNER',
145
+ authenticated: true,
146
+ },
147
+ conversation: {
148
+ channel: 'direct',
149
+ correlationId: `corr-${Date.now()}`,
150
+ },
151
+ };
152
+ }
153
+ // =========================================================================
154
+ // Test A: Conversational role teaching in Teach mode
155
+ // =========================================================================
156
+ it('Test A: teaches companion role in Teach mode, approves proposal, and reflects role in Active Self', async () => {
157
+ const db = new index_1.SiduriDatabase({ dbPath });
158
+ const companionId = 'comp-test-a';
159
+ const self = createSelfRepository(db);
160
+ const memory = createMemoryOrgan(db);
161
+ const behavior = createBehaviorCompiler();
162
+ const mockBrain = {
163
+ generatePlan: jest.fn().mockImplementation(async (brainCtx) => {
164
+ return {
165
+ speech: 'Understood, I have acknowledged my role.',
166
+ language: 'en',
167
+ // Brain context should have received the system prompt
168
+ _receivedSystemPrompt: brainCtx.systemPrompt,
169
+ };
170
+ }),
171
+ };
172
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
173
+ brain: mockBrain,
174
+ memory,
175
+ self,
176
+ behavior,
177
+ });
178
+ await runtime.initialize();
179
+ // 1. Send conversational role teaching message
180
+ const perceptionResult = await runtime.processPerception({
181
+ source: 'text_chat',
182
+ text: 'Siduri, you are an AI researcher at VXNUS Studio.',
183
+ context: createRequestContext(companionId, 'teach'),
184
+ });
185
+ expect(perceptionResult.status).toBe('APPROVED');
186
+ // 2. Verify proposal was generated
187
+ const proposals = perceptionResult.metadata?.proposals || [];
188
+ expect(proposals.length).toBeGreaterThanOrEqual(1);
189
+ const roleProposal = proposals.find((p) => p.predicate === 'role' || p.predicate === 'archetype');
190
+ expect(roleProposal).toBeDefined();
191
+ expect(roleProposal.value).toBe('AI researcher at VXNUS Studio');
192
+ expect(roleProposal.status).toBe('pending');
193
+ // 3. Truth Gate check: Before approval, SelfRepository must NOT contain the new role
194
+ const identityBefore = await self.getIdentity(companionId);
195
+ expect(identityBefore?.role).toBeUndefined();
196
+ expect(identityBefore?.archetype).toBeUndefined();
197
+ // 4. Operator / Owner approves the proposal
198
+ const approveResult = await runtime.approveProposal(roleProposal.id, { companionId });
199
+ expect(approveResult.success).toBe(true);
200
+ // 5. Verify SelfRepository now has the learned role
201
+ const identityAfter = await self.getIdentity(companionId);
202
+ expect(identityAfter).toBeDefined();
203
+ expect(identityAfter?.role).toBe('AI researcher at VXNUS Studio');
204
+ expect(identityAfter?.archetype).toBe('AI researcher at VXNUS Studio');
205
+ // Also verify a relational directive acknowledging the role was added
206
+ const directives = await self.getActiveDirectives(companionId);
207
+ const roleDirective = directives.find((d) => d.directive.includes('AI researcher at VXNUS Studio'));
208
+ expect(roleDirective).toBeDefined();
209
+ // 6. Next conversational turn: verify prompt compiler injects the updated Active Self
210
+ await runtime.processPerception({
211
+ source: 'text_chat',
212
+ text: 'What is your primary mission?',
213
+ context: createRequestContext(companionId, 'hybrid'),
214
+ });
215
+ expect(mockBrain.generatePlan).toHaveBeenCalled();
216
+ const lastCallCtx = mockBrain.generatePlan.mock.calls[1][0];
217
+ expect(lastCallCtx.systemPrompt).toContain('<active_self>');
218
+ expect(lastCallCtx.systemPrompt).toContain('Role: AI researcher at VXNUS Studio');
219
+ db.close();
220
+ });
221
+ // =========================================================================
222
+ // Test B: Creator relationship teaching
223
+ // =========================================================================
224
+ it('Test B: teaches creator relationship, approves proposal, and updates relationship stance', async () => {
225
+ const db = new index_1.SiduriDatabase({ dbPath });
226
+ const companionId = 'comp-test-b';
227
+ const self = createSelfRepository(db);
228
+ const memory = createMemoryOrgan(db);
229
+ const behavior = createBehaviorCompiler();
230
+ const mockBrain = {
231
+ generatePlan: jest.fn().mockResolvedValue({
232
+ speech: 'Greetings, Creator.',
233
+ language: 'en',
234
+ }),
235
+ };
236
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
237
+ brain: mockBrain,
238
+ memory,
239
+ self,
240
+ behavior,
241
+ });
242
+ await runtime.initialize();
243
+ // 1. Send creator teaching message
244
+ const perceptionResult = await runtime.processPerception({
245
+ source: 'text_chat',
246
+ text: 'I am your creator, Kur Zagin.',
247
+ context: createRequestContext(companionId, 'teach', 'actor:kur-zagin'),
248
+ });
249
+ const proposals = perceptionResult.metadata?.proposals || [];
250
+ const relProposal = proposals.find((p) => p.predicate === 'stated_relationship' ||
251
+ p.predicate === 'relationship' ||
252
+ p.claimType === 'relationship');
253
+ expect(relProposal).toBeDefined();
254
+ expect(relProposal.value).toBe('creator');
255
+ // 2. Truth Gate: Before approval, relationship does not have creator stance
256
+ const relBefore = await self.getRelationship?.(companionId, 'actor:kur-zagin');
257
+ expect(relBefore?.role).toBeUndefined();
258
+ // 3. Approve the relationship proposal
259
+ await runtime.approveProposal(relProposal.id, { companionId });
260
+ // 4. Verify SelfRepository now has creator relationship with loyal stance
261
+ const relAfter = await self.getRelationship?.(companionId, 'actor:kur-zagin');
262
+ expect(relAfter).toBeDefined();
263
+ expect(relAfter?.role).toBe('creator');
264
+ expect(relAfter?.stance).toBe('familiar_loyal');
265
+ expect(relAfter?.trustScore).toBe(1.0);
266
+ // 5. Subsequent conversation retrieves relationship in prompt
267
+ await runtime.processPerception({
268
+ source: 'text_chat',
269
+ text: 'Status update please.',
270
+ context: createRequestContext(companionId, 'hybrid', 'actor:kur-zagin'),
271
+ });
272
+ const lastCallCtx = mockBrain.generatePlan.mock.calls[1][0];
273
+ expect(lastCallCtx.systemPrompt).toContain('Stance toward actor:kur-zagin: familiar_loyal (Role: creator)');
274
+ db.close();
275
+ });
276
+ // =========================================================================
277
+ // Test C: Behavioral rule teaching
278
+ // =========================================================================
279
+ it('Test C: teaches behavioral rule, generates proposal, approves, and activates directive in Self', async () => {
280
+ const db = new index_1.SiduriDatabase({ dbPath });
281
+ const companionId = 'comp-test-c';
282
+ const self = createSelfRepository(db);
283
+ const memory = createMemoryOrgan(db);
284
+ const behavior = createBehaviorCompiler();
285
+ const mockBrain = {
286
+ generatePlan: jest.fn().mockResolvedValue({
287
+ speech: 'Rule noted.',
288
+ language: 'en',
289
+ }),
290
+ };
291
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
292
+ brain: mockBrain,
293
+ memory,
294
+ self,
295
+ behavior,
296
+ });
297
+ await runtime.initialize();
298
+ // 1. Send behavioral rule teaching message
299
+ const perceptionResult = await runtime.processPerception({
300
+ source: 'text_chat',
301
+ text: 'Remember this rule: be concise when answering technical questions.',
302
+ context: createRequestContext(companionId, 'teach'),
303
+ });
304
+ // Check behavioral proposals or memory proposals
305
+ const behavioralReceipts = perceptionResult.metadata?.behavioral_proposals || [];
306
+ const memoryProposals = perceptionResult.metadata?.proposals || [];
307
+ const hasDirectiveProposal = behavioralReceipts.length > 0 ||
308
+ memoryProposals.some((p) => p.predicate === 'rule' || p.predicate === 'behavioral_rule');
309
+ expect(hasDirectiveProposal).toBe(true);
310
+ const directiveId = behavioralReceipts[0]?.directive_id;
311
+ const proposalId = memoryProposals[0]?.id;
312
+ // 2. Active directives before approval must not include the new rule
313
+ const directivesBefore = await self.getActiveDirectives(companionId);
314
+ expect(directivesBefore.some((d) => d.directive.toLowerCase().includes('be concise when answering technical questions'))).toBe(false);
315
+ // 3. Approve directive via runtime
316
+ if (directiveId) {
317
+ await runtime.approveDirective(directiveId, { companionId });
318
+ }
319
+ else if (proposalId) {
320
+ await runtime.approveProposal(proposalId, { companionId });
321
+ }
322
+ // 4. Verify directive is now ACTIVE in SelfRepository
323
+ const directivesAfter = await self.getActiveDirectives(companionId);
324
+ expect(directivesAfter.some((d) => d.directive.toLowerCase().includes('be concise when answering technical questions'))).toBe(true);
325
+ db.close();
326
+ });
327
+ // =========================================================================
328
+ // Test D: Casual banter in Casual mode (Zero Memory Drift)
329
+ // =========================================================================
330
+ it('Test D: casual banter in casual mode causes Zero Memory Drift and does NOT mutate Self', async () => {
331
+ const db = new index_1.SiduriDatabase({ dbPath });
332
+ const companionId = 'comp-test-d';
333
+ const self = createSelfRepository(db);
334
+ const memory = createMemoryOrgan(db);
335
+ const mockBrain = {
336
+ generatePlan: jest.fn().mockResolvedValue({
337
+ speech: 'Haha, thank you! I try my best.',
338
+ language: 'en',
339
+ }),
340
+ };
341
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
342
+ brain: mockBrain,
343
+ memory,
344
+ self,
345
+ });
346
+ await runtime.initialize();
347
+ // "You are probably the funniest AI I've ever talked to." in casual mode
348
+ const perceptionResult = await runtime.processPerception({
349
+ source: 'text_chat',
350
+ text: "You are probably the funniest AI I've ever talked to.",
351
+ context: createRequestContext(companionId, 'casual'),
352
+ });
353
+ expect(perceptionResult.status).toBe('APPROVED');
354
+ // Zero proposals allowed in casual mode
355
+ expect(perceptionResult.metadata?.proposals).toEqual([]);
356
+ expect(perceptionResult.metadata?.memory_proposals).toEqual([]);
357
+ // Self must remain unchanged
358
+ const identity = await self.getIdentity(companionId);
359
+ expect(identity?.role).toBeUndefined();
360
+ expect(identity?.archetype).toBeUndefined();
361
+ const directives = await self.getActiveDirectives(companionId);
362
+ expect(directives).toHaveLength(0);
363
+ db.close();
364
+ });
365
+ // =========================================================================
366
+ // Test E: Approval gating: rejection leaves Self unchanged
367
+ // =========================================================================
368
+ it('Test E: rejecting a proposal prevents mutation of Self', async () => {
369
+ const db = new index_1.SiduriDatabase({ dbPath });
370
+ const companionId = 'comp-test-e';
371
+ const self = createSelfRepository(db);
372
+ const memory = createMemoryOrgan(db);
373
+ const mockBrain = {
374
+ generatePlan: jest.fn().mockResolvedValue({
375
+ speech: 'Understood.',
376
+ language: 'en',
377
+ }),
378
+ };
379
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
380
+ brain: mockBrain,
381
+ memory,
382
+ self,
383
+ });
384
+ await runtime.initialize();
385
+ // 1. Propose a role change
386
+ const perceptionResult = await runtime.processPerception({
387
+ source: 'text_chat',
388
+ text: 'Siduri, your role is Security Officer.',
389
+ context: createRequestContext(companionId, 'teach'),
390
+ });
391
+ const proposal = perceptionResult.metadata?.proposals?.[0];
392
+ expect(proposal).toBeDefined();
393
+ // 2. Reject the proposal
394
+ await runtime.rejectProposal(proposal.id, { companionId });
395
+ // 3. Verify Self is NOT mutated
396
+ const identity = await self.getIdentity(companionId);
397
+ expect(identity?.role).toBeUndefined();
398
+ expect(identity?.archetype).toBeUndefined();
399
+ db.close();
400
+ });
401
+ // =========================================================================
402
+ // Test F: Cross-restart durability (survives runtime restart)
403
+ // =========================================================================
404
+ it('Test F: persists learned state across runtime destruction and recreation', async () => {
405
+ const companionId = 'comp-test-f';
406
+ // --- Phase 1: Runtime 1 teaches and approves ---
407
+ {
408
+ const db1 = new index_1.SiduriDatabase({ dbPath });
409
+ const self1 = createSelfRepository(db1);
410
+ const memory1 = createMemoryOrgan(db1);
411
+ const mockBrain1 = {
412
+ generatePlan: jest.fn().mockResolvedValue({
413
+ speech: 'I have recorded my new role as Lead Architect.',
414
+ language: 'en',
415
+ }),
416
+ };
417
+ const runtime1 = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
418
+ brain: mockBrain1,
419
+ memory: memory1,
420
+ self: self1,
421
+ });
422
+ await runtime1.initialize();
423
+ const res = await runtime1.processPerception({
424
+ source: 'text_chat',
425
+ text: 'Siduri, you are the Lead Architect at VXNUS Studio.',
426
+ context: createRequestContext(companionId, 'teach'),
427
+ });
428
+ const roleProposal = res.metadata?.proposals?.find((p) => p.predicate === 'role');
429
+ expect(roleProposal).toBeDefined();
430
+ await runtime1.approveProposal(roleProposal.id, { companionId });
431
+ // Verify in runtime 1
432
+ const id1 = await self1.getIdentity(companionId);
433
+ expect(id1?.role).toBe('Lead Architect at VXNUS Studio');
434
+ // Close and destroy runtime 1 completely
435
+ db1.close();
436
+ }
437
+ // --- Phase 2: Runtime 2 initialized afresh pointing to same SQLite database ---
438
+ {
439
+ const db2 = new index_1.SiduriDatabase({ dbPath });
440
+ const self2 = createSelfRepository(db2);
441
+ const memory2 = createMemoryOrgan(db2);
442
+ const behavior2 = createBehaviorCompiler();
443
+ const mockBrain2 = {
444
+ generatePlan: jest.fn().mockResolvedValue({
445
+ speech: 'Ready to build architecture.',
446
+ language: 'en',
447
+ }),
448
+ };
449
+ const runtime2 = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
450
+ brain: mockBrain2,
451
+ memory: memory2,
452
+ self: self2,
453
+ behavior: behavior2,
454
+ });
455
+ await runtime2.initialize();
456
+ // Verify that runtime 2 retrieves the learned role from SQLite!
457
+ const id2 = await self2.getIdentity(companionId);
458
+ expect(id2).toBeDefined();
459
+ expect(id2?.role).toBe('Lead Architect at VXNUS Studio');
460
+ expect(id2?.archetype).toBe('Lead Architect at VXNUS Studio');
461
+ // Verify that conversation in runtime 2 retrieves the learned role in prompt compilation
462
+ await runtime2.processPerception({
463
+ source: 'text_chat',
464
+ text: 'What do you do?',
465
+ context: createRequestContext(companionId, 'hybrid'),
466
+ });
467
+ const lastCallCtx = mockBrain2.generatePlan.mock.calls[0][0];
468
+ expect(lastCallCtx.systemPrompt).toContain('Role: Lead Architect at VXNUS Studio');
469
+ db2.close();
470
+ }
471
+ });
472
+ // =========================================================================
473
+ // Test G: Idempotency (duplicate approval does not corrupt database)
474
+ // =========================================================================
475
+ it('Test G: approving the same proposal twice is idempotent and does not create duplicate entries', async () => {
476
+ const db = new index_1.SiduriDatabase({ dbPath });
477
+ const companionId = 'comp-test-g';
478
+ const self = createSelfRepository(db);
479
+ const memory = createMemoryOrgan(db);
480
+ const mockBrain = {
481
+ generatePlan: jest.fn().mockResolvedValue({
482
+ speech: 'Understood.',
483
+ language: 'en',
484
+ }),
485
+ };
486
+ const runtime = new index_1.SiduriRuntime(companionId, { name: 'Siduri' }, {
487
+ brain: mockBrain,
488
+ memory,
489
+ self,
490
+ });
491
+ await runtime.initialize();
492
+ const res = await runtime.processPerception({
493
+ source: 'text_chat',
494
+ text: 'Siduri, you are a Research Specialist.',
495
+ context: createRequestContext(companionId, 'teach'),
496
+ });
497
+ const proposal = res.metadata?.proposals?.[0];
498
+ expect(proposal).toBeDefined();
499
+ // Approve once
500
+ await runtime.approveProposal(proposal.id, { companionId });
501
+ const idFirst = await self.getIdentity(companionId);
502
+ expect(idFirst?.role).toBe('Research Specialist');
503
+ // Approve a second time (should be completely idempotent and not error)
504
+ await runtime.approveProposal(proposal.id, { companionId });
505
+ const idSecond = await self.getIdentity(companionId);
506
+ expect(idSecond?.role).toBe('Research Specialist');
507
+ // Check directives: should only have one directive for acknowledging the role
508
+ const directives = await self.getActiveDirectives(companionId);
509
+ const roleDirectives = directives.filter((d) => d.directive.includes('Research Specialist'));
510
+ expect(roleDirectives).toHaveLength(1);
511
+ db.close();
512
+ });
513
+ });