Skip to content

fix: prevent TTSR messagesTransformHook infinite correction loop#2203

Merged
marcusquinn merged 2 commits intomainfrom
bugfix/ttsr-message-transform-loop
Feb 24, 2026
Merged

fix: prevent TTSR messagesTransformHook infinite correction loop#2203
marcusquinn merged 2 commits intomainfrom
bugfix/ttsr-message-transform-loop

Conversation

@marcusquinn
Copy link
Owner

@marcusquinn marcusquinn commented Feb 24, 2026

Summary

The TTSR (soft rule enforcement) messagesTransformHook was causing an infinite correction loop. It scanned all text in assistant messages — including tool output from Read/Bash (code the assistant read, not code it wrote). The shell-local-params rule matched $1 in read shell files, injected a correction, the assistant acknowledged it, and the same rule fired again on the next turn because the read output was still in the scan window.

Fixes

  1. extractTextFromParts — new excludeToolOutput option skips parts with toolCallId/toolInvocationId fields (tool results)
  2. Cross-turn dedup_ttsrFiredState Map tracks which rules fired on which message IDs, preventing re-firing on the same content across turns
  3. Skip synthetic messages — TTSR correction messages (id starts with ttsr-correction-) are excluded from scanning
  4. Tightened shell-local-params regex — only matches bare $N in shell command contexts (echo/printf/return/if), not in prose or documentation

Evidence

This bug was observed live during this session — the shell-local-params rule fired ~20+ times on shell code from cron.sh and ttsr-rule-loader.sh that was read via the Read tool, not written by the assistant. Each acknowledgment kept the content in context, re-triggering the rule.

Summary by CodeRabbit

  • Bug Fixes
    • Reduced false positives in parameter detection with improved pattern matching
    • Fixed infinite correction loops using enhanced deduplication tracking across message turns
    • Enhanced text filtering to prevent tool outputs from being incorrectly processed

Three fixes to the soft TTSR rule enforcement:

1. extractTextFromParts now supports excludeToolOutput option to skip
   tool-result parts (Read/Bash output contains code the assistant *read*,
   not code it *wrote* — scanning those causes false positives)

2. Cross-turn dedup via _ttsrFiredState Map — tracks which rules fired on
   which message IDs, preventing the same rule from re-firing on the same
   content every turn (which caused an infinite acknowledgment loop)

3. Tightened shell-local-params regex to only match bare $N in shell
   command contexts (echo/printf/return/if), not in prose or documentation

4. Skip synthetic TTSR correction messages from being scanned themselves
@gemini-code-assist
Copy link

Summary of Changes

Hello @marcusquinn, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses and resolves an infinite correction loop issue within the TTSR (soft rule enforcement) messagesTransformHook. The problem stemmed from the system incorrectly scanning tool output embedded in assistant messages, leading to repeated rule violations and subsequent corrections. The implemented changes introduce several critical safeguards, including filtering out tool output from scans, implementing cross-turn deduplication for fired rules, and ignoring synthetic correction messages, alongside a more precise regular expression for shell parameter detection.

Highlights

  • Tool Output Exclusion: Modified the extractTextFromParts function to prevent scanning tool output (e.g., from Read/Bash tools) embedded in assistant messages, which previously caused false positives for TTSR rules.
  • Cross-Turn Deduplication: Introduced a new _ttsrFiredState Map to track and prevent TTSR rules from re-firing on the same content across multiple turns, effectively addressing infinite correction loops.
  • Synthetic Message Skipping: Configured the TTSR messagesTransformHook to ignore its own synthetic correction messages (those with IDs starting with ttsr-correction-), avoiding redundant processing.
  • Regex Tightening for shell-local-params: Refined the regular expression for the shell-local-params rule to only match bare $N parameters within specific shell command contexts, reducing false positives in prose or documentation.
Changelog
  • .agents/plugins/opencode-aidevops/index.mjs
    • Updated the shell-local-params rule's regular expression to be more specific, matching $N only in shell command contexts.
    • Modified the extractTextFromParts function to accept an excludeToolOutput option, preventing the extraction of text from tool-related parts.
    • Added a new _ttsrFiredState Map to track which TTSR rules have fired on specific message IDs, enabling cross-turn deduplication.
    • Updated the messagesTransformHook to utilize the excludeToolOutput option, filter out previously injected synthetic TTSR correction messages, and apply the cross-turn deduplication logic.
  • MODELS-PERFORMANCE.md
    • Updated the "Last updated" timestamp.
  • MODELS.md
    • Updated the "Last updated" timestamp.
Activity
  • No human activity has occurred on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Walkthrough

Enhanced TTSR implementation with improved regex pattern for shell parameters, added cross-turn dedup state tracking via _ttsrFiredState map to prevent infinite correction loops, and extended extractTextFromParts API with options filtering for tool-output exclusion. Updated documentation and timestamps.

Changes

Cohort / File(s) Summary
TTSR Enhancement
.agents/plugins/opencode-aidevops/index.mjs
Refactored shell TTSR rule pattern (shell-local-params) for context-aware matching; enhanced extractTextFromParts with options parameter (excludeToolOutput filtering); introduced cross-turn dedup state (_ttsrFiredState) to track rule firings per message ID; enhanced messagesTransformHook to filter synthetic corrections and implement cross-turn dedup logic; added documentation for dedup and filtering mechanisms.
Timestamp Updates
MODELS-PERFORMANCE.md, MODELS.md
Updated last-modified timestamps to 2026-02-24T02:58:49Z.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

needs-review

Poem

🤖 Cross-turn dedup flows through the state,
Shell patterns sharpen—false positives abate,
Synthetic corrections filtered with care,
DevOps precision—A-grade quality there! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title precisely describes the main fix: preventing an infinite correction loop in TTSR's messagesTransformHook. It directly maps to the primary changeset objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix/ttsr-message-transform-loop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

🔍 Code Quality Report

�[0;35m[MONITOR]�[0m Code Review Monitoring Report

�[0;34m[INFO]�[0m Latest Quality Status:
SonarCloud: 0 bugs, 0 vulnerabilities, 42 code smells

�[0;34m[INFO]�[0m Recent monitoring activity:
Tue Feb 24 03:46:17 UTC 2026: Code review monitoring started
Tue Feb 24 03:46:18 UTC 2026: SonarCloud - Bugs: 0, Vulnerabilities: 0, Code Smells: 42

📈 Current Quality Metrics

  • BUGS: 0
  • CODE SMELLS: 42
  • VULNERABILITIES: 0

Generated on: Tue Feb 24 03:46:20 UTC 2026


Generated by AI DevOps Framework Code Review Monitoring

@sonarqubecloud
Copy link

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively addresses an infinite loop bug in the TTSR mechanism by introducing several targeted fixes. The approach of excluding tool output from scans, adding cross-turn deduplication, and skipping synthetic messages is solid. My review focuses on further refining the implementation to improve robustness, efficiency, and prevent potential issues like memory leaks in a long-running process. I've also suggested a small tweak to the new regex to improve its accuracy.

* across multiple LLM turns (which caused an infinite correction loop).
* @type {Map<string, Set<string>>} ruleId -> Set of messageIDs already corrected
*/
const _ttsrFiredState = new Map();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The _ttsrFiredState map is declared as a module-level constant. This will cause problems in environments where the agent process is long-lived and handles multiple independent conversations or tasks, either sequentially or concurrently.

  1. Cross-session state leakage: The deduplication state from one conversation will incorrectly affect subsequent conversations.
  2. Memory leak: The map will grow indefinitely over the lifetime of the process, as it's never cleared.

This state should be scoped to a single conversation/session. Consider either:

  • Passing a session-specific state object through the hook chain.
  • Using a session/conversation ID to key the _ttsrFiredState map, e.g., Map<sessionId, Map<ruleId, Set<messageId>>>.
  • Finding a hook that fires at the start of a session to clear this global state.

// Only match bare $N at the start of a line or after whitespace in what looks
// like a shell assignment/command context — avoids matching $1 inside prose,
// documentation, quoted examples, or tool output from file reads.
pattern: "^\\s+(?:echo|printf|return|if|\\[\\[).*\\$[1-9](?!.*local\\s+\\w+=)",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The updated regex is a great improvement for reducing false positives by being more context-aware. However, it seems to have become a bit too restrictive and might miss valid cases. For instance, it won't catch direct use of positional parameters in variable assignments like my_var=$1, which this rule should probably flag.

Also, the negative lookahead (?!.*local\s+\w+=) might not reliably prevent matches on the declaration line itself (e.g., local var="$1"), because the lookahead only checks characters after the $1.

Consider adjusting the pattern to include assignment contexts and use a negative lookahead at the start of the line to more robustly exclude local declarations.

Suggested change
pattern: "^\\s+(?:echo|printf|return|if|\\[\\[).*\\$[1-9](?!.*local\\s+\\w+=)",
pattern: "^\\s*(?!\\s*local\\b)(?:\\w+=|echo|printf|return|if|\\[\\[).*\\$[1-9]",

Comment on lines +1652 to +1664
.filter((p) => {
if (!p || typeof p.text !== "string") return false;
if (p.type !== "text") return false;
// When excludeToolOutput is set, skip parts that contain tool output.
// Tool results are embedded in assistant messages as text parts whose
// content is the serialized tool response. We detect these by checking
// for the tool-invocation/tool-result type or by the presence of
// toolCallId/toolInvocationId fields that OpenCode attaches.
if (options.excludeToolOutput) {
if (p.toolCallId || p.toolInvocationId) return false;
}
return true;
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The filtering logic here is correct, but it could be made more concise and arguably more readable by combining the conditions into a single boolean expression. This avoids the nested if statements and multiple return points.

    .filter(
      (p) =>
        p &&
        p.type === "text" &&
        typeof p.text === "string" &&
        !(options.excludeToolOutput && (p.toolCallId || p.toolInvocationId))
    )

Comment on lines +1735 to 1737
if (!allViolations.some((av) => av.rule.id === ruleId)) {
allViolations.push({ ...v, msgId });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using allViolations.some(...) for deduplication inside a loop has a time complexity of O(n) for each check, making the overall violation collection O(n^2) in the worst case (where n is the number of violations).

While the number of violations is likely small in practice, using a Set to track seen rule IDs is more efficient (O(1) lookup) and can make the code's intent clearer.

You could introduce a Set before iterating through the messages to track rule IDs found within this scan.

For example:

  const allViolations = [];
  const seenRuleIdsInScan = new Set(); // New

  for (const msg of assistantMessages) {
    // ...
    for (const v of violations) {
      const ruleId = v.rule.id;
      // ... cross-turn dedup
      
      // Deduplicate by rule id within this scan
      if (!seenRuleIdsInScan.has(ruleId)) {
        allViolations.push({ ...v, msgId });
        seenRuleIdsInScan.add(ruleId);
      }
    }
  }

@marcusquinn marcusquinn merged commit 060d4ca into main Feb 24, 2026
18 of 19 checks passed
@marcusquinn marcusquinn deleted the bugfix/ttsr-message-transform-loop branch February 24, 2026 03:48
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
.agents/plugins/opencode-aidevops/index.mjs (1)

1669-1676: Cap _ttsrFiredState to prevent unbounded growth.
The new Map grows with every message ID across long sessions. For A‑grade reliability, prune like intentByCallId (Line 1392) or cap per-rule entries.

♻️ Suggested cap (example)
+const TTSR_DEDUP_MAX = 5000;
+const TTSR_DEDUP_PRUNE = 2500;
-    _ttsrFiredState.get(v.rule.id).add(v.msgId);
+    const firedSet = _ttsrFiredState.get(v.rule.id);
+    firedSet.add(v.msgId);
+    if (firedSet.size > TTSR_DEDUP_MAX) {
+      const it = firedSet.values();
+      for (let i = 0; i < TTSR_DEDUP_PRUNE; i++) {
+        firedSet.delete(it.next().value);
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/plugins/opencode-aidevops/index.mjs around lines 1669 - 1676, _tt
srFiredState currently grows unbounded; implement the same pruning/capping
approach used by intentByCallId to avoid memory bloat: when adding a message ID
to _ttsrFiredState, enforce a maximum number of rule keys (evict oldest entries
when the Map exceeds cap) and also cap each rule's Set size (drop oldest message
IDs or use a sliding/TTL policy) so per-rule Sets and the Map cannot grow
indefinitely; update functions that insert into _ttsrFiredState to perform
eviction and optionally expose constants for MAX_RULE_KEYS and MAX_IDS_PER_RULE
for tuning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.agents/plugins/opencode-aidevops/index.mjs:
- Around line 1505-1509: The regex in the pattern field
("^\\s+(?:echo|printf|return|if|\\[\\[).*\\$[1-9](?!.*local\\s+\\w+=)") requires
at least one leading whitespace so it misses unindented shell lines like "echo
$1"; update the pattern to allow zero or more leading spaces (use ^\\s* instead
of ^\\s+) so it matches commands at column 0 as well, keeping the rest of the
expression and the negative lookahead (?!.*local\\s+\\w+=) intact to still
exclude cases using local var assignment.
- Around line 1717-1749: The _ttsrFiredState dedup currently keys only by
ruleId->Set of msgId which can suppress violations across sessions; change the
dedup to use a composite key built from sessionID and msgId (e.g.
`${sessionID}:${msgId}`) everywhere _ttsrFiredState is read and written (the
checks in the loop that reads firedOn via _ttsrFiredState.get(ruleId) and the
recording loop that calls _ttsrFiredState.set/get), and ensure you skip
composite dedup logic when either sessionID or msgId is empty/unavailable so no
cross-session suppression occurs.
- Around line 1644-1664: The comment above extractTextFromParts is misleading
about detection order: update it to state that parts are first filtered to type
=== "text", so excludeToolOutput only needs to skip text parts that represent
embedded tool output by checking toolCallId/toolInvocationId; remove any mention
of trying to detect tool-invocation/tool-result types (those are already
filtered out by the type check) and make the comment clearly describe that
excludeToolOutput targets text parts with OpenCode's toolCallId/toolInvocationId
markers.

---

Nitpick comments:
In @.agents/plugins/opencode-aidevops/index.mjs:
- Around line 1669-1676: _tt srFiredState currently grows unbounded; implement
the same pruning/capping approach used by intentByCallId to avoid memory bloat:
when adding a message ID to _ttsrFiredState, enforce a maximum number of rule
keys (evict oldest entries when the Map exceeds cap) and also cap each rule's
Set size (drop oldest message IDs or use a sliding/TTL policy) so per-rule Sets
and the Map cannot grow indefinitely; update functions that insert into
_ttsrFiredState to perform eviction and optionally expose constants for
MAX_RULE_KEYS and MAX_IDS_PER_RULE for tuning.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6cdd6d and fc7aca8.

📒 Files selected for processing (3)
  • .agents/plugins/opencode-aidevops/index.mjs
  • MODELS-PERFORMANCE.md
  • MODELS.md

Comment on lines +1505 to 1509
// Only match bare $N at the start of a line or after whitespace in what looks
// like a shell assignment/command context — avoids matching $1 inside prose,
// documentation, quoted examples, or tool output from file reads.
pattern: "^\\s+(?:echo|printf|return|if|\\[\\[).*\\$[1-9](?!.*local\\s+\\w+=)",
correction: "Use `local var=\"$1\"` pattern — never use positional parameters directly (SonarCloud S7679).",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Regex misses unindented shell lines (Line 1508).
The comment says “start of a line,” but ^\\s+ won’t match echo $1 at column 0. That weakens enforcement and could let violations slip, hurting shell quality coverage.

🔧 Suggested fix
-    pattern: "^\\s+(?:echo|printf|return|if|\\[\\[).*\\$[1-9](?!.*local\\s+\\w+=)",
+    pattern: "^\\s*(?:echo|printf|return|if|\\[\\[).*\\$[1-9](?!.*local\\s+\\w+=)",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/plugins/opencode-aidevops/index.mjs around lines 1505 - 1509, The
regex in the pattern field
("^\\s+(?:echo|printf|return|if|\\[\\[).*\\$[1-9](?!.*local\\s+\\w+=)") requires
at least one leading whitespace so it misses unindented shell lines like "echo
$1"; update the pattern to allow zero or more leading spaces (use ^\\s* instead
of ^\\s+) so it matches commands at column 0 as well, keeping the rest of the
expression and the negative lookahead (?!.*local\\s+\\w+=) intact to still
exclude cases using local var assignment.

Comment on lines 1644 to +1664
* @param {Array<object>} parts - Array of Part objects
* @param {object} [options] - Extraction options
* @param {boolean} [options.excludeToolOutput=false] - Skip tool-result/tool-invocation parts
* @returns {string} Concatenated text content
*/
function extractTextFromParts(parts) {
function extractTextFromParts(parts, options = {}) {
if (!Array.isArray(parts)) return "";
return parts
.filter((p) => p && p.type === "text" && typeof p.text === "string")
.filter((p) => {
if (!p || typeof p.text !== "string") return false;
if (p.type !== "text") return false;
// When excludeToolOutput is set, skip parts that contain tool output.
// Tool results are embedded in assistant messages as text parts whose
// content is the serialized tool response. We detect these by checking
// for the tool-invocation/tool-result type or by the presence of
// toolCallId/toolInvocationId fields that OpenCode attaches.
if (options.excludeToolOutput) {
if (p.toolCallId || p.toolInvocationId) return false;
}
return true;
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the file and examine the context around the function
head -1700 .agents/plugins/opencode-aidevops/index.mjs | tail -100

Repository: marcusquinn/aidevops

Length of output: 4340


🏁 Script executed:

# Search for Part-related type definitions, schemas, or comments
rg -n "Part\s*=|Part\s*{|interface Part|type Part|class Part" -A 5

Repository: marcusquinn/aidevops

Length of output: 216


🏁 Script executed:

# Look for tool-result and tool-invocation type usage
rg -n "tool-result|tool-invocation" -B 2 -A 2

Repository: marcusquinn/aidevops

Length of output: 1142


🏁 Script executed:

# Search for where toolCallId and toolInvocationId are set or checked
rg -n "toolCallId|toolInvocationId" -B 2 -A 2

Repository: marcusquinn/aidevops

Length of output: 752


🏁 Script executed:

# Look for OpenCode-specific documentation or comments about Part structure
rg -n "OpenCode|tool output|tool response|Part.*schema" -i

Repository: marcusquinn/aidevops

Length of output: 50377


🏁 Script executed:

# Search for where parts are created or populated in the file
rg -n "type.*tool|toolCallId|toolInvocationId" .agents/plugins/opencode-aidevops/index.mjs -B 2 -A 2

Repository: marcusquinn/aidevops

Length of output: 444


🏁 Script executed:

# Look for function calls that create or process parts with tool data
rg -n "parts\[|parts\.push|\.parts\s*=" .agents/plugins/opencode-aidevops/index.mjs -B 1 -A 1 | head -100

Repository: marcusquinn/aidevops

Length of output: 524


🏁 Script executed:

# Search for any type definitions or interfaces for Part
fd "\.d\.ts|\.ts$|\.mjs$" .agents/plugins/opencode-aidevops/ -exec grep -l "Part" {} \;

Repository: marcusquinn/aidevops

Length of output: 262


🏁 Script executed:

# Check OpenCode documentation or configuration comments about Part structure
rg -n "TextPart|ToolResultPart|type.*:.*string" .agents/plugins/opencode-aidevops/ -A 2 | head -80

Repository: marcusquinn/aidevops

Length of output: 1620


🏁 Script executed:

# Search for how messagesTransformHook processes parts and messages
rg -n "messagesTransformHook|tool-result|tool output" .agents/plugins/opencode-aidevops/index.mjs -B 3 -A 3

Repository: marcusquinn/aidevops

Length of output: 3230


🏁 Script executed:

# Look for where extractTextFromParts is called to understand the context
rg -n "extractTextFromParts" .agents/plugins/opencode-aidevops/index.mjs -B 2 -A 2

Repository: marcusquinn/aidevops

Length of output: 522


🏁 Script executed:

# Search for any test or validation code that shows Part structure
rg -n "toolCallId|toolInvocationId|excludeToolOutput" .agents/plugins/opencode-aidevops/ -B 2 -A 2

Repository: marcusquinn/aidevops

Length of output: 2198


🏁 Script executed:

# Check if there's OpenCode API documentation in the repo
find .agents -name "*opencode*.md" -type f | xargs grep -l "Part\|part\|tool-result" 2>/dev/null | head -5

Repository: marcusquinn/aidevops

Length of output: 160


🏁 Script executed:

# Search for any real usage of parts with tool information in the entire codebase
rg -n "\.parts|message.*parts|msg\.parts" .agents/plugins/opencode-aidevops/ -B 2 -A 2 | head -100

Repository: marcusquinn/aidevops

Length of output: 1578


🏁 Script executed:

# Look for type field being checked elsewhere in the code
rg -n "p\.type|\.type\s*===|\.type\s*===" .agents/plugins/opencode-aidevops/index.mjs -C 1

Repository: marcusquinn/aidevops

Length of output: 414


🏁 Script executed:

# Search OpenCode documentation files for Part schema
find . -name "*.md" -path "*opencode*" -exec grep -l "Part\|TextPart\|ToolResult" {} \; 2>/dev/null

Repository: marcusquinn/aidevops

Length of output: 110


🏁 Script executed:

# Check the opencode-server.md for Part schema info
cat .agents/tools/ai-assistants/opencode-server.md | grep -A 30 -i "part\|textpart\|tool.*result" | head -60

Repository: marcusquinn/aidevops

Length of output: 1318


🏁 Script executed:

# Check the t1305 issue file which might have plugin details
cat ./todo/t1305-opencode-streaming-hooks-issue.md | head -100

Repository: marcusquinn/aidevops

Length of output: 4282


🏁 Script executed:

# Look at the actual plugin hook parameters to understand Part structure better
rg -n "experimental\.chat\.messages\.transform|plugin.*part" .agents/plugins/opencode-aidevops/index.mjs -B 5 -A 5

Repository: marcusquinn/aidevops

Length of output: 1009


Update comment to accurately reflect the code flow — type filtering happens before excludeToolOutput check.

The function filters parts by type === "text" (line 1654) before checking excludeToolOutput (line 1660). Since non-text parts like type: "tool-result" or type: "tool-invocation" are already rejected, the comment suggesting a check for those types is misleading. The excludeToolOutput logic correctly targets text parts containing tool output, which OpenCode marks via toolCallId/toolInvocationId fields. Clarify the comment to remove the reference to type-based detection, which cannot occur due to the upstream filter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/plugins/opencode-aidevops/index.mjs around lines 1644 - 1664, The
comment above extractTextFromParts is misleading about detection order: update
it to state that parts are first filtered to type === "text", so
excludeToolOutput only needs to skip text parts that represent embedded tool
output by checking toolCallId/toolInvocationId; remove any mention of trying to
detect tool-invocation/tool-result types (those are already filtered out by the
type check) and make the comment clearly describe that excludeToolOutput targets
text parts with OpenCode's toolCallId/toolInvocationId markers.

Comment on lines 1717 to +1749
for (const msg of assistantMessages) {
const text = extractTextFromParts(msg.parts);
const msgId = msg.info?.id || "";

// Extract only assistant-authored text, excluding tool output.
// Tool results (Read, Bash, etc.) contain code the assistant *read*,
// not code it *wrote* — scanning those causes false positives.
const text = extractTextFromParts(msg.parts, { excludeToolOutput: true });
if (!text) continue;

const violations = scanForViolations(text);
for (const v of violations) {
// Deduplicate by rule id
if (!allViolations.some((av) => av.rule.id === v.rule.id)) {
allViolations.push(v);
const ruleId = v.rule.id;

// Cross-turn dedup: skip if this rule already fired on this message
const firedOn = _ttsrFiredState.get(ruleId);
if (firedOn && firedOn.has(msgId)) continue;

// Deduplicate by rule id within this scan
if (!allViolations.some((av) => av.rule.id === ruleId)) {
allViolations.push({ ...v, msgId });
}
}
}

if (allViolations.length === 0) return;

// Record that these rules fired on these messages (cross-turn dedup)
for (const v of allViolations) {
if (!_ttsrFiredState.has(v.rule.id)) {
_ttsrFiredState.set(v.rule.id, new Set());
}
_ttsrFiredState.get(v.rule.id).add(v.msgId);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's search for messageID and sessionID patterns in the codebase
rg -n "messageID|sessionID|msg\.info\.id" --type mjs -B 2 -A 2

Repository: marcusquinn/aidevops

Length of output: 91


🏁 Script executed:

# Look for where _ttsrFiredState is defined and initialized
rg -n "_ttsrFiredState" --type mjs -B 3 -A 3

Repository: marcusquinn/aidevops

Length of output: 91


🏁 Script executed:

# Search for how info.id is set or created in messages
rg -n "info.*id|\.id\s*=" --type mjs | head -50

Repository: marcusquinn/aidevops

Length of output: 91


🏁 Script executed:

# Look for session-related handling and function context
rg -n "sessionID|session\s*ID|session.*id" --type mjs -B 2 -A 2

Repository: marcusquinn/aidevops

Length of output: 91


🏁 Script executed:

# Check the function containing the dedup code to see what parameters/scope it has
sed -n '1700,1760p' ".agents/plugins/opencode-aidevops/index.mjs"

Repository: marcusquinn/aidevops

Length of output: 2131


🏁 Script executed:

# Search for the function definition and _ttsrFiredState initialization
rg -n "function|const.*=.*function|\(.*sessionID" ".agents/plugins/opencode-aidevops/index.mjs" -B 5 -A 2 | head -100

Repository: marcusquinn/aidevops

Length of output: 3737


🏁 Script executed:

# Find _ttsrFiredState definition/initialization
rg -n "_ttsrFiredState\s*=" ".agents/plugins/opencode-aidevops/index.mjs" -B 5 -A 5

Repository: marcusquinn/aidevops

Length of output: 580


🏁 Script executed:

# Check the file structure to understand context better
head -100 ".agents/plugins/opencode-aidevops/index.mjs"

Repository: marcusquinn/aidevops

Length of output: 2773


🏁 Script executed:

# Look for where assistantMessages come from and the broader context
sed -n '1650,1720p' ".agents/plugins/opencode-aidevops/index.mjs"

Repository: marcusquinn/aidevops

Length of output: 2963


🏁 Script executed:

# Look for how output is structured and if it contains sessionID
rg -n "output\.|session" ".agents/plugins/opencode-aidevops/index.mjs" | grep -E "(output\.|sessionID|session.*context)" | head -30

Repository: marcusquinn/aidevops

Length of output: 1813


🏁 Script executed:

# Check if there's any session/conversation ID tracking in the hook context
sed -n '1680,1800p' ".agents/plugins/opencode-aidevops/index.mjs"

Repository: marcusquinn/aidevops

Length of output: 4313


🏁 Script executed:

# Look for where messagesTransformHook is called and what parameters are available
rg -n "messagesTransformHook|experimental\.chat\.messages\.transform" ".agents/plugins/opencode-aidevops/index.mjs" -B 5 -A 5

Repository: marcusquinn/aidevops

Length of output: 1503


Include sessionID in the dedup key to prevent cross-session suppression.

The global _ttsrFiredState Map uses only msgId, which can cause rule violations to be incorrectly suppressed across different sessions if message IDs collide or are missing. Build a composite dedup key like ${sessionID}:${msgId} and skip dedup when either component is empty or unavailable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/plugins/opencode-aidevops/index.mjs around lines 1717 - 1749, The
_ttsrFiredState dedup currently keys only by ruleId->Set of msgId which can
suppress violations across sessions; change the dedup to use a composite key
built from sessionID and msgId (e.g. `${sessionID}:${msgId}`) everywhere
_ttsrFiredState is read and written (the checks in the loop that reads firedOn
via _ttsrFiredState.get(ruleId) and the recording loop that calls
_ttsrFiredState.set/get), and ensure you skip composite dedup logic when either
sessionID or msgId is empty/unavailable so no cross-session suppression occurs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant