@tjamescouch/gro 1.3.7 → 1.3.8
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/session.js +45 -1
- package/package.json +1 -1
package/dist/session.js
CHANGED
|
@@ -44,8 +44,52 @@ export function saveSession(id, messages, meta) {
|
|
|
44
44
|
writeFileSync(join(dir, "messages.json"), JSON.stringify(messages, null, 2));
|
|
45
45
|
writeFileSync(join(dir, "meta.json"), JSON.stringify(fullMeta, null, 2));
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Sanitize a message array so every assistant tool_use has a matching tool_result.
|
|
49
|
+
* When a session is killed mid-tool-call (e.g. SIGTERM from niki), the assistant
|
|
50
|
+
* message with tool_calls is saved but the tool result messages are not.
|
|
51
|
+
* The Anthropic API rejects this with a 400 error, causing an infinite crash loop.
|
|
52
|
+
*
|
|
53
|
+
* Strategy: walk backwards from the end. If we find an assistant message with
|
|
54
|
+
* tool_calls that have no matching tool-role responses, inject synthetic
|
|
55
|
+
* tool_result placeholders so the API accepts the history.
|
|
56
|
+
*/
|
|
57
|
+
function sanitizeToolPairs(messages) {
|
|
58
|
+
if (messages.length === 0)
|
|
59
|
+
return messages;
|
|
60
|
+
// Collect all tool_call IDs that have results
|
|
61
|
+
const answeredIds = new Set();
|
|
62
|
+
for (const m of messages) {
|
|
63
|
+
if (m.role === "tool" && m.tool_call_id) {
|
|
64
|
+
answeredIds.add(m.tool_call_id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Find assistant messages with unanswered tool_calls and inject placeholders
|
|
68
|
+
const result = [];
|
|
69
|
+
for (const m of messages) {
|
|
70
|
+
result.push(m);
|
|
71
|
+
const toolCalls = m.tool_calls;
|
|
72
|
+
if (m.role === "assistant" && Array.isArray(toolCalls)) {
|
|
73
|
+
for (const tc of toolCalls) {
|
|
74
|
+
if (!answeredIds.has(tc.id)) {
|
|
75
|
+
result.push({
|
|
76
|
+
role: "tool",
|
|
77
|
+
from: "system",
|
|
78
|
+
content: "[Session interrupted — tool call was not completed. The agent was terminated before this tool could return a result.]",
|
|
79
|
+
tool_call_id: tc.id,
|
|
80
|
+
name: tc.function?.name,
|
|
81
|
+
});
|
|
82
|
+
answeredIds.add(tc.id);
|
|
83
|
+
Logger.warn(`Session repair: injected placeholder tool_result for orphaned call ${tc.id} (${tc.function?.name ?? "unknown"})`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
47
90
|
/**
|
|
48
91
|
* Load a session from disk. Returns null if not found.
|
|
92
|
+
* Automatically repairs orphaned tool_use blocks from interrupted sessions.
|
|
49
93
|
*/
|
|
50
94
|
export function loadSession(id) {
|
|
51
95
|
const dir = sessionDir(id);
|
|
@@ -55,7 +99,7 @@ export function loadSession(id) {
|
|
|
55
99
|
return null;
|
|
56
100
|
}
|
|
57
101
|
try {
|
|
58
|
-
const messages = JSON.parse(readFileSync(msgPath, "utf-8"));
|
|
102
|
+
const messages = sanitizeToolPairs(JSON.parse(readFileSync(msgPath, "utf-8")));
|
|
59
103
|
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
60
104
|
return { messages, meta };
|
|
61
105
|
}
|