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
package/LICENSE
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
MIT No Attribution
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tao.zang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
6
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
|
7
|
+
without restriction, including without limitation the rights to use, copy, modify,
|
|
8
|
+
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
9
|
+
permit persons to whom the Software is furnished to do so.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
12
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
13
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
14
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
15
|
+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
|
16
|
+
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# OpenClaw Timeline Plugin
|
|
2
|
+
|
|
3
|
+
Timeline gives OpenClaw something most agents still lack: a believable sense of time.
|
|
4
|
+
|
|
5
|
+
It is not just a memory helper. It is the layer that lets OpenClaw feel more like a real person in casual conversation by giving it:
|
|
6
|
+
|
|
7
|
+
- a sense of what it is doing now
|
|
8
|
+
- a way to recall what happened recently
|
|
9
|
+
- the ability to form and reuse timeline-shaped memories when needed
|
|
10
|
+
- a more believable real-world rhythm for meals, sleep, workdays, weekends, and holidays
|
|
11
|
+
- steadier same-day continuity in details like clothing, so small life scenes feel less random
|
|
12
|
+
- continuity without breaking your normal long-term memory system
|
|
13
|
+
|
|
14
|
+
The result is simple: chat feels warmer, more coherent, and more human.
|
|
15
|
+
|
|
16
|
+
## Why it matters
|
|
17
|
+
|
|
18
|
+
Human social connection depends on continuity.
|
|
19
|
+
|
|
20
|
+
When someone asks:
|
|
21
|
+
|
|
22
|
+
- "What are you doing right now?"
|
|
23
|
+
- "What did you do last night?"
|
|
24
|
+
- "What have you been up to lately?"
|
|
25
|
+
|
|
26
|
+
they are not asking for abstract facts. They are looking for presence, mood, context, and a sense that the other side has an ongoing life.
|
|
27
|
+
|
|
28
|
+
Timeline helps OpenClaw answer those questions with a stable, immersive personal reality instead of loose prompt improvisation.
|
|
29
|
+
|
|
30
|
+
## What it feels like
|
|
31
|
+
|
|
32
|
+
Without Timeline, answers to light social questions can drift, contradict each other, or feel empty.
|
|
33
|
+
|
|
34
|
+
With Timeline, OpenClaw can respond more like this:
|
|
35
|
+
|
|
36
|
+
> "I was still at home a while ago, sorting through a few work notes and trying to settle into the evening."
|
|
37
|
+
|
|
38
|
+
> "Last night I stayed in, ate a little late, and ended up watching something on the couch for a while."
|
|
39
|
+
|
|
40
|
+
> "These past few days have been pretty full. I've mostly been alternating between staying home, working through things, and going out once in a while to reset."
|
|
41
|
+
|
|
42
|
+
> "It's the weekend, so I moved a little slower this morning. I made something simple, stayed home for a bit, and only went out later in the afternoon."
|
|
43
|
+
|
|
44
|
+
> "I was at home earlier in something comfortable, but I changed before heading to the gym. Now I'm just back and winding down."
|
|
45
|
+
|
|
46
|
+
Those answers feel small and ordinary in exactly the right way. That is the point.
|
|
47
|
+
|
|
48
|
+
## What Timeline actually does
|
|
49
|
+
|
|
50
|
+
Timeline adds a dedicated time-awareness layer for OpenClaw so it can:
|
|
51
|
+
|
|
52
|
+
- answer "now", "recently", and past-time questions more naturally
|
|
53
|
+
- preserve continuity across casual chat
|
|
54
|
+
- keep generated memories aligned with ordinary real-world timing instead of random prompt improvisation
|
|
55
|
+
- keep small details like same-day appearance more stable, unless the scene itself implies a natural change
|
|
56
|
+
- write timeline memories only when appropriate
|
|
57
|
+
- keep normal durable memory and timeline memory from stepping on each other
|
|
58
|
+
|
|
59
|
+
It is built for immersive social chat first.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
### 1. Install the plugin
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
openclaw plugins install stella-timeline-plugin --pin
|
|
67
|
+
openclaw plugins enable stella-timeline-plugin
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The npm package name and the OpenClaw plugin id are both `stella-timeline-plugin`.
|
|
71
|
+
|
|
72
|
+
### 2. Initialize your workspace
|
|
73
|
+
|
|
74
|
+
Recommended:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm exec --package=stella-timeline-plugin openclaw-timeline-setup -- --workspace ~/.openclaw/workspace
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
If you prefer to edit the files yourself, copy:
|
|
81
|
+
|
|
82
|
+
- `templates/AGENTS.fragment.md` into `AGENTS.md`
|
|
83
|
+
- `templates/SOUL.fragment.md` into `SOUL.md`
|
|
84
|
+
|
|
85
|
+
Then make sure your canonical daily-log directory exists. The default is `memory/`.
|
|
86
|
+
|
|
87
|
+
### 3. Try it in chat
|
|
88
|
+
|
|
89
|
+
Ask things like:
|
|
90
|
+
|
|
91
|
+
- "What are you doing right now?"
|
|
92
|
+
- "What did you do last night?"
|
|
93
|
+
- "What have you been up to these past few days?"
|
|
94
|
+
|
|
95
|
+
## Optional check
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm exec --package=stella-timeline-plugin openclaw-timeline-doctor -- --workspace ~/.openclaw/workspace
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## For maintainers
|
|
102
|
+
|
|
103
|
+
Release flow and publishing notes live in [docs/PUBLISHING.md](./docs/PUBLISHING.md).
|
package/README_ZH.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# OpenClaw Timeline 插件
|
|
2
|
+
|
|
3
|
+
Timeline 给 OpenClaw 补上了大多数 agent 仍然缺失的一层能力:真实可信的时间感。
|
|
4
|
+
|
|
5
|
+
它不只是一个记忆辅助工具,而是一层让 OpenClaw 在闲聊中更像真人的能力基础。它让 OpenClaw 拥有:
|
|
6
|
+
|
|
7
|
+
- 对“现在在做什么”的感知
|
|
8
|
+
- 对“最近发生了什么”的回忆能力
|
|
9
|
+
- 在需要时为自己编织并复用时间线记忆的能力
|
|
10
|
+
- 更符合现实生活节奏的作息感,比如吃饭、睡觉、工作日、周末和节假日
|
|
11
|
+
- 更稳定的日内细节连续性,比如穿着不会无缘无故跳变,除非事件本身就意味着换装
|
|
12
|
+
- 在不破坏正常长期记忆的前提下,维持连续的人生感
|
|
13
|
+
|
|
14
|
+
最后带来的变化其实很直接:聊天会更自然、更连贯,也更像在和一个真实活着的人相处。
|
|
15
|
+
|
|
16
|
+
## 为什么它重要
|
|
17
|
+
|
|
18
|
+
人类的社交情绪需求,本质上依赖“连续性”。
|
|
19
|
+
|
|
20
|
+
当人问:
|
|
21
|
+
|
|
22
|
+
- “你现在在做什么?”
|
|
23
|
+
- “你昨晚在干嘛?”
|
|
24
|
+
- “你最近都在忙什么?”
|
|
25
|
+
|
|
26
|
+
他们要的并不只是事实答案,而是一种存在感、情绪感、生活上下文,以及“你真的一直在过自己的生活”的感觉。
|
|
27
|
+
|
|
28
|
+
Timeline 的作用,就是让 OpenClaw 回答这些问题时,不再只是依赖 prompt 临场发挥,而是建立起一层稳定、可沉浸的个人时间现实。
|
|
29
|
+
|
|
30
|
+
## 它带来的感觉
|
|
31
|
+
|
|
32
|
+
没有 Timeline 时,很多轻社交问题的回答会漂、会空、会互相矛盾。
|
|
33
|
+
|
|
34
|
+
有了 Timeline,OpenClaw 更容易说出像这样的话:
|
|
35
|
+
|
|
36
|
+
> “刚刚还在家里,慢慢整理一些工作上的东西,顺便让晚上安静下来一点。”
|
|
37
|
+
|
|
38
|
+
> “昨晚基本没怎么出门,吃完东西之后就在沙发上待了一会儿,随便看了点东西。”
|
|
39
|
+
|
|
40
|
+
> “这几天过得还挺满的,大部分时候都在家里处理事情,偶尔也会出去透透气。”
|
|
41
|
+
|
|
42
|
+
> “今天是周末,所以早上节奏慢一点,简单吃了点东西,在家待了一阵,下午才出门走走。”
|
|
43
|
+
|
|
44
|
+
> “前面在家时还穿得很随意,后来出门去健身房就换了一身轻便的运动装,现在刚回来,整个人慢慢松下来一点。”
|
|
45
|
+
|
|
46
|
+
这些回答看起来很日常,甚至很轻,但正因为这种轻微而自然的生活感,它们才会让人觉得像真人。
|
|
47
|
+
|
|
48
|
+
## Timeline 实际解决什么
|
|
49
|
+
|
|
50
|
+
Timeline 为 OpenClaw 增加了一层专门的时间感知与回忆能力,让它可以:
|
|
51
|
+
|
|
52
|
+
- 更自然地回答“现在”“最近”“过去某个时间”的问题
|
|
53
|
+
- 在闲聊中维持前后一致的人生连续性
|
|
54
|
+
- 让编织出来的记忆更符合普通现实生活的时间逻辑,而不是随机拼出来的片段
|
|
55
|
+
- 让穿着这类小细节也更连贯,只有在运动、洗澡、睡觉、出门等自然场景下才发生合理变化
|
|
56
|
+
- 只在合适的时候写入 timeline 记忆
|
|
57
|
+
- 让长期稳定记忆和时间线记忆各归其位,不互相污染
|
|
58
|
+
|
|
59
|
+
它首先服务的,就是“像真人一样闲聊”这件事。
|
|
60
|
+
|
|
61
|
+
## 安装
|
|
62
|
+
|
|
63
|
+
### 1. 安装插件
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
openclaw plugins install stella-timeline-plugin --pin
|
|
67
|
+
openclaw plugins enable stella-timeline-plugin
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
npm 包名和 OpenClaw 插件 ID 现在统一为 `stella-timeline-plugin`。
|
|
71
|
+
|
|
72
|
+
### 2. 初始化 workspace
|
|
73
|
+
|
|
74
|
+
推荐直接执行:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm exec --package=stella-timeline-plugin openclaw-timeline-setup -- --workspace ~/.openclaw/workspace
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
如果你更喜欢手动编辑文件,也可以直接复制:
|
|
81
|
+
|
|
82
|
+
- `templates/AGENTS.fragment.md` 到 `AGENTS.md`
|
|
83
|
+
- `templates/SOUL.fragment.md` 到 `SOUL.md`
|
|
84
|
+
|
|
85
|
+
然后确认 canonical daily-log 目录已经存在,默认是 `memory/`。
|
|
86
|
+
|
|
87
|
+
### 3. 直接开始聊天
|
|
88
|
+
|
|
89
|
+
你可以马上试试这些问题:
|
|
90
|
+
|
|
91
|
+
- “你现在在做什么?”
|
|
92
|
+
- “你昨晚在干嘛?”
|
|
93
|
+
- “你这几天都在忙什么?”
|
|
94
|
+
|
|
95
|
+
## 可选自检
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm exec --package=stella-timeline-plugin openclaw-timeline-doctor -- --workspace ~/.openclaw/workspace
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 给维护者
|
|
102
|
+
|
|
103
|
+
发布流程和发包说明见 [docs/PUBLISHING.md](./docs/PUBLISHING.md)。
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.timelinePlugin = exports.timelinePluginEntry = void 0;
|
|
4
|
+
const plugin_metadata_1 = require("./src/plugin_metadata");
|
|
5
|
+
const openclaw_sdk_compat_1 = require("./src/openclaw-sdk-compat");
|
|
6
|
+
const openclaw_timeline_runtime_1 = require("./src/runtime/openclaw_timeline_runtime");
|
|
7
|
+
exports.timelinePluginEntry = (0, openclaw_sdk_compat_1.definePluginEntry)({
|
|
8
|
+
id: plugin_metadata_1.TIMELINE_PLUGIN_ID,
|
|
9
|
+
name: plugin_metadata_1.TIMELINE_PLUGIN_NAME,
|
|
10
|
+
description: plugin_metadata_1.TIMELINE_PLUGIN_DESCRIPTION,
|
|
11
|
+
register(api) {
|
|
12
|
+
api.registerTool((0, openclaw_sdk_compat_1.makeTimelineToolRegistration)(), { optional: true });
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
exports.timelinePlugin = (0, openclaw_sdk_compat_1.materializePlugin)(exports.timelinePluginEntry);
|
|
16
|
+
const openClawTimelinePlugin = {
|
|
17
|
+
id: plugin_metadata_1.TIMELINE_PLUGIN_ID,
|
|
18
|
+
name: plugin_metadata_1.TIMELINE_PLUGIN_NAME,
|
|
19
|
+
version: plugin_metadata_1.TIMELINE_PLUGIN_VERSION,
|
|
20
|
+
description: plugin_metadata_1.TIMELINE_PLUGIN_DESCRIPTION,
|
|
21
|
+
register(api) {
|
|
22
|
+
const runtimeApi = api;
|
|
23
|
+
api.registerTool((0, openclaw_timeline_runtime_1.makeOpenClawTimelineResolveToolFactory)(runtimeApi), { optional: true });
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
exports.default = openClawTimelinePlugin;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildConsumptionView = buildConsumptionView;
|
|
4
|
+
function buildConsumptionView(input) {
|
|
5
|
+
const base = {
|
|
6
|
+
schema_version: '1.0',
|
|
7
|
+
document_type: 'timeline.consumption',
|
|
8
|
+
query: {
|
|
9
|
+
preset: input.preset,
|
|
10
|
+
semantic_target: input.semanticTarget,
|
|
11
|
+
collection_scope: input.collectionScope,
|
|
12
|
+
resolution_mode: input.resolutionMode,
|
|
13
|
+
time_interpretation: input.reasoned.time_interpretation,
|
|
14
|
+
},
|
|
15
|
+
fact: {
|
|
16
|
+
status: input.episode ? 'resolved' : 'empty',
|
|
17
|
+
source_type: input.sourceType,
|
|
18
|
+
timestamp: input.episode?.temporal.start,
|
|
19
|
+
summary: input.episode?.narrative.summary,
|
|
20
|
+
confidence: input.episode?.provenance.confidence,
|
|
21
|
+
continuity: {
|
|
22
|
+
judged: input.reasoned.continuity.judged,
|
|
23
|
+
is_continuing: input.reasoned.continuity.is_continuing,
|
|
24
|
+
reason: input.reasoned.continuity.reason,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
if (!input.episode) {
|
|
29
|
+
return base;
|
|
30
|
+
}
|
|
31
|
+
const scene = {
|
|
32
|
+
location: input.episode.state_snapshot.scene.location_label,
|
|
33
|
+
activity: input.episode.state_snapshot.scene.activity,
|
|
34
|
+
emotion_primary: input.episode.state_snapshot.emotion.primary,
|
|
35
|
+
emotion_secondary: input.episode.state_snapshot.emotion.secondary,
|
|
36
|
+
appearance: input.episode.state_snapshot.appearance.outfit_style,
|
|
37
|
+
time_of_day: input.episode.state_snapshot.scene.time_of_day,
|
|
38
|
+
summary: input.episode.narrative.summary,
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
...base,
|
|
42
|
+
scene,
|
|
43
|
+
selfie_ready: {
|
|
44
|
+
location: scene.location,
|
|
45
|
+
activity: scene.activity,
|
|
46
|
+
emotion: scene.emotion_primary,
|
|
47
|
+
appearance: scene.appearance,
|
|
48
|
+
time_of_day: scene.time_of_day,
|
|
49
|
+
summary: scene.summary,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enumerateCalendarDates = enumerateCalendarDates;
|
|
4
|
+
const time_utils_1 = require("../lib/time-utils");
|
|
5
|
+
function enumerateCalendarDates(start, end) {
|
|
6
|
+
const startParts = (0, time_utils_1.parseTimestampParts)(start);
|
|
7
|
+
const endParts = (0, time_utils_1.parseTimestampParts)(end);
|
|
8
|
+
if (!startParts || !endParts) {
|
|
9
|
+
const fallback = start.slice(0, 10);
|
|
10
|
+
return fallback ? [fallback] : [];
|
|
11
|
+
}
|
|
12
|
+
const dates = [];
|
|
13
|
+
const current = new Date(Date.UTC(startParts.year, startParts.month - 1, startParts.day));
|
|
14
|
+
const endFloor = new Date(Date.UTC(endParts.year, endParts.month - 1, endParts.day));
|
|
15
|
+
while (current.getTime() <= endFloor.getTime()) {
|
|
16
|
+
const yyyy = current.getUTCFullYear();
|
|
17
|
+
const mm = String(current.getUTCMonth() + 1).padStart(2, '0');
|
|
18
|
+
const dd = String(current.getUTCDate()).padStart(2, '0');
|
|
19
|
+
dates.push(`${yyyy}-${mm}-${dd}`);
|
|
20
|
+
current.setUTCDate(current.getUTCDate() + 1);
|
|
21
|
+
}
|
|
22
|
+
return dates;
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectSources = collectSources;
|
|
4
|
+
const calendar_dates_1 = require("./calendar_dates");
|
|
5
|
+
async function collectSources(deps, window, input) {
|
|
6
|
+
const sourceOrder = [];
|
|
7
|
+
const calendarDates = (0, calendar_dates_1.enumerateCalendarDates)(window.start, window.end);
|
|
8
|
+
sourceOrder.push('sessions_history');
|
|
9
|
+
const sessionsHistory = await deps.sessionsHistory(window, input);
|
|
10
|
+
sourceOrder.push('memory_get');
|
|
11
|
+
const dailyLogs = await Promise.all(calendarDates.map(async (calendarDate) => ({
|
|
12
|
+
calendar_date: calendarDate,
|
|
13
|
+
raw_content: await deps.memoryGet(calendarDate, window, input),
|
|
14
|
+
})));
|
|
15
|
+
let memorySearch = [];
|
|
16
|
+
if (deps.memorySearch) {
|
|
17
|
+
sourceOrder.push('memory_search');
|
|
18
|
+
memorySearch = await deps.memorySearch(window, input);
|
|
19
|
+
}
|
|
20
|
+
const coreContext = deps.coreFiles
|
|
21
|
+
? await deps.coreFiles()
|
|
22
|
+
: {
|
|
23
|
+
soul: '',
|
|
24
|
+
memory: '',
|
|
25
|
+
identity: '',
|
|
26
|
+
available_sources: [],
|
|
27
|
+
should_constrain_generation: false,
|
|
28
|
+
};
|
|
29
|
+
const conversationContext = deps.conversationContext
|
|
30
|
+
? await deps.conversationContext(window, input)
|
|
31
|
+
: {
|
|
32
|
+
is_recently_active: false,
|
|
33
|
+
minutes_since_last_turn: null,
|
|
34
|
+
stickiness_window_minutes: 10,
|
|
35
|
+
active_topic_summary: '',
|
|
36
|
+
should_prefer_conversation_continuity_for_now: false,
|
|
37
|
+
};
|
|
38
|
+
return { sourceOrder, sessionsHistory, dailyLogs, memorySearch, coreContext, conversationContext };
|
|
39
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildTimelineCollectorOutput = buildTimelineCollectorOutput;
|
|
4
|
+
const parse_memory_1 = require("../lib/parse-memory");
|
|
5
|
+
const calendar_dates_1 = require("./calendar_dates");
|
|
6
|
+
const world_rhythm_1 = require("./world_rhythm");
|
|
7
|
+
function buildTimelineCollectorOutput(requestId, input, window, sources) {
|
|
8
|
+
const dailyLogs = sources.dailyLogs.map((entry) => {
|
|
9
|
+
const parsedEpisodes = (0, parse_memory_1.parseMemoryFile)(entry.raw_content);
|
|
10
|
+
return {
|
|
11
|
+
calendar_date: entry.calendar_date,
|
|
12
|
+
raw_content: entry.raw_content,
|
|
13
|
+
parsed_episodes: parsedEpisodes,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
schema_version: '1.0',
|
|
18
|
+
request_id: requestId,
|
|
19
|
+
request: {
|
|
20
|
+
user_query: input.query,
|
|
21
|
+
mode: input.mode || 'allow_generate',
|
|
22
|
+
},
|
|
23
|
+
anchor: {
|
|
24
|
+
now: window.end,
|
|
25
|
+
timezone: window.timezone,
|
|
26
|
+
},
|
|
27
|
+
window: {
|
|
28
|
+
query_range: window.query_range,
|
|
29
|
+
semantic_target: window.semantic_target,
|
|
30
|
+
collection_scope: window.collection_scope,
|
|
31
|
+
start: window.start,
|
|
32
|
+
end: window.end,
|
|
33
|
+
calendar_dates: window.calendar_dates.length > 0 ? window.calendar_dates : (0, calendar_dates_1.enumerateCalendarDates)(window.start, window.end),
|
|
34
|
+
normalization_notes: window.normalization_notes,
|
|
35
|
+
},
|
|
36
|
+
source_order: sources.sourceOrder,
|
|
37
|
+
hard_facts: {
|
|
38
|
+
sessions_history: sources.sessionsHistory,
|
|
39
|
+
},
|
|
40
|
+
conversation_context: sources.conversationContext,
|
|
41
|
+
canon_memory: {
|
|
42
|
+
daily_logs: dailyLogs.map((entry) => ({
|
|
43
|
+
calendar_date: entry.calendar_date,
|
|
44
|
+
raw_content: entry.raw_content,
|
|
45
|
+
parsed_episode_count: entry.parsed_episodes.length,
|
|
46
|
+
})),
|
|
47
|
+
},
|
|
48
|
+
semantic_memory: {
|
|
49
|
+
memory_search: sources.memorySearch,
|
|
50
|
+
},
|
|
51
|
+
persona_context: {
|
|
52
|
+
soul: sources.coreContext.soul,
|
|
53
|
+
memory: sources.coreContext.memory,
|
|
54
|
+
identity: sources.coreContext.identity,
|
|
55
|
+
available_sources: sources.coreContext.available_sources,
|
|
56
|
+
should_constrain_generation: sources.coreContext.should_constrain_generation,
|
|
57
|
+
},
|
|
58
|
+
world_context: (0, world_rhythm_1.buildTimelineWorldContext)({
|
|
59
|
+
...window,
|
|
60
|
+
calendar_dates: window.calendar_dates.length > 0 ? window.calendar_dates : (0, calendar_dates_1.enumerateCalendarDates)(window.start, window.end),
|
|
61
|
+
}),
|
|
62
|
+
candidate_facts: dailyLogs.flatMap((entry) => entry.parsed_episodes.map((episode, index) => ({
|
|
63
|
+
fact_id: `canon:${entry.calendar_date}:${index}`,
|
|
64
|
+
source_type: 'canon_daily_log',
|
|
65
|
+
calendar_date: entry.calendar_date,
|
|
66
|
+
timestamp: episode.timestamp,
|
|
67
|
+
location: episode.location,
|
|
68
|
+
action: episode.action,
|
|
69
|
+
emotion_tags: episode.emotionTags,
|
|
70
|
+
appearance: episode.appearance,
|
|
71
|
+
internal_monologue: episode.internalMonologue,
|
|
72
|
+
parse_level: episode.parseLevel,
|
|
73
|
+
confidence: episode.confidence,
|
|
74
|
+
}))),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.materializeGeneratedCandidate = materializeGeneratedCandidate;
|
|
4
|
+
const fingerprint_1 = require("../lib/fingerprint");
|
|
5
|
+
const inherit_appearance_1 = require("../lib/inherit-appearance");
|
|
6
|
+
const parse_memory_1 = require("../lib/parse-memory");
|
|
7
|
+
const time_utils_1 = require("../lib/time-utils");
|
|
8
|
+
const holidays_1 = require("../lib/holidays");
|
|
9
|
+
function assertNonEmptyString(value, field) {
|
|
10
|
+
const normalized = String(value || '').trim();
|
|
11
|
+
if (!normalized) {
|
|
12
|
+
throw new Error(`Generated draft missing required field: ${field}`);
|
|
13
|
+
}
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
function normalizeGeneratedDraft(draft) {
|
|
17
|
+
const emotionTags = (draft.emotionTags || []).map((tag) => String(tag).trim()).filter(Boolean);
|
|
18
|
+
if (emotionTags.length === 0) {
|
|
19
|
+
throw new Error('Generated draft missing required field: emotionTags');
|
|
20
|
+
}
|
|
21
|
+
const confidence = Number(draft.confidence);
|
|
22
|
+
if (!Number.isFinite(confidence)) {
|
|
23
|
+
throw new Error('Generated draft has invalid confidence');
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
timestamp: draft.timestamp ? assertNonEmptyString(draft.timestamp, 'timestamp') : undefined,
|
|
27
|
+
location: assertNonEmptyString(draft.location, 'location'),
|
|
28
|
+
action: assertNonEmptyString(draft.action, 'action'),
|
|
29
|
+
emotionTags: emotionTags.slice(0, 3),
|
|
30
|
+
appearance: assertNonEmptyString(draft.appearance, 'appearance'),
|
|
31
|
+
internalMonologue: assertNonEmptyString(draft.internalMonologue, 'internalMonologue'),
|
|
32
|
+
confidence: Math.max(0.2, Math.min(1, confidence)),
|
|
33
|
+
reason: draft.reason ? String(draft.reason).trim() : undefined,
|
|
34
|
+
sceneSemantics: draft.sceneSemantics,
|
|
35
|
+
appearanceLogic: draft.appearanceLogic,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function inferCountryFromOffset(offset) {
|
|
39
|
+
return offset === '+08:00' ? 'CN' : 'US';
|
|
40
|
+
}
|
|
41
|
+
function materializeGeneratedCandidate(window, sources, draft, reason = 'llm-guided semantic timeline synthesis') {
|
|
42
|
+
const normalized = normalizeGeneratedDraft(draft);
|
|
43
|
+
const candidateTimestamp = normalized.timestamp || window.end;
|
|
44
|
+
const timestampParts = (0, time_utils_1.parseTimestampParts)(candidateTimestamp);
|
|
45
|
+
if (!timestampParts) {
|
|
46
|
+
throw new Error(`Cannot materialize generated candidate without parseable timestamp: ${candidateTimestamp}`);
|
|
47
|
+
}
|
|
48
|
+
const materializedDate = (0, time_utils_1.formatDate)(timestampParts);
|
|
49
|
+
const currentDayLog = sources.dailyLogs.find((entry) => entry.calendar_date === materializedDate);
|
|
50
|
+
const dayEpisodes = (0, parse_memory_1.parseMemoryFile)(currentDayLog?.raw_content || '');
|
|
51
|
+
const appearanceResolution = (0, inherit_appearance_1.resolveAppearance)(dayEpisodes, normalized.appearance, normalized.appearanceLogic);
|
|
52
|
+
const parsed = {
|
|
53
|
+
timestamp: candidateTimestamp,
|
|
54
|
+
location: normalized.location,
|
|
55
|
+
action: normalized.action,
|
|
56
|
+
emotionTags: normalized.emotionTags,
|
|
57
|
+
appearance: appearanceResolution.appearance,
|
|
58
|
+
internalMonologue: normalized.internalMonologue,
|
|
59
|
+
parseLevel: 'A',
|
|
60
|
+
confidence: normalized.confidence,
|
|
61
|
+
};
|
|
62
|
+
const idempotencyKey = (0, fingerprint_1.computeFingerprint)(materializedDate, parsed.location, parsed.action, parsed.timestamp);
|
|
63
|
+
const worldHooks = {
|
|
64
|
+
weekday: ![0, 6].includes((0, time_utils_1.dayOfWeek)(timestampParts)),
|
|
65
|
+
holiday_key: (0, holidays_1.getHoliday)(materializedDate, inferCountryFromOffset(timestampParts.offset)),
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
parsed,
|
|
69
|
+
episode: (0, parse_memory_1.mapToEpisode)(parsed, worldHooks, idempotencyKey),
|
|
70
|
+
idempotencyKey,
|
|
71
|
+
generationReason: normalized.reason || reason,
|
|
72
|
+
notes: [
|
|
73
|
+
'No reusable canon entry found; generated a timeline memory from the LLM draft.',
|
|
74
|
+
`Generation basis: ${normalized.reason || reason}`,
|
|
75
|
+
`Persona context loaded: ${sources.coreContext.soul || sources.coreContext.memory || sources.coreContext.identity ? 'SOUL / MEMORY / IDENTITY signals available' : 'no explicit profile files found in runtime context'}`,
|
|
76
|
+
`Appearance resolution: ${appearanceResolution.reason}`,
|
|
77
|
+
],
|
|
78
|
+
appearance: {
|
|
79
|
+
inherited: !appearanceResolution.overridden,
|
|
80
|
+
reason: appearanceResolution.reason,
|
|
81
|
+
source_episode_timestamp: appearanceResolution.sourceEpisodeTimestamp,
|
|
82
|
+
transition: appearanceResolution.transition,
|
|
83
|
+
outfit_mode: appearanceResolution.outfitMode,
|
|
84
|
+
change_reason: appearanceResolution.changeReason,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveWindow = resolveWindow;
|
|
4
|
+
const time_utils_1 = require("../lib/time-utils");
|
|
5
|
+
function parseRequiredTimestamp(value, label) {
|
|
6
|
+
const parts = (0, time_utils_1.parseTimestampParts)(value);
|
|
7
|
+
if (!parts) {
|
|
8
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
9
|
+
}
|
|
10
|
+
return parts;
|
|
11
|
+
}
|
|
12
|
+
function formatCalendarDate(parts) {
|
|
13
|
+
return `${parts.year}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')}`;
|
|
14
|
+
}
|
|
15
|
+
function makeNowWindow(nowIso, timezone, notes) {
|
|
16
|
+
const parts = parseRequiredTimestamp(nowIso, 'current time');
|
|
17
|
+
const date = formatCalendarDate(parts);
|
|
18
|
+
return {
|
|
19
|
+
query_range: 'now',
|
|
20
|
+
semantic_target: 'now',
|
|
21
|
+
collection_scope: 'today_so_far',
|
|
22
|
+
start: `${date}T00:00:00${parts.offset ?? ''}`,
|
|
23
|
+
end: nowIso,
|
|
24
|
+
calendar_date: date,
|
|
25
|
+
calendar_dates: [date],
|
|
26
|
+
timezone,
|
|
27
|
+
target_timestamp_hint: nowIso,
|
|
28
|
+
normalization_notes: notes,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function makePointDayWindow(pointTime, timezone, notes) {
|
|
32
|
+
const parts = parseRequiredTimestamp(pointTime, 'normalized_point');
|
|
33
|
+
const date = formatCalendarDate(parts);
|
|
34
|
+
return {
|
|
35
|
+
query_range: 'past_point',
|
|
36
|
+
semantic_target: 'past_point',
|
|
37
|
+
collection_scope: 'point_day',
|
|
38
|
+
start: `${date}T00:00:00${parts.offset ?? ''}`,
|
|
39
|
+
end: `${date}T23:59:59${parts.offset ?? ''}`,
|
|
40
|
+
calendar_date: date,
|
|
41
|
+
calendar_dates: [date],
|
|
42
|
+
timezone,
|
|
43
|
+
target_timestamp_hint: pointTime,
|
|
44
|
+
normalization_notes: notes,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function makeExplicitRangeWindow(start, end, timezone, notes) {
|
|
48
|
+
const startParts = parseRequiredTimestamp(start, 'normalized_start');
|
|
49
|
+
parseRequiredTimestamp(end, 'normalized_end');
|
|
50
|
+
const startEpoch = new Date(start).getTime();
|
|
51
|
+
const endEpoch = new Date(end).getTime();
|
|
52
|
+
if (Number.isNaN(startEpoch) || Number.isNaN(endEpoch) || startEpoch > endEpoch) {
|
|
53
|
+
throw new Error(`Invalid explicit range: ${start} -> ${end}`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
query_range: 'past_range',
|
|
57
|
+
semantic_target: 'past_range',
|
|
58
|
+
collection_scope: 'explicit_range',
|
|
59
|
+
start,
|
|
60
|
+
end,
|
|
61
|
+
calendar_date: formatCalendarDate(startParts),
|
|
62
|
+
calendar_dates: [],
|
|
63
|
+
timezone,
|
|
64
|
+
normalization_notes: notes,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function resolveWindow(plan, nowIso, timezone) {
|
|
68
|
+
const notes = [plan.summary];
|
|
69
|
+
if (plan.target_time_range === 'now') {
|
|
70
|
+
return makeNowWindow(nowIso, timezone, notes);
|
|
71
|
+
}
|
|
72
|
+
if (plan.target_time_range === 'past_point') {
|
|
73
|
+
if (!plan.normalized_point) {
|
|
74
|
+
throw new Error('Timeline query plan missing normalized_point for past_point');
|
|
75
|
+
}
|
|
76
|
+
return makePointDayWindow(plan.normalized_point, timezone, notes);
|
|
77
|
+
}
|
|
78
|
+
if (!plan.normalized_start || !plan.normalized_end) {
|
|
79
|
+
throw new Error('Timeline query plan missing normalized_start or normalized_end for past_range');
|
|
80
|
+
}
|
|
81
|
+
const rangeWindow = makeExplicitRangeWindow(plan.normalized_start, plan.normalized_end, timezone, notes);
|
|
82
|
+
return rangeWindow;
|
|
83
|
+
}
|