stella-timeline-plugin 2.0.0
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/LICENSE +16 -0
- package/README.md +103 -0
- package/README_ZH.md +103 -0
- package/bin/openclaw-timeline-doctor.mjs +2 -0
- package/bin/openclaw-timeline-setup.mjs +2 -0
- package/dist/index.js +26 -0
- package/dist/src/core/build_consumption_view.js +52 -0
- package/dist/src/core/calendar_dates.js +23 -0
- package/dist/src/core/collect_sources.js +39 -0
- package/dist/src/core/collect_timeline_request.js +76 -0
- package/dist/src/core/materialize_generated_candidate.js +87 -0
- package/dist/src/core/resolve_window.js +83 -0
- package/dist/src/core/runtime_guard.js +170 -0
- package/dist/src/core/timeline_reasoner_contract.js +2 -0
- package/dist/src/core/trace.js +22 -0
- package/dist/src/core/world_rhythm.js +258 -0
- package/dist/src/lib/fingerprint.js +46 -0
- package/dist/src/lib/holidays.js +95 -0
- package/dist/src/lib/inherit-appearance.js +46 -0
- package/dist/src/lib/parse-memory.js +171 -0
- package/dist/src/lib/time-utils.js +49 -0
- package/dist/src/lib/timeline_semantics.js +63 -0
- package/dist/src/lib/types.js +2 -0
- package/dist/src/openclaw-sdk-compat.js +39 -0
- package/dist/src/plugin_metadata.js +9 -0
- package/dist/src/runtime/conversation_context.js +128 -0
- package/dist/src/runtime/openclaw_timeline_runtime.js +655 -0
- package/dist/src/storage/daily_log.js +60 -0
- package/dist/src/storage/lock.js +74 -0
- package/dist/src/storage/trace_log.js +70 -0
- package/dist/src/storage/write-episode.js +164 -0
- package/dist/src/tools/timeline_resolve.js +689 -0
- package/openclaw.plugin.json +54 -0
- package/package.json +73 -0
- package/scripts/doctor-openclaw-workspace.mjs +94 -0
- package/scripts/migrate-existing-memory.mjs +153 -0
- package/scripts/release.mjs +99 -0
- package/scripts/run-openclaw-live-e2e.mjs +64 -0
- package/scripts/run-openclaw-smoke.mjs +21 -0
- package/scripts/setup-openclaw-workspace.mjs +119 -0
- package/scripts/workspace-contract.mjs +47 -0
- package/skills/timeline/SKILL.md +111 -0
- package/templates/AGENTS.fragment.md +29 -0
- package/templates/SOUL.fragment.md +17 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseMemoryFile = parseMemoryFile;
|
|
4
|
+
exports.mapTimeOfDay = mapTimeOfDay;
|
|
5
|
+
exports.mapToEpisode = mapToEpisode;
|
|
6
|
+
const time_utils_1 = require("./time-utils");
|
|
7
|
+
function parseMemoryFile(content) {
|
|
8
|
+
const episodes = [];
|
|
9
|
+
if (!content || !content.trim()) {
|
|
10
|
+
return episodes;
|
|
11
|
+
}
|
|
12
|
+
// Split into paragraphs by '### [' to handle sections
|
|
13
|
+
const parts = content.split(/^### \[/m);
|
|
14
|
+
for (const part of parts) {
|
|
15
|
+
if (!part.trim())
|
|
16
|
+
continue;
|
|
17
|
+
// We already split by '### [', so let's put it back to parse cleanly if needed,
|
|
18
|
+
// actually we just extract fields directly.
|
|
19
|
+
// Extract timestamp
|
|
20
|
+
const timestampMatch = part.match(/[-*]\s*Timestamp:\s*([^\n]+)/i);
|
|
21
|
+
if (!timestampMatch) {
|
|
22
|
+
continue; // Skip if no timestamp
|
|
23
|
+
}
|
|
24
|
+
const timestamp = timestampMatch[1].trim();
|
|
25
|
+
// Extract other fields with permissive matching
|
|
26
|
+
const locationMatch = part.match(/[-*]\s*Location:\s*([^\n]+)/i);
|
|
27
|
+
const actionMatch = part.match(/[-*]\s*Action:\s*([^\n]+)/i);
|
|
28
|
+
const emotionTagsMatch = part.match(/[-*]\s*Emotion_Tags:\s*\[([^\]]+)\]/i)
|
|
29
|
+
|| part.match(/[-*]\s*Emotion_Tags:\s*([^\n]+)/i);
|
|
30
|
+
const appearanceMatch = part.match(/[-*]\s*Appearance:\s*([^\n]+)/i);
|
|
31
|
+
const monologueMatch = part.match(/[-*]\s*Internal_Monologue:\s*([^\n]+)/i);
|
|
32
|
+
// Extract natural text: everything after the last key-value field
|
|
33
|
+
const lastKeyValIndex = part.lastIndexOf('-');
|
|
34
|
+
let naturalText = undefined;
|
|
35
|
+
if (lastKeyValIndex !== -1) {
|
|
36
|
+
const remainingBytes = part.substring(lastKeyValIndex);
|
|
37
|
+
const afterFieldMatch = remainingBytes.match(/\n\s*([^- \n][\s\S]*)/);
|
|
38
|
+
if (afterFieldMatch && afterFieldMatch[1].trim()) {
|
|
39
|
+
naturalText = afterFieldMatch[1].trim();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const location = locationMatch ? locationMatch[1].trim() : "unknown";
|
|
43
|
+
const action = actionMatch ? actionMatch[1].trim() : "unknown";
|
|
44
|
+
let emotionTags = ['neutral'];
|
|
45
|
+
if (emotionTagsMatch) {
|
|
46
|
+
emotionTags = emotionTagsMatch[1].split(',').map(tag => tag.replace(/[\[\]]/g, '').trim()).filter(Boolean);
|
|
47
|
+
if (emotionTags.length === 0)
|
|
48
|
+
emotionTags = ['neutral'];
|
|
49
|
+
}
|
|
50
|
+
const appearance = appearanceMatch ? appearanceMatch[1].trim() : "unknown";
|
|
51
|
+
const internalMonologue = monologueMatch ? monologueMatch[1].trim() : undefined;
|
|
52
|
+
// Check Level A vs Level B
|
|
53
|
+
let parseLevel = 'A';
|
|
54
|
+
let confidence = 1.0;
|
|
55
|
+
if (!appearanceMatch || appearance === 'unknown') {
|
|
56
|
+
parseLevel = 'B';
|
|
57
|
+
confidence = 0.6;
|
|
58
|
+
}
|
|
59
|
+
if (!emotionTagsMatch || emotionTags[0] === 'neutral') {
|
|
60
|
+
parseLevel = 'B';
|
|
61
|
+
confidence = Math.min(confidence, 0.5);
|
|
62
|
+
}
|
|
63
|
+
episodes.push({
|
|
64
|
+
timestamp,
|
|
65
|
+
location,
|
|
66
|
+
action,
|
|
67
|
+
emotionTags,
|
|
68
|
+
appearance,
|
|
69
|
+
internalMonologue,
|
|
70
|
+
naturalText,
|
|
71
|
+
parseLevel,
|
|
72
|
+
confidence
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return episodes;
|
|
76
|
+
}
|
|
77
|
+
function mapTimeOfDay(timestamp) {
|
|
78
|
+
const parts = (0, time_utils_1.parseTimestampParts)(timestamp);
|
|
79
|
+
if (parts) {
|
|
80
|
+
const hour = parts.hour;
|
|
81
|
+
if (hour >= 0 && hour <= 5)
|
|
82
|
+
return 'night';
|
|
83
|
+
if (hour >= 6 && hour <= 11)
|
|
84
|
+
return 'morning';
|
|
85
|
+
if (hour >= 12 && hour <= 17)
|
|
86
|
+
return 'afternoon';
|
|
87
|
+
return 'evening';
|
|
88
|
+
}
|
|
89
|
+
const dateObj = new Date(timestamp);
|
|
90
|
+
if (isNaN(dateObj.getTime()))
|
|
91
|
+
return 'unknown';
|
|
92
|
+
const hour = dateObj.getHours();
|
|
93
|
+
if (hour >= 0 && hour <= 5)
|
|
94
|
+
return 'night';
|
|
95
|
+
if (hour >= 6 && hour <= 11)
|
|
96
|
+
return 'morning';
|
|
97
|
+
if (hour >= 12 && hour <= 17)
|
|
98
|
+
return 'afternoon';
|
|
99
|
+
return 'evening';
|
|
100
|
+
}
|
|
101
|
+
function buildNarrativeSummary(parsed) {
|
|
102
|
+
const action = parsed.action && parsed.action !== 'unknown' ? parsed.action : '记录一个时间片段';
|
|
103
|
+
const location = parsed.location && parsed.location !== 'unknown' ? `在${parsed.location}` : '';
|
|
104
|
+
return `${location}${action}`;
|
|
105
|
+
}
|
|
106
|
+
function mapToEpisode(parsed, worldHooks, idempotencyKey, writer = 'stella-timeline-plugin') {
|
|
107
|
+
const timeOfDay = mapTimeOfDay(parsed.timestamp);
|
|
108
|
+
// End time defaults to start + 1 hour as per spec (v1 default)
|
|
109
|
+
let endStr = parsed.timestamp;
|
|
110
|
+
const parts = (0, time_utils_1.parseTimestampParts)(parsed.timestamp);
|
|
111
|
+
if (parts) {
|
|
112
|
+
endStr = (0, time_utils_1.formatTimestamp)((0, time_utils_1.addHours)(parts, 1));
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const startDate = new Date(parsed.timestamp);
|
|
116
|
+
if (!isNaN(startDate.getTime())) {
|
|
117
|
+
startDate.setHours(startDate.getHours() + 1);
|
|
118
|
+
const offsetMatcher = parsed.timestamp.match(/(Z|[+-]\d{2}:\d{2})$/);
|
|
119
|
+
const tzString = offsetMatcher ? offsetMatcher[1] : '';
|
|
120
|
+
const yyyy = startDate.getFullYear();
|
|
121
|
+
const mm = String(startDate.getMonth() + 1).padStart(2, '0');
|
|
122
|
+
const dd = String(startDate.getDate()).padStart(2, '0');
|
|
123
|
+
const hh = String(startDate.getHours()).padStart(2, '0');
|
|
124
|
+
const mins = String(startDate.getMinutes()).padStart(2, '0');
|
|
125
|
+
const secs = String(startDate.getSeconds()).padStart(2, '0');
|
|
126
|
+
endStr = `${yyyy}-${mm}-${dd}T${hh}:${mins}:${secs}${tzString}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
// Generate a simple v4-like uuid or mock one since it's v1.
|
|
131
|
+
// In a real env, crypto.randomUUID() could be used.
|
|
132
|
+
episode_id: `sys-${Date.now()}-${Math.floor(Math.random() * 10000)}`,
|
|
133
|
+
schema_version: "1.0",
|
|
134
|
+
document_type: "timeline.episode",
|
|
135
|
+
temporal: {
|
|
136
|
+
start: parsed.timestamp,
|
|
137
|
+
end: endStr,
|
|
138
|
+
time_of_day: timeOfDay,
|
|
139
|
+
granularity: "block"
|
|
140
|
+
},
|
|
141
|
+
narrative: {
|
|
142
|
+
summary: buildNarrativeSummary(parsed),
|
|
143
|
+
detail: parsed.internalMonologue
|
|
144
|
+
},
|
|
145
|
+
state_snapshot: {
|
|
146
|
+
scene: {
|
|
147
|
+
location_kind: 'literal',
|
|
148
|
+
location_label: parsed.location,
|
|
149
|
+
activity: parsed.action,
|
|
150
|
+
time_of_day: timeOfDay
|
|
151
|
+
},
|
|
152
|
+
emotion: {
|
|
153
|
+
primary: parsed.emotionTags[0] || "neutral",
|
|
154
|
+
secondary: parsed.emotionTags.length > 1 ? parsed.emotionTags[1] : null,
|
|
155
|
+
intensity: 0.0 // Placeholder for v1
|
|
156
|
+
},
|
|
157
|
+
appearance: {
|
|
158
|
+
outfit_style: parsed.appearance,
|
|
159
|
+
grooming: null,
|
|
160
|
+
posture_energy: null
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
world_hooks: worldHooks,
|
|
164
|
+
provenance: {
|
|
165
|
+
writer,
|
|
166
|
+
written_at: new Date().toISOString(),
|
|
167
|
+
idempotency_key: idempotencyKey,
|
|
168
|
+
confidence: parsed.confidence
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseTimestampParts = parseTimestampParts;
|
|
4
|
+
exports.formatDate = formatDate;
|
|
5
|
+
exports.formatTime = formatTime;
|
|
6
|
+
exports.formatTimestamp = formatTimestamp;
|
|
7
|
+
exports.addHours = addHours;
|
|
8
|
+
exports.dayOfWeek = dayOfWeek;
|
|
9
|
+
const TIMESTAMP_RE = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(Z|[+-]\d{2}:\d{2})?$/;
|
|
10
|
+
function parseTimestampParts(timestamp) {
|
|
11
|
+
const match = timestamp.trim().match(TIMESTAMP_RE);
|
|
12
|
+
if (!match)
|
|
13
|
+
return null;
|
|
14
|
+
return {
|
|
15
|
+
year: Number(match[1]),
|
|
16
|
+
month: Number(match[2]),
|
|
17
|
+
day: Number(match[3]),
|
|
18
|
+
hour: Number(match[4]),
|
|
19
|
+
minute: Number(match[5]),
|
|
20
|
+
second: Number(match[6]),
|
|
21
|
+
offset: match[7],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function formatDate(parts) {
|
|
25
|
+
return `${parts.year}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')}`;
|
|
26
|
+
}
|
|
27
|
+
function formatTime(parts) {
|
|
28
|
+
return `${String(parts.hour).padStart(2, '0')}:${String(parts.minute).padStart(2, '0')}:${String(parts.second).padStart(2, '0')}`;
|
|
29
|
+
}
|
|
30
|
+
function formatTimestamp(parts, includeOffset = true) {
|
|
31
|
+
const base = `${formatDate(parts)}T${formatTime(parts)}`;
|
|
32
|
+
return includeOffset && parts.offset ? `${base}${parts.offset}` : base;
|
|
33
|
+
}
|
|
34
|
+
function addHours(parts, hoursToAdd) {
|
|
35
|
+
const date = new Date(Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second));
|
|
36
|
+
date.setUTCHours(date.getUTCHours() + hoursToAdd);
|
|
37
|
+
return {
|
|
38
|
+
year: date.getUTCFullYear(),
|
|
39
|
+
month: date.getUTCMonth() + 1,
|
|
40
|
+
day: date.getUTCDate(),
|
|
41
|
+
hour: date.getUTCHours(),
|
|
42
|
+
minute: date.getUTCMinutes(),
|
|
43
|
+
second: date.getUTCSeconds(),
|
|
44
|
+
offset: parts.offset,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function dayOfWeek(parts) {
|
|
48
|
+
return new Date(Date.UTC(parts.year, parts.month - 1, parts.day)).getUTCDay();
|
|
49
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OUTFIT_MODES = exports.APPEARANCE_TRANSITIONS = exports.CONTINUITY_RELATIONS = exports.ACTIVITY_MODES = void 0;
|
|
4
|
+
exports.isActivityMode = isActivityMode;
|
|
5
|
+
exports.isContinuityRelation = isContinuityRelation;
|
|
6
|
+
exports.isAppearanceTransition = isAppearanceTransition;
|
|
7
|
+
exports.isOutfitMode = isOutfitMode;
|
|
8
|
+
exports.ACTIVITY_MODES = [
|
|
9
|
+
'sleep',
|
|
10
|
+
'bath',
|
|
11
|
+
'meal',
|
|
12
|
+
'work_or_study',
|
|
13
|
+
'commute',
|
|
14
|
+
'exercise',
|
|
15
|
+
'social',
|
|
16
|
+
'shopping',
|
|
17
|
+
'leisure',
|
|
18
|
+
'domestic',
|
|
19
|
+
'errands',
|
|
20
|
+
'transition',
|
|
21
|
+
'rest',
|
|
22
|
+
'unknown',
|
|
23
|
+
];
|
|
24
|
+
exports.CONTINUITY_RELATIONS = [
|
|
25
|
+
'same_day_continuation',
|
|
26
|
+
'same_scene_continuation',
|
|
27
|
+
'shifted_scene',
|
|
28
|
+
'return_home',
|
|
29
|
+
'fresh_moment',
|
|
30
|
+
'unknown',
|
|
31
|
+
];
|
|
32
|
+
exports.APPEARANCE_TRANSITIONS = [
|
|
33
|
+
'inherit',
|
|
34
|
+
'change_required',
|
|
35
|
+
'change_allowed',
|
|
36
|
+
'unknown',
|
|
37
|
+
];
|
|
38
|
+
exports.OUTFIT_MODES = [
|
|
39
|
+
'casual_home',
|
|
40
|
+
'casual_outing',
|
|
41
|
+
'workwear',
|
|
42
|
+
'sportswear',
|
|
43
|
+
'sleepwear',
|
|
44
|
+
'bathrobe',
|
|
45
|
+
'dressed_up',
|
|
46
|
+
'fresh_purchase',
|
|
47
|
+
'unknown',
|
|
48
|
+
];
|
|
49
|
+
function includesValue(values, value) {
|
|
50
|
+
return values.includes(value);
|
|
51
|
+
}
|
|
52
|
+
function isActivityMode(value) {
|
|
53
|
+
return typeof value === 'string' && includesValue(exports.ACTIVITY_MODES, value);
|
|
54
|
+
}
|
|
55
|
+
function isContinuityRelation(value) {
|
|
56
|
+
return typeof value === 'string' && includesValue(exports.CONTINUITY_RELATIONS, value);
|
|
57
|
+
}
|
|
58
|
+
function isAppearanceTransition(value) {
|
|
59
|
+
return typeof value === 'string' && includesValue(exports.APPEARANCE_TRANSITIONS, value);
|
|
60
|
+
}
|
|
61
|
+
function isOutfitMode(value) {
|
|
62
|
+
return typeof value === 'string' && includesValue(exports.OUTFIT_MODES, value);
|
|
63
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.definePluginEntry = definePluginEntry;
|
|
4
|
+
exports.materializePlugin = materializePlugin;
|
|
5
|
+
exports.makeTimelineToolRegistration = makeTimelineToolRegistration;
|
|
6
|
+
const timeline_resolve_1 = require("./tools/timeline_resolve");
|
|
7
|
+
function definePluginEntry(definition) {
|
|
8
|
+
return definition;
|
|
9
|
+
}
|
|
10
|
+
function materializePlugin(definition) {
|
|
11
|
+
const tools = [];
|
|
12
|
+
definition.register({
|
|
13
|
+
registerTool(tool, options) {
|
|
14
|
+
tools.push({ ...tool, optional: options?.optional });
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
id: definition.id,
|
|
19
|
+
name: definition.name,
|
|
20
|
+
description: definition.description,
|
|
21
|
+
tools,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function wrapToolData(data) {
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
27
|
+
data,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function makeTimelineToolRegistration() {
|
|
31
|
+
return {
|
|
32
|
+
name: timeline_resolve_1.timelineResolveToolSpec.name,
|
|
33
|
+
description: timeline_resolve_1.timelineResolveToolSpec.description,
|
|
34
|
+
parameters: timeline_resolve_1.timelineResolveToolSpec.inputSchema,
|
|
35
|
+
async execute(_callId, params) {
|
|
36
|
+
return wrapToolData(await timeline_resolve_1.timelineResolveToolSpec.run(params));
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TIMELINE_SKILL_PATHS = exports.TIMELINE_TOOL_NAMES = exports.TIMELINE_PLUGIN_DESCRIPTION = exports.TIMELINE_PLUGIN_VERSION = exports.TIMELINE_PLUGIN_NAME = exports.TIMELINE_PLUGIN_ID = void 0;
|
|
4
|
+
exports.TIMELINE_PLUGIN_ID = 'stella-timeline-plugin';
|
|
5
|
+
exports.TIMELINE_PLUGIN_NAME = 'Stella Timeline Plugin';
|
|
6
|
+
exports.TIMELINE_PLUGIN_VERSION = '2.0.0';
|
|
7
|
+
exports.TIMELINE_PLUGIN_DESCRIPTION = 'OpenClaw timeline runtime with canonical timeline_resolve, LLM-based temporal reasoning, and guarded append-only writes.';
|
|
8
|
+
exports.TIMELINE_TOOL_NAMES = ['timeline_resolve'];
|
|
9
|
+
exports.TIMELINE_SKILL_PATHS = ['skills/timeline'];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildConversationContextFromMessages = buildConversationContextFromMessages;
|
|
4
|
+
const time_utils_1 = require("../lib/time-utils");
|
|
5
|
+
function extractMessageText(value) {
|
|
6
|
+
if (typeof value === 'string')
|
|
7
|
+
return value.trim();
|
|
8
|
+
if (!value || typeof value !== 'object')
|
|
9
|
+
return '';
|
|
10
|
+
const record = value;
|
|
11
|
+
const directKeys = ['bodyText', 'body', 'text', 'contentText'];
|
|
12
|
+
for (const key of directKeys) {
|
|
13
|
+
if (typeof record[key] === 'string' && String(record[key]).trim()) {
|
|
14
|
+
return String(record[key]).trim();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(record.content)) {
|
|
18
|
+
const joined = record.content
|
|
19
|
+
.map((part) => extractMessageText(part))
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.join('\n');
|
|
22
|
+
if (joined)
|
|
23
|
+
return joined;
|
|
24
|
+
}
|
|
25
|
+
if (record.content && typeof record.content === 'object') {
|
|
26
|
+
const nestedContent = extractMessageText(record.content);
|
|
27
|
+
if (nestedContent)
|
|
28
|
+
return nestedContent;
|
|
29
|
+
}
|
|
30
|
+
if (record.message && typeof record.message === 'object') {
|
|
31
|
+
const nestedMessage = extractMessageText(record.message);
|
|
32
|
+
if (nestedMessage)
|
|
33
|
+
return nestedMessage;
|
|
34
|
+
}
|
|
35
|
+
if (typeof record.type === 'string' && record.type === 'text' && typeof record.text === 'string') {
|
|
36
|
+
return record.text.trim();
|
|
37
|
+
}
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
function extractRole(value) {
|
|
41
|
+
if (!value || typeof value !== 'object')
|
|
42
|
+
return 'unknown';
|
|
43
|
+
const directRole = value.role;
|
|
44
|
+
if (typeof directRole === 'string' && directRole.trim()) {
|
|
45
|
+
return directRole.trim();
|
|
46
|
+
}
|
|
47
|
+
const nestedMessage = value.message;
|
|
48
|
+
if (nestedMessage && typeof nestedMessage === 'object') {
|
|
49
|
+
const nestedRole = nestedMessage.role;
|
|
50
|
+
if (typeof nestedRole === 'string' && nestedRole.trim()) {
|
|
51
|
+
return nestedRole.trim();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return 'unknown';
|
|
55
|
+
}
|
|
56
|
+
function extractTimestamp(value) {
|
|
57
|
+
if (!value || typeof value !== 'object')
|
|
58
|
+
return null;
|
|
59
|
+
const tryKeys = (record) => {
|
|
60
|
+
const keys = ['createdAt', 'created_at', 'timestamp', 'ts', 'sentAt', 'sent_at'];
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
if (typeof record[key] === 'string' && String(record[key]).trim()) {
|
|
63
|
+
const candidate = String(record[key]).trim();
|
|
64
|
+
if (!Number.isNaN(Date.parse(candidate)) || (0, time_utils_1.parseTimestampParts)(candidate)) {
|
|
65
|
+
return candidate;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
71
|
+
const record = value;
|
|
72
|
+
const direct = tryKeys(record);
|
|
73
|
+
if (direct)
|
|
74
|
+
return direct;
|
|
75
|
+
const nestedMessage = record.message;
|
|
76
|
+
if (nestedMessage && typeof nestedMessage === 'object') {
|
|
77
|
+
return tryKeys(nestedMessage);
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function minutesBetween(nowIso, thenIso) {
|
|
82
|
+
const nowMs = Date.parse(nowIso);
|
|
83
|
+
const thenMs = Date.parse(thenIso);
|
|
84
|
+
if (Number.isNaN(nowMs) || Number.isNaN(thenMs))
|
|
85
|
+
return null;
|
|
86
|
+
return Math.max(0, Math.round((nowMs - thenMs) / 60000));
|
|
87
|
+
}
|
|
88
|
+
function normalizeLine(text) {
|
|
89
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
90
|
+
}
|
|
91
|
+
function summarizeTopic(messages, input) {
|
|
92
|
+
const currentQuery = normalizeLine(String(input.query || ''));
|
|
93
|
+
const candidates = [...messages]
|
|
94
|
+
.reverse()
|
|
95
|
+
.map((message) => ({
|
|
96
|
+
role: extractRole(message),
|
|
97
|
+
text: normalizeLine(extractMessageText(message)),
|
|
98
|
+
}))
|
|
99
|
+
.filter((entry) => entry.text)
|
|
100
|
+
.filter((entry) => entry.text !== currentQuery);
|
|
101
|
+
const userMessage = candidates.find((entry) => entry.role === 'user')?.text;
|
|
102
|
+
const assistantMessage = candidates.find((entry) => entry.role === 'assistant')?.text;
|
|
103
|
+
if (userMessage && assistantMessage) {
|
|
104
|
+
return `${userMessage} / ${assistantMessage}`.slice(0, 240);
|
|
105
|
+
}
|
|
106
|
+
if (userMessage)
|
|
107
|
+
return userMessage.slice(0, 180);
|
|
108
|
+
if (assistantMessage)
|
|
109
|
+
return assistantMessage.slice(0, 180);
|
|
110
|
+
return currentQuery.slice(0, 180);
|
|
111
|
+
}
|
|
112
|
+
function buildConversationContextFromMessages(messages, windowEnd, input, stickinessWindowMinutes, requestedRange) {
|
|
113
|
+
const lastTimestamp = [...messages]
|
|
114
|
+
.reverse()
|
|
115
|
+
.map((message) => extractTimestamp(message))
|
|
116
|
+
.find(Boolean) || undefined;
|
|
117
|
+
const minutesSinceLastTurn = lastTimestamp ? minutesBetween(windowEnd, lastTimestamp) : null;
|
|
118
|
+
const isRecentlyActive = minutesSinceLastTurn !== null && minutesSinceLastTurn <= stickinessWindowMinutes;
|
|
119
|
+
const activeTopicSummary = summarizeTopic(messages, input);
|
|
120
|
+
return {
|
|
121
|
+
is_recently_active: isRecentlyActive,
|
|
122
|
+
minutes_since_last_turn: minutesSinceLastTurn,
|
|
123
|
+
stickiness_window_minutes: stickinessWindowMinutes,
|
|
124
|
+
active_topic_summary: activeTopicSummary,
|
|
125
|
+
should_prefer_conversation_continuity_for_now: requestedRange === 'now' && isRecentlyActive,
|
|
126
|
+
last_active_timestamp: lastTimestamp,
|
|
127
|
+
};
|
|
128
|
+
}
|