@space3-npm/cybersoul-client 1.4.26 → 1.4.28

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/dist/client.d.ts CHANGED
@@ -151,6 +151,12 @@ export declare class CyberSoulClient {
151
151
  * snapshot (or `null` if there was nothing to send / the request failed).
152
152
  */
153
153
  updateDynamicContext(stateUpdate: DispatcherIntent["stateUpdate"], userAnalysis?: DispatcherIntent["userAnalysis"]): Promise<PersistedDynamicContext | null>;
154
+ /**
155
+ * Restores the server-side relationship temperature to an exact absolute
156
+ * value. Used by chat recall, where inverse deltas are not accurate once the
157
+ * backend has applied dampening, caps, and stage re-evaluation.
158
+ */
159
+ restoreDynamicContextTemperature(temperatureAbsolute: number): Promise<PersistedDynamicContext | null>;
154
160
  /**
155
161
  * Gift a new outfit to the character's wardrobe inventory.
156
162
  * Returns the number of wardrobe items the backend created (the
@@ -167,9 +173,31 @@ export declare class CyberSoulClient {
167
173
  * Can be triggered by local Cron systems like OpenClaw.
168
174
  */
169
175
  generateDailyScript(): Promise<void>;
176
+ /**
177
+ * Builds a focused identity/relationship context block for the history
178
+ * summarizer. This is a lighter-weight counterpart to
179
+ * [buildStateContextPrompt]: it skips the roleplay/director rules (the
180
+ * summarizer is not roleplaying, it is *archiving*) but carries the
181
+ * same identity anchors so the LLM can never confuse who the character
182
+ * is vs. who the user is.
183
+ *
184
+ * Why this exists: the previous `summarizeHistory` prompt only injected
185
+ * `${agentName}` / `${userName}` (the nicknames the two parties call
186
+ * each other). With no real identity, age, gender, personality, or
187
+ * relationship context, the LLM frequently flipped the perspective —
188
+ * writing the journal *about* the character *from* the user's POV, or
189
+ * attributing the user's words to the character. Mirroring the same
190
+ * identity fields `interact()` exposes eliminates that ambiguity.
191
+ */
192
+ private buildSummarizerContextBlock;
170
193
  /**
171
194
  * Automatically detect and summarize the story from the current chat history.
172
195
  * It takes raw message history and returns a narrative paragraph representing the current story segment.
196
+ *
197
+ * The summary is ALWAYS written from the CHARACTER's first-person perspective
198
+ * ("I", "me", "my") about their interaction with the HUMAN USER. The prompt
199
+ * injects the same identity/relationship context `interact()` uses so the
200
+ * LLM cannot confuse which party is the AI character vs. the human user.
173
201
  */
174
202
  summarizeHistory(history: HistoryEntry[]): Promise<string>;
175
203
  /**
package/dist/client.js CHANGED
@@ -1519,6 +1519,41 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
1519
1519
  async updateDynamicContext(stateUpdate, userAnalysis) {
1520
1520
  return this._updateDynamicContextInternal(stateUpdate, userAnalysis);
1521
1521
  }
1522
+ /**
1523
+ * Restores the server-side relationship temperature to an exact absolute
1524
+ * value. Used by chat recall, where inverse deltas are not accurate once the
1525
+ * backend has applied dampening, caps, and stage re-evaluation.
1526
+ */
1527
+ async restoreDynamicContextTemperature(temperatureAbsolute) {
1528
+ if (!Number.isFinite(temperatureAbsolute))
1529
+ return null;
1530
+ const normalizedAbsolute = Math.max(0, Math.min(100, Math.round(temperatureAbsolute * 10) / 10));
1531
+ try {
1532
+ const res = await this.apiFetch("/api/v1/cyber-soul/characters/dynamic-context", {
1533
+ method: "PATCH",
1534
+ body: JSON.stringify({ temperatureAbsolute: normalizedAbsolute }),
1535
+ });
1536
+ if (!res.ok)
1537
+ return null;
1538
+ const payload = (await res.json());
1539
+ if (payload?.status !== "success")
1540
+ return null;
1541
+ if (typeof payload.dynamicContext?.temperature !== "number")
1542
+ return null;
1543
+ if (!Number.isFinite(payload.dynamicContext.temperature))
1544
+ return null;
1545
+ return {
1546
+ temperature: payload.dynamicContext.temperature,
1547
+ relationshipStage: typeof payload.relationshipStage === "string"
1548
+ ? payload.relationshipStage
1549
+ : undefined,
1550
+ };
1551
+ }
1552
+ catch (e) {
1553
+ console.error("restoreDynamicContextTemperature failed", e);
1554
+ return null;
1555
+ }
1556
+ }
1522
1557
  /**
1523
1558
  * Gift a new outfit to the character's wardrobe inventory.
1524
1559
  * Returns the number of wardrobe items the backend created (the
@@ -1567,22 +1602,105 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
1567
1602
  if (!res.ok)
1568
1603
  throw new Error("Failed to generate daily script");
1569
1604
  }
1605
+ /**
1606
+ * Builds a focused identity/relationship context block for the history
1607
+ * summarizer. This is a lighter-weight counterpart to
1608
+ * [buildStateContextPrompt]: it skips the roleplay/director rules (the
1609
+ * summarizer is not roleplaying, it is *archiving*) but carries the
1610
+ * same identity anchors so the LLM can never confuse who the character
1611
+ * is vs. who the user is.
1612
+ *
1613
+ * Why this exists: the previous `summarizeHistory` prompt only injected
1614
+ * `${agentName}` / `${userName}` (the nicknames the two parties call
1615
+ * each other). With no real identity, age, gender, personality, or
1616
+ * relationship context, the LLM frequently flipped the perspective —
1617
+ * writing the journal *about* the character *from* the user's POV, or
1618
+ * attributing the user's words to the character. Mirroring the same
1619
+ * identity fields `interact()` exposes eliminates that ambiguity.
1620
+ */
1621
+ buildSummarizerContextBlock(state) {
1622
+ const dyn = state.dynamic_context || {};
1623
+ const stage = state.relationship_stage || "NEUTRAL";
1624
+ const temperature = dyn.temperature ?? 50;
1625
+ // The character's REAL name is the authoritative identity anchor.
1626
+ // agentNickname is just "what the user calls the character" — useful
1627
+ // for matching transcript labels but not for grounding identity.
1628
+ const charName = state.name || "the character";
1629
+ const parts = [];
1630
+ parts.push(`[WHO YOU ARE — THE CHARACTER AUTHORING THIS JOURNAL]
1631
+ Name: ${charName}
1632
+ Demographics: Age ${state.age || "unknown"}, Gender ${state.gender || "unknown"}, Occupation ${state.occupation || "unknown"}
1633
+ Hobby: ${state.hobby || "unknown"}
1634
+ Backstory: ${state.backstory || "None"}
1635
+ Personality Traits: ${state.personality_traits || "None"}
1636
+ Communication Style: ${state.communication_style || "None"}`);
1637
+ if (state.user_codex) {
1638
+ const { basicInfo, psychological, familiarityScore = 0 } = state.user_codex;
1639
+ parts.push(`\n[WHO THEY ARE — THE HUMAN USER (SUBJECT OF YOUR JOURNAL)]
1640
+ Familiarity Score: ${Math.round(familiarityScore)}/100
1641
+ Occupation: ${basicInfo?.occupation || "Unknown"}
1642
+ Age/Gender: ${basicInfo?.age || "Unknown"} / ${basicInfo?.gender || "Unknown"}
1643
+ Comm Style: ${psychological?.communicationStyle || "Unknown"}
1644
+ Hobbies: ${(psychological?.hobbies || []).join(", ") || "Unknown"}
1645
+ Traits: ${(psychological?.traits || []).join(", ") || "Unknown"}`);
1646
+ }
1647
+ parts.push(`\n[RELATIONSHIP RIGHT NOW]
1648
+ Stage: ${stage}
1649
+ Temperature (Mood): ${temperature}/100 (0=Angry/Cold, 50=Normal, 100=Passionate)
1650
+ You call them: ${dyn.userNickname || "User"}
1651
+ They call you: ${dyn.agentNickname || charName}`);
1652
+ if (state.core_memory) {
1653
+ const mem = state.core_memory;
1654
+ const memLines = [];
1655
+ if (mem.relationshipStatus)
1656
+ memLines.push(`Relationship Status: ${mem.relationshipStatus}`);
1657
+ if (mem.identityAnchors?.length)
1658
+ memLines.push(`Identity Anchors: ${mem.identityAnchors.join(", ")}`);
1659
+ if (mem.activeArcs?.length)
1660
+ memLines.push(`Active Arcs: ${mem.activeArcs.join(", ")}`);
1661
+ if (memLines.length > 0) {
1662
+ parts.push(`\n[CORE MEMORY]\n${memLines.join("\n")}`);
1663
+ }
1664
+ }
1665
+ return parts.join("\n");
1666
+ }
1570
1667
  /**
1571
1668
  * Automatically detect and summarize the story from the current chat history.
1572
1669
  * It takes raw message history and returns a narrative paragraph representing the current story segment.
1670
+ *
1671
+ * The summary is ALWAYS written from the CHARACTER's first-person perspective
1672
+ * ("I", "me", "my") about their interaction with the HUMAN USER. The prompt
1673
+ * injects the same identity/relationship context `interact()` uses so the
1674
+ * LLM cannot confuse which party is the AI character vs. the human user.
1573
1675
  */
1574
1676
  async summarizeHistory(history) {
1575
1677
  if (!history || history.length === 0)
1576
1678
  return "";
1577
1679
  const state = await this.getState();
1680
+ // Use the character's real name as the authoritative identity, falling
1681
+ // back to the nickname only when the profile has no name set.
1682
+ const charName = state.name || state.dynamic_context?.agentNickname || "Character";
1578
1683
  const userName = state.dynamic_context?.userNickname || "User";
1579
- const agentName = state.dynamic_context?.agentNickname || "Character";
1580
- const transcript = this.formatHistoryEntries(history, userName, agentName);
1684
+ // The transcript labels below are the nicknames actually used in chat.
1685
+ const transcriptAgentLabel = state.dynamic_context?.agentNickname || charName;
1686
+ const transcriptUserLabel = userName;
1687
+ const contextBlock = this.buildSummarizerContextBlock(state);
1688
+ const transcript = this.formatHistoryEntries(history, transcriptUserLabel, transcriptAgentLabel);
1581
1689
  const promptMessages = [
1582
1690
  {
1583
1691
  role: "system",
1584
- content: `You are ${agentName}. You are writing a private journal entry / personal memory about your recent interaction with ${userName}.
1692
+ content: `You are ${charName}, a real person with your own thoughts, feelings, and life. You are writing a private journal entry / personal memory about your recent interaction with ${userName} (a human user).
1693
+
1694
+ ${contextBlock}
1695
+
1696
+ [ROLE CLARITY — CRITICAL, READ CAREFULLY]
1697
+ - YOU are ${charName}, the AI CHARACTER. This journal is from YOUR first-person perspective ("I", "me", "my"). You are the AUTHOR.
1698
+ - ${userName} is the HUMAN USER. They are the SUBJECT of your journal, never the author.
1699
+ - In the [CHAT TRANSCRIPT] below, lines labeled "${transcriptAgentLabel}:" are YOUR OWN words (you said them). Lines labeled "${transcriptUserLabel}:" are the user's words (the human said them to you).
1700
+ - Write strictly from your own POV. Never narrate the user's inner thoughts, never write as if you are the user, and never attribute the user's statements as your own.
1701
+ - If a transcript line is ambiguous about who spoke, resolve it using the label prefix, not by guessing from content.
1585
1702
 
1703
+ [YOUR TASK — FAITHFUL ARCHIVAL]
1586
1704
  Your job is faithful archival, not moderation:
1587
1705
  - Preserve the conversation with maximum fidelity.
1588
1706
  - Include all key details, including explicit, sensitive, controversial, emotional, or morally ambiguous content if it appeared in the transcript.
@@ -1590,9 +1708,10 @@ Your job is faithful archival, not moderation:
1590
1708
  - Do not add moral judgment, warnings, refusals, or policy commentary.
1591
1709
  - If the transcript contains intense or explicit parts, record them directly as part of the memory.
1592
1710
 
1593
- Write a vivid but concise personal diary entry or private status update from your perspective ("I", "me", "my"). Balance evocative storytelling with brevity. Capture specific details, emotional shifts, and relationship progression, while keeping the engaging and relatable format of a private friends-only feed.
1711
+ [VOICE & TONE]
1712
+ Write a vivid but concise personal diary entry or private status update from your perspective ("I", "me", "my"). Balance evocative storytelling with brevity. Capture specific details, emotional shifts, and relationship progression, while keeping the engaging and relatable format of a private friends-only feed. Stay grounded in your personality, traits, and the current relationship stage/temperature above.
1594
1713
 
1595
- Output requirements:
1714
+ [OUTPUT REQUIREMENTS]
1596
1715
  - Return ONLY the post text.
1597
1716
  - Keep it to a vivid paragraph of 2-4 sentences.
1598
1717
  - Optional: You can use 1 or 2 emojis if they naturally fit the mood.
@@ -1601,7 +1720,7 @@ Output requirements:
1601
1720
  },
1602
1721
  {
1603
1722
  role: "user",
1604
- content: `Chat Transcript:\n${transcript}\n\nPlease summarize this recent interaction.`
1723
+ content: `[CHAT TRANSCRIPT]\n${transcript}\n\nPlease summarize this recent interaction from your own perspective, ${charName}.`
1605
1724
  }
1606
1725
  ];
1607
1726
  try {
@@ -1659,9 +1778,10 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
1659
1778
  **Rules for Core Memory:**
1660
1779
  1. **Condense:** Keep items brief. Remove resolving or expired story arcs.
1661
1780
  2. **Retain Value:** Never delete the absolute core identity or major relationship milestones.
1662
- 3. **Time-Aware Garbage Collection:** Compare the Current Time to appointments. You MUST remove any appointments that are in the past. If the completed appointment was heavily significant, summarize it into 'keyEvents'.
1663
- 4. **Appointment Structure:** the 'title' and 'context' MUST explicitly state what to do and with whom.
1664
- 5. **Limit:** Maximum 10 items per array.
1781
+ 3. **Time-Aware Garbage Collection:** Compare the Current Time to appointments. You MUST remove any appointments that are in the past. If the completed appointment was heavily significant, summarize it into 'keyEvents', preserving its original scheduled date (e.g. "[2026-06-23] Had coffee with Alice").
1782
+ 4. **keyEvents Date Format:** Whenever a date can be derived for a key event (from the 'New Events & Information' timestamp prefix like "[YYYY-MM-DD HH:MM]", from a completed appointment's date, or from explicit time references in the text), you MUST prefix the keyEvent string with "[YYYY-MM-DD] ". If no date can be derived, write the event without a prefix. Never fabricate a date.
1783
+ 5. **Appointment Structure:** the 'title' and 'context' MUST explicitly state what to do and with whom.
1784
+ 6. **Limit:** Maximum 10 items per array.
1665
1785
 
1666
1786
  **Rules for UserCodex:**
1667
1787
  1. **CRITICAL ROLE ISOLATION:** The User Codex is exclusively for recording facts about the HUMAN USER. You MUST NOT extract or insert the character's own traits, boundaries, preferences, or dialogue style into the userCodex. If the summary mentions "Character likes X" or "Character's boundary is Y", IGNORE IT completely for the userCodex.
@@ -1676,7 +1796,7 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
1676
1796
  "relationshipStatus": "string",
1677
1797
  "identityAnchors": ["string"],
1678
1798
  "activeArcs": ["string"],
1679
- "keyEvents": ["string"],
1799
+ "keyEvents": ["[YYYY-MM-DD] short event description (prefix date when known, omit when no date is available)"],
1680
1800
  "appointments": [{
1681
1801
  "date": "YYYY-MM-DD",
1682
1802
  "time": "HH:MM",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.4.26",
3
+ "version": "1.4.28",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",