@ducci/jarvis 1.0.29 → 1.0.30

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,59 @@
1
+ # Finding 013 — stderr Visibility and Output Truncation
2
+
3
+ ## Observed Behaviour
4
+
5
+ During a multi-run ZAP security scan session, the agent repeatedly failed to diagnose and fix the root cause of scan failures. It issued `pkill`/`kill` variants dozens of times, burned through 6 iteration limits and 2 handoffs, and ultimately gave up without producing results.
6
+
7
+ Post-mortem analysis of the debug session logs revealed two compounding problems.
8
+
9
+ ## Root Causes
10
+
11
+ ### 1. Head-only truncation buried errors at the end of output
12
+
13
+ `MAX_TOOL_RESULT = 4000` was applied as a simple head slice: `resultStr.slice(0, 4000)`. ZAP produces verbose startup logs (JVM init, add-on loading, database migration) that easily exceed 4000 characters. Five separate ZAP exec results were truncated exactly at the limit, cutting off during database migration messages. Any errors that appeared later in the output — after the verbose preamble — were silently dropped before the model ever saw them.
14
+
15
+ ### 2. Model ignored stderr even when it was visible
16
+
17
+ In one un-truncated result (810 chars), the critical error was plainly present:
18
+
19
+ ```
20
+ g_module_open() failed for libpixbufloader-tiff.so: libtiff.so.5: cannot open shared object file: No such file or directory
21
+ ```
22
+
23
+ The model's subsequent response ignored it entirely and concluded only that "no results were found in the output directory." There was no mechanism forcing the model to re-examine stderr before forming its conclusion or retrying.
24
+
25
+ Similarly, all `pkill`/`kill` commands returned `exitCode: 1` with clear stderr — yet the model continued issuing variations of the same commands without diagnosing why process termination was failing.
26
+
27
+ ## Fixes
28
+
29
+ ### Fix 1 — Head + tail truncation (`agent.js`)
30
+
31
+ Replace head-only truncation with a head+tail strategy:
32
+
33
+ ```js
34
+ // Before
35
+ resultStr.slice(0, MAX_TOOL_RESULT) + '\n[...truncated]'
36
+
37
+ // After
38
+ resultStr.slice(0, 2000) + `\n[...${resultStr.length - 4000} chars truncated...]\n` + resultStr.slice(-2000)
39
+ ```
40
+
41
+ The first 2000 chars preserve startup context; the last 2000 chars preserve the diagnostic tail where errors typically appear. The marker in the middle shows how much was dropped. Total budget stays at 4000 chars.
42
+
43
+ ### Fix 2 — Stderr nudge injection (`agent.js`)
44
+
45
+ After each iteration's tool-call loop, if any tool failed (`status === 'error'`) with non-empty `stderr`, inject a system message:
46
+
47
+ ```
48
+ [System: A command failed and produced stderr output. Examine the stderr field in the tool result carefully — it likely describes the root cause of the failure. Do not retry the same command without first addressing what stderr reports.]
49
+ ```
50
+
51
+ This creates an active forcing function — the model cannot continue to the next iteration without the nudge appearing in its context. The nudge is suppressed if loop detection already fired (to avoid contradictory instructions).
52
+
53
+ ## Known Gap
54
+
55
+ The stderr nudge only fires when `toolFailed` is true (i.e., `exec` returned `status: 'error'`). Commands that return `exitCode: 0` but still emit meaningful errors to stderr (e.g., a shell script that succeeds but a subprocess inside it fails) will not trigger the nudge. Catching that case without generating noise from normal stderr usage (npm warnings, apt-get progress) requires more context than is available at this level. Documented as a known limitation.
56
+
57
+ ## Files Changed
58
+
59
+ - `src/server/agent.js` — truncation strategy + stderr nudge injection
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ducci/jarvis",
3
- "version": "1.0.29",
3
+ "version": "1.0.30",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -139,6 +139,7 @@ async function runAgentLoop(client, config, session, prepareMessages) {
139
139
  });
140
140
 
141
141
  let toolsModified = false;
142
+ let stderrErrorInIteration = false;
142
143
  for (const toolCall of assistantMessage.tool_calls) {
143
144
  const toolName = toolCall.function.name;
144
145
  let toolArgs;
@@ -165,6 +166,9 @@ async function runAgentLoop(client, config, session, prepareMessages) {
165
166
  const toolFailed = toolStatus === 'error' || (resultObj && resultObj.status === 'error');
166
167
  if (toolFailed) {
167
168
  consecutiveFailures++;
169
+ if (resultObj && resultObj.stderr) {
170
+ stderrErrorInIteration = true;
171
+ }
168
172
  } else {
169
173
  consecutiveFailures = 0;
170
174
  }
@@ -173,7 +177,7 @@ async function runAgentLoop(client, config, session, prepareMessages) {
173
177
  runToolCalls.push({ name: toolName, args: toolArgs, status: toolStatus, result: resultStr });
174
178
 
175
179
  const sessionContent = resultStr.length > MAX_TOOL_RESULT
176
- ? resultStr.slice(0, MAX_TOOL_RESULT) + '\n[...truncated]'
180
+ ? resultStr.slice(0, 2000) + `\n[...${resultStr.length - 4000} chars truncated...]\n` + resultStr.slice(-2000)
177
181
  : resultStr;
178
182
  session.messages.push({
179
183
  role: 'tool',
@@ -201,6 +205,13 @@ async function runAgentLoop(client, config, session, prepareMessages) {
201
205
  });
202
206
  }
203
207
 
208
+ if (stderrErrorInIteration && !loopDetected) {
209
+ session.messages.push({
210
+ role: 'user',
211
+ content: '[System: A command failed and produced stderr output. Examine the stderr field in the tool result carefully — it likely describes the root cause of the failure. Do not retry the same command without first addressing what stderr reports.]',
212
+ });
213
+ }
214
+
204
215
  // Reload tools if any were created/updated this iteration
205
216
  if (toolsModified) {
206
217
  tools = await loadTools();