@ducci/jarvis 1.0.25 → 1.0.26

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.
@@ -0,0 +1,153 @@
1
+ # Finding 009: Non-String `response` Field Crashes Telegram Delivery
2
+
3
+ **Date:** 2026-03-01
4
+ **Severity:** High — caused "Sorry, something went wrong sending the response" with no useful information for the user
5
+ **Status:** Fixed
6
+
7
+ ---
8
+
9
+ ## Observed Session
10
+
11
+ The session ran 19 agent runs, all completing successfully (`ok` or `checkpoint_reached`). The crash occurred on run 19. The user asked:
12
+
13
+ > "List me all tool calls you did In this session. Tool name and args are enough to display for each entry."
14
+
15
+ The model returned valid JSON but placed the list of tool calls as a JSON **array** (not a string) in the `response` field:
16
+
17
+ ```json
18
+ {
19
+ "response": [
20
+ { "tool": "exec", "args": { "cmd": "find ..." } },
21
+ ...16 entries...
22
+ ],
23
+ "logSummary": "Enumerated every tool call made during the session..."
24
+ }
25
+ ```
26
+
27
+ The Telegram user received: **"Sorry, something went wrong sending the response. Please try again."**
28
+
29
+ ---
30
+
31
+ ## Bug Chain
32
+
33
+ ### Step 1 — Agent parses valid JSON, stores non-string response
34
+
35
+ `runAgentLoop` in `src/server/agent.js` successfully parsed the model's response JSON. The extraction logic had no type check:
36
+
37
+ ```js
38
+ response = parsed.response || content;
39
+ ```
40
+
41
+ `parsed.response` was an array (truthy) → `response` was set to the array. No validation. The array propagated through `finalResponse` all the way to the return value of `handleChat`.
42
+
43
+ ### Step 2 — Telegram handler crashes calling `.trim()` on an array
44
+
45
+ In `src/channels/telegram/index.js`:
46
+
47
+ ```js
48
+ const text = result.response?.trim()
49
+ || 'The agent encountered an error...';
50
+ ```
51
+
52
+ `?.` guards against `null` and `undefined` only — not against wrong types. Arrays do not have a `.trim()` method. This threw:
53
+
54
+ ```
55
+ TypeError: result.response.trim is not a function
56
+ ```
57
+
58
+ ### Step 3 — Delivery catch block sends the generic error
59
+
60
+ The TypeError was caught by the outer delivery try/catch, which replied:
61
+
62
+ ```
63
+ Sorry, something went wrong sending the response. Please try again.
64
+ ```
65
+
66
+ The user had no idea what failed. The agent had completed successfully — only the delivery step crashed.
67
+
68
+ ---
69
+
70
+ ## Root Causes
71
+
72
+ **Primary**: `agent.js` never validates that `parsed.response` is a string after JSON parsing. The response contract ("Your message to the user, in plain text.") is documented but never enforced. Any JSON value — array, object, number, null — passes through silently.
73
+
74
+ **Secondary**: `telegram/index.js` assumed `result.response` would always be a string or null/undefined, and called `.trim()` without type-guarding.
75
+
76
+ The same primary bug exists in the wrap-up path (line ~315):
77
+ ```js
78
+ response = parsedWrapUp.response || '';
79
+ ```
80
+ This would fail identically if the wrap-up model returned a non-string `response`.
81
+
82
+ ---
83
+
84
+ ## What Was Not Caught Earlier
85
+
86
+ - The JSONL log stored `response: [array]` but the run status was `ok` — nothing flagged as an error on the agent side.
87
+ - The error only surfaces in the Telegram delivery layer, which has no visibility into the JSONL log.
88
+ - The model had valid intent (listing tool calls as a structured data type) — it just put the data in the wrong JSON field type.
89
+
90
+ ---
91
+
92
+ ## Fix
93
+
94
+ ### 1. `src/server/agent.js` — normalize response to string at both sites
95
+
96
+ **Main response path:**
97
+ ```js
98
+ // Before:
99
+ response = parsed.response || content;
100
+
101
+ // After:
102
+ response = typeof parsed.response === 'string'
103
+ ? parsed.response
104
+ : JSON.stringify(parsed.response, null, 2);
105
+ ```
106
+
107
+ **Wrap-up path:**
108
+ ```js
109
+ // Before:
110
+ response = parsedWrapUp.response || '';
111
+
112
+ // After:
113
+ response = typeof parsedWrapUp.response === 'string'
114
+ ? parsedWrapUp.response
115
+ : parsedWrapUp.response != null ? JSON.stringify(parsedWrapUp.response, null, 2) : '';
116
+ ```
117
+
118
+ When the model returns a non-string (array, object), it is JSON-stringified with 2-space indentation. The user gets a readable representation of the intended content rather than a crash. This preserves the model's intent while enforcing the string contract.
119
+
120
+ ### 2. `src/channels/telegram/index.js` — defense-in-depth type guard
121
+
122
+ ```js
123
+ // Before:
124
+ const text = result.response?.trim()
125
+ || 'The agent encountered an error and could not produce a response. Please try again.';
126
+
127
+ // After:
128
+ const rawResponse = typeof result.response === 'string'
129
+ ? result.response
130
+ : result.response != null ? JSON.stringify(result.response, null, 2) : '';
131
+ const text = rawResponse.trim()
132
+ || 'The agent encountered an error and could not produce a response. Please try again.';
133
+ ```
134
+
135
+ ### 3. `docs/system-prompt.md` — explicit type constraint on `response`
136
+
137
+ Added one sentence to the `## Response Format` section:
138
+
139
+ ```
140
+ The `response` value must be a plain text string — never an array or object. If you need to present structured data (e.g. a list of items), format it as text within the string value.
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Outcome
146
+
147
+ | Fix | Files changed |
148
+ |-----|--------------|
149
+ | Coerce `parsed.response` to string in main and wrap-up paths | `src/server/agent.js` |
150
+ | Type guard before `.trim()` call | `src/channels/telegram/index.js` |
151
+ | Explicit type constraint on `response` field | `docs/system-prompt.md` |
152
+
153
+ **Effect on the debugging session**: instead of "Sorry, something went wrong sending the response", the user would have received the tool call list formatted as a readable JSON string.
@@ -30,6 +30,8 @@ There are two types of responses depending on whether you need to use tools:
30
30
  "logSummary": "A concise explanation of what you did and why, written for a human reading the logs."
31
31
  }
32
32
 
33
+ The `response` value must be a plain text string — never an array or object. If you need to present structured data (e.g. a list of items), format it as text within the string value.
34
+
33
35
  Never include markdown code fences, preamble, or any text outside this JSON object. If you cannot complete a task, explain why in the `response` field — still as valid JSON.
34
36
 
35
37
  ## Tool Use
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ducci/jarvis",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -67,8 +67,11 @@ export async function startTelegramChannel(config) {
67
67
 
68
68
  try {
69
69
  const MAX_TG = 4096;
70
- // Guard against empty response (e.g. format_error returns empty string)
71
- const text = result.response?.trim()
70
+ // Guard against empty or non-string response (e.g. model returns array instead of string)
71
+ const rawResponse = typeof result.response === 'string'
72
+ ? result.response
73
+ : result.response != null ? JSON.stringify(result.response, null, 2) : '';
74
+ const text = rawResponse.trim()
72
75
  || 'The agent encountered an error and could not produce a response. Please try again.';
73
76
  if (text.length <= MAX_TG) {
74
77
  await ctx.reply(text);
@@ -247,7 +247,9 @@ async function runAgentLoop(client, config, session, prepareMessages) {
247
247
  }
248
248
 
249
249
  session.messages.push({ role: 'assistant', content });
250
- response = parsed.response || content;
250
+ response = typeof parsed.response === 'string'
251
+ ? parsed.response
252
+ : JSON.stringify(parsed.response, null, 2);
251
253
  logSummary = parsed.logSummary || '';
252
254
 
253
255
  done = true;
@@ -312,7 +314,9 @@ async function runAgentLoop(client, config, session, prepareMessages) {
312
314
  session.messages.push({ role: 'assistant', content: wrapUpContent });
313
315
 
314
316
  if (parsedWrapUp) {
315
- response = parsedWrapUp.response || '';
317
+ response = typeof parsedWrapUp.response === 'string'
318
+ ? parsedWrapUp.response
319
+ : parsedWrapUp.response != null ? JSON.stringify(parsedWrapUp.response, null, 2) : '';
316
320
  logSummary = parsedWrapUp.logSummary || '';
317
321
  if (parsedWrapUp.checkpoint) {
318
322
  return {