@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.
Files changed (2) hide show
  1. package/dist/session.js +45 -1
  2. 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tjamescouch/gro",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
4
4
  "description": "Provider-agnostic LLM runtime with context management",
5
5
  "bin": {
6
6
  "gro": "./dist/main.js"