stella-timeline-plugin 2.0.0 → 2.0.1
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/src/runtime/openclaw_timeline_runtime.js +375 -92
- package/dist/src/tools/timeline_resolve.js +8 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +35 -35
- package/scripts/migrate-existing-memory.mjs +153 -153
- package/scripts/release.mjs +2 -46
- package/scripts/run-openclaw-live-e2e.mjs +16 -16
- package/scripts/run-openclaw-smoke.mjs +285 -10
- package/skills/timeline/SKILL.md +111 -111
- package/templates/AGENTS.fragment.md +29 -29
- package/templates/SOUL.fragment.md +17 -17
|
@@ -37,6 +37,7 @@ exports.makeOpenClawTimelineResolveToolFactory = makeOpenClawTimelineResolveTool
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const conversation_context_1 = require("./conversation_context");
|
|
40
|
+
const time_utils_1 = require("../lib/time-utils");
|
|
40
41
|
const timeline_resolve_1 = require("../tools/timeline_resolve");
|
|
41
42
|
function readString(value) {
|
|
42
43
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
@@ -52,7 +53,7 @@ function resolvePluginRuntimeConfig(pluginConfig) {
|
|
|
52
53
|
enableTrace: readBoolean(pluginConfig?.enableTrace, true),
|
|
53
54
|
traceLogPath: readString(pluginConfig?.traceLogPath),
|
|
54
55
|
canonicalMemoryRoot: readString(pluginConfig?.canonicalMemoryRoot) || 'memory',
|
|
55
|
-
reasonerTimeoutMs: readInteger(pluginConfig?.reasonerTimeoutMs,
|
|
56
|
+
reasonerTimeoutMs: readInteger(pluginConfig?.reasonerTimeoutMs, 90000),
|
|
56
57
|
reasonerSessionPrefix: readString(pluginConfig?.reasonerSessionPrefix) || 'timeline-reasoner',
|
|
57
58
|
reasonerMessageLimit: readInteger(pluginConfig?.reasonerMessageLimit, 24),
|
|
58
59
|
sessionHistoryLimit: readInteger(pluginConfig?.sessionHistoryLimit, 12),
|
|
@@ -183,9 +184,7 @@ function normalizeSessionHistory(messages, limit) {
|
|
|
183
184
|
function extractLatestAssistantText(messages) {
|
|
184
185
|
const reversed = [...messages].reverse();
|
|
185
186
|
for (const message of reversed) {
|
|
186
|
-
const role =
|
|
187
|
-
? String(message.role).trim()
|
|
188
|
-
: '';
|
|
187
|
+
const role = extractMessageRole(message);
|
|
189
188
|
const text = extractMessageText(message);
|
|
190
189
|
if (role === 'assistant' && text)
|
|
191
190
|
return text;
|
|
@@ -240,12 +239,12 @@ function collectBalancedJsonObjects(text) {
|
|
|
240
239
|
}
|
|
241
240
|
return objects;
|
|
242
241
|
}
|
|
243
|
-
function tryExtractJsonObject(text, expectedRequestId) {
|
|
242
|
+
function tryExtractJsonObject(text, sourceLabel, expectedRequestId) {
|
|
244
243
|
const fencedMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
245
244
|
const candidate = fencedMatch?.[1]?.trim() || text.trim();
|
|
246
245
|
const objects = collectBalancedJsonObjects(candidate);
|
|
247
246
|
if (objects.length === 0) {
|
|
248
|
-
throw new Error(
|
|
247
|
+
throw new Error(`${sourceLabel} did not return a JSON object`);
|
|
249
248
|
}
|
|
250
249
|
let firstParsedObject = null;
|
|
251
250
|
for (const objectText of objects) {
|
|
@@ -262,10 +261,56 @@ function tryExtractJsonObject(text, expectedRequestId) {
|
|
|
262
261
|
continue;
|
|
263
262
|
}
|
|
264
263
|
}
|
|
265
|
-
if (firstParsedObject) {
|
|
264
|
+
if (firstParsedObject && !expectedRequestId) {
|
|
266
265
|
return firstParsedObject;
|
|
267
266
|
}
|
|
268
|
-
|
|
267
|
+
if (firstParsedObject && expectedRequestId) {
|
|
268
|
+
throw new Error(`${sourceLabel} returned mismatched request_id`);
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`${sourceLabel} did not return a parseable JSON object`);
|
|
271
|
+
}
|
|
272
|
+
function collectRelevantTranscriptTexts(messages) {
|
|
273
|
+
return [...messages]
|
|
274
|
+
.reverse()
|
|
275
|
+
.map((message) => ({
|
|
276
|
+
role: extractMessageRole(message),
|
|
277
|
+
text: extractMessageText(message),
|
|
278
|
+
}))
|
|
279
|
+
.filter((entry) => Boolean(entry.text))
|
|
280
|
+
.filter((entry) => entry.role === 'assistant' || entry.role === 'unknown');
|
|
281
|
+
}
|
|
282
|
+
function extractJsonObjectFromMessages(messages, sourceLabel, expectedRequestId) {
|
|
283
|
+
const relevantMessages = collectRelevantTranscriptTexts(messages);
|
|
284
|
+
let sawMismatchedRequestId = false;
|
|
285
|
+
let sawJsonLikeOutput = false;
|
|
286
|
+
for (const entry of relevantMessages) {
|
|
287
|
+
try {
|
|
288
|
+
return tryExtractJsonObject(entry.text, sourceLabel, expectedRequestId);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
292
|
+
if (message.includes('mismatched request_id')) {
|
|
293
|
+
sawMismatchedRequestId = true;
|
|
294
|
+
sawJsonLikeOutput = true;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (message.includes('parseable JSON object')) {
|
|
298
|
+
sawJsonLikeOutput = true;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (message.includes('did not return a JSON object')) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (sawMismatchedRequestId) {
|
|
308
|
+
throw new Error(`${sourceLabel} returned mismatched request_id`);
|
|
309
|
+
}
|
|
310
|
+
if (sawJsonLikeOutput) {
|
|
311
|
+
throw new Error(`${sourceLabel} did not return a parseable JSON object`);
|
|
312
|
+
}
|
|
313
|
+
throw new Error(`${sourceLabel} did not return a JSON object`);
|
|
269
314
|
}
|
|
270
315
|
function makePlannerRequestId() {
|
|
271
316
|
return `timeline-plan-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
|
|
@@ -344,64 +389,295 @@ function buildTimelineReasonerSystemPrompt() {
|
|
|
344
389
|
'26. 如果当前会话仍处于粘连窗口内,优先把当前状态理解为还在和用户继续刚才的话题、思考上一轮内容或准备回应,而不是立即跳到脱离当前会话的生活片段。',
|
|
345
390
|
].join('\n');
|
|
346
391
|
}
|
|
347
|
-
function
|
|
348
|
-
return
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
392
|
+
function setClock(parts, hour, minute = 0, second = 0) {
|
|
393
|
+
return {
|
|
394
|
+
...parts,
|
|
395
|
+
hour,
|
|
396
|
+
minute,
|
|
397
|
+
second,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function normalizeHourFromQuery(query) {
|
|
401
|
+
const digitMatch = query.match(/(\d{1,2})点/);
|
|
402
|
+
let hour = digitMatch ? Number(digitMatch[1]) : null;
|
|
403
|
+
if (hour === null) {
|
|
404
|
+
const chineseHourMap = {
|
|
405
|
+
零: 0,
|
|
406
|
+
一: 1,
|
|
407
|
+
二: 2,
|
|
408
|
+
两: 2,
|
|
409
|
+
三: 3,
|
|
410
|
+
四: 4,
|
|
411
|
+
五: 5,
|
|
412
|
+
六: 6,
|
|
413
|
+
七: 7,
|
|
414
|
+
八: 8,
|
|
415
|
+
九: 9,
|
|
416
|
+
十: 10,
|
|
417
|
+
十一: 11,
|
|
418
|
+
十二: 12,
|
|
419
|
+
};
|
|
420
|
+
const chineseMatch = query.match(/(十一|十二|十|零|一|二|两|三|四|五|六|七|八|九)点/);
|
|
421
|
+
if (chineseMatch) {
|
|
422
|
+
hour = chineseHourMap[chineseMatch[1]] ?? null;
|
|
352
423
|
}
|
|
353
|
-
|
|
354
|
-
|
|
424
|
+
}
|
|
425
|
+
if (hour === null)
|
|
426
|
+
return null;
|
|
427
|
+
if ((/昨晚|晚上|傍晚|夜里/.test(query)) && hour < 12) {
|
|
428
|
+
return hour === 12 ? 12 : hour + 12;
|
|
429
|
+
}
|
|
430
|
+
if ((/下午/.test(query)) && hour < 12) {
|
|
431
|
+
return hour === 12 ? 12 : hour + 12;
|
|
432
|
+
}
|
|
433
|
+
return hour;
|
|
434
|
+
}
|
|
435
|
+
function buildFallbackTimelineQueryPlan(input, anchor) {
|
|
436
|
+
const query = String(input.query || '').trim();
|
|
437
|
+
const anchorParts = (0, time_utils_1.parseTimestampParts)(anchor.now);
|
|
438
|
+
if (!anchorParts) {
|
|
439
|
+
return {
|
|
440
|
+
schema_version: '1.0',
|
|
441
|
+
target_time_range: 'now',
|
|
442
|
+
summary: 'Fallback planner defaulted to now because anchor.now was not parseable.',
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (/昨晚|昨天|上周|前天/.test(query) && /点/.test(query)) {
|
|
446
|
+
const targetHour = normalizeHourFromQuery(query) ?? 20;
|
|
447
|
+
const targetDay = (0, time_utils_1.addHours)(anchorParts, -24);
|
|
448
|
+
return {
|
|
449
|
+
schema_version: '1.0',
|
|
450
|
+
target_time_range: 'past_point',
|
|
451
|
+
normalized_point: (0, time_utils_1.formatTimestamp)(setClock(targetDay, targetHour, 0, 0)),
|
|
452
|
+
summary: 'Fallback planner normalized the query into a concrete past point.',
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (/最近|这几天|昨晚|今天都|昨天都/.test(query)) {
|
|
456
|
+
if (/昨晚/.test(query)) {
|
|
457
|
+
const targetDay = (0, time_utils_1.addHours)(anchorParts, -24);
|
|
458
|
+
return {
|
|
459
|
+
schema_version: '1.0',
|
|
460
|
+
target_time_range: 'past_range',
|
|
461
|
+
normalized_start: (0, time_utils_1.formatTimestamp)(setClock(targetDay, 18, 0, 0)),
|
|
462
|
+
normalized_end: (0, time_utils_1.formatTimestamp)(setClock(targetDay, 23, 59, 59)),
|
|
463
|
+
summary: 'Fallback planner normalized the query into last night\'s evening range.',
|
|
464
|
+
};
|
|
355
465
|
}
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
466
|
+
const recentStart = (0, time_utils_1.addHours)(anchorParts, -24 * 7);
|
|
467
|
+
return {
|
|
468
|
+
schema_version: '1.0',
|
|
469
|
+
target_time_range: 'past_range',
|
|
470
|
+
normalized_start: (0, time_utils_1.formatTimestamp)(setClock(recentStart, 0, 0, 0)),
|
|
471
|
+
normalized_end: anchor.now,
|
|
472
|
+
summary: 'Fallback planner normalized the query into a recent past range.',
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
schema_version: '1.0',
|
|
477
|
+
target_time_range: 'now',
|
|
478
|
+
summary: 'Fallback planner normalized the query into the current moment.',
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function parseTimestampMs(timestamp) {
|
|
482
|
+
if (!timestamp)
|
|
483
|
+
return Number.NaN;
|
|
484
|
+
const parsed = Date.parse(timestamp);
|
|
485
|
+
return Number.isNaN(parsed) ? Number.NaN : parsed;
|
|
486
|
+
}
|
|
487
|
+
function selectLatestFact(facts) {
|
|
488
|
+
return [...facts]
|
|
489
|
+
.sort((left, right) => parseTimestampMs(right.timestamp) - parseTimestampMs(left.timestamp))[0] || null;
|
|
490
|
+
}
|
|
491
|
+
function scorePastRangeFact(fact, query) {
|
|
492
|
+
let score = parseTimestampMs(fact.timestamp);
|
|
493
|
+
const haystack = `${fact.location} ${fact.action} ${(fact.emotion_tags || []).join(' ')}`;
|
|
494
|
+
if (/有趣|好玩|开心|趣事/.test(query) && /(朋友|球|篮球|烧烤|聊天|公园|运动|开心)/.test(haystack)) {
|
|
495
|
+
score += 1000 * 60 * 60 * 24 * 30;
|
|
496
|
+
}
|
|
497
|
+
if (/昨晚/.test(query)) {
|
|
498
|
+
const parts = (0, time_utils_1.parseTimestampParts)(fact.timestamp);
|
|
499
|
+
if (parts && parts.hour >= 18 && parts.hour <= 23) {
|
|
500
|
+
score += 1000 * 60 * 60 * 12;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return score;
|
|
504
|
+
}
|
|
505
|
+
function selectFallbackFact(collector) {
|
|
506
|
+
const facts = collector.candidate_facts || [];
|
|
507
|
+
if (facts.length === 0)
|
|
508
|
+
return null;
|
|
509
|
+
if (collector.window.query_range === 'now') {
|
|
510
|
+
return selectLatestFact(facts);
|
|
511
|
+
}
|
|
512
|
+
if (collector.window.query_range === 'past_point') {
|
|
513
|
+
const query = String(collector.request.user_query || '');
|
|
514
|
+
const explicitHour = normalizeHourFromQuery(query);
|
|
515
|
+
if (explicitHour === null) {
|
|
516
|
+
return selectLatestFact(facts);
|
|
517
|
+
}
|
|
518
|
+
return [...facts].sort((left, right) => {
|
|
519
|
+
const leftParts = (0, time_utils_1.parseTimestampParts)(left.timestamp);
|
|
520
|
+
const rightParts = (0, time_utils_1.parseTimestampParts)(right.timestamp);
|
|
521
|
+
const leftDistance = leftParts ? Math.abs((leftParts.hour * 60 + leftParts.minute) - explicitHour * 60) : Number.MAX_SAFE_INTEGER;
|
|
522
|
+
const rightDistance = rightParts ? Math.abs((rightParts.hour * 60 + rightParts.minute) - explicitHour * 60) : Number.MAX_SAFE_INTEGER;
|
|
523
|
+
return leftDistance - rightDistance || (parseTimestampMs(right.timestamp) - parseTimestampMs(left.timestamp));
|
|
524
|
+
})[0] || null;
|
|
525
|
+
}
|
|
526
|
+
const query = String(collector.request.user_query || '');
|
|
527
|
+
return [...facts].sort((left, right) => scorePastRangeFact(right, query) - scorePastRangeFact(left, query))[0] || null;
|
|
528
|
+
}
|
|
529
|
+
function buildFallbackTimeInterpretation(collector, selectedFact) {
|
|
530
|
+
if (collector.window.query_range === 'now') {
|
|
531
|
+
return {
|
|
532
|
+
normalized_kind: 'now',
|
|
533
|
+
match_strategy: selectedFact ? 'continuation' : 'generated',
|
|
534
|
+
summary: 'Fallback reasoner treated the request as a current-moment query.',
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (collector.window.query_range === 'past_point') {
|
|
538
|
+
const query = String(collector.request.user_query || '');
|
|
539
|
+
const hour = normalizeHourFromQuery(query);
|
|
540
|
+
const anchorParts = (0, time_utils_1.parseTimestampParts)(collector.anchor.now);
|
|
541
|
+
const targetDay = anchorParts ? (0, time_utils_1.addHours)(anchorParts, -24) : null;
|
|
542
|
+
return {
|
|
543
|
+
normalized_kind: 'point',
|
|
544
|
+
normalized_point: targetDay && hour !== null ? (0, time_utils_1.formatTimestamp)(setClock(targetDay, hour, 0, 0)) : selectedFact?.timestamp,
|
|
545
|
+
match_strategy: selectedFact ? 'continuation' : 'generated',
|
|
546
|
+
summary: 'Fallback reasoner treated the request as a concrete past point query.',
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
normalized_kind: 'range',
|
|
551
|
+
normalized_start: collector.window.start,
|
|
552
|
+
normalized_end: collector.window.end,
|
|
553
|
+
match_strategy: selectedFact ? 'range_summary' : 'generated',
|
|
554
|
+
summary: 'Fallback reasoner treated the request as a past range query.',
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function buildFallbackReasonerOutput(collector, error) {
|
|
558
|
+
const selectedFact = selectFallbackFact(collector);
|
|
559
|
+
const fallbackReason = error instanceof Error ? error.message : String(error);
|
|
560
|
+
if (!selectedFact) {
|
|
561
|
+
return {
|
|
562
|
+
schema_version: '1.0',
|
|
563
|
+
request_id: collector.request_id,
|
|
564
|
+
request_type: collector.window.query_range,
|
|
565
|
+
time_interpretation: buildFallbackTimeInterpretation(collector, null),
|
|
566
|
+
decision: {
|
|
567
|
+
action: 'return_empty',
|
|
568
|
+
should_write_canon: false,
|
|
569
|
+
},
|
|
570
|
+
continuity: {
|
|
571
|
+
judged: collector.window.query_range !== 'past_range',
|
|
572
|
+
is_continuing: false,
|
|
573
|
+
reason: `Fallback reasoner could not find a reusable canon fact after: ${fallbackReason}`,
|
|
574
|
+
},
|
|
575
|
+
rationale: {
|
|
576
|
+
summary: 'Fallback reasoner returned empty because no reusable canon fact was available.',
|
|
577
|
+
hard_fact_basis: collector.hard_facts.sessions_history.slice(0, 2),
|
|
578
|
+
canon_basis: [],
|
|
579
|
+
persona_basis: [],
|
|
580
|
+
constraint_basis: [],
|
|
581
|
+
uncertainty: fallbackReason,
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const selectedParts = (0, time_utils_1.parseTimestampParts)(selectedFact.timestamp);
|
|
586
|
+
const targetHour = normalizeHourFromQuery(String(collector.request.user_query || ''));
|
|
587
|
+
const isPastPointContinuation = Boolean(collector.window.query_range === 'past_point'
|
|
588
|
+
&& selectedParts
|
|
589
|
+
&& targetHour !== null
|
|
590
|
+
&& Math.abs((selectedParts.hour * 60 + selectedParts.minute) - targetHour * 60) <= 90);
|
|
591
|
+
return {
|
|
592
|
+
schema_version: '1.0',
|
|
593
|
+
request_id: collector.request_id,
|
|
594
|
+
request_type: collector.window.query_range,
|
|
595
|
+
time_interpretation: buildFallbackTimeInterpretation(collector, selectedFact),
|
|
596
|
+
decision: {
|
|
597
|
+
action: 'reuse_existing_fact',
|
|
598
|
+
selected_fact_id: selectedFact.fact_id,
|
|
599
|
+
should_write_canon: false,
|
|
600
|
+
},
|
|
601
|
+
continuity: {
|
|
602
|
+
judged: collector.window.query_range !== 'past_range',
|
|
603
|
+
is_continuing: collector.window.query_range === 'now' || isPastPointContinuation,
|
|
604
|
+
reason: 'Fallback reasoner reused the strongest available canon fact when the subagent result was unavailable.',
|
|
605
|
+
},
|
|
606
|
+
rationale: {
|
|
607
|
+
summary: 'Fallback reasoner reused an existing canon fact because the subagent result was unavailable or invalid.',
|
|
608
|
+
hard_fact_basis: collector.hard_facts.sessions_history.slice(0, 2),
|
|
609
|
+
canon_basis: [selectedFact.fact_id],
|
|
610
|
+
persona_basis: [],
|
|
611
|
+
constraint_basis: [],
|
|
612
|
+
uncertainty: fallbackReason,
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function createTimelineQueryPlanner(pluginApi, toolContext, runtimeConfig) {
|
|
617
|
+
return async (input, anchor) => {
|
|
359
618
|
try {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
extraSystemPrompt: buildTimelineQueryPlannerSystemPrompt(),
|
|
364
|
-
deliver: false,
|
|
365
|
-
idempotencyKey: requestId,
|
|
366
|
-
});
|
|
367
|
-
const waitResult = await subagentRuntime.waitForRun({
|
|
368
|
-
runId: runResult.runId,
|
|
369
|
-
timeoutMs: runtimeConfig.reasonerTimeoutMs,
|
|
370
|
-
});
|
|
371
|
-
if (waitResult.status === 'timeout') {
|
|
372
|
-
throw new Error('Timeline query planner returned no decision');
|
|
373
|
-
}
|
|
374
|
-
if (waitResult.status === 'error') {
|
|
375
|
-
throw new Error(waitResult.error || 'Timeline query planner returned no decision');
|
|
619
|
+
const subagentRuntime = pluginApi.runtime?.subagent;
|
|
620
|
+
if (!subagentRuntime?.run || !subagentRuntime.waitForRun || !subagentRuntime.getSessionMessages) {
|
|
621
|
+
throw new Error('Timeline query planner dependency missing');
|
|
376
622
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
limit: runtimeConfig.reasonerMessageLimit,
|
|
380
|
-
});
|
|
381
|
-
const assistantText = extractLatestAssistantText(session.messages || []);
|
|
382
|
-
const jsonText = tryExtractJsonObject(assistantText, requestId);
|
|
383
|
-
const parsed = JSON.parse(jsonText);
|
|
384
|
-
if (parsed.request_id && parsed.request_id !== requestId) {
|
|
385
|
-
throw new Error('Timeline query planner returned mismatched request_id');
|
|
386
|
-
}
|
|
387
|
-
if (!['now', 'past_point', 'past_range'].includes(parsed.target_time_range)) {
|
|
388
|
-
throw new Error('Timeline query planner returned an invalid target_time_range');
|
|
623
|
+
if (!String(input.query || '').trim()) {
|
|
624
|
+
throw new Error('Timeline query planner dependency missing query');
|
|
389
625
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
626
|
+
const requestId = makePlannerRequestId();
|
|
627
|
+
const baseSessionKey = toolContext.sessionKey || `plugin:${runtimeConfig.reasonerSessionPrefix}`;
|
|
628
|
+
const plannerSessionKey = `${baseSessionKey}:${runtimeConfig.reasonerSessionPrefix}:planner:${requestId}`;
|
|
393
629
|
try {
|
|
394
|
-
await subagentRuntime.
|
|
630
|
+
const runResult = await subagentRuntime.run({
|
|
395
631
|
sessionKey: plannerSessionKey,
|
|
396
|
-
|
|
632
|
+
message: buildTimelineQueryPlannerMessage(input, anchor, requestId),
|
|
633
|
+
extraSystemPrompt: buildTimelineQueryPlannerSystemPrompt(),
|
|
634
|
+
deliver: false,
|
|
635
|
+
idempotencyKey: requestId,
|
|
397
636
|
});
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
error: error instanceof Error ? error.message : String(error),
|
|
637
|
+
const waitResult = await subagentRuntime.waitForRun({
|
|
638
|
+
runId: runResult.runId,
|
|
639
|
+
timeoutMs: runtimeConfig.reasonerTimeoutMs,
|
|
402
640
|
});
|
|
641
|
+
if (waitResult.status === 'timeout') {
|
|
642
|
+
throw new Error('Timeline query planner returned no decision');
|
|
643
|
+
}
|
|
644
|
+
if (waitResult.status === 'error') {
|
|
645
|
+
throw new Error(waitResult.error || 'Timeline query planner returned no decision');
|
|
646
|
+
}
|
|
647
|
+
const session = await subagentRuntime.getSessionMessages({
|
|
648
|
+
sessionKey: plannerSessionKey,
|
|
649
|
+
limit: runtimeConfig.reasonerMessageLimit,
|
|
650
|
+
});
|
|
651
|
+
const jsonText = extractJsonObjectFromMessages(session.messages || [], 'Timeline query planner', requestId);
|
|
652
|
+
const parsed = JSON.parse(jsonText);
|
|
653
|
+
if (parsed.request_id && parsed.request_id !== requestId) {
|
|
654
|
+
throw new Error('Timeline query planner returned mismatched request_id');
|
|
655
|
+
}
|
|
656
|
+
if (!['now', 'past_point', 'past_range'].includes(parsed.target_time_range)) {
|
|
657
|
+
throw new Error('Timeline query planner returned an invalid target_time_range');
|
|
658
|
+
}
|
|
659
|
+
return parsed;
|
|
660
|
+
}
|
|
661
|
+
finally {
|
|
662
|
+
try {
|
|
663
|
+
await subagentRuntime.deleteSession?.({
|
|
664
|
+
sessionKey: plannerSessionKey,
|
|
665
|
+
deleteTranscript: true,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
pluginApi.logger?.debug?.('timeline query planner session cleanup skipped', {
|
|
670
|
+
error: error instanceof Error ? error.message : String(error),
|
|
671
|
+
});
|
|
672
|
+
}
|
|
403
673
|
}
|
|
404
674
|
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
pluginApi.logger?.warn?.('timeline query planner fallback engaged', {
|
|
677
|
+
error: error instanceof Error ? error.message : String(error),
|
|
678
|
+
});
|
|
679
|
+
return buildFallbackTimelineQueryPlan(input, anchor);
|
|
680
|
+
}
|
|
405
681
|
};
|
|
406
682
|
}
|
|
407
683
|
function buildTimelineReasonerMessage(collector) {
|
|
@@ -474,50 +750,57 @@ function buildTimelineReasonerMessage(collector) {
|
|
|
474
750
|
}
|
|
475
751
|
function createSubagentReasoner(pluginApi, toolContext, runtimeConfig) {
|
|
476
752
|
return async (collector) => {
|
|
477
|
-
const subagentRuntime = pluginApi.runtime?.subagent;
|
|
478
|
-
if (!subagentRuntime?.run || !subagentRuntime.waitForRun || !subagentRuntime.getSessionMessages) {
|
|
479
|
-
throw new Error('Timeline reasoner dependency missing');
|
|
480
|
-
}
|
|
481
|
-
const baseSessionKey = toolContext.sessionKey || `plugin:${runtimeConfig.reasonerSessionPrefix}`;
|
|
482
|
-
const reasonerSessionKey = `${baseSessionKey}:${runtimeConfig.reasonerSessionPrefix}:${collector.request_id}`;
|
|
483
753
|
try {
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
extraSystemPrompt: buildTimelineReasonerSystemPrompt(),
|
|
488
|
-
deliver: false,
|
|
489
|
-
idempotencyKey: collector.request_id,
|
|
490
|
-
});
|
|
491
|
-
const waitResult = await subagentRuntime.waitForRun({
|
|
492
|
-
runId: runResult.runId,
|
|
493
|
-
timeoutMs: runtimeConfig.reasonerTimeoutMs,
|
|
494
|
-
});
|
|
495
|
-
if (waitResult.status === 'timeout') {
|
|
496
|
-
throw new Error('Timeline reasoner returned no decision');
|
|
497
|
-
}
|
|
498
|
-
if (waitResult.status === 'error') {
|
|
499
|
-
throw new Error(waitResult.error || 'Timeline reasoner returned no decision');
|
|
754
|
+
const subagentRuntime = pluginApi.runtime?.subagent;
|
|
755
|
+
if (!subagentRuntime?.run || !subagentRuntime.waitForRun || !subagentRuntime.getSessionMessages) {
|
|
756
|
+
throw new Error('Timeline reasoner dependency missing');
|
|
500
757
|
}
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
limit: runtimeConfig.reasonerMessageLimit,
|
|
504
|
-
});
|
|
505
|
-
const assistantText = extractLatestAssistantText(session.messages || []);
|
|
506
|
-
const jsonText = tryExtractJsonObject(assistantText, collector.request_id);
|
|
507
|
-
return JSON.parse(jsonText);
|
|
508
|
-
}
|
|
509
|
-
finally {
|
|
758
|
+
const baseSessionKey = toolContext.sessionKey || `plugin:${runtimeConfig.reasonerSessionPrefix}`;
|
|
759
|
+
const reasonerSessionKey = `${baseSessionKey}:${runtimeConfig.reasonerSessionPrefix}:${collector.request_id}`;
|
|
510
760
|
try {
|
|
511
|
-
await subagentRuntime.
|
|
761
|
+
const runResult = await subagentRuntime.run({
|
|
512
762
|
sessionKey: reasonerSessionKey,
|
|
513
|
-
|
|
763
|
+
message: buildTimelineReasonerMessage(collector),
|
|
764
|
+
extraSystemPrompt: buildTimelineReasonerSystemPrompt(),
|
|
765
|
+
deliver: false,
|
|
766
|
+
idempotencyKey: collector.request_id,
|
|
514
767
|
});
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
768
|
+
const waitResult = await subagentRuntime.waitForRun({
|
|
769
|
+
runId: runResult.runId,
|
|
770
|
+
timeoutMs: runtimeConfig.reasonerTimeoutMs,
|
|
771
|
+
});
|
|
772
|
+
if (waitResult.status === 'timeout') {
|
|
773
|
+
throw new Error('Timeline reasoner returned no decision');
|
|
774
|
+
}
|
|
775
|
+
if (waitResult.status === 'error') {
|
|
776
|
+
throw new Error(waitResult.error || 'Timeline reasoner returned no decision');
|
|
777
|
+
}
|
|
778
|
+
const session = await subagentRuntime.getSessionMessages({
|
|
779
|
+
sessionKey: reasonerSessionKey,
|
|
780
|
+
limit: runtimeConfig.reasonerMessageLimit,
|
|
519
781
|
});
|
|
782
|
+
const jsonText = extractJsonObjectFromMessages(session.messages || [], 'Timeline reasoner', collector.request_id);
|
|
783
|
+
return JSON.parse(jsonText);
|
|
520
784
|
}
|
|
785
|
+
finally {
|
|
786
|
+
try {
|
|
787
|
+
await subagentRuntime.deleteSession?.({
|
|
788
|
+
sessionKey: reasonerSessionKey,
|
|
789
|
+
deleteTranscript: true,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
catch (error) {
|
|
793
|
+
pluginApi.logger?.debug?.('timeline reasoner session cleanup skipped', {
|
|
794
|
+
error: error instanceof Error ? error.message : String(error),
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
pluginApi.logger?.warn?.('timeline reasoner fallback engaged', {
|
|
801
|
+
error: error instanceof Error ? error.message : String(error),
|
|
802
|
+
});
|
|
803
|
+
return buildFallbackReasonerOutput(collector, error);
|
|
521
804
|
}
|
|
522
805
|
};
|
|
523
806
|
}
|
|
@@ -124,6 +124,14 @@ function classifyTimelineResolveError(error) {
|
|
|
124
124
|
|| message.includes('Timeline query planner returned no decision')) {
|
|
125
125
|
return 'REASONER_UNAVAILABLE';
|
|
126
126
|
}
|
|
127
|
+
if (message.includes('Timeline reasoner returned mismatched request_id')
|
|
128
|
+
|| message.includes('Timeline reasoner did not return a JSON object')
|
|
129
|
+
|| message.includes('Timeline reasoner did not return a parseable JSON object')
|
|
130
|
+
|| message.includes('Timeline query planner returned mismatched request_id')
|
|
131
|
+
|| message.includes('Timeline query planner did not return a JSON object')
|
|
132
|
+
|| message.includes('Timeline query planner did not return a parseable JSON object')) {
|
|
133
|
+
return 'INVALID_REASONER_OUTPUT';
|
|
134
|
+
}
|
|
127
135
|
if (message.includes('LLM generation'))
|
|
128
136
|
return 'GENERATION_UNAVAILABLE';
|
|
129
137
|
if (message.includes('Generated draft'))
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
2
|
"name": "stella-timeline-plugin",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "Native OpenClaw timeline plugin with a canonical timeline_resolve tool, bundled skill routing, and guarded append-only writes.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"openclaw",
|
|
7
|
-
"skill",
|
|
8
|
-
"timeline",
|
|
9
|
-
"memory",
|
|
10
|
-
"ai-agent",
|
|
11
|
-
"episode",
|
|
12
|
-
"persona"
|
|
13
|
-
],
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Native OpenClaw timeline plugin with a canonical timeline_resolve tool, bundled skill routing, and guarded append-only writes.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"openclaw",
|
|
7
|
+
"skill",
|
|
8
|
+
"timeline",
|
|
9
|
+
"memory",
|
|
10
|
+
"ai-agent",
|
|
11
|
+
"episode",
|
|
12
|
+
"persona"
|
|
13
|
+
],
|
|
14
14
|
"author": "tao.zang",
|
|
15
|
-
"license": "MIT-0",
|
|
16
|
-
"repository": {
|
|
17
|
-
"type": "git",
|
|
18
|
-
"url": "https://github.com/tower1229/Her"
|
|
19
|
-
},
|
|
15
|
+
"license": "MIT-0",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/tower1229/Her"
|
|
19
|
+
},
|
|
20
20
|
"homepage": "https://github.com/tower1229/Her#readme",
|
|
21
21
|
"bugs": {
|
|
22
22
|
"url": "https://github.com/tower1229/Her/issues"
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
"engines": {
|
|
25
25
|
"node": ">=22.0.0"
|
|
26
26
|
},
|
|
27
|
-
"scripts": {
|
|
27
|
+
"scripts": {
|
|
28
28
|
"prebuild": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
29
|
-
"build": "tsc -p tsconfig.build.json",
|
|
30
|
-
"typecheck": "tsc --noEmit",
|
|
31
|
-
"test": "node node_modules/jest/bin/jest.js --runInBand",
|
|
32
|
-
"test:unit": "node node_modules/jest/bin/jest.js --runInBand",
|
|
29
|
+
"build": "tsc -p tsconfig.build.json",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "node node_modules/jest/bin/jest.js --runInBand",
|
|
32
|
+
"test:unit": "node node_modules/jest/bin/jest.js --runInBand",
|
|
33
33
|
"test:smoke": "node ./scripts/run-openclaw-smoke.mjs",
|
|
34
34
|
"test:live-experience": "node ./scripts/run-openclaw-live-e2e.mjs",
|
|
35
35
|
"migrate:memory": "node ./scripts/migrate-existing-memory.mjs --apply",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@types/jest": "^30.0.0",
|
|
51
|
-
"@types/node": "^20.x",
|
|
52
|
-
"jest": "^29.x",
|
|
53
|
-
"ts-jest": "^29.x",
|
|
54
|
-
"typescript": "^5.x"
|
|
55
|
-
},
|
|
56
|
-
"main": "dist/index.js",
|
|
50
|
+
"@types/jest": "^30.0.0",
|
|
51
|
+
"@types/node": "^20.x",
|
|
52
|
+
"jest": "^29.x",
|
|
53
|
+
"ts-jest": "^29.x",
|
|
54
|
+
"typescript": "^5.x"
|
|
55
|
+
},
|
|
56
|
+
"main": "dist/index.js",
|
|
57
57
|
"files": [
|
|
58
58
|
"dist",
|
|
59
59
|
"bin",
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"README.md",
|
|
66
66
|
"README_ZH.md"
|
|
67
67
|
],
|
|
68
|
-
"openclaw": {
|
|
69
|
-
"extensions": [
|
|
70
|
-
"./dist/index.js"
|
|
71
|
-
]
|
|
72
|
-
}
|
|
73
|
-
}
|
|
68
|
+
"openclaw": {
|
|
69
|
+
"extensions": [
|
|
70
|
+
"./dist/index.js"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|