clawvault 1.4.2 → 1.5.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/bin/clawvault.js +48 -0
- package/dist/chunk-AZRV2I5U.js +133 -0
- package/dist/chunk-L53L5FCL.js +200 -0
- package/dist/commands/repair-session.d.ts +38 -0
- package/dist/commands/repair-session.js +121 -0
- package/dist/lib/session-repair.d.ts +110 -0
- package/dist/lib/session-repair.js +16 -0
- package/dist/lib/session-utils.d.ts +59 -0
- package/dist/lib/session-utils.js +24 -0
- package/package.json +2 -2
package/bin/clawvault.js
CHANGED
|
@@ -1081,5 +1081,53 @@ program
|
|
|
1081
1081
|
}
|
|
1082
1082
|
});
|
|
1083
1083
|
|
|
1084
|
+
// === REPAIR-SESSION ===
|
|
1085
|
+
program
|
|
1086
|
+
.command('repair-session')
|
|
1087
|
+
.description('Repair corrupted OpenClaw session transcripts')
|
|
1088
|
+
.option('-s, --session <id>', 'Session ID (defaults to current main session)')
|
|
1089
|
+
.option('-a, --agent <id>', 'Agent ID (defaults to configured agent)')
|
|
1090
|
+
.option('--backup', 'Create backup before repair (default: true)', true)
|
|
1091
|
+
.option('--no-backup', 'Skip backup creation')
|
|
1092
|
+
.option('--dry-run', 'Show what would be repaired without writing')
|
|
1093
|
+
.option('--list', 'List available sessions')
|
|
1094
|
+
.option('--json', 'Output as JSON')
|
|
1095
|
+
.action(async (options) => {
|
|
1096
|
+
try {
|
|
1097
|
+
const {
|
|
1098
|
+
repairSessionCommand,
|
|
1099
|
+
formatRepairResult,
|
|
1100
|
+
listAgentSessions
|
|
1101
|
+
} = await import('../dist/commands/repair-session.js');
|
|
1102
|
+
|
|
1103
|
+
// List mode
|
|
1104
|
+
if (options.list) {
|
|
1105
|
+
console.log(listAgentSessions(options.agent));
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const result = await repairSessionCommand({
|
|
1110
|
+
sessionId: options.session,
|
|
1111
|
+
agentId: options.agent,
|
|
1112
|
+
backup: options.backup,
|
|
1113
|
+
dryRun: options.dryRun
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
if (options.json) {
|
|
1117
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1118
|
+
} else {
|
|
1119
|
+
console.log(formatRepairResult(result, { dryRun: options.dryRun }));
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Exit with code 1 if corruption was found but not fixed (dry-run)
|
|
1123
|
+
if (result.corruptedEntries.length > 0 && !result.repaired) {
|
|
1124
|
+
process.exit(1);
|
|
1125
|
+
}
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1084
1132
|
// Parse and run
|
|
1085
1133
|
program.parse();
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// src/lib/session-utils.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
function getOpenClawAgentsDir() {
|
|
6
|
+
return path.join(os.homedir(), ".openclaw", "agents");
|
|
7
|
+
}
|
|
8
|
+
function getSessionsDir(agentId) {
|
|
9
|
+
return path.join(getOpenClawAgentsDir(), agentId, "sessions");
|
|
10
|
+
}
|
|
11
|
+
function getSessionsJsonPath(agentId) {
|
|
12
|
+
return path.join(getSessionsDir(agentId), "sessions.json");
|
|
13
|
+
}
|
|
14
|
+
function getSessionFilePath(agentId, sessionId) {
|
|
15
|
+
return path.join(getSessionsDir(agentId), `${sessionId}.jsonl`);
|
|
16
|
+
}
|
|
17
|
+
function listAgents() {
|
|
18
|
+
const agentsDir = getOpenClawAgentsDir();
|
|
19
|
+
if (!fs.existsSync(agentsDir)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return fs.readdirSync(agentsDir).filter((name) => {
|
|
23
|
+
const sessionsDir = getSessionsDir(name);
|
|
24
|
+
return fs.existsSync(sessionsDir) && fs.statSync(sessionsDir).isDirectory();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function loadSessionsStore(agentId) {
|
|
28
|
+
const sessionsJsonPath = getSessionsJsonPath(agentId);
|
|
29
|
+
if (!fs.existsSync(sessionsJsonPath)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const content = fs.readFileSync(sessionsJsonPath, "utf-8");
|
|
34
|
+
return JSON.parse(content);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function findMainSession(agentId) {
|
|
40
|
+
const store = loadSessionsStore(agentId);
|
|
41
|
+
if (!store) return null;
|
|
42
|
+
const mainKey = `agent:${agentId}:main`;
|
|
43
|
+
const entry = store[mainKey];
|
|
44
|
+
if (entry?.sessionId) {
|
|
45
|
+
const filePath = getSessionFilePath(agentId, entry.sessionId);
|
|
46
|
+
if (fs.existsSync(filePath)) {
|
|
47
|
+
return {
|
|
48
|
+
sessionId: entry.sessionId,
|
|
49
|
+
sessionKey: mainKey,
|
|
50
|
+
agentId,
|
|
51
|
+
filePath,
|
|
52
|
+
updatedAt: entry.updatedAt
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function findSessionById(agentId, sessionId) {
|
|
59
|
+
const filePath = getSessionFilePath(agentId, sessionId);
|
|
60
|
+
if (!fs.existsSync(filePath)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const store = loadSessionsStore(agentId);
|
|
64
|
+
let sessionKey;
|
|
65
|
+
let updatedAt;
|
|
66
|
+
if (store) {
|
|
67
|
+
for (const [key, entry] of Object.entries(store)) {
|
|
68
|
+
if (entry.sessionId === sessionId) {
|
|
69
|
+
sessionKey = key;
|
|
70
|
+
updatedAt = entry.updatedAt;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
sessionId,
|
|
77
|
+
sessionKey: sessionKey || `agent:${agentId}:unknown`,
|
|
78
|
+
agentId,
|
|
79
|
+
filePath,
|
|
80
|
+
updatedAt
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function listSessions(agentId) {
|
|
84
|
+
const sessionsDir = getSessionsDir(agentId);
|
|
85
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const store = loadSessionsStore(agentId);
|
|
89
|
+
const sessions = [];
|
|
90
|
+
const files = fs.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl") && !f.includes(".backup") && !f.includes(".deleted") && !f.includes(".corrupted"));
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const sessionId = file.replace(".jsonl", "");
|
|
93
|
+
const filePath = path.join(sessionsDir, file);
|
|
94
|
+
let sessionKey = `agent:${agentId}:unknown`;
|
|
95
|
+
let updatedAt;
|
|
96
|
+
if (store) {
|
|
97
|
+
for (const [key, entry] of Object.entries(store)) {
|
|
98
|
+
if (entry.sessionId === sessionId) {
|
|
99
|
+
sessionKey = key;
|
|
100
|
+
updatedAt = entry.updatedAt;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
sessions.push({
|
|
106
|
+
sessionId,
|
|
107
|
+
sessionKey,
|
|
108
|
+
agentId,
|
|
109
|
+
filePath,
|
|
110
|
+
updatedAt
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return sessions.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
114
|
+
}
|
|
115
|
+
function backupSession(filePath) {
|
|
116
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace("T", "-").slice(0, 15);
|
|
117
|
+
const backupPath = `${filePath}.backup-${timestamp}`;
|
|
118
|
+
fs.copyFileSync(filePath, backupPath);
|
|
119
|
+
return backupPath;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export {
|
|
123
|
+
getOpenClawAgentsDir,
|
|
124
|
+
getSessionsDir,
|
|
125
|
+
getSessionsJsonPath,
|
|
126
|
+
getSessionFilePath,
|
|
127
|
+
listAgents,
|
|
128
|
+
loadSessionsStore,
|
|
129
|
+
findMainSession,
|
|
130
|
+
findSessionById,
|
|
131
|
+
listSessions,
|
|
132
|
+
backupSession
|
|
133
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// src/lib/session-repair.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
function parseTranscript(filePath) {
|
|
4
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
5
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
6
|
+
const entries = [];
|
|
7
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8
|
+
const raw = lines[i];
|
|
9
|
+
try {
|
|
10
|
+
const entry = JSON.parse(raw);
|
|
11
|
+
entries.push({ line: i + 1, entry, raw });
|
|
12
|
+
} catch {
|
|
13
|
+
console.warn(`Warning: Could not parse line ${i + 1}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return entries;
|
|
17
|
+
}
|
|
18
|
+
function extractToolUses(entries) {
|
|
19
|
+
const toolUses = /* @__PURE__ */ new Map();
|
|
20
|
+
for (const { line, entry } of entries) {
|
|
21
|
+
if (entry.type !== "message") continue;
|
|
22
|
+
if (entry.message?.role !== "assistant") continue;
|
|
23
|
+
const isAborted = entry.message.stopReason === "aborted";
|
|
24
|
+
const content = entry.message.content || [];
|
|
25
|
+
for (const block of content) {
|
|
26
|
+
if (block.type === "toolCall" || block.type === "tool_use" || block.type === "functionCall") {
|
|
27
|
+
if (block.id) {
|
|
28
|
+
const isPartial = !!block.partialJson;
|
|
29
|
+
toolUses.set(block.id, {
|
|
30
|
+
id: block.id,
|
|
31
|
+
lineNumber: line,
|
|
32
|
+
entryId: entry.id,
|
|
33
|
+
isAborted: isAborted || isPartial,
|
|
34
|
+
isPartial,
|
|
35
|
+
name: block.name
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return toolUses;
|
|
42
|
+
}
|
|
43
|
+
function findCorruptedEntries(entries, toolUses) {
|
|
44
|
+
const corrupted = [];
|
|
45
|
+
const entriesToRemove = /* @__PURE__ */ new Set();
|
|
46
|
+
for (const [toolId, info] of toolUses) {
|
|
47
|
+
if (info.isAborted) {
|
|
48
|
+
corrupted.push({
|
|
49
|
+
lineNumber: info.lineNumber,
|
|
50
|
+
entryId: info.entryId,
|
|
51
|
+
type: "aborted_tool_use",
|
|
52
|
+
toolUseId: toolId,
|
|
53
|
+
description: `Aborted tool_use${info.name ? ` (${info.name})` : ""} with id: ${toolId}`
|
|
54
|
+
});
|
|
55
|
+
entriesToRemove.add(info.entryId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const { line, entry } of entries) {
|
|
59
|
+
if (entry.type !== "message") continue;
|
|
60
|
+
if (entry.message?.role !== "toolResult") continue;
|
|
61
|
+
const content = entry.message.content || [];
|
|
62
|
+
let toolCallId;
|
|
63
|
+
const msg = entry.message;
|
|
64
|
+
toolCallId = msg.toolCallId || msg.toolUseId;
|
|
65
|
+
if (!toolCallId) {
|
|
66
|
+
for (const block of content) {
|
|
67
|
+
if (block.toolCallId || block.toolUseId) {
|
|
68
|
+
toolCallId = block.toolCallId || block.toolUseId;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!toolCallId) continue;
|
|
74
|
+
const toolUse = toolUses.get(toolCallId);
|
|
75
|
+
if (!toolUse || toolUse.isAborted) {
|
|
76
|
+
corrupted.push({
|
|
77
|
+
lineNumber: line,
|
|
78
|
+
entryId: entry.id,
|
|
79
|
+
type: "orphaned_tool_result",
|
|
80
|
+
toolUseId: toolCallId,
|
|
81
|
+
description: toolUse ? `Orphaned tool_result references aborted tool_use: ${toolCallId}` : `Orphaned tool_result references non-existent tool_use: ${toolCallId}`
|
|
82
|
+
});
|
|
83
|
+
entriesToRemove.add(entry.id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { corrupted, entriesToRemove };
|
|
87
|
+
}
|
|
88
|
+
function computeParentRelinks(entries, entriesToRemove) {
|
|
89
|
+
const relinks = [];
|
|
90
|
+
const entryParents = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const { entry } of entries) {
|
|
92
|
+
entryParents.set(entry.id, entry.parentId);
|
|
93
|
+
}
|
|
94
|
+
for (const { line, entry } of entries) {
|
|
95
|
+
if (entriesToRemove.has(entry.id)) continue;
|
|
96
|
+
if (!entry.parentId) continue;
|
|
97
|
+
if (!entriesToRemove.has(entry.parentId)) continue;
|
|
98
|
+
let newParentId = entry.parentId;
|
|
99
|
+
while (newParentId && entriesToRemove.has(newParentId)) {
|
|
100
|
+
newParentId = entryParents.get(newParentId) || null;
|
|
101
|
+
}
|
|
102
|
+
if (newParentId !== entry.parentId) {
|
|
103
|
+
relinks.push({
|
|
104
|
+
lineNumber: line,
|
|
105
|
+
entryId: entry.id,
|
|
106
|
+
oldParentId: entry.parentId,
|
|
107
|
+
newParentId: newParentId || "null"
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return relinks;
|
|
112
|
+
}
|
|
113
|
+
function analyzeSession(filePath) {
|
|
114
|
+
const entries = parseTranscript(filePath);
|
|
115
|
+
const sessionEntry = entries.find((e) => e.entry.type === "session");
|
|
116
|
+
const sessionId = sessionEntry?.entry.id || "unknown";
|
|
117
|
+
const toolUses = extractToolUses(entries);
|
|
118
|
+
const { corrupted, entriesToRemove } = findCorruptedEntries(entries, toolUses);
|
|
119
|
+
const parentRelinks = computeParentRelinks(entries, entriesToRemove);
|
|
120
|
+
return {
|
|
121
|
+
sessionId,
|
|
122
|
+
totalLines: entries.length,
|
|
123
|
+
corruptedEntries: corrupted,
|
|
124
|
+
parentRelinks,
|
|
125
|
+
removedCount: entriesToRemove.size,
|
|
126
|
+
relinkedCount: parentRelinks.length,
|
|
127
|
+
repaired: false
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function repairSession(filePath, options = {}) {
|
|
131
|
+
const { backup = true, dryRun = false } = options;
|
|
132
|
+
const entries = parseTranscript(filePath);
|
|
133
|
+
const sessionEntry = entries.find((e) => e.entry.type === "session");
|
|
134
|
+
const sessionId = sessionEntry?.entry.id || "unknown";
|
|
135
|
+
const toolUses = extractToolUses(entries);
|
|
136
|
+
const { corrupted, entriesToRemove } = findCorruptedEntries(entries, toolUses);
|
|
137
|
+
const parentRelinks = computeParentRelinks(entries, entriesToRemove);
|
|
138
|
+
if (corrupted.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
sessionId,
|
|
141
|
+
totalLines: entries.length,
|
|
142
|
+
corruptedEntries: [],
|
|
143
|
+
parentRelinks: [],
|
|
144
|
+
removedCount: 0,
|
|
145
|
+
relinkedCount: 0,
|
|
146
|
+
repaired: false
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (dryRun) {
|
|
150
|
+
return {
|
|
151
|
+
sessionId,
|
|
152
|
+
totalLines: entries.length,
|
|
153
|
+
corruptedEntries: corrupted,
|
|
154
|
+
parentRelinks,
|
|
155
|
+
removedCount: entriesToRemove.size,
|
|
156
|
+
relinkedCount: parentRelinks.length,
|
|
157
|
+
repaired: false
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
let backupPath;
|
|
161
|
+
if (backup) {
|
|
162
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace("T", "-").slice(0, 15);
|
|
163
|
+
backupPath = `${filePath}.backup-${timestamp}`;
|
|
164
|
+
fs.copyFileSync(filePath, backupPath);
|
|
165
|
+
}
|
|
166
|
+
const relinkMap = /* @__PURE__ */ new Map();
|
|
167
|
+
for (const relink of parentRelinks) {
|
|
168
|
+
relinkMap.set(relink.entryId, relink.newParentId === "null" ? null : relink.newParentId);
|
|
169
|
+
}
|
|
170
|
+
const repairedLines = [];
|
|
171
|
+
for (const { entry, raw } of entries) {
|
|
172
|
+
if (entriesToRemove.has(entry.id)) continue;
|
|
173
|
+
if (relinkMap.has(entry.id)) {
|
|
174
|
+
const newEntry = { ...entry, parentId: relinkMap.get(entry.id) };
|
|
175
|
+
repairedLines.push(JSON.stringify(newEntry));
|
|
176
|
+
} else {
|
|
177
|
+
repairedLines.push(raw);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
fs.writeFileSync(filePath, repairedLines.join("\n") + "\n");
|
|
181
|
+
return {
|
|
182
|
+
sessionId,
|
|
183
|
+
totalLines: entries.length,
|
|
184
|
+
corruptedEntries: corrupted,
|
|
185
|
+
parentRelinks,
|
|
186
|
+
removedCount: entriesToRemove.size,
|
|
187
|
+
relinkedCount: parentRelinks.length,
|
|
188
|
+
backupPath,
|
|
189
|
+
repaired: true
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export {
|
|
194
|
+
parseTranscript,
|
|
195
|
+
extractToolUses,
|
|
196
|
+
findCorruptedEntries,
|
|
197
|
+
computeParentRelinks,
|
|
198
|
+
analyzeSession,
|
|
199
|
+
repairSession
|
|
200
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { SessionInfo } from '../lib/session-utils.js';
|
|
2
|
+
import { RepairResult } from '../lib/session-repair.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* repair-session command - Repair corrupted OpenClaw session transcripts
|
|
6
|
+
*
|
|
7
|
+
* Fixes issues like:
|
|
8
|
+
* - Aborted tool calls with partial JSON
|
|
9
|
+
* - Orphaned tool_result messages referencing non-existent tool_use IDs
|
|
10
|
+
* - Broken parent chain references
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface RepairSessionOptions {
|
|
14
|
+
sessionId?: string;
|
|
15
|
+
agentId?: string;
|
|
16
|
+
backup?: boolean;
|
|
17
|
+
dryRun?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the session to repair
|
|
21
|
+
*/
|
|
22
|
+
declare function resolveSession(options: RepairSessionOptions): SessionInfo | null;
|
|
23
|
+
/**
|
|
24
|
+
* Format repair result for CLI output
|
|
25
|
+
*/
|
|
26
|
+
declare function formatRepairResult(result: RepairResult, options?: {
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
}): string;
|
|
29
|
+
/**
|
|
30
|
+
* Main repair-session command handler
|
|
31
|
+
*/
|
|
32
|
+
declare function repairSessionCommand(options: RepairSessionOptions): Promise<RepairResult>;
|
|
33
|
+
/**
|
|
34
|
+
* List available sessions for an agent (for --list flag)
|
|
35
|
+
*/
|
|
36
|
+
declare function listAgentSessions(agentId?: string): string;
|
|
37
|
+
|
|
38
|
+
export { type RepairSessionOptions, formatRepairResult, listAgentSessions, repairSessionCommand, resolveSession };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyzeSession,
|
|
3
|
+
repairSession
|
|
4
|
+
} from "../chunk-L53L5FCL.js";
|
|
5
|
+
import {
|
|
6
|
+
findMainSession,
|
|
7
|
+
findSessionById,
|
|
8
|
+
listAgents
|
|
9
|
+
} from "../chunk-AZRV2I5U.js";
|
|
10
|
+
|
|
11
|
+
// src/commands/repair-session.ts
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
function resolveSession(options) {
|
|
14
|
+
const { sessionId, agentId } = options;
|
|
15
|
+
if (sessionId && agentId) {
|
|
16
|
+
return findSessionById(agentId, sessionId);
|
|
17
|
+
}
|
|
18
|
+
if (sessionId) {
|
|
19
|
+
const agents = listAgents();
|
|
20
|
+
for (const agent of agents) {
|
|
21
|
+
const session = findSessionById(agent, sessionId);
|
|
22
|
+
if (session) return session;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (agentId) {
|
|
27
|
+
return findMainSession(agentId);
|
|
28
|
+
}
|
|
29
|
+
const defaultAgent = process.env.OPENCLAW_AGENT_ID || "clawdious";
|
|
30
|
+
return findMainSession(defaultAgent);
|
|
31
|
+
}
|
|
32
|
+
function formatRepairResult(result, options = {}) {
|
|
33
|
+
const { dryRun = false } = options;
|
|
34
|
+
const lines = [];
|
|
35
|
+
lines.push(`Analyzing session: ${result.sessionId}`);
|
|
36
|
+
lines.push("");
|
|
37
|
+
if (result.corruptedEntries.length === 0) {
|
|
38
|
+
lines.push("\u2705 No corruption detected. Session is clean.");
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
if (dryRun) {
|
|
42
|
+
lines.push(`Found ${result.corruptedEntries.length} corrupted entries:`);
|
|
43
|
+
} else {
|
|
44
|
+
lines.push(`Found and fixed ${result.corruptedEntries.length} corrupted entries:`);
|
|
45
|
+
}
|
|
46
|
+
for (const entry of result.corruptedEntries) {
|
|
47
|
+
const prefix = entry.type === "aborted_tool_use" ? "Aborted tool_use" : "Orphaned tool_result";
|
|
48
|
+
lines.push(` - Line ${entry.lineNumber}: ${prefix} (id: ${entry.toolUseId})`);
|
|
49
|
+
}
|
|
50
|
+
if (result.parentRelinks.length > 0) {
|
|
51
|
+
lines.push("");
|
|
52
|
+
if (dryRun) {
|
|
53
|
+
lines.push(`Would relink ${result.parentRelinks.length} parent reference(s):`);
|
|
54
|
+
} else {
|
|
55
|
+
lines.push(`Relinked ${result.parentRelinks.length} parent reference(s):`);
|
|
56
|
+
}
|
|
57
|
+
for (const relink of result.parentRelinks.slice(0, 5)) {
|
|
58
|
+
lines.push(` - Line ${relink.lineNumber}: parentId ${relink.oldParentId.slice(0, 8)}\u2026 \u2192 ${relink.newParentId === "null" ? "null" : relink.newParentId.slice(0, 8)}\u2026`);
|
|
59
|
+
}
|
|
60
|
+
if (result.parentRelinks.length > 5) {
|
|
61
|
+
lines.push(` ... and ${result.parentRelinks.length - 5} more`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
lines.push("");
|
|
65
|
+
if (dryRun) {
|
|
66
|
+
lines.push(`Would remove ${result.removedCount} entries, relink ${result.relinkedCount} parent references.`);
|
|
67
|
+
} else {
|
|
68
|
+
lines.push(`\u2705 Session repaired: removed ${result.removedCount} entries, relinked ${result.relinkedCount} parent references`);
|
|
69
|
+
if (result.backupPath) {
|
|
70
|
+
const backupName = result.backupPath.split("/").pop();
|
|
71
|
+
lines.push(`Backup saved: ${backupName}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
async function repairSessionCommand(options) {
|
|
77
|
+
const { backup = true, dryRun = false } = options;
|
|
78
|
+
const session = resolveSession(options);
|
|
79
|
+
if (!session) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
options.sessionId ? `Session not found: ${options.sessionId}` : options.agentId ? `No main session found for agent: ${options.agentId}` : "No session found. Specify --session or --agent."
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (!fs.existsSync(session.filePath)) {
|
|
85
|
+
throw new Error(`Session file not found: ${session.filePath}`);
|
|
86
|
+
}
|
|
87
|
+
if (dryRun) {
|
|
88
|
+
return analyzeSession(session.filePath);
|
|
89
|
+
}
|
|
90
|
+
return repairSession(session.filePath, { backup, dryRun: false });
|
|
91
|
+
}
|
|
92
|
+
function listAgentSessions(agentId) {
|
|
93
|
+
const agents = agentId ? [agentId] : listAgents();
|
|
94
|
+
const lines = [];
|
|
95
|
+
if (agents.length === 0) {
|
|
96
|
+
return "No agents found in ~/.openclaw/agents/";
|
|
97
|
+
}
|
|
98
|
+
for (const agent of agents) {
|
|
99
|
+
const mainSession = findMainSession(agent);
|
|
100
|
+
if (mainSession) {
|
|
101
|
+
lines.push(`${agent}:`);
|
|
102
|
+
lines.push(` Main session: ${mainSession.sessionId}`);
|
|
103
|
+
lines.push(` File: ${mainSession.filePath}`);
|
|
104
|
+
if (mainSession.updatedAt) {
|
|
105
|
+
const date = new Date(mainSession.updatedAt);
|
|
106
|
+
lines.push(` Updated: ${date.toISOString()}`);
|
|
107
|
+
}
|
|
108
|
+
lines.push("");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (lines.length === 0) {
|
|
112
|
+
return agentId ? `No sessions found for agent: ${agentId}` : "No sessions found.";
|
|
113
|
+
}
|
|
114
|
+
return lines.join("\n");
|
|
115
|
+
}
|
|
116
|
+
export {
|
|
117
|
+
formatRepairResult,
|
|
118
|
+
listAgentSessions,
|
|
119
|
+
repairSessionCommand,
|
|
120
|
+
resolveSession
|
|
121
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session transcript repair logic
|
|
3
|
+
*
|
|
4
|
+
* Repairs corrupted OpenClaw session transcripts by:
|
|
5
|
+
* 1. Finding aborted tool_use blocks (stopReason: "aborted", partialJson present)
|
|
6
|
+
* 2. Finding orphaned tool_result messages that reference non-existent tool_use IDs
|
|
7
|
+
* 3. Removing both the aborted entries and orphaned results
|
|
8
|
+
* 4. Relinking parent chain references
|
|
9
|
+
*/
|
|
10
|
+
interface TranscriptEntry {
|
|
11
|
+
type: 'session' | 'message' | 'compaction' | 'custom' | 'thinking_level_change' | string;
|
|
12
|
+
id: string;
|
|
13
|
+
parentId: string | null;
|
|
14
|
+
timestamp: string;
|
|
15
|
+
message?: {
|
|
16
|
+
role: 'user' | 'assistant' | 'toolResult' | 'system';
|
|
17
|
+
content: Array<{
|
|
18
|
+
type: string;
|
|
19
|
+
id?: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
arguments?: unknown;
|
|
22
|
+
toolCallId?: string;
|
|
23
|
+
toolUseId?: string;
|
|
24
|
+
partialJson?: string;
|
|
25
|
+
text?: string;
|
|
26
|
+
}>;
|
|
27
|
+
stopReason?: string;
|
|
28
|
+
errorMessage?: string;
|
|
29
|
+
};
|
|
30
|
+
summary?: string;
|
|
31
|
+
customType?: string;
|
|
32
|
+
data?: unknown;
|
|
33
|
+
thinkingLevel?: string;
|
|
34
|
+
}
|
|
35
|
+
interface ToolUseInfo {
|
|
36
|
+
id: string;
|
|
37
|
+
lineNumber: number;
|
|
38
|
+
entryId: string;
|
|
39
|
+
isAborted: boolean;
|
|
40
|
+
isPartial: boolean;
|
|
41
|
+
name?: string;
|
|
42
|
+
}
|
|
43
|
+
interface CorruptedEntry {
|
|
44
|
+
lineNumber: number;
|
|
45
|
+
entryId: string;
|
|
46
|
+
type: 'aborted_tool_use' | 'orphaned_tool_result';
|
|
47
|
+
toolUseId: string;
|
|
48
|
+
description: string;
|
|
49
|
+
}
|
|
50
|
+
interface ParentRelink {
|
|
51
|
+
lineNumber: number;
|
|
52
|
+
entryId: string;
|
|
53
|
+
oldParentId: string;
|
|
54
|
+
newParentId: string;
|
|
55
|
+
}
|
|
56
|
+
interface RepairResult {
|
|
57
|
+
sessionId: string;
|
|
58
|
+
totalLines: number;
|
|
59
|
+
corruptedEntries: CorruptedEntry[];
|
|
60
|
+
parentRelinks: ParentRelink[];
|
|
61
|
+
removedCount: number;
|
|
62
|
+
relinkedCount: number;
|
|
63
|
+
backupPath?: string;
|
|
64
|
+
repaired: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse a JSONL file into transcript entries with line numbers
|
|
68
|
+
*/
|
|
69
|
+
declare function parseTranscript(filePath: string): Array<{
|
|
70
|
+
line: number;
|
|
71
|
+
entry: TranscriptEntry;
|
|
72
|
+
raw: string;
|
|
73
|
+
}>;
|
|
74
|
+
/**
|
|
75
|
+
* Extract all tool_use IDs from assistant messages
|
|
76
|
+
*/
|
|
77
|
+
declare function extractToolUses(entries: Array<{
|
|
78
|
+
line: number;
|
|
79
|
+
entry: TranscriptEntry;
|
|
80
|
+
}>): Map<string, ToolUseInfo>;
|
|
81
|
+
/**
|
|
82
|
+
* Find orphaned tool_result messages that reference non-existent or aborted tool_use IDs
|
|
83
|
+
*/
|
|
84
|
+
declare function findCorruptedEntries(entries: Array<{
|
|
85
|
+
line: number;
|
|
86
|
+
entry: TranscriptEntry;
|
|
87
|
+
}>, toolUses: Map<string, ToolUseInfo>): {
|
|
88
|
+
corrupted: CorruptedEntry[];
|
|
89
|
+
entriesToRemove: Set<string>;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Compute parent chain relinks after removing entries
|
|
93
|
+
*/
|
|
94
|
+
declare function computeParentRelinks(entries: Array<{
|
|
95
|
+
line: number;
|
|
96
|
+
entry: TranscriptEntry;
|
|
97
|
+
}>, entriesToRemove: Set<string>): ParentRelink[];
|
|
98
|
+
/**
|
|
99
|
+
* Analyze a session transcript for corruption without modifying it
|
|
100
|
+
*/
|
|
101
|
+
declare function analyzeSession(filePath: string): RepairResult;
|
|
102
|
+
/**
|
|
103
|
+
* Repair a session transcript
|
|
104
|
+
*/
|
|
105
|
+
declare function repairSession(filePath: string, options?: {
|
|
106
|
+
backup?: boolean;
|
|
107
|
+
dryRun?: boolean;
|
|
108
|
+
}): RepairResult;
|
|
109
|
+
|
|
110
|
+
export { type CorruptedEntry, type ParentRelink, type RepairResult, type ToolUseInfo, type TranscriptEntry, analyzeSession, computeParentRelinks, extractToolUses, findCorruptedEntries, parseTranscript, repairSession };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyzeSession,
|
|
3
|
+
computeParentRelinks,
|
|
4
|
+
extractToolUses,
|
|
5
|
+
findCorruptedEntries,
|
|
6
|
+
parseTranscript,
|
|
7
|
+
repairSession
|
|
8
|
+
} from "../chunk-L53L5FCL.js";
|
|
9
|
+
export {
|
|
10
|
+
analyzeSession,
|
|
11
|
+
computeParentRelinks,
|
|
12
|
+
extractToolUses,
|
|
13
|
+
findCorruptedEntries,
|
|
14
|
+
parseTranscript,
|
|
15
|
+
repairSession
|
|
16
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session discovery utilities for OpenClaw transcripts
|
|
3
|
+
*/
|
|
4
|
+
interface SessionInfo {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
sessionKey: string;
|
|
7
|
+
agentId: string;
|
|
8
|
+
filePath: string;
|
|
9
|
+
updatedAt?: number;
|
|
10
|
+
}
|
|
11
|
+
interface SessionsStore {
|
|
12
|
+
[sessionKey: string]: {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
updatedAt?: number;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get the OpenClaw agents directory
|
|
20
|
+
*/
|
|
21
|
+
declare function getOpenClawAgentsDir(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Get the sessions directory for an agent
|
|
24
|
+
*/
|
|
25
|
+
declare function getSessionsDir(agentId: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Get the path to sessions.json for an agent
|
|
28
|
+
*/
|
|
29
|
+
declare function getSessionsJsonPath(agentId: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Get the path to a session JSONL file
|
|
32
|
+
*/
|
|
33
|
+
declare function getSessionFilePath(agentId: string, sessionId: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* List all available agents
|
|
36
|
+
*/
|
|
37
|
+
declare function listAgents(): string[];
|
|
38
|
+
/**
|
|
39
|
+
* Load sessions.json for an agent
|
|
40
|
+
*/
|
|
41
|
+
declare function loadSessionsStore(agentId: string): SessionsStore | null;
|
|
42
|
+
/**
|
|
43
|
+
* Find the current/main session for an agent
|
|
44
|
+
*/
|
|
45
|
+
declare function findMainSession(agentId: string): SessionInfo | null;
|
|
46
|
+
/**
|
|
47
|
+
* Find a session by ID
|
|
48
|
+
*/
|
|
49
|
+
declare function findSessionById(agentId: string, sessionId: string): SessionInfo | null;
|
|
50
|
+
/**
|
|
51
|
+
* List all sessions for an agent
|
|
52
|
+
*/
|
|
53
|
+
declare function listSessions(agentId: string): SessionInfo[];
|
|
54
|
+
/**
|
|
55
|
+
* Create a backup of a session file
|
|
56
|
+
*/
|
|
57
|
+
declare function backupSession(filePath: string): string;
|
|
58
|
+
|
|
59
|
+
export { type SessionInfo, type SessionsStore, backupSession, findMainSession, findSessionById, getOpenClawAgentsDir, getSessionFilePath, getSessionsDir, getSessionsJsonPath, listAgents, listSessions, loadSessionsStore };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
backupSession,
|
|
3
|
+
findMainSession,
|
|
4
|
+
findSessionById,
|
|
5
|
+
getOpenClawAgentsDir,
|
|
6
|
+
getSessionFilePath,
|
|
7
|
+
getSessionsDir,
|
|
8
|
+
getSessionsJsonPath,
|
|
9
|
+
listAgents,
|
|
10
|
+
listSessions,
|
|
11
|
+
loadSessionsStore
|
|
12
|
+
} from "../chunk-AZRV2I5U.js";
|
|
13
|
+
export {
|
|
14
|
+
backupSession,
|
|
15
|
+
findMainSession,
|
|
16
|
+
findSessionById,
|
|
17
|
+
getOpenClawAgentsDir,
|
|
18
|
+
getSessionFilePath,
|
|
19
|
+
getSessionsDir,
|
|
20
|
+
getSessionsJsonPath,
|
|
21
|
+
listAgents,
|
|
22
|
+
listSessions,
|
|
23
|
+
loadSessionsStore
|
|
24
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawvault",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "🐘 An elephant never forgets. Structured memory for OpenClaw agents. Context death resilience, Obsidian-compatible markdown, local semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
]
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "tsup src/index.ts src/commands/entities.ts src/commands/link.ts src/commands/checkpoint.ts src/commands/recover.ts src/commands/status.ts src/commands/template.ts src/commands/setup.ts src/commands/wake.ts src/commands/sleep.ts src/commands/doctor.ts src/commands/shell-init.ts src/lib/entity-index.ts src/lib/auto-linker.ts src/lib/config.ts src/lib/template-engine.ts --format esm --dts --clean",
|
|
31
|
+
"build": "tsup src/index.ts src/commands/entities.ts src/commands/link.ts src/commands/checkpoint.ts src/commands/recover.ts src/commands/status.ts src/commands/template.ts src/commands/setup.ts src/commands/wake.ts src/commands/sleep.ts src/commands/doctor.ts src/commands/shell-init.ts src/commands/repair-session.ts src/lib/entity-index.ts src/lib/auto-linker.ts src/lib/config.ts src/lib/template-engine.ts src/lib/session-utils.ts src/lib/session-repair.ts --format esm --dts --clean",
|
|
32
32
|
"dev": "tsup src/index.ts src/commands/*.ts src/lib/*.ts --format esm --dts --watch",
|
|
33
33
|
"lint": "eslint src",
|
|
34
34
|
"typecheck": "tsc --noEmit",
|