@space3-npm/cybersoul-client 1.4.20 → 1.4.22

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
@@ -29,6 +29,11 @@ export declare class CyberSoulClient {
29
29
  private _updateDynamicContextInternal;
30
30
  private normalizeRequestTypes;
31
31
  private getElapsedTimeInfo;
32
+ /**
33
+ * Calculate time period from a timestamp.
34
+ * Pre-computes morning/afternoon/evening to reduce LLM cognitive load.
35
+ */
36
+ private getTimePeriodInfo;
32
37
  private buildStateContextPrompt;
33
38
  private normalizeOngoingSceneState;
34
39
  private getImageSchemaParams;
package/dist/client.js CHANGED
@@ -258,6 +258,41 @@ export class CyberSoulClient {
258
258
  displayStr = `${Math.floor(elapsedMins)} mins`;
259
259
  return { elapsedMs, elapsedMins, elapsedHours, elapsedDays, elapsedYears, displayStr };
260
260
  }
261
+ /**
262
+ * Calculate time period from a timestamp.
263
+ * Pre-computes morning/afternoon/evening to reduce LLM cognitive load.
264
+ */
265
+ getTimePeriodInfo(timeMs) {
266
+ const date = new Date(timeMs);
267
+ const hour = parseInt(date.toLocaleString("zh-CN", {
268
+ timeZone: "Asia/Shanghai",
269
+ hour: "2-digit",
270
+ hour12: false,
271
+ }).split(":")[0]);
272
+ let period;
273
+ if (hour >= 6 && hour < 9) {
274
+ period = "Early Morning";
275
+ }
276
+ else if (hour >= 9 && hour < 12) {
277
+ period = "Late Morning";
278
+ }
279
+ else if (hour >= 12 && hour < 13) {
280
+ period = "Noon";
281
+ }
282
+ else if (hour >= 13 && hour < 18) {
283
+ period = "Afternoon";
284
+ }
285
+ else if (hour >= 18 && hour < 19) {
286
+ period = "Evening";
287
+ }
288
+ else if (hour >= 19 && hour < 23) {
289
+ period = "Night";
290
+ }
291
+ else {
292
+ period = "Late Night";
293
+ }
294
+ return { hour, period };
295
+ }
261
296
  buildStateContextPrompt(state, isProactive = false) {
262
297
  const dyn = state.dynamic_context || {};
263
298
  const stage = state.relationship_stage || "NEUTRAL";
@@ -275,8 +310,10 @@ Communication Style: ${state.communication_style || "None"}
275
310
  Interaction Boundaries: ${state.interaction_boundaries || "None"}`);
276
311
  // [2] SITUATIONAL CONTEXT
277
312
  const currentTimeMs = state.current_time ? new Date(state.current_time).getTime() : Date.now();
313
+ const timePeriod = this.getTimePeriodInfo(currentTimeMs);
314
+ const timeStr = new Date(currentTimeMs).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
278
315
  contextParts.push(`\n[SITUATIONAL CONTEXT]
279
- Current time: ${new Date(currentTimeMs).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`);
316
+ Current time: ${timeStr} (${timePeriod.period})`);
280
317
  if (dyn.lastInteractionAt) {
281
318
  contextParts.push(`Last interaction at: ${new Date(dyn.lastInteractionAt).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`);
282
319
  }
@@ -1061,23 +1098,40 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
1061
1098
  */
1062
1099
  async proactiveInteract(params) {
1063
1100
  try {
1064
- // 1. Spam guard (the only hard-coded gate). Counts assistant messages
1065
- // since the last user reply; bails out if the user has clearly
1066
- // stopped responding.
1101
+ // 1. Spam guard (the only hard-coded gate). Counts assistant
1102
+ // *turns* since the last user reply; bails out if the user has
1103
+ // clearly stopped responding.
1104
+ //
1105
+ // A single character response can be emitted as multiple
1106
+ // HistoryEntry rows (one per modality: text + image + voice).
1107
+ // The host typically writes them within seconds of each other.
1108
+ // Counting entries directly would treat one multimodal reply
1109
+ // as 2-3 "unreplied messages" and trip `maxUnreplied = 2` on
1110
+ // the very first proactive attempt. Collapse consecutive
1111
+ // assistant entries whose timestamps are within
1112
+ // SAME_TURN_WINDOW_MS into a single turn before counting.
1067
1113
  const history = params.history || [];
1068
1114
  const maxUnreplied = params.maxUnreplied ?? 2;
1115
+ const SAME_TURN_WINDOW_MS = 60_000;
1069
1116
  let consecutiveProactive = 0;
1117
+ let lastAssistantTs = null;
1070
1118
  for (let i = history.length - 1; i >= 0; i--) {
1071
1119
  const msg = history[i];
1072
1120
  if (msg.role === "user")
1073
1121
  break;
1074
- if (msg.role === "assistant")
1122
+ if (msg.role !== "assistant")
1123
+ continue;
1124
+ const ts = typeof msg.timestamp === "number" ? msg.timestamp : 0;
1125
+ if (lastAssistantTs === null ||
1126
+ Math.abs(lastAssistantTs - ts) > SAME_TURN_WINDOW_MS) {
1075
1127
  consecutiveProactive++;
1128
+ }
1129
+ lastAssistantTs = ts;
1076
1130
  }
1077
1131
  if (consecutiveProactive >= maxUnreplied) {
1078
1132
  return {
1079
1133
  status: "skipped",
1080
- reason: `Spam guard: ${consecutiveProactive} consecutive un-replied messages already sent.`,
1134
+ reason: `Spam guard: ${consecutiveProactive} consecutive un-replied turns already sent.`,
1081
1135
  };
1082
1136
  }
1083
1137
  // 2. Fetch state. baseContext below already includes stage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.4.20",
3
+ "version": "1.4.22",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",