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,689 @@
|
|
|
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
|
+
exports.timelineResolveToolSpec = void 0;
|
|
37
|
+
exports.setTimelineResolveDependencies = setTimelineResolveDependencies;
|
|
38
|
+
exports.resetTimelineResolveDependencies = resetTimelineResolveDependencies;
|
|
39
|
+
exports.timelineResolve = timelineResolve;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const collect_sources_1 = require("../core/collect_sources");
|
|
44
|
+
const collect_timeline_request_1 = require("../core/collect_timeline_request");
|
|
45
|
+
const materialize_generated_candidate_1 = require("../core/materialize_generated_candidate");
|
|
46
|
+
const runtime_guard_1 = require("../core/runtime_guard");
|
|
47
|
+
const trace_1 = require("../core/trace");
|
|
48
|
+
const build_consumption_view_1 = require("../core/build_consumption_view");
|
|
49
|
+
const parse_memory_1 = require("../lib/parse-memory");
|
|
50
|
+
const fingerprint_1 = require("../lib/fingerprint");
|
|
51
|
+
const time_utils_1 = require("../lib/time-utils");
|
|
52
|
+
const holidays_1 = require("../lib/holidays");
|
|
53
|
+
const daily_log_1 = require("../storage/daily_log");
|
|
54
|
+
const lock_1 = require("../storage/lock");
|
|
55
|
+
const trace_log_1 = require("../storage/trace_log");
|
|
56
|
+
const write_episode_1 = require("../storage/write-episode");
|
|
57
|
+
const resolve_window_1 = require("../core/resolve_window");
|
|
58
|
+
function readOptionalTextFile(filePath) {
|
|
59
|
+
try {
|
|
60
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const defaultDependencies = {
|
|
67
|
+
currentTime: async () => ({
|
|
68
|
+
now: new Date().toISOString(),
|
|
69
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
|
|
70
|
+
}),
|
|
71
|
+
sessionsHistory: async () => [],
|
|
72
|
+
memoryGet: async () => '',
|
|
73
|
+
memorySearch: async () => [],
|
|
74
|
+
coreFiles: async () => ({
|
|
75
|
+
soul: readOptionalTextFile(path.join(process.cwd(), 'SOUL.md')),
|
|
76
|
+
memory: readOptionalTextFile(path.join(process.cwd(), 'MEMORY.md')) || readOptionalTextFile(path.join(process.cwd(), 'memory.md')),
|
|
77
|
+
identity: readOptionalTextFile(path.join(process.cwd(), 'IDENTITY.md')) || readOptionalTextFile(path.join(process.cwd(), 'IDENTITY')),
|
|
78
|
+
available_sources: [
|
|
79
|
+
readOptionalTextFile(path.join(process.cwd(), 'SOUL.md')).trim() ? 'soul' : '',
|
|
80
|
+
(readOptionalTextFile(path.join(process.cwd(), 'MEMORY.md')) || readOptionalTextFile(path.join(process.cwd(), 'memory.md'))).trim() ? 'memory' : '',
|
|
81
|
+
(readOptionalTextFile(path.join(process.cwd(), 'IDENTITY.md')) || readOptionalTextFile(path.join(process.cwd(), 'IDENTITY'))).trim() ? 'identity' : '',
|
|
82
|
+
].filter(Boolean),
|
|
83
|
+
should_constrain_generation: Boolean(readOptionalTextFile(path.join(process.cwd(), 'SOUL.md')).trim()
|
|
84
|
+
|| (readOptionalTextFile(path.join(process.cwd(), 'MEMORY.md')) || readOptionalTextFile(path.join(process.cwd(), 'memory.md'))).trim()
|
|
85
|
+
|| (readOptionalTextFile(path.join(process.cwd(), 'IDENTITY.md')) || readOptionalTextFile(path.join(process.cwd(), 'IDENTITY'))).trim()),
|
|
86
|
+
}),
|
|
87
|
+
writeEpisode: write_episode_1.writeEpisode,
|
|
88
|
+
memoryFilePath: (calendarDate) => `memory/${calendarDate}.md`,
|
|
89
|
+
canonicalRootName: 'memory',
|
|
90
|
+
traceLogPath: path.join(os.tmpdir(), 'stella-timeline-plugin-trace.log'),
|
|
91
|
+
};
|
|
92
|
+
let runtimeDependencies = defaultDependencies;
|
|
93
|
+
function setTimelineResolveDependencies(deps) {
|
|
94
|
+
runtimeDependencies = { ...runtimeDependencies, ...deps };
|
|
95
|
+
}
|
|
96
|
+
function resetTimelineResolveDependencies() {
|
|
97
|
+
runtimeDependencies = defaultDependencies;
|
|
98
|
+
}
|
|
99
|
+
function getEffectiveTimelineDependencies(overrides) {
|
|
100
|
+
return {
|
|
101
|
+
...runtimeDependencies,
|
|
102
|
+
...overrides,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function validateTimelineResolveInput(input) {
|
|
106
|
+
if (!String(input.query || '').trim()) {
|
|
107
|
+
throw new Error('timeline_resolve requires query');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function classifyTimelineResolveError(error) {
|
|
111
|
+
const message = error.message || 'Unknown timeline_resolve failure';
|
|
112
|
+
if (message.includes('timeline_resolve requires query')) {
|
|
113
|
+
return 'INVALID_INPUT';
|
|
114
|
+
}
|
|
115
|
+
if (message.includes('Invalid explicit range')
|
|
116
|
+
|| message.includes('normalized_point')
|
|
117
|
+
|| message.includes('normalized_start')
|
|
118
|
+
|| message.includes('normalized_end')) {
|
|
119
|
+
return 'INVALID_RANGE';
|
|
120
|
+
}
|
|
121
|
+
if (message.includes('Timeline reasoner dependency missing')
|
|
122
|
+
|| message.includes('Timeline reasoner returned no decision')
|
|
123
|
+
|| message.includes('Timeline query planner dependency missing')
|
|
124
|
+
|| message.includes('Timeline query planner returned no decision')) {
|
|
125
|
+
return 'REASONER_UNAVAILABLE';
|
|
126
|
+
}
|
|
127
|
+
if (message.includes('LLM generation'))
|
|
128
|
+
return 'GENERATION_UNAVAILABLE';
|
|
129
|
+
if (message.includes('Generated draft'))
|
|
130
|
+
return 'GENERATION_UNAVAILABLE';
|
|
131
|
+
if (message.includes('Invalid reasoner output'))
|
|
132
|
+
return 'INVALID_REASONER_OUTPUT';
|
|
133
|
+
if (message.includes('Canonical daily logs'))
|
|
134
|
+
return 'WRITE_BLOCKED';
|
|
135
|
+
if (message.includes('Lock already held'))
|
|
136
|
+
return 'WRITE_CONFLICT';
|
|
137
|
+
if (message.includes('write dependency missing'))
|
|
138
|
+
return 'WRITE_BLOCKED';
|
|
139
|
+
if (message.includes('parse'))
|
|
140
|
+
return 'PARSE_ERROR';
|
|
141
|
+
if (message.includes('sessions_history') || message.includes('memory_get') || message.includes('memory_search')) {
|
|
142
|
+
return 'SOURCE_FAILURE';
|
|
143
|
+
}
|
|
144
|
+
return 'INTERNAL';
|
|
145
|
+
}
|
|
146
|
+
function normalizeMode(mode) {
|
|
147
|
+
return mode || 'allow_generate';
|
|
148
|
+
}
|
|
149
|
+
function classifyWriteFailure(writeResult) {
|
|
150
|
+
if (writeResult.error_code === 'CONFLICT_EXISTS') {
|
|
151
|
+
return {
|
|
152
|
+
mode: 'write_conflict',
|
|
153
|
+
errorCode: 'WRITE_CONFLICT',
|
|
154
|
+
guard: 'conflict',
|
|
155
|
+
recoveryHint: writeResult.recovery_hint,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (writeResult.error_code === 'LOCK_EXISTS') {
|
|
159
|
+
return {
|
|
160
|
+
mode: 'write_conflict',
|
|
161
|
+
errorCode: 'WRITE_CONFLICT',
|
|
162
|
+
guard: 'lock',
|
|
163
|
+
recoveryHint: writeResult.recovery_hint ?? 'Retry once the current timeline writer releases the file lock.',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (writeResult.error === 'write dependency missing') {
|
|
167
|
+
return {
|
|
168
|
+
mode: 'write_blocked',
|
|
169
|
+
errorCode: 'WRITE_BLOCKED',
|
|
170
|
+
guard: 'write_dependency',
|
|
171
|
+
recoveryHint: 'Configure the timeline writer dependencies before allowing generated writes.',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if ((writeResult.error || '').includes('Canonical daily logs')) {
|
|
175
|
+
return {
|
|
176
|
+
mode: 'write_blocked',
|
|
177
|
+
errorCode: 'WRITE_BLOCKED',
|
|
178
|
+
guard: 'canonical_path',
|
|
179
|
+
recoveryHint: writeResult.recovery_hint ?? 'Use the canonical memory/YYYY-MM-DD.md path before allowing generated writes.',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
mode: 'write_failed',
|
|
184
|
+
errorCode: 'WRITE_FAILED',
|
|
185
|
+
guard: 'canonical_path',
|
|
186
|
+
recoveryHint: writeResult.recovery_hint,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function calendarDateFromTimestamp(timestamp) {
|
|
190
|
+
const parts = (0, time_utils_1.parseTimestampParts)(timestamp);
|
|
191
|
+
if (!parts)
|
|
192
|
+
throw new Error(`Generated timestamp is not parseable: ${timestamp}`);
|
|
193
|
+
return (0, time_utils_1.formatDate)(parts);
|
|
194
|
+
}
|
|
195
|
+
function buildReasonerNotes(reasoned) {
|
|
196
|
+
const notes = [reasoned.rationale.summary];
|
|
197
|
+
const interpretation = reasoned.time_interpretation?.summary?.trim();
|
|
198
|
+
if (interpretation) {
|
|
199
|
+
notes.push(`Time interpretation: ${interpretation}`);
|
|
200
|
+
}
|
|
201
|
+
return notes;
|
|
202
|
+
}
|
|
203
|
+
function persistTraceIfConfigured(output, input, deps, requestedRange) {
|
|
204
|
+
if (!deps.traceLogPath)
|
|
205
|
+
return false;
|
|
206
|
+
(0, trace_log_1.appendTraceLog)({
|
|
207
|
+
trace_id: output.trace_id,
|
|
208
|
+
event: 'timeline_resolve',
|
|
209
|
+
ts: new Date().toISOString(),
|
|
210
|
+
payload: {
|
|
211
|
+
ok: output.ok,
|
|
212
|
+
requested_range: requestedRange,
|
|
213
|
+
error: output.ok ? null : output.error,
|
|
214
|
+
resolution_mode: output.resolution_summary.mode,
|
|
215
|
+
notes: output.notes,
|
|
216
|
+
trace: output.trace ?? null,
|
|
217
|
+
},
|
|
218
|
+
}, deps.traceLogPath);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
function finalizeTimelineOutput(output, input, deps, requestedRange) {
|
|
222
|
+
persistTraceIfConfigured(output, input, deps, requestedRange);
|
|
223
|
+
if (!input.trace) {
|
|
224
|
+
delete output.trace;
|
|
225
|
+
}
|
|
226
|
+
return output;
|
|
227
|
+
}
|
|
228
|
+
function makeRequestId() {
|
|
229
|
+
return `timeline-request-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
|
|
230
|
+
}
|
|
231
|
+
async function maybePlanTimelineQuery(input, deps, anchor) {
|
|
232
|
+
if (!deps.planTimelineQuery) {
|
|
233
|
+
throw new Error('Timeline query planner dependency missing');
|
|
234
|
+
}
|
|
235
|
+
const plan = await deps.planTimelineQuery(input, anchor);
|
|
236
|
+
if (!plan) {
|
|
237
|
+
throw new Error('Timeline query planner returned no decision');
|
|
238
|
+
}
|
|
239
|
+
return plan;
|
|
240
|
+
}
|
|
241
|
+
function buildWorldHooks(timestamp) {
|
|
242
|
+
const parts = (0, time_utils_1.parseTimestampParts)(timestamp);
|
|
243
|
+
if (!parts) {
|
|
244
|
+
return { weekday: true, holiday_key: null };
|
|
245
|
+
}
|
|
246
|
+
const date = (0, time_utils_1.formatDate)(parts);
|
|
247
|
+
return {
|
|
248
|
+
weekday: ![0, 6].includes((0, time_utils_1.dayOfWeek)(parts)),
|
|
249
|
+
holiday_key: (0, holidays_1.getHoliday)(date),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function buildReadOnlyHitOutput(selectedFact, traceId, window, collector, reasoned) {
|
|
253
|
+
const date = selectedFact.calendar_date;
|
|
254
|
+
const fp = (0, fingerprint_1.computeFingerprint)(date, selectedFact.location, selectedFact.action, selectedFact.timestamp);
|
|
255
|
+
const episode = (0, parse_memory_1.mapToEpisode)({
|
|
256
|
+
timestamp: selectedFact.timestamp,
|
|
257
|
+
location: selectedFact.location,
|
|
258
|
+
action: selectedFact.action,
|
|
259
|
+
emotionTags: selectedFact.emotion_tags,
|
|
260
|
+
appearance: selectedFact.appearance,
|
|
261
|
+
internalMonologue: selectedFact.internal_monologue,
|
|
262
|
+
parseLevel: selectedFact.parse_level,
|
|
263
|
+
confidence: selectedFact.confidence,
|
|
264
|
+
}, buildWorldHooks(selectedFact.timestamp), fp);
|
|
265
|
+
return {
|
|
266
|
+
ok: true,
|
|
267
|
+
schema_version: '1.0',
|
|
268
|
+
trace_id: traceId,
|
|
269
|
+
resolution_summary: {
|
|
270
|
+
mode: 'read_only_hit',
|
|
271
|
+
writes_attempted: 0,
|
|
272
|
+
writes_succeeded: 0,
|
|
273
|
+
sources: collector.source_order,
|
|
274
|
+
confidence_min: selectedFact.confidence,
|
|
275
|
+
confidence_max: selectedFact.confidence,
|
|
276
|
+
},
|
|
277
|
+
result: {
|
|
278
|
+
schema_version: '1.0',
|
|
279
|
+
document_type: 'timeline.window',
|
|
280
|
+
anchor: { now: window.end, timezone: window.timezone },
|
|
281
|
+
window: {
|
|
282
|
+
calendar_date: window.calendar_date,
|
|
283
|
+
preset: window.query_range,
|
|
284
|
+
semantic_target: window.semantic_target,
|
|
285
|
+
collection_scope: window.collection_scope,
|
|
286
|
+
start: window.start,
|
|
287
|
+
end: window.end,
|
|
288
|
+
idempotency_key: fp,
|
|
289
|
+
},
|
|
290
|
+
resolution: {
|
|
291
|
+
mode: 'read_only_hit',
|
|
292
|
+
notes: buildReasonerNotes(reasoned).join(' | '),
|
|
293
|
+
},
|
|
294
|
+
consumption: (0, build_consumption_view_1.buildConsumptionView)({
|
|
295
|
+
preset: window.query_range,
|
|
296
|
+
semanticTarget: window.semantic_target,
|
|
297
|
+
collectionScope: window.collection_scope,
|
|
298
|
+
resolutionMode: 'read_only_hit',
|
|
299
|
+
reasoned,
|
|
300
|
+
episode,
|
|
301
|
+
sourceType: 'canon',
|
|
302
|
+
}),
|
|
303
|
+
episodes: [episode],
|
|
304
|
+
},
|
|
305
|
+
notes: buildReasonerNotes(reasoned),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function buildEmptyOutput(traceId, window, collector, reasoned) {
|
|
309
|
+
return {
|
|
310
|
+
ok: true,
|
|
311
|
+
schema_version: '1.0',
|
|
312
|
+
trace_id: traceId,
|
|
313
|
+
resolution_summary: {
|
|
314
|
+
mode: 'empty_window',
|
|
315
|
+
writes_attempted: 0,
|
|
316
|
+
writes_succeeded: 0,
|
|
317
|
+
sources: collector.source_order,
|
|
318
|
+
confidence_min: 0,
|
|
319
|
+
confidence_max: 0,
|
|
320
|
+
},
|
|
321
|
+
result: {
|
|
322
|
+
schema_version: '1.0',
|
|
323
|
+
document_type: 'timeline.window',
|
|
324
|
+
anchor: { now: window.end, timezone: window.timezone },
|
|
325
|
+
window: {
|
|
326
|
+
calendar_date: window.calendar_date,
|
|
327
|
+
preset: window.query_range,
|
|
328
|
+
semantic_target: window.semantic_target,
|
|
329
|
+
collection_scope: window.collection_scope,
|
|
330
|
+
start: window.start,
|
|
331
|
+
end: window.end,
|
|
332
|
+
idempotency_key: 'none',
|
|
333
|
+
},
|
|
334
|
+
resolution: {
|
|
335
|
+
mode: 'empty_window',
|
|
336
|
+
notes: buildReasonerNotes(reasoned).join(' | '),
|
|
337
|
+
},
|
|
338
|
+
consumption: (0, build_consumption_view_1.buildConsumptionView)({
|
|
339
|
+
preset: window.query_range,
|
|
340
|
+
semanticTarget: window.semantic_target,
|
|
341
|
+
collectionScope: window.collection_scope,
|
|
342
|
+
resolutionMode: 'empty_window',
|
|
343
|
+
reasoned,
|
|
344
|
+
sourceType: 'none',
|
|
345
|
+
}),
|
|
346
|
+
episodes: [],
|
|
347
|
+
},
|
|
348
|
+
notes: buildReasonerNotes(reasoned),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
async function timelineResolve(input, dependencyOverrides) {
|
|
352
|
+
const deps = getEffectiveTimelineDependencies(dependencyOverrides);
|
|
353
|
+
let sourceOrder = [];
|
|
354
|
+
let requestedRange = 'unknown';
|
|
355
|
+
try {
|
|
356
|
+
validateTimelineResolveInput(input);
|
|
357
|
+
const currentTime = await deps.currentTime();
|
|
358
|
+
const queryPlan = await maybePlanTimelineQuery(input, deps, {
|
|
359
|
+
now: currentTime.now,
|
|
360
|
+
timezone: currentTime.timezone,
|
|
361
|
+
});
|
|
362
|
+
requestedRange = queryPlan.target_time_range;
|
|
363
|
+
const window = (0, resolve_window_1.resolveWindow)(queryPlan, currentTime.now, currentTime.timezone);
|
|
364
|
+
const sources = await (0, collect_sources_1.collectSources)(deps, window, input);
|
|
365
|
+
sourceOrder = sources.sourceOrder;
|
|
366
|
+
const collector = (0, collect_timeline_request_1.buildTimelineCollectorOutput)(makeRequestId(), input, window, sources);
|
|
367
|
+
if (!deps.reasonTimeline) {
|
|
368
|
+
throw new Error('Timeline reasoner dependency missing');
|
|
369
|
+
}
|
|
370
|
+
const reasoned = await deps.reasonTimeline(collector);
|
|
371
|
+
if (!reasoned) {
|
|
372
|
+
throw new Error('Timeline reasoner returned no decision');
|
|
373
|
+
}
|
|
374
|
+
const guard = (0, runtime_guard_1.validateTimelineReasonerOutput)(collector, reasoned);
|
|
375
|
+
if (!guard.ok) {
|
|
376
|
+
throw new Error(`Invalid reasoner output: ${guard.block_reason}`);
|
|
377
|
+
}
|
|
378
|
+
let output;
|
|
379
|
+
let traceAppearance = { inherited: false, reason: 'not-applicable' };
|
|
380
|
+
let traceWrite = {
|
|
381
|
+
attempted: false,
|
|
382
|
+
succeeded: false,
|
|
383
|
+
guard: 'not_attempted',
|
|
384
|
+
outcome: 'not_attempted',
|
|
385
|
+
writer: 'stella-timeline-plugin',
|
|
386
|
+
};
|
|
387
|
+
let traceFingerprint = {
|
|
388
|
+
checked: collector.candidate_facts.length > 0,
|
|
389
|
+
matched: false,
|
|
390
|
+
compared_episodes: collector.candidate_facts.length,
|
|
391
|
+
reason: reasoned.rationale.summary,
|
|
392
|
+
};
|
|
393
|
+
let decision = {
|
|
394
|
+
resolution_mode: guard.outcome === 'reuse_existing_fact'
|
|
395
|
+
? 'read_only_hit'
|
|
396
|
+
: guard.outcome === 'generate_new_fact'
|
|
397
|
+
? 'generated_new'
|
|
398
|
+
: 'empty_window',
|
|
399
|
+
write_outcome: traceWrite.outcome,
|
|
400
|
+
category: reasoned.request_type,
|
|
401
|
+
};
|
|
402
|
+
if (guard.outcome === 'reuse_existing_fact' && guard.selected_fact) {
|
|
403
|
+
output = buildReadOnlyHitOutput(guard.selected_fact, '', window, collector, reasoned);
|
|
404
|
+
traceAppearance = {
|
|
405
|
+
inherited: false,
|
|
406
|
+
reason: 'existing canon reused after LLM reasoner selected a matching fact',
|
|
407
|
+
source_episode_timestamp: guard.selected_fact.timestamp,
|
|
408
|
+
};
|
|
409
|
+
traceFingerprint = {
|
|
410
|
+
checked: collector.candidate_facts.length > 0,
|
|
411
|
+
matched: true,
|
|
412
|
+
compared_episodes: collector.candidate_facts.length,
|
|
413
|
+
idempotency_key: output.result?.window.idempotency_key,
|
|
414
|
+
matched_episode_timestamp: guard.selected_fact.timestamp,
|
|
415
|
+
reason: reasoned.rationale.summary,
|
|
416
|
+
};
|
|
417
|
+
decision = {
|
|
418
|
+
resolution_mode: output.resolution_summary.mode,
|
|
419
|
+
write_outcome: traceWrite.outcome,
|
|
420
|
+
category: reasoned.request_type,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
else if (guard.outcome === 'generate_new_fact' && guard.generated_fact) {
|
|
424
|
+
const generated = (0, materialize_generated_candidate_1.materializeGeneratedCandidate)(window, sources, guard.generated_fact, guard.generated_fact.reason || reasoned.rationale.summary || 'llm-guided semantic timeline synthesis');
|
|
425
|
+
traceAppearance = generated.appearance;
|
|
426
|
+
const generatedCalendarDate = calendarDateFromTimestamp(generated.parsed.timestamp);
|
|
427
|
+
const requestedPath = deps.memoryFilePath
|
|
428
|
+
? deps.memoryFilePath(generatedCalendarDate)
|
|
429
|
+
: `memory/${generatedCalendarDate}.md`;
|
|
430
|
+
let filePath = requestedPath;
|
|
431
|
+
let writeResult = {
|
|
432
|
+
success: false,
|
|
433
|
+
written_at: '',
|
|
434
|
+
outcome: 'failed',
|
|
435
|
+
error: 'write dependency missing',
|
|
436
|
+
recovery_hint: 'Configure the timeline writer dependencies before allowing generated writes.',
|
|
437
|
+
};
|
|
438
|
+
let writeGuard = 'canonical_path';
|
|
439
|
+
try {
|
|
440
|
+
filePath = (0, daily_log_1.assertCanonicalDailyLogPath)(requestedPath, generatedCalendarDate, deps.canonicalRootName || 'memory');
|
|
441
|
+
writeResult = deps.writeEpisode
|
|
442
|
+
? await (0, lock_1.withFileLock)(filePath, async () => deps.writeEpisode({
|
|
443
|
+
timestamp: generated.parsed.timestamp,
|
|
444
|
+
location: generated.parsed.location,
|
|
445
|
+
action: generated.parsed.action,
|
|
446
|
+
emotionTags: generated.parsed.emotionTags,
|
|
447
|
+
appearance: generated.parsed.appearance,
|
|
448
|
+
internalMonologue: generated.parsed.internalMonologue,
|
|
449
|
+
filePath,
|
|
450
|
+
confidence: generated.parsed.confidence,
|
|
451
|
+
}))
|
|
452
|
+
: writeResult;
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
if (error instanceof lock_1.FileLockError) {
|
|
456
|
+
writeGuard = 'lock';
|
|
457
|
+
writeResult = {
|
|
458
|
+
success: false,
|
|
459
|
+
written_at: '',
|
|
460
|
+
outcome: 'conflict',
|
|
461
|
+
error_code: 'LOCK_EXISTS',
|
|
462
|
+
error: error.message,
|
|
463
|
+
recovery_hint: 'Retry once the current timeline writer releases the file lock.',
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
writeResult = {
|
|
468
|
+
success: false,
|
|
469
|
+
written_at: '',
|
|
470
|
+
outcome: 'failed',
|
|
471
|
+
error_code: String(error.message || '').includes('Canonical daily logs') ? 'IO_ERROR' : undefined,
|
|
472
|
+
error: error.message,
|
|
473
|
+
recovery_hint: String(error.message || '').includes('Canonical daily logs')
|
|
474
|
+
? 'Use the canonical memory/YYYY-MM-DD.md path before allowing generated writes.'
|
|
475
|
+
: undefined,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const normalizedWriteResult = writeResult.success && !writeResult.outcome
|
|
480
|
+
? { ...writeResult, outcome: 'appended' }
|
|
481
|
+
: writeResult;
|
|
482
|
+
const failedWrite = !normalizedWriteResult.success ? classifyWriteFailure(normalizedWriteResult) : null;
|
|
483
|
+
if (failedWrite) {
|
|
484
|
+
writeGuard = failedWrite.guard;
|
|
485
|
+
}
|
|
486
|
+
const resolutionMode = normalizedWriteResult.success
|
|
487
|
+
? normalizedWriteResult.outcome === 'noop_existing'
|
|
488
|
+
? 'already_present'
|
|
489
|
+
: 'generated_new'
|
|
490
|
+
: failedWrite?.mode ?? 'write_failed';
|
|
491
|
+
const resolutionNotes = normalizedWriteResult.success
|
|
492
|
+
? normalizedWriteResult.outcome === 'noop_existing'
|
|
493
|
+
? 'a matching canon entry already existed, so the append-only writer skipped the write'
|
|
494
|
+
: 'generated candidate persisted via append-only writer'
|
|
495
|
+
: normalizedWriteResult.error;
|
|
496
|
+
traceWrite = {
|
|
497
|
+
attempted: true,
|
|
498
|
+
succeeded: normalizedWriteResult.success && normalizedWriteResult.outcome === 'appended',
|
|
499
|
+
file_path: filePath,
|
|
500
|
+
outcome: normalizedWriteResult.outcome,
|
|
501
|
+
error_code: normalizedWriteResult.error_code,
|
|
502
|
+
error: normalizedWriteResult.success ? undefined : normalizedWriteResult.error,
|
|
503
|
+
recovery_hint: normalizedWriteResult.recovery_hint,
|
|
504
|
+
guard: writeGuard,
|
|
505
|
+
writer: 'stella-timeline-plugin',
|
|
506
|
+
};
|
|
507
|
+
traceFingerprint = {
|
|
508
|
+
checked: true,
|
|
509
|
+
matched: false,
|
|
510
|
+
compared_episodes: collector.candidate_facts.length,
|
|
511
|
+
idempotency_key: normalizedWriteResult.idempotency_key || generated.idempotencyKey,
|
|
512
|
+
reason: generated.generationReason,
|
|
513
|
+
};
|
|
514
|
+
decision = {
|
|
515
|
+
resolution_mode: resolutionMode,
|
|
516
|
+
write_outcome: normalizedWriteResult.outcome,
|
|
517
|
+
category: normalizedWriteResult.success ? reasoned.request_type : 'write_failure',
|
|
518
|
+
error_code: failedWrite?.errorCode,
|
|
519
|
+
};
|
|
520
|
+
output = {
|
|
521
|
+
ok: true,
|
|
522
|
+
schema_version: '1.0',
|
|
523
|
+
trace_id: '',
|
|
524
|
+
resolution_summary: {
|
|
525
|
+
mode: resolutionMode,
|
|
526
|
+
writes_attempted: 1,
|
|
527
|
+
writes_succeeded: normalizedWriteResult.success && normalizedWriteResult.outcome === 'appended' ? 1 : 0,
|
|
528
|
+
sources: sources.sourceOrder,
|
|
529
|
+
confidence_min: generated.parsed.confidence,
|
|
530
|
+
confidence_max: generated.parsed.confidence,
|
|
531
|
+
},
|
|
532
|
+
result: {
|
|
533
|
+
schema_version: '1.0',
|
|
534
|
+
document_type: 'timeline.window',
|
|
535
|
+
anchor: { now: window.end, timezone: window.timezone },
|
|
536
|
+
window: {
|
|
537
|
+
calendar_date: generatedCalendarDate,
|
|
538
|
+
preset: window.query_range,
|
|
539
|
+
semantic_target: window.semantic_target,
|
|
540
|
+
collection_scope: window.collection_scope,
|
|
541
|
+
start: window.start,
|
|
542
|
+
end: window.end,
|
|
543
|
+
idempotency_key: normalizedWriteResult.idempotency_key || generated.idempotencyKey,
|
|
544
|
+
},
|
|
545
|
+
resolution: {
|
|
546
|
+
mode: resolutionMode,
|
|
547
|
+
notes: [...buildReasonerNotes(reasoned), resolutionNotes].join(' | '),
|
|
548
|
+
},
|
|
549
|
+
consumption: (0, build_consumption_view_1.buildConsumptionView)({
|
|
550
|
+
preset: window.query_range,
|
|
551
|
+
semanticTarget: window.semantic_target,
|
|
552
|
+
collectionScope: window.collection_scope,
|
|
553
|
+
resolutionMode,
|
|
554
|
+
reasoned,
|
|
555
|
+
episode: generated.episode,
|
|
556
|
+
sourceType: normalizedWriteResult.success ? 'generated' : 'generated',
|
|
557
|
+
}),
|
|
558
|
+
episodes: [generated.episode],
|
|
559
|
+
},
|
|
560
|
+
notes: buildReasonerNotes(reasoned).concat(generated.notes, normalizedWriteResult.success
|
|
561
|
+
? normalizedWriteResult.outcome === 'noop_existing'
|
|
562
|
+
? [`A matching canon entry was already present at ${filePath}; append skipped.`]
|
|
563
|
+
: [`Generated episode persisted to ${filePath}.`]
|
|
564
|
+
: [
|
|
565
|
+
`Generation attempted but write failed: ${normalizedWriteResult.error ?? 'unknown error'}.`,
|
|
566
|
+
...(normalizedWriteResult.recovery_hint ? [`Recovery hint: ${normalizedWriteResult.recovery_hint}`] : []),
|
|
567
|
+
]),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
output = buildEmptyOutput('', window, collector, reasoned);
|
|
572
|
+
traceAppearance = {
|
|
573
|
+
inherited: false,
|
|
574
|
+
reason: 'no-canon-hit',
|
|
575
|
+
};
|
|
576
|
+
traceWrite = {
|
|
577
|
+
attempted: false,
|
|
578
|
+
succeeded: false,
|
|
579
|
+
guard: 'not_attempted',
|
|
580
|
+
outcome: 'not_attempted',
|
|
581
|
+
writer: 'stella-timeline-plugin',
|
|
582
|
+
};
|
|
583
|
+
traceFingerprint = {
|
|
584
|
+
checked: false,
|
|
585
|
+
matched: false,
|
|
586
|
+
compared_episodes: collector.candidate_facts.length,
|
|
587
|
+
reason: reasoned.rationale.summary,
|
|
588
|
+
};
|
|
589
|
+
decision = {
|
|
590
|
+
resolution_mode: output.resolution_summary.mode,
|
|
591
|
+
write_outcome: traceWrite.outcome,
|
|
592
|
+
category: reasoned.request_type,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
const trace = (0, trace_1.buildTrace)({
|
|
596
|
+
requested_range: requestedRange,
|
|
597
|
+
actual_range: window.semantic_target,
|
|
598
|
+
source_order: sources.sourceOrder,
|
|
599
|
+
source_summary: {
|
|
600
|
+
sessions_history_count: sources.sessionsHistory.length,
|
|
601
|
+
sessions_history_preview: sources.sessionsHistory[0] || null,
|
|
602
|
+
memory_chars: sources.dailyLogs.reduce((total, entry) => total + entry.raw_content.length, 0),
|
|
603
|
+
memory_search_count: sources.memorySearch.length,
|
|
604
|
+
memory_search_preview: sources.memorySearch.slice(0, 3),
|
|
605
|
+
parsed_episode_count: collector.candidate_facts.length,
|
|
606
|
+
selected_episode_timestamp: guard.selected_fact?.timestamp,
|
|
607
|
+
},
|
|
608
|
+
fingerprint: traceFingerprint,
|
|
609
|
+
appearance: traceAppearance,
|
|
610
|
+
write: traceWrite,
|
|
611
|
+
decision,
|
|
612
|
+
notes: output.notes,
|
|
613
|
+
});
|
|
614
|
+
output.trace_id = trace.trace_id;
|
|
615
|
+
output.trace = trace;
|
|
616
|
+
return finalizeTimelineOutput(output, input, deps, requestedRange);
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
const timelineError = error instanceof Error ? error : new Error(String(error));
|
|
620
|
+
const errorCode = classifyTimelineResolveError(timelineError);
|
|
621
|
+
const trace = (0, trace_1.buildTrace)({
|
|
622
|
+
requested_range: requestedRange,
|
|
623
|
+
actual_range: 'error',
|
|
624
|
+
source_order: sourceOrder,
|
|
625
|
+
source_summary: {
|
|
626
|
+
sessions_history_count: 0,
|
|
627
|
+
sessions_history_preview: null,
|
|
628
|
+
memory_chars: 0,
|
|
629
|
+
memory_search_count: 0,
|
|
630
|
+
memory_search_preview: [],
|
|
631
|
+
parsed_episode_count: 0,
|
|
632
|
+
},
|
|
633
|
+
fingerprint: {
|
|
634
|
+
checked: false,
|
|
635
|
+
matched: false,
|
|
636
|
+
compared_episodes: 0,
|
|
637
|
+
reason: timelineError.message,
|
|
638
|
+
},
|
|
639
|
+
appearance: { inherited: false, reason: 'error' },
|
|
640
|
+
write: {
|
|
641
|
+
attempted: false,
|
|
642
|
+
succeeded: false,
|
|
643
|
+
guard: 'not_attempted',
|
|
644
|
+
outcome: 'not_attempted',
|
|
645
|
+
writer: 'stella-timeline-plugin',
|
|
646
|
+
},
|
|
647
|
+
decision: {
|
|
648
|
+
resolution_mode: 'error',
|
|
649
|
+
write_outcome: 'not_attempted',
|
|
650
|
+
category: 'error',
|
|
651
|
+
error_code: errorCode,
|
|
652
|
+
},
|
|
653
|
+
notes: [timelineError.message],
|
|
654
|
+
});
|
|
655
|
+
const output = {
|
|
656
|
+
ok: false,
|
|
657
|
+
schema_version: '1.0',
|
|
658
|
+
trace_id: trace.trace_id,
|
|
659
|
+
resolution_summary: {
|
|
660
|
+
mode: 'error',
|
|
661
|
+
writes_attempted: 0,
|
|
662
|
+
writes_succeeded: 0,
|
|
663
|
+
sources: sourceOrder,
|
|
664
|
+
confidence_min: 0,
|
|
665
|
+
confidence_max: 0,
|
|
666
|
+
},
|
|
667
|
+
notes: [timelineError.message],
|
|
668
|
+
error: {
|
|
669
|
+
code: errorCode,
|
|
670
|
+
message: timelineError.message,
|
|
671
|
+
},
|
|
672
|
+
trace,
|
|
673
|
+
};
|
|
674
|
+
return finalizeTimelineOutput(output, input, deps, requestedRange);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
exports.timelineResolveToolSpec = {
|
|
678
|
+
name: 'timeline_resolve',
|
|
679
|
+
description: '处理“你在干嘛”“你现在在哪”“最近有什么有趣的事吗”“昨晚八点你在做什么”这类时间现实与回忆问题的统一入口;直接接收自然语言 query,内部会先理解时间语义,再检索或生成并 append-only 写入 canon。',
|
|
680
|
+
inputSchema: {
|
|
681
|
+
type: 'object',
|
|
682
|
+
properties: {
|
|
683
|
+
query: { type: 'string' },
|
|
684
|
+
},
|
|
685
|
+
required: ['query'],
|
|
686
|
+
additionalProperties: false,
|
|
687
|
+
},
|
|
688
|
+
run: timelineResolve,
|
|
689
|
+
};
|