agenr 0.8.36 → 0.8.38
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/CHANGELOG.md +37 -0
- package/dist/openclaw-plugin/index.d.ts +11 -1
- package/dist/openclaw-plugin/index.js +276 -152
- package/openclaw.plugin.json +23 -0
- package/package.json +13 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.38] - 2026-02-24
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Handoff log line now shows model ID string instead of [object Object]
|
|
7
|
+
- Upgraded handoff retirement and browse debug logs from logger.debug to
|
|
8
|
+
console.log for production visibility
|
|
9
|
+
- Handoff transcript now strips OpenClaw/agenr injected context (memory
|
|
10
|
+
blocks, signals, conversation metadata, timestamp prefixes) before
|
|
11
|
+
sending to the LLM, preventing the summarizer from summarizing its own
|
|
12
|
+
metadata (#235)
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Opt-in `handoff.includeBackground` config flag for handoff summarizer: when
|
|
16
|
+
enabled, prior session messages are included as background context with strong
|
|
17
|
+
section headers so the LLM can orient without blending stale facts into the
|
|
18
|
+
current session summary (#235)
|
|
19
|
+
- New system prompt variant with anti-hallucination instructions for background
|
|
20
|
+
context mode ("BACKGROUND CONTEXT (DO NOT SUMMARIZE)" / "SUMMARIZE THIS
|
|
21
|
+
SESSION ONLY" section headers)
|
|
22
|
+
- Optional `handoff.logDir` config: when set, writes the full LLM request
|
|
23
|
+
transcript and response to files for prompt tuning and debugging (#235)
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Default handoff behavior unchanged: current session only, no prior messages
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
- All temporary [AGENR-PROBE] debug logging from openclaw-plugin (replaced with
|
|
30
|
+
clean operational logs where needed)
|
|
31
|
+
|
|
32
|
+
## [0.8.37] - 2026-02-24
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- openclaw-plugin: await runHandoffForSession in session_start handler instead of void
|
|
36
|
+
fire-and-forget; webchat /new goes through sessions.reset RPC which does not trigger
|
|
37
|
+
before_reset, so session_start is the only hook that fires on that path - making it
|
|
38
|
+
void meant the LLM summary was always dropped (closes #232)
|
|
39
|
+
|
|
3
40
|
## [0.8.36] - 2026-02-24
|
|
4
41
|
|
|
5
42
|
### Fixed
|
|
@@ -76,12 +76,14 @@ type HandoffMessage = {
|
|
|
76
76
|
content: string;
|
|
77
77
|
timestamp: string;
|
|
78
78
|
};
|
|
79
|
+
declare function stripInjectedContext(text: string): string;
|
|
79
80
|
declare function getBaseSessionPath(filePath: string): string;
|
|
80
81
|
declare function readSessionsJson(sessionsDir: string): Promise<Record<string, unknown>>;
|
|
81
82
|
declare function readAndParseSessionJsonl(sessionFile: string): Promise<unknown[]>;
|
|
82
83
|
declare function getSurfaceForSessionFile(sessionFilePath: string, sessionsJson: Record<string, unknown>): string;
|
|
83
84
|
declare function readMessagesFromJsonl(filePath: string): Promise<HandoffMessage[]>;
|
|
84
85
|
declare function findPriorResetFile(sessionsDir: string, currentSessionFile: string): Promise<string | null>;
|
|
86
|
+
declare function buildTranscript(messages: HandoffMessage[], surface: string): string;
|
|
85
87
|
declare function buildMergedTranscript(priorMessages: HandoffMessage[], priorSurface: string, currentMessages: HandoffMessage[], currentSurface: string): string;
|
|
86
88
|
declare function capTranscriptLength(params: {
|
|
87
89
|
priorMessages: HandoffMessage[];
|
|
@@ -90,7 +92,7 @@ declare function capTranscriptLength(params: {
|
|
|
90
92
|
currentSurface: string;
|
|
91
93
|
maxChars: number;
|
|
92
94
|
}): string;
|
|
93
|
-
declare function summarizeSessionForHandoff(currentRawMessages: BeforeResetEvent["messages"], sessionsDir: string, currentSessionFile: string, logger: PluginApi["logger"], streamSimpleImpl?: StreamSimpleFn): Promise<string | null>;
|
|
95
|
+
declare function summarizeSessionForHandoff(currentRawMessages: BeforeResetEvent["messages"], sessionsDir: string, currentSessionFile: string, logger: PluginApi["logger"], includeBackground: boolean, streamSimpleImpl?: StreamSimpleFn, logEnabled?: boolean, logDir?: string): Promise<string | null>;
|
|
94
96
|
declare function runHandoffForSession(opts: {
|
|
95
97
|
messages: unknown[];
|
|
96
98
|
sessionFile: string | null;
|
|
@@ -102,8 +104,12 @@ declare function runHandoffForSession(opts: {
|
|
|
102
104
|
defaultProject: string | undefined;
|
|
103
105
|
storeConfig: Record<string, unknown>;
|
|
104
106
|
sessionsDir: string;
|
|
107
|
+
includeBackground?: boolean;
|
|
108
|
+
logEnabled?: boolean;
|
|
109
|
+
logDir?: string;
|
|
105
110
|
logger: PluginLogger | undefined;
|
|
106
111
|
source: "before_reset" | "command" | "session_start";
|
|
112
|
+
dbPath?: string;
|
|
107
113
|
}): Promise<void>;
|
|
108
114
|
declare const plugin: {
|
|
109
115
|
id: string;
|
|
@@ -118,9 +124,13 @@ declare const __testing: {
|
|
|
118
124
|
getBaseSessionPath: typeof getBaseSessionPath;
|
|
119
125
|
getSurfaceForSessionFile: typeof getSurfaceForSessionFile;
|
|
120
126
|
readMessagesFromJsonl: typeof readMessagesFromJsonl;
|
|
127
|
+
stripInjectedContext: typeof stripInjectedContext;
|
|
121
128
|
findPriorResetFile: typeof findPriorResetFile;
|
|
129
|
+
buildTranscript: typeof buildTranscript;
|
|
122
130
|
buildMergedTranscript: typeof buildMergedTranscript;
|
|
123
131
|
capTranscriptLength: typeof capTranscriptLength;
|
|
132
|
+
HANDOFF_SUMMARY_SYSTEM_PROMPT: string;
|
|
133
|
+
HANDOFF_SUMMARY_SYSTEM_PROMPT_WITH_BACKGROUND: string;
|
|
124
134
|
summarizeSessionForHandoff: typeof summarizeSessionForHandoff;
|
|
125
135
|
runHandoffForSession: typeof runHandoffForSession;
|
|
126
136
|
};
|
|
@@ -37,7 +37,7 @@ function buildSpawnArgs(agenrPath) {
|
|
|
37
37
|
}
|
|
38
38
|
return { cmd: agenrPath, args: [] };
|
|
39
39
|
}
|
|
40
|
-
async function runRecall(agenrPath, budget, project, query, options) {
|
|
40
|
+
async function runRecall(agenrPath, budget, project, query, options, dbPath) {
|
|
41
41
|
return await new Promise((resolve) => {
|
|
42
42
|
let stdout = "";
|
|
43
43
|
let settled = false;
|
|
@@ -64,7 +64,8 @@ async function runRecall(agenrPath, budget, project, query, options) {
|
|
|
64
64
|
args.push(truncatedQuery);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
const
|
|
67
|
+
const finalArgs = dbPath ? [...args, "--db", dbPath] : args;
|
|
68
|
+
const child = spawn(spawnArgs.cmd, [...spawnArgs.args, ...finalArgs], {
|
|
68
69
|
stdio: ["ignore", "pipe", "ignore"]
|
|
69
70
|
});
|
|
70
71
|
const timer = setTimeout(() => {
|
|
@@ -592,7 +593,7 @@ function inferPlatformFromEntries(entries) {
|
|
|
592
593
|
}
|
|
593
594
|
return void 0;
|
|
594
595
|
}
|
|
595
|
-
async function runAgenrCommand(agenrPath, args, stdinPayload, timeoutMs = TOOL_TIMEOUT_MS) {
|
|
596
|
+
async function runAgenrCommand(agenrPath, args, stdinPayload, timeoutMs = TOOL_TIMEOUT_MS, dbPath) {
|
|
596
597
|
return await new Promise((resolve) => {
|
|
597
598
|
const resolvedAgenrPath = agenrPath.trim() || resolveAgenrPath();
|
|
598
599
|
const spawnArgs = buildSpawnArgs(resolvedAgenrPath);
|
|
@@ -608,7 +609,8 @@ async function runAgenrCommand(agenrPath, args, stdinPayload, timeoutMs = TOOL_T
|
|
|
608
609
|
clearTimeout(timer);
|
|
609
610
|
resolve(result);
|
|
610
611
|
};
|
|
611
|
-
const
|
|
612
|
+
const finalArgs = dbPath ? [...args, "--db", dbPath] : args;
|
|
613
|
+
const child = spawn2(spawnArgs.cmd, [...spawnArgs.args, ...finalArgs], {
|
|
612
614
|
stdio: ["pipe", "pipe", "pipe"]
|
|
613
615
|
});
|
|
614
616
|
const timer = setTimeout(() => {
|
|
@@ -634,7 +636,7 @@ async function runAgenrCommand(agenrPath, args, stdinPayload, timeoutMs = TOOL_T
|
|
|
634
636
|
child.stdin.end();
|
|
635
637
|
});
|
|
636
638
|
}
|
|
637
|
-
async function runRecallTool(agenrPath, params, defaultProject) {
|
|
639
|
+
async function runRecallTool(agenrPath, params, defaultProject, dbPath) {
|
|
638
640
|
const args = ["recall", "--json"];
|
|
639
641
|
const query = asString(params.query);
|
|
640
642
|
const context = asString(params.context);
|
|
@@ -671,7 +673,7 @@ async function runRecallTool(agenrPath, params, defaultProject) {
|
|
|
671
673
|
if (project) {
|
|
672
674
|
args.push("--project", project);
|
|
673
675
|
}
|
|
674
|
-
const result = await runAgenrCommand(agenrPath, args);
|
|
676
|
+
const result = await runAgenrCommand(agenrPath, args, void 0, void 0, dbPath);
|
|
675
677
|
if (result.timedOut) {
|
|
676
678
|
return {
|
|
677
679
|
content: [{ type: "text", text: "agenr_recall failed: command timed out" }]
|
|
@@ -708,7 +710,7 @@ async function runRecallTool(agenrPath, params, defaultProject) {
|
|
|
708
710
|
};
|
|
709
711
|
}
|
|
710
712
|
}
|
|
711
|
-
async function runStoreTool(agenrPath, params, pluginConfig, defaultProject) {
|
|
713
|
+
async function runStoreTool(agenrPath, params, pluginConfig, defaultProject, dbPath) {
|
|
712
714
|
const entries = Array.isArray(params.entries) ? params.entries : [];
|
|
713
715
|
const project = asString(params.project) || defaultProject;
|
|
714
716
|
const warn = resolveWarn(pluginConfig);
|
|
@@ -760,7 +762,7 @@ async function runStoreTool(agenrPath, params, pluginConfig, defaultProject) {
|
|
|
760
762
|
if (typeof dedupConfig?.threshold === "number") {
|
|
761
763
|
storeArgs.push("--dedup-threshold", String(dedupConfig.threshold));
|
|
762
764
|
}
|
|
763
|
-
const result = await runAgenrCommand(agenrPath, storeArgs, JSON.stringify(processedEntries));
|
|
765
|
+
const result = await runAgenrCommand(agenrPath, storeArgs, JSON.stringify(processedEntries), void 0, dbPath);
|
|
764
766
|
if (result.timedOut) {
|
|
765
767
|
return {
|
|
766
768
|
content: [{ type: "text", text: "agenr_store timed out" }]
|
|
@@ -782,7 +784,7 @@ async function runStoreTool(agenrPath, params, pluginConfig, defaultProject) {
|
|
|
782
784
|
details: { count: entries.length }
|
|
783
785
|
};
|
|
784
786
|
}
|
|
785
|
-
async function runExtractTool(agenrPath, params) {
|
|
787
|
+
async function runExtractTool(agenrPath, params, dbPath) {
|
|
786
788
|
const text = asString(params.text);
|
|
787
789
|
if (!text) {
|
|
788
790
|
return {
|
|
@@ -797,7 +799,7 @@ async function runExtractTool(agenrPath, params) {
|
|
|
797
799
|
try {
|
|
798
800
|
writeFileSync(tempFile, text, "utf8");
|
|
799
801
|
args.push(tempFile);
|
|
800
|
-
const extractResult = await runAgenrCommand(agenrPath, args, void 0, EXTRACT_TIMEOUT_MS);
|
|
802
|
+
const extractResult = await runAgenrCommand(agenrPath, args, void 0, EXTRACT_TIMEOUT_MS, dbPath);
|
|
801
803
|
if (extractResult.timedOut) {
|
|
802
804
|
return {
|
|
803
805
|
content: [{ type: "text", text: "agenr_extract failed: command timed out" }]
|
|
@@ -842,7 +844,7 @@ async function runExtractTool(agenrPath, params) {
|
|
|
842
844
|
}
|
|
843
845
|
}
|
|
844
846
|
}
|
|
845
|
-
const storeResult = await runAgenrCommand(agenrPath, ["store"], JSON.stringify(entries));
|
|
847
|
+
const storeResult = await runAgenrCommand(agenrPath, ["store"], JSON.stringify(entries), void 0, dbPath);
|
|
846
848
|
if (storeResult.timedOut) {
|
|
847
849
|
return { content: [{ type: "text", text: "agenr_extract store step timed out" }] };
|
|
848
850
|
}
|
|
@@ -874,7 +876,7 @@ async function runExtractTool(agenrPath, params) {
|
|
|
874
876
|
}
|
|
875
877
|
}
|
|
876
878
|
}
|
|
877
|
-
async function runRetireTool(agenrPath, params) {
|
|
879
|
+
async function runRetireTool(agenrPath, params, dbPath) {
|
|
878
880
|
const entryId = asString(params.entry_id);
|
|
879
881
|
if (!entryId) {
|
|
880
882
|
return {
|
|
@@ -891,7 +893,7 @@ async function runRetireTool(agenrPath, params) {
|
|
|
891
893
|
args.push("--persist");
|
|
892
894
|
}
|
|
893
895
|
args.push("--force");
|
|
894
|
-
const result = await runAgenrCommand(agenrPath, args);
|
|
896
|
+
const result = await runAgenrCommand(agenrPath, args, void 0, void 0, dbPath);
|
|
895
897
|
if (result.timedOut) {
|
|
896
898
|
return {
|
|
897
899
|
content: [{ type: "text", text: "agenr_retire failed: command timed out" }]
|
|
@@ -992,15 +994,13 @@ var HANDOFF_SUMMARY_SYSTEM_PROMPT = `You are a session summarizer for an AI agen
|
|
|
992
994
|
to produce a concise handoff note from a conversation transcript so the
|
|
993
995
|
next session can orient quickly.
|
|
994
996
|
|
|
995
|
-
The transcript
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
begin WORKING ON with a note like "Resumed 4 hours after a telegram
|
|
999
|
-
session." If sessions are continuous, omit the gap.
|
|
997
|
+
The transcript contains messages from a single session labeled with a
|
|
998
|
+
timestamp and surface (webchat, telegram, etc.). Only include information
|
|
999
|
+
explicitly present in the transcript. Do not infer or invent details.
|
|
1000
1000
|
|
|
1001
|
-
If the
|
|
1002
|
-
|
|
1003
|
-
|
|
1001
|
+
If the session contains no substantive work (e.g. just greetings or
|
|
1002
|
+
small talk), write "No significant activity" for WORKING ON and "None"
|
|
1003
|
+
for all other sections.
|
|
1004
1004
|
|
|
1005
1005
|
Produce these sections in plain text (no markdown, no bullet symbols,
|
|
1006
1006
|
use plain dashes for lists):
|
|
@@ -1019,6 +1019,45 @@ IMPORTANT FACTS: Stateful facts only - file paths, version numbers,
|
|
|
1019
1019
|
env states, config values. Do NOT repeat decisions already in KEY
|
|
1020
1020
|
FINDINGS. Write "None" if nothing new.
|
|
1021
1021
|
|
|
1022
|
+
Keep the total response under 500 words. Plain text only.`;
|
|
1023
|
+
var HANDOFF_SUMMARY_SYSTEM_PROMPT_WITH_BACKGROUND = `You are a session summarizer for an AI agent memory system. Your job is
|
|
1024
|
+
to produce a concise handoff note so the next session can orient quickly.
|
|
1025
|
+
|
|
1026
|
+
The transcript has two clearly labeled sections:
|
|
1027
|
+
|
|
1028
|
+
1. "BACKGROUND CONTEXT (DO NOT SUMMARIZE)" - a prior session included
|
|
1029
|
+
ONLY for orientation. Do NOT include facts, versions, or states from
|
|
1030
|
+
this section in your summary. It is there so you understand what came
|
|
1031
|
+
before.
|
|
1032
|
+
|
|
1033
|
+
2. "SUMMARIZE THIS SESSION ONLY" - the session that just ended. Your
|
|
1034
|
+
summary must cover ONLY this section.
|
|
1035
|
+
|
|
1036
|
+
If the current session contains no substantive work (e.g. just greetings
|
|
1037
|
+
or small talk), write "No significant activity" for WORKING ON and
|
|
1038
|
+
"None" for all other sections. Do NOT fall back to summarizing the
|
|
1039
|
+
background context.
|
|
1040
|
+
|
|
1041
|
+
Only include information explicitly present in the current session
|
|
1042
|
+
transcript. Do not infer or invent details.
|
|
1043
|
+
|
|
1044
|
+
Produce these sections in plain text (no markdown, no bullet symbols,
|
|
1045
|
+
use plain dashes for lists):
|
|
1046
|
+
|
|
1047
|
+
WORKING ON: One to two sentences on the main task or topic, including
|
|
1048
|
+
the project or system being worked on (e.g. "Working on agenr plugin
|
|
1049
|
+
(#199) in the agenr-ai/agenr repo.").
|
|
1050
|
+
|
|
1051
|
+
KEY FINDINGS: Decisions, discoveries, or conclusions reached in the
|
|
1052
|
+
current session only.
|
|
1053
|
+
|
|
1054
|
+
OPEN THREADS: Unresolved questions or next steps. Write "None" if
|
|
1055
|
+
everything was wrapped up.
|
|
1056
|
+
|
|
1057
|
+
IMPORTANT FACTS: Stateful facts only - file paths, version numbers,
|
|
1058
|
+
env states, config values. Do NOT repeat decisions already in KEY
|
|
1059
|
+
FINDINGS. Write "None" if nothing new.
|
|
1060
|
+
|
|
1022
1061
|
Keep the total response under 500 words. Plain text only.`;
|
|
1023
1062
|
function isRecord2(value) {
|
|
1024
1063
|
return typeof value === "object" && value !== null;
|
|
@@ -1049,6 +1088,27 @@ function extractTextFromContent(content, separator) {
|
|
|
1049
1088
|
function extractHandoffContent(content) {
|
|
1050
1089
|
return extractTextFromContent(content, " ");
|
|
1051
1090
|
}
|
|
1091
|
+
function stripInjectedContext(text) {
|
|
1092
|
+
if (!text) {
|
|
1093
|
+
return "";
|
|
1094
|
+
}
|
|
1095
|
+
let cleaned = text;
|
|
1096
|
+
cleaned = cleaned.replace(
|
|
1097
|
+
/(?:^|\n)## (?:Recent session|Recent memory|Relevant memory)\b[\s\S]*?(?=(?:^|\n)(?:## (?!Recent session|Recent memory|Relevant memory|agenr Memory Context)|Conversation info|\[(?:user|assistant)\]:|AGENR SIGNAL:|=== )|$)/g,
|
|
1098
|
+
""
|
|
1099
|
+
);
|
|
1100
|
+
cleaned = cleaned.replace(
|
|
1101
|
+
/(?:^|\n)## agenr Memory Context[\s\S]*?(?=(?:^|\n)(?:## (?!agenr Memory Context)|Conversation info|\[(?:user|assistant)\]:|AGENR SIGNAL:|=== )|$)/g,
|
|
1102
|
+
""
|
|
1103
|
+
);
|
|
1104
|
+
cleaned = cleaned.replace(/^\s*AGENR SIGNAL:.*(?:\n\s*-\s*\[[^\n]*)*/gm, "");
|
|
1105
|
+
cleaned = cleaned.replace(
|
|
1106
|
+
/Conversation info \(untrusted metadata\):\s*\`\`\`json[\s\S]*?\`\`\`/g,
|
|
1107
|
+
""
|
|
1108
|
+
);
|
|
1109
|
+
cleaned = cleaned.replace(/\[\w{3} \d{4}-\d{2}-\d{2} \d{2}:\d{2} (?:[A-Z]{2,5}|GMT[+-]\d{1,2})\] /g, "");
|
|
1110
|
+
return cleaned.trim();
|
|
1111
|
+
}
|
|
1052
1112
|
function formatHandoffTimestamp(timestamp) {
|
|
1053
1113
|
if (!timestamp) {
|
|
1054
1114
|
return "unknown time";
|
|
@@ -1060,6 +1120,9 @@ function formatHandoffTimestamp(timestamp) {
|
|
|
1060
1120
|
const pad2 = (value) => value.toString().padStart(2, "0");
|
|
1061
1121
|
return `${parsed.getFullYear()}-${pad2(parsed.getMonth() + 1)}-${pad2(parsed.getDate())} ${pad2(parsed.getHours())}:${pad2(parsed.getMinutes())}`;
|
|
1062
1122
|
}
|
|
1123
|
+
function formatHandoffLogTimestamp(date) {
|
|
1124
|
+
return date.toISOString().slice(0, 19).replace(/:/g, "-");
|
|
1125
|
+
}
|
|
1063
1126
|
function getBaseSessionPath(filePath) {
|
|
1064
1127
|
const resetMatch = filePath.match(/^(.+\.jsonl)\.reset\..+$/);
|
|
1065
1128
|
return resetMatch ? resetMatch[1] : filePath;
|
|
@@ -1157,13 +1220,14 @@ async function readMessagesFromJsonl(filePath) {
|
|
|
1157
1220
|
continue;
|
|
1158
1221
|
}
|
|
1159
1222
|
const content = extractHandoffContent(message["content"]);
|
|
1160
|
-
|
|
1223
|
+
const strippedContent = stripInjectedContext(content);
|
|
1224
|
+
if (!strippedContent) {
|
|
1161
1225
|
continue;
|
|
1162
1226
|
}
|
|
1163
1227
|
const timestamp = typeof parsed["timestamp"] === "string" ? parsed["timestamp"] : "";
|
|
1164
1228
|
messages.push({
|
|
1165
1229
|
role,
|
|
1166
|
-
content,
|
|
1230
|
+
content: strippedContent,
|
|
1167
1231
|
timestamp
|
|
1168
1232
|
});
|
|
1169
1233
|
}
|
|
@@ -1174,7 +1238,8 @@ async function readMessagesFromJsonl(filePath) {
|
|
|
1174
1238
|
}
|
|
1175
1239
|
async function findPriorResetFile(sessionsDir, currentSessionFile) {
|
|
1176
1240
|
try {
|
|
1177
|
-
const
|
|
1241
|
+
const currentBasename = path3.basename(currentSessionFile);
|
|
1242
|
+
const currentUuid = currentBasename.split(".jsonl")[0];
|
|
1178
1243
|
const entries = await fs.promises.readdir(sessionsDir);
|
|
1179
1244
|
const resetEntries = entries.filter((entry) => /\.jsonl\.reset\./.test(entry));
|
|
1180
1245
|
const candidates = resetEntries.filter((entry) => entry.split(".jsonl")[0] !== currentUuid);
|
|
@@ -1205,17 +1270,26 @@ async function findPriorResetFile(sessionsDir, currentSessionFile) {
|
|
|
1205
1270
|
return null;
|
|
1206
1271
|
}
|
|
1207
1272
|
}
|
|
1273
|
+
function buildTranscript(messages, surface) {
|
|
1274
|
+
const cleanMessages = messages.map((message) => ({ ...message, content: message.content.trim() })).filter((message) => message.content.length > 0);
|
|
1275
|
+
const lines = [];
|
|
1276
|
+
lines.push(`--- Session ${formatHandoffTimestamp(cleanMessages[0]?.timestamp ?? "")} [${surface}] (ended) ---`);
|
|
1277
|
+
lines.push(...cleanMessages.map((message) => `[${message.role}]: ${message.content}`));
|
|
1278
|
+
return lines.join("\n");
|
|
1279
|
+
}
|
|
1208
1280
|
function buildMergedTranscript(priorMessages, priorSurface, currentMessages, currentSurface) {
|
|
1209
1281
|
const cleanPrior = priorMessages.map((message) => ({ ...message, content: message.content.trim() })).filter((message) => message.content.length > 0);
|
|
1210
1282
|
const cleanCurrent = currentMessages.map((message) => ({ ...message, content: message.content.trim() })).filter((message) => message.content.length > 0);
|
|
1211
1283
|
const lines = [];
|
|
1212
1284
|
if (cleanPrior.length > 0) {
|
|
1213
|
-
lines.push(
|
|
1285
|
+
lines.push("=== BACKGROUND CONTEXT (DO NOT SUMMARIZE) ===");
|
|
1286
|
+
lines.push(`Prior session ${formatHandoffTimestamp(cleanPrior[0]?.timestamp ?? "")} [${priorSurface}]`);
|
|
1214
1287
|
lines.push(...cleanPrior.map((message) => `[${message.role}]: ${message.content}`));
|
|
1215
1288
|
lines.push("");
|
|
1216
1289
|
}
|
|
1290
|
+
lines.push("=== SUMMARIZE THIS SESSION ONLY ===");
|
|
1217
1291
|
lines.push(
|
|
1218
|
-
|
|
1292
|
+
`Current session ${formatHandoffTimestamp(cleanCurrent[0]?.timestamp ?? "")} [${currentSurface}] (ended)`
|
|
1219
1293
|
);
|
|
1220
1294
|
lines.push(...cleanCurrent.map((message) => `[${message.role}]: ${message.content}`));
|
|
1221
1295
|
return lines.join("\n");
|
|
@@ -1264,51 +1338,71 @@ function mapCurrentSessionMessage(message) {
|
|
|
1264
1338
|
return null;
|
|
1265
1339
|
}
|
|
1266
1340
|
const content = extractHandoffContent(message["content"]);
|
|
1267
|
-
|
|
1341
|
+
const strippedContent = stripInjectedContext(content);
|
|
1342
|
+
if (!strippedContent) {
|
|
1268
1343
|
return null;
|
|
1269
1344
|
}
|
|
1270
1345
|
const timestamp = typeof message["timestamp"] === "string" ? message["timestamp"] : "";
|
|
1271
1346
|
return {
|
|
1272
1347
|
role,
|
|
1273
|
-
content,
|
|
1348
|
+
content: strippedContent,
|
|
1274
1349
|
timestamp
|
|
1275
1350
|
};
|
|
1276
1351
|
}
|
|
1277
|
-
async function summarizeSessionForHandoff(currentRawMessages, sessionsDir, currentSessionFile, logger, streamSimpleImpl) {
|
|
1352
|
+
async function summarizeSessionForHandoff(currentRawMessages, sessionsDir, currentSessionFile, logger, includeBackground, streamSimpleImpl, logEnabled = false, logDir) {
|
|
1278
1353
|
try {
|
|
1279
1354
|
const sessionsJson = await readSessionsJson(sessionsDir);
|
|
1280
1355
|
const currentSurface = getSurfaceForSessionFile(currentSessionFile, sessionsJson);
|
|
1281
1356
|
const currentMessages = (Array.isArray(currentRawMessages) ? currentRawMessages : []).map((message) => mapCurrentSessionMessage(message)).filter((message) => message !== null);
|
|
1282
|
-
|
|
1357
|
+
let currentSlice = currentMessages.slice(
|
|
1283
1358
|
-Math.min(currentMessages.length, HANDOFF_TRANSCRIPT_MAX_MESSAGES)
|
|
1284
1359
|
);
|
|
1285
|
-
|
|
1360
|
+
if (currentSlice.length < 4) {
|
|
1361
|
+
console.log("[agenr] before_reset: skipping LLM summary - reason: too few messages");
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1286
1364
|
let priorSlice = [];
|
|
1287
1365
|
let priorSurface = "prior session";
|
|
1288
|
-
|
|
1289
|
-
if (
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
|
|
1366
|
+
let transcript = "";
|
|
1367
|
+
if (includeBackground) {
|
|
1368
|
+
const priorBudget = HANDOFF_TRANSCRIPT_MAX_MESSAGES - currentSlice.length;
|
|
1369
|
+
const priorFile = await findPriorResetFile(sessionsDir, currentSessionFile);
|
|
1370
|
+
if (priorFile && priorBudget > 0) {
|
|
1371
|
+
const priorAllMessages = await readMessagesFromJsonl(priorFile);
|
|
1372
|
+
priorSlice = priorAllMessages.slice(-priorBudget);
|
|
1373
|
+
priorSurface = getSurfaceForSessionFile(getBaseSessionPath(priorFile), sessionsJson);
|
|
1374
|
+
}
|
|
1375
|
+
transcript = buildMergedTranscript(priorSlice, priorSurface, currentSlice, currentSurface);
|
|
1376
|
+
} else {
|
|
1377
|
+
transcript = buildTranscript(currentSlice, currentSurface);
|
|
1293
1378
|
}
|
|
1294
|
-
let transcript = buildMergedTranscript(priorSlice, priorSurface, currentSlice, currentSurface);
|
|
1295
1379
|
const nonHeaderLineCount = countTranscriptContentLines(transcript);
|
|
1296
1380
|
if (nonHeaderLineCount < 3) {
|
|
1297
1381
|
console.log("[agenr] before_reset: skipping LLM summary - reason: short transcript");
|
|
1298
1382
|
return null;
|
|
1299
1383
|
}
|
|
1300
|
-
if (
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1384
|
+
if (includeBackground) {
|
|
1385
|
+
if (transcript.length > HANDOFF_TRANSCRIPT_MAX_CHARS) {
|
|
1386
|
+
transcript = capTranscriptLength({
|
|
1387
|
+
priorMessages: priorSlice,
|
|
1388
|
+
priorSurface,
|
|
1389
|
+
currentMessages: currentSlice,
|
|
1390
|
+
currentSurface,
|
|
1391
|
+
maxChars: HANDOFF_TRANSCRIPT_MAX_CHARS
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
} else if (transcript.length > HANDOFF_TRANSCRIPT_MAX_CHARS) {
|
|
1395
|
+
while (transcript.length > HANDOFF_TRANSCRIPT_MAX_CHARS && currentSlice.length > 1) {
|
|
1396
|
+
currentSlice = currentSlice.slice(1);
|
|
1397
|
+
transcript = buildTranscript(currentSlice, currentSurface);
|
|
1398
|
+
}
|
|
1399
|
+
if (transcript.length > HANDOFF_TRANSCRIPT_MAX_CHARS) {
|
|
1400
|
+
transcript = transcript.slice(-HANDOFF_TRANSCRIPT_MAX_CHARS);
|
|
1401
|
+
const firstNewline = transcript.indexOf("\n");
|
|
1402
|
+
if (firstNewline > 0) {
|
|
1403
|
+
transcript = transcript.slice(firstNewline + 1);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1312
1406
|
}
|
|
1313
1407
|
let llmClient;
|
|
1314
1408
|
try {
|
|
@@ -1324,14 +1418,21 @@ async function summarizeSessionForHandoff(currentRawMessages, sessionsDir, curre
|
|
|
1324
1418
|
console.log("[agenr] before_reset: no apiKey available, skipping LLM summary");
|
|
1325
1419
|
return null;
|
|
1326
1420
|
}
|
|
1421
|
+
const systemPrompt = includeBackground ? HANDOFF_SUMMARY_SYSTEM_PROMPT_WITH_BACKGROUND : HANDOFF_SUMMARY_SYSTEM_PROMPT;
|
|
1327
1422
|
const context = {
|
|
1328
|
-
systemPrompt
|
|
1423
|
+
systemPrompt,
|
|
1329
1424
|
messages: [{ role: "user", content: transcript, timestamp: Date.now() }],
|
|
1330
1425
|
tools: []
|
|
1331
1426
|
};
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1427
|
+
if (includeBackground) {
|
|
1428
|
+
console.log(
|
|
1429
|
+
`[agenr] before_reset: sending to LLM model=${resolvedModel.modelId} chars=${transcript.length} currentMsgs=${currentSlice.length} priorMsgs=${priorSlice.length}`
|
|
1430
|
+
);
|
|
1431
|
+
} else {
|
|
1432
|
+
console.log(
|
|
1433
|
+
`[agenr] before_reset: sending to LLM model=${resolvedModel.modelId} chars=${transcript.length} msgs=${currentSlice.length}`
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1335
1436
|
const assistantMsg = await runSimpleStream({
|
|
1336
1437
|
model: resolvedModel.model,
|
|
1337
1438
|
context,
|
|
@@ -1348,6 +1449,47 @@ async function summarizeSessionForHandoff(currentRawMessages, sessionsDir, curre
|
|
|
1348
1449
|
console.log("[agenr] before_reset: skipping LLM summary - reason: empty summary text");
|
|
1349
1450
|
return null;
|
|
1350
1451
|
}
|
|
1452
|
+
const normalizedLogDir = typeof logDir === "string" ? logDir.trim() : "";
|
|
1453
|
+
if (logEnabled && normalizedLogDir) {
|
|
1454
|
+
const timestamp = formatHandoffLogTimestamp(/* @__PURE__ */ new Date());
|
|
1455
|
+
const requestPath = path3.join(normalizedLogDir, `handoff-${timestamp}-request.txt`);
|
|
1456
|
+
const responsePath = path3.join(normalizedLogDir, `handoff-${timestamp}-response.txt`);
|
|
1457
|
+
const requestPayload = [
|
|
1458
|
+
"=== SYSTEM PROMPT ===",
|
|
1459
|
+
systemPrompt,
|
|
1460
|
+
"",
|
|
1461
|
+
"=== TRANSCRIPT ===",
|
|
1462
|
+
transcript,
|
|
1463
|
+
"",
|
|
1464
|
+
"=== METADATA ===",
|
|
1465
|
+
`model: ${resolvedModel.modelId}`,
|
|
1466
|
+
`currentMsgs: ${currentSlice.length}`,
|
|
1467
|
+
`priorMsgs: ${priorSlice.length}`,
|
|
1468
|
+
`includeBackground: ${includeBackground}`,
|
|
1469
|
+
`transcriptChars: ${transcript.length}`
|
|
1470
|
+
].join("\n");
|
|
1471
|
+
const responsePayload = [
|
|
1472
|
+
"=== LLM RESPONSE ===",
|
|
1473
|
+
summaryText,
|
|
1474
|
+
"",
|
|
1475
|
+
"=== METADATA ===",
|
|
1476
|
+
`model: ${resolvedModel.modelId}`,
|
|
1477
|
+
`stopReason: ${assistantMsg.stopReason}`,
|
|
1478
|
+
`responseChars: ${summaryText.length}`
|
|
1479
|
+
].join("\n");
|
|
1480
|
+
try {
|
|
1481
|
+
await fs.promises.mkdir(normalizedLogDir, { recursive: true });
|
|
1482
|
+
await Promise.all([
|
|
1483
|
+
fs.promises.writeFile(requestPath, requestPayload, "utf8"),
|
|
1484
|
+
fs.promises.writeFile(responsePath, responsePayload, "utf8")
|
|
1485
|
+
]);
|
|
1486
|
+
console.log(`[agenr] handoff: logged LLM request/response to ${normalizedLogDir}`);
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
console.log(
|
|
1489
|
+
`[agenr] handoff: failed to write LLM request/response logs: ${err instanceof Error ? err.message : String(err)}`
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1351
1493
|
console.log(`[agenr] before_reset: LLM summary received chars=${summaryText.length}`);
|
|
1352
1494
|
return summaryText;
|
|
1353
1495
|
} catch (err) {
|
|
@@ -1393,7 +1535,7 @@ async function retireFallbackHandoffEntries(params) {
|
|
|
1393
1535
|
context: "browse",
|
|
1394
1536
|
since: "1d",
|
|
1395
1537
|
limit: 100
|
|
1396
|
-
});
|
|
1538
|
+
}, params.dbPath);
|
|
1397
1539
|
if (!browseResult) {
|
|
1398
1540
|
return;
|
|
1399
1541
|
}
|
|
@@ -1423,7 +1565,9 @@ async function retireFallbackHandoffEntries(params) {
|
|
|
1423
1565
|
runRetireTool(params.agenrPath, {
|
|
1424
1566
|
entry_id: entryId,
|
|
1425
1567
|
reason: "superseded by LLM handoff"
|
|
1426
|
-
}).then(() =>
|
|
1568
|
+
}, params.dbPath).then(() => {
|
|
1569
|
+
console.log(`[agenr] session-start: retired handoff ${entryId}`);
|
|
1570
|
+
}).catch((err) => {
|
|
1427
1571
|
params.logger.debug?.(
|
|
1428
1572
|
`[agenr] ${params.source}: fallback retire failed for ${entryId}: ${err instanceof Error ? err.message : String(err)}`
|
|
1429
1573
|
);
|
|
@@ -1458,10 +1602,7 @@ function normalizeHandoffMessages(messages) {
|
|
|
1458
1602
|
async function runHandoffForSession(opts) {
|
|
1459
1603
|
const sessionId = opts.sessionId.trim() || opts.sessionKey;
|
|
1460
1604
|
if (handoffSeenSessionIds.has(sessionId)) {
|
|
1461
|
-
|
|
1462
|
-
`[AGENR-PROBE] ${opts.source} hook: dedup skip sessionId=${sessionId} source=${opts.source}
|
|
1463
|
-
`
|
|
1464
|
-
);
|
|
1605
|
+
console.log(`[agenr] ${opts.source}: dedup skip sessionId=${sessionId}`);
|
|
1465
1606
|
return;
|
|
1466
1607
|
}
|
|
1467
1608
|
handoffSeenSessionIds.add(sessionId);
|
|
@@ -1473,8 +1614,6 @@ async function runHandoffForSession(opts) {
|
|
|
1473
1614
|
}
|
|
1474
1615
|
const normalizedMessages = normalizeHandoffMessages(opts.messages);
|
|
1475
1616
|
if (normalizedMessages.length === 0) {
|
|
1476
|
-
process.stderr.write(`[AGENR-PROBE] ${opts.source} hook: no messages after normalization source=${opts.source}
|
|
1477
|
-
`);
|
|
1478
1617
|
return;
|
|
1479
1618
|
}
|
|
1480
1619
|
if (!opts.sessionFile) {
|
|
@@ -1497,7 +1636,7 @@ async function runHandoffForSession(opts) {
|
|
|
1497
1636
|
]
|
|
1498
1637
|
};
|
|
1499
1638
|
try {
|
|
1500
|
-
await runStoreTool(opts.agenrPath, fallbackEntry, opts.storeConfig, opts.defaultProject);
|
|
1639
|
+
await runStoreTool(opts.agenrPath, fallbackEntry, opts.storeConfig, opts.defaultProject, opts.dbPath);
|
|
1501
1640
|
console.log(`[agenr] ${opts.source}: fallback handoff stored`);
|
|
1502
1641
|
} catch (err) {
|
|
1503
1642
|
console.log(
|
|
@@ -1507,6 +1646,7 @@ async function runHandoffForSession(opts) {
|
|
|
1507
1646
|
}
|
|
1508
1647
|
}
|
|
1509
1648
|
if (opts.sessionFile) {
|
|
1649
|
+
const includeBackground = opts.includeBackground ?? false;
|
|
1510
1650
|
const summary = await testingApi.summarizeSessionForHandoff(
|
|
1511
1651
|
normalizedMessages,
|
|
1512
1652
|
opts.sessionsDir,
|
|
@@ -1514,7 +1654,11 @@ async function runHandoffForSession(opts) {
|
|
|
1514
1654
|
opts.logger ?? {
|
|
1515
1655
|
warn: () => void 0,
|
|
1516
1656
|
error: () => void 0
|
|
1517
|
-
}
|
|
1657
|
+
},
|
|
1658
|
+
includeBackground,
|
|
1659
|
+
void 0,
|
|
1660
|
+
opts.logEnabled ?? false,
|
|
1661
|
+
opts.logDir
|
|
1518
1662
|
);
|
|
1519
1663
|
if (summary) {
|
|
1520
1664
|
if (fallbackEntrySubject) {
|
|
@@ -1522,6 +1666,7 @@ async function runHandoffForSession(opts) {
|
|
|
1522
1666
|
agenrPath: opts.agenrPath,
|
|
1523
1667
|
budget: opts.budget,
|
|
1524
1668
|
defaultProject: opts.defaultProject,
|
|
1669
|
+
dbPath: opts.dbPath,
|
|
1525
1670
|
fallbackSubject: fallbackEntrySubject,
|
|
1526
1671
|
logger: opts.logger ?? {
|
|
1527
1672
|
warn: () => void 0,
|
|
@@ -1543,7 +1688,7 @@ async function runHandoffForSession(opts) {
|
|
|
1543
1688
|
]
|
|
1544
1689
|
};
|
|
1545
1690
|
try {
|
|
1546
|
-
await runStoreTool(opts.agenrPath, llmEntry, opts.storeConfig, opts.defaultProject);
|
|
1691
|
+
await runStoreTool(opts.agenrPath, llmEntry, opts.storeConfig, opts.defaultProject, opts.dbPath);
|
|
1547
1692
|
console.log(`[agenr] ${opts.source}: LLM handoff stored`);
|
|
1548
1693
|
} catch (err) {
|
|
1549
1694
|
console.log(
|
|
@@ -1564,9 +1709,13 @@ var testingApi = {
|
|
|
1564
1709
|
getBaseSessionPath,
|
|
1565
1710
|
getSurfaceForSessionFile,
|
|
1566
1711
|
readMessagesFromJsonl,
|
|
1712
|
+
stripInjectedContext,
|
|
1567
1713
|
findPriorResetFile,
|
|
1714
|
+
buildTranscript,
|
|
1568
1715
|
buildMergedTranscript,
|
|
1569
1716
|
capTranscriptLength,
|
|
1717
|
+
HANDOFF_SUMMARY_SYSTEM_PROMPT,
|
|
1718
|
+
HANDOFF_SUMMARY_SYSTEM_PROMPT_WITH_BACKGROUND,
|
|
1570
1719
|
summarizeSessionForHandoff,
|
|
1571
1720
|
runHandoffForSession
|
|
1572
1721
|
};
|
|
@@ -1576,8 +1725,10 @@ var plugin = {
|
|
|
1576
1725
|
description: "Injects agenr long-term memory into every agent session via before_prompt_build",
|
|
1577
1726
|
register(api) {
|
|
1578
1727
|
const config = api.pluginConfig;
|
|
1579
|
-
|
|
1580
|
-
|
|
1728
|
+
const includeBackground = config?.handoff?.includeBackground ?? false;
|
|
1729
|
+
const handoffLogEnabled = config?.handoff?.logEnabled === true;
|
|
1730
|
+
const handoffLogDirRaw = config?.handoff?.logDir;
|
|
1731
|
+
const handoffLogDir = typeof handoffLogDirRaw === "string" && handoffLogDirRaw.trim() ? handoffLogDirRaw.trim() : void 0;
|
|
1581
1732
|
api.on(
|
|
1582
1733
|
"before_prompt_build",
|
|
1583
1734
|
async (event, ctx) => {
|
|
@@ -1613,7 +1764,7 @@ var plugin = {
|
|
|
1613
1764
|
context: "browse",
|
|
1614
1765
|
since: "1d",
|
|
1615
1766
|
limit: 20
|
|
1616
|
-
})
|
|
1767
|
+
}, config?.dbPath)
|
|
1617
1768
|
]);
|
|
1618
1769
|
if (previousSessionFile) {
|
|
1619
1770
|
let messages = null;
|
|
@@ -1623,41 +1774,37 @@ var plugin = {
|
|
|
1623
1774
|
messages = null;
|
|
1624
1775
|
}
|
|
1625
1776
|
if (Array.isArray(messages) && messages.length > 0) {
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1777
|
+
const sessionKey2 = ctx.sessionKey ?? "";
|
|
1778
|
+
console.log("[agenr] session_start: triggered sessionKey=" + sessionKey2);
|
|
1779
|
+
try {
|
|
1780
|
+
await testingApi.runHandoffForSession({
|
|
1781
|
+
messages,
|
|
1782
|
+
sessionFile: previousSessionFile,
|
|
1783
|
+
sessionId: deriveSessionIdFromSessionFile(previousSessionFile) || (ctx.sessionId ?? ctx.sessionKey ?? ""),
|
|
1784
|
+
sessionKey: ctx.sessionKey ?? "",
|
|
1785
|
+
agentId,
|
|
1786
|
+
agenrPath,
|
|
1787
|
+
budget,
|
|
1788
|
+
defaultProject: project,
|
|
1789
|
+
storeConfig: {
|
|
1790
|
+
...config,
|
|
1791
|
+
logger: api.logger
|
|
1792
|
+
},
|
|
1793
|
+
sessionsDir,
|
|
1794
|
+
includeBackground,
|
|
1795
|
+
logEnabled: handoffLogEnabled,
|
|
1796
|
+
logDir: handoffLogDir,
|
|
1797
|
+
logger: api.logger,
|
|
1798
|
+
source: "session_start",
|
|
1799
|
+
dbPath: config?.dbPath
|
|
1800
|
+
});
|
|
1801
|
+
console.log("[agenr] session_start: runHandoffForSession completed");
|
|
1802
|
+
} catch (err) {
|
|
1803
|
+
console.log(
|
|
1804
|
+
`[agenr] session_start: handoff failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1649
1805
|
);
|
|
1650
|
-
}
|
|
1651
|
-
} else if (Array.isArray(messages)) {
|
|
1652
|
-
process.stderr.write(
|
|
1653
|
-
`[AGENR-PROBE] session_start: skipping handoff - no messages in prev file=${previousSessionFile}
|
|
1654
|
-
`
|
|
1655
|
-
);
|
|
1806
|
+
}
|
|
1656
1807
|
}
|
|
1657
|
-
} else {
|
|
1658
|
-
process.stderr.write(
|
|
1659
|
-
"[AGENR-PROBE] session_start: skipping handoff - no previous session file found\n"
|
|
1660
|
-
);
|
|
1661
1808
|
}
|
|
1662
1809
|
const seed = buildSemanticSeed(previousTurns, event.prompt ?? "");
|
|
1663
1810
|
let semanticResult = null;
|
|
@@ -1669,7 +1816,7 @@ var plugin = {
|
|
|
1669
1816
|
browseIds.add(id);
|
|
1670
1817
|
}
|
|
1671
1818
|
}
|
|
1672
|
-
const rawSemantic = await runRecall(agenrPath, budget, project, seed);
|
|
1819
|
+
const rawSemantic = await runRecall(agenrPath, budget, project, seed, void 0, config?.dbPath);
|
|
1673
1820
|
if (rawSemantic) {
|
|
1674
1821
|
rawSemantic.results = rawSemantic.results.filter((item) => {
|
|
1675
1822
|
const id = typeof item.entry?.id === "string" && item.entry.id.trim() ? item.entry.id.trim() : null;
|
|
@@ -1682,6 +1829,7 @@ var plugin = {
|
|
|
1682
1829
|
}
|
|
1683
1830
|
}
|
|
1684
1831
|
if (browseResult) {
|
|
1832
|
+
console.log(`[agenr] session-start: browse returned ${browseResult.results.length} entries, checking for handoffs to retire`);
|
|
1685
1833
|
const retirePromises = [];
|
|
1686
1834
|
for (const item of browseResult.results) {
|
|
1687
1835
|
const entry = item.entry;
|
|
@@ -1695,14 +1843,16 @@ var plugin = {
|
|
|
1695
1843
|
runRetireTool(agenrPath, {
|
|
1696
1844
|
entry_id: entryId,
|
|
1697
1845
|
reason: "consumed at session start"
|
|
1698
|
-
}).then(() =>
|
|
1699
|
-
|
|
1846
|
+
}, config?.dbPath).then(() => {
|
|
1847
|
+
console.log(`[agenr] session-start: retired handoff ${entryId}`);
|
|
1848
|
+
}).catch((err) => {
|
|
1849
|
+
console.log(
|
|
1700
1850
|
`[agenr] session-start: retire handoff ${entryId} failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1701
1851
|
);
|
|
1702
1852
|
})
|
|
1703
1853
|
);
|
|
1704
1854
|
} else {
|
|
1705
|
-
|
|
1855
|
+
console.log(
|
|
1706
1856
|
"[agenr] session-start: handoff entry missing id, skipping retire"
|
|
1707
1857
|
);
|
|
1708
1858
|
}
|
|
@@ -1761,35 +1911,18 @@ ${formatted.trim()}`);
|
|
|
1761
1911
|
}
|
|
1762
1912
|
}
|
|
1763
1913
|
);
|
|
1764
|
-
process.stderr.write(`[AGENR-PROBE] before_prompt_build hook registered
|
|
1765
|
-
`);
|
|
1766
|
-
api.on("session_start", async (_event, ctx) => {
|
|
1767
|
-
process.stderr.write(
|
|
1768
|
-
`[AGENR-PROBE] session_start FIRED sessionKey=${ctx.sessionKey ?? "none"}
|
|
1769
|
-
`
|
|
1770
|
-
);
|
|
1771
|
-
});
|
|
1772
1914
|
api.on("before_reset", async (event, ctx) => {
|
|
1773
1915
|
try {
|
|
1774
|
-
process.stderr.write(
|
|
1775
|
-
`[AGENR-PROBE] before_reset FIRED sessionKey=${ctx.sessionKey ?? "none"} msgs=${Array.isArray(event.messages) ? event.messages.length : "non-array"} source=before_reset
|
|
1776
|
-
`
|
|
1777
|
-
);
|
|
1778
1916
|
api.logger.info?.(`[agenr] before_reset: fired sessionKey=${ctx.sessionKey ?? "none"} agentId=${ctx.agentId ?? "none"} msgs=${Array.isArray(event.messages) ? event.messages.length : "non-array"} sessionFile=${event.sessionFile ?? "none"} source=before_reset`);
|
|
1779
1917
|
const sessionKey = ctx.sessionKey;
|
|
1780
1918
|
if (!sessionKey) {
|
|
1781
1919
|
return;
|
|
1782
1920
|
}
|
|
1783
|
-
process.stderr.write(`[AGENR-PROBE] before_reset: sessionKey ok source=before_reset
|
|
1784
|
-
`);
|
|
1785
1921
|
const messages = event.messages;
|
|
1786
1922
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
1787
1923
|
return;
|
|
1788
1924
|
}
|
|
1789
|
-
|
|
1790
|
-
`[AGENR-PROBE] before_reset: messages ok count=${messages.length} source=before_reset
|
|
1791
|
-
`
|
|
1792
|
-
);
|
|
1925
|
+
console.log("[agenr] before_reset: triggered sessionKey=" + sessionKey + " msgs=" + messages.length);
|
|
1793
1926
|
const currentSessionFile = typeof event.sessionFile === "string" && event.sessionFile.trim() ? event.sessionFile.trim() : null;
|
|
1794
1927
|
const agentId = ctx.agentId?.trim() || "main";
|
|
1795
1928
|
const sessionsDir = config?.sessionsDir ?? path3.join(os.homedir(), `.openclaw/agents/${agentId}/sessions`);
|
|
@@ -1812,8 +1945,12 @@ ${formatted.trim()}`);
|
|
|
1812
1945
|
defaultProject,
|
|
1813
1946
|
storeConfig,
|
|
1814
1947
|
sessionsDir,
|
|
1948
|
+
includeBackground,
|
|
1949
|
+
logEnabled: handoffLogEnabled,
|
|
1950
|
+
logDir: handoffLogDir,
|
|
1815
1951
|
logger: api.logger,
|
|
1816
|
-
source: "before_reset"
|
|
1952
|
+
source: "before_reset",
|
|
1953
|
+
dbPath: config?.dbPath
|
|
1817
1954
|
});
|
|
1818
1955
|
} catch (err) {
|
|
1819
1956
|
api.logger.warn(
|
|
@@ -1821,49 +1958,28 @@ ${formatted.trim()}`);
|
|
|
1821
1958
|
);
|
|
1822
1959
|
}
|
|
1823
1960
|
});
|
|
1824
|
-
process.stderr.write(`[AGENR-PROBE] before_reset hook registered
|
|
1825
|
-
`);
|
|
1826
1961
|
api.on(
|
|
1827
1962
|
"command",
|
|
1828
1963
|
async (event, ctx) => {
|
|
1829
1964
|
try {
|
|
1830
|
-
|
|
1831
|
-
`[AGENR-PROBE] command hook FIRED action=${event.action ?? "none"} sessionKey=${event.sessionKey ?? "none"} source=${String(event.context?.commandSource ?? "unknown")}
|
|
1832
|
-
`
|
|
1833
|
-
);
|
|
1965
|
+
console.log("[agenr] command: triggered action=" + event.action + " sessionKey=" + event.sessionKey);
|
|
1834
1966
|
if (event.action !== "new" && event.action !== "reset") {
|
|
1835
|
-
process.stderr.write(
|
|
1836
|
-
`[AGENR-PROBE] command hook: skipping action=${event.action ?? "none"} source=command
|
|
1837
|
-
`
|
|
1838
|
-
);
|
|
1839
1967
|
return;
|
|
1840
1968
|
}
|
|
1841
1969
|
const sessionKey = event.sessionKey;
|
|
1842
1970
|
if (!sessionKey) {
|
|
1843
|
-
process.stderr.write("[AGENR-PROBE] command hook: no sessionKey, skipping source=command\n");
|
|
1844
1971
|
return;
|
|
1845
1972
|
}
|
|
1846
1973
|
const sessionFile = event.context?.sessionEntry?.sessionFile ?? null;
|
|
1847
1974
|
const sessionId = event.context?.sessionEntry?.sessionId ?? sessionKey;
|
|
1848
|
-
process.stderr.write(
|
|
1849
|
-
`[AGENR-PROBE] command hook: new/reset detected sessionKey=${sessionKey} sessionFile=${sessionFile ?? "none"} source=command
|
|
1850
|
-
`
|
|
1851
|
-
);
|
|
1852
1975
|
api.logger.info?.(
|
|
1853
1976
|
`[agenr] command hook: fired action=${event.action} sessionKey=${sessionKey} sessionFile=${sessionFile ?? "none"} source=command`
|
|
1854
1977
|
);
|
|
1855
1978
|
let messages = [];
|
|
1856
1979
|
if (sessionFile) {
|
|
1857
1980
|
messages = await testingApi.readAndParseSessionJsonl(sessionFile);
|
|
1858
|
-
process.stderr.write(
|
|
1859
|
-
`[AGENR-PROBE] command hook: parsed ${messages.length} messages from JSONL source=command
|
|
1860
|
-
`
|
|
1861
|
-
);
|
|
1862
|
-
} else {
|
|
1863
|
-
process.stderr.write("[AGENR-PROBE] command hook: no sessionFile available source=command\n");
|
|
1864
1981
|
}
|
|
1865
1982
|
if (messages.length === 0) {
|
|
1866
|
-
process.stderr.write("[AGENR-PROBE] command hook: no messages, skipping handoff source=command\n");
|
|
1867
1983
|
return;
|
|
1868
1984
|
}
|
|
1869
1985
|
const agentId = ctx.agentId?.trim() || "main";
|
|
@@ -1886,10 +2002,14 @@ ${formatted.trim()}`);
|
|
|
1886
2002
|
defaultProject,
|
|
1887
2003
|
storeConfig,
|
|
1888
2004
|
sessionsDir,
|
|
2005
|
+
includeBackground,
|
|
2006
|
+
logEnabled: handoffLogEnabled,
|
|
2007
|
+
logDir: handoffLogDir,
|
|
1889
2008
|
logger: api.logger,
|
|
1890
|
-
source: "command"
|
|
2009
|
+
source: "command",
|
|
2010
|
+
dbPath: config?.dbPath
|
|
1891
2011
|
});
|
|
1892
|
-
|
|
2012
|
+
console.log("[agenr] command: handoff complete");
|
|
1893
2013
|
} catch (err) {
|
|
1894
2014
|
api.logger.warn(
|
|
1895
2015
|
`agenr plugin command hook failed: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -1897,7 +2017,6 @@ ${formatted.trim()}`);
|
|
|
1897
2017
|
}
|
|
1898
2018
|
}
|
|
1899
2019
|
);
|
|
1900
|
-
process.stderr.write("[AGENR-PROBE] command hook registered\n");
|
|
1901
2020
|
if (api.registerTool) {
|
|
1902
2021
|
if (config?.enabled === false) {
|
|
1903
2022
|
return;
|
|
@@ -1938,7 +2057,8 @@ ${formatted.trim()}`);
|
|
|
1938
2057
|
}
|
|
1939
2058
|
const agenrPath = resolveAgenrPath(runtimeConfig);
|
|
1940
2059
|
const defaultProject = runtimeConfig?.project?.trim() || void 0;
|
|
1941
|
-
|
|
2060
|
+
const dbPath = runtimeConfig?.dbPath;
|
|
2061
|
+
return runRecallTool(agenrPath, params, defaultProject, dbPath);
|
|
1942
2062
|
}
|
|
1943
2063
|
}
|
|
1944
2064
|
);
|
|
@@ -1997,11 +2117,13 @@ ${formatted.trim()}`);
|
|
|
1997
2117
|
...runtimeConfig,
|
|
1998
2118
|
logger: api.logger
|
|
1999
2119
|
};
|
|
2120
|
+
const dbPath = runtimeConfig?.dbPath;
|
|
2000
2121
|
return runStoreTool(
|
|
2001
2122
|
agenrPath,
|
|
2002
2123
|
params,
|
|
2003
2124
|
toolConfig,
|
|
2004
|
-
defaultProject
|
|
2125
|
+
defaultProject,
|
|
2126
|
+
dbPath
|
|
2005
2127
|
);
|
|
2006
2128
|
}
|
|
2007
2129
|
}
|
|
@@ -2022,7 +2144,8 @@ ${formatted.trim()}`);
|
|
|
2022
2144
|
return makeDisabledToolResult();
|
|
2023
2145
|
}
|
|
2024
2146
|
const agenrPath = resolveAgenrPath(runtimeConfig);
|
|
2025
|
-
|
|
2147
|
+
const dbPath = runtimeConfig?.dbPath;
|
|
2148
|
+
return runExtractTool(agenrPath, params, dbPath);
|
|
2026
2149
|
}
|
|
2027
2150
|
}
|
|
2028
2151
|
);
|
|
@@ -2042,7 +2165,8 @@ ${formatted.trim()}`);
|
|
|
2042
2165
|
return makeDisabledToolResult();
|
|
2043
2166
|
}
|
|
2044
2167
|
const agenrPath = resolveAgenrPath(runtimeConfig);
|
|
2045
|
-
|
|
2168
|
+
const dbPath = runtimeConfig?.dbPath;
|
|
2169
|
+
return runRetireTool(agenrPath, params, dbPath);
|
|
2046
2170
|
}
|
|
2047
2171
|
}
|
|
2048
2172
|
);
|
package/openclaw.plugin.json
CHANGED
|
@@ -52,6 +52,29 @@
|
|
|
52
52
|
"dbPath": {
|
|
53
53
|
"type": "string",
|
|
54
54
|
"description": "Path to agenr DB. Defaults to AGENR_DB_PATH env or ~/.agenr/knowledge.db."
|
|
55
|
+
},
|
|
56
|
+
"sessionsDir": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "Path to OpenClaw sessions directory. Defaults to ~/.openclaw/agents/{agentId}/sessions."
|
|
59
|
+
},
|
|
60
|
+
"handoff": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"description": "Handoff summarizer options.",
|
|
63
|
+
"additionalProperties": false,
|
|
64
|
+
"properties": {
|
|
65
|
+
"includeBackground": {
|
|
66
|
+
"type": "boolean",
|
|
67
|
+
"description": "Include prior session as background context in handoff summary (default: false)."
|
|
68
|
+
},
|
|
69
|
+
"logEnabled": {
|
|
70
|
+
"type": "boolean",
|
|
71
|
+
"description": "Enable writing handoff LLM request/response logs to logDir (default: false)."
|
|
72
|
+
},
|
|
73
|
+
"logDir": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"description": "Directory to write handoff LLM request/response logs for debugging. Requires logEnabled: true."
|
|
76
|
+
}
|
|
77
|
+
}
|
|
55
78
|
}
|
|
56
79
|
}
|
|
57
80
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agenr",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.38",
|
|
4
4
|
"openclaw": {
|
|
5
5
|
"extensions": [
|
|
6
6
|
"dist/openclaw-plugin/index.js"
|
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
"bin": {
|
|
12
12
|
"agenr": "dist/cli.js"
|
|
13
13
|
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup src/cli.ts src/cli-main.ts src/openclaw-plugin/index.ts --format esm --dts",
|
|
16
|
+
"dev": "tsup src/cli.ts src/cli-main.ts --format esm --watch",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
20
|
+
},
|
|
14
21
|
"dependencies": {
|
|
15
22
|
"@clack/prompts": "^1.0.1",
|
|
16
23
|
"@libsql/client": "^0.17.0",
|
|
@@ -54,11 +61,9 @@
|
|
|
54
61
|
"README.md"
|
|
55
62
|
],
|
|
56
63
|
"author": "agenr-ai",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"test:watch": "vitest",
|
|
62
|
-
"typecheck": "tsc --noEmit"
|
|
64
|
+
"pnpm": {
|
|
65
|
+
"overrides": {
|
|
66
|
+
"fast-xml-parser": "^5.3.6"
|
|
67
|
+
}
|
|
63
68
|
}
|
|
64
|
-
}
|
|
69
|
+
}
|