Skip to content

Refactor agentdrain event processing pipeline - #54977

Merged
pelikhan merged 6 commits into
mainfrom
copilot/refactor-duplicated-event-pipeline
Aug 23, 2026
Merged

Refactor agentdrain event processing pipeline#54977
pelikhan merged 6 commits into
mainfrom
copilot/refactor-duplicated-event-pipeline

Conversation

Copilot AI commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Miner.AnalyzeEvent flattened, masked, and tokenized every event twice, rebuilt an immutable anomaly detector per call, and split inference/training/reporting across separate lock scopes.

  • Event pipeline

    • Add shared event preparation and token-level training.
    • Process inference, mutation, and report generation under one lock.
    tokens, err := m.prepare(evt)
    m.mu.Lock()
    defer m.mu.Unlock()
    
    inferResult, _ := m.findBestMatchingCluster(tokens)
    result := m.trainTokens(tokens, evt.Stage)
    report := m.detector.Analyze(result, inferResult == nil, cluster)
  • Detector lifecycle

    • Construct and validate AnomalyDetector in NewMiner.
    • Refresh it during LoadJSON, so invalid persisted thresholds fail before state replacement.
  • Package organization

    • Move tokenization and template algebra into template.go.
    • Delegate latest action-pin reference lookup to GetLatestActionPinByRepo.
  • Coverage

    • Add focused tests for miner construction and snapshot loading with valid and invalid anomaly thresholds.

Run: https://github.com/github/gh-aw/actions/runs/32618879655> Generated by 👨‍🍳 PR Sous Chef · gpt54 · 6.44 AIC · ⌖ 8.16 AIC · ⊞ 9.5K ·

Comment /souschef to run again

Copilot AI and others added 2 commits August 23, 2026 03:40
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor duplicated event pipeline in agentdrain.Miner.AnalyzeEvent Refactor agentdrain event processing pipeline Aug 23, 2026
Copilot AI requested a review from pelikhan August 23, 2026 03:42
@pelikhan
pelikhan marked this pull request as ready for review August 23, 2026 04:09
Copilot AI balanced review requested due to automatic review settings August 23, 2026 04:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Refactors agent event processing to reduce duplicate work, centralize anomaly-detector lifecycle, and improve package organization.

Changes:

  • Reuses prepared tokens and consolidates inference, training, and reporting under one lock.
  • Validates and refreshes anomaly detectors during construction and snapshot loading.
  • Moves template utilities and simplifies latest action-pin lookup.
Show a summary per file
File Description
pkg/agentdrain/template.go Houses tokenization and template algebra.
pkg/agentdrain/persist.go Validates and restores detector configuration.
pkg/agentdrain/miner.go Refactors event processing and detector lifecycle.
pkg/agentdrain/miner_test.go Tests threshold validation and snapshot refresh.
pkg/agentdrain/event.go Removes relocated tokenization logic.
pkg/agentdrain/cluster.go Removes relocated template operations.
pkg/actionpins/references.go Uses the dedicated latest-pin helper.

Review details

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/agentdrain/miner.go Outdated
Comment on lines 145 to 146
// AnalyzeEvent performs inference on the event, builds an AnomalyReport, and
// then calls TrainEvent to update the miner. Returns the match result and report.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the AnalyzeEvent doc comment in d1e95c2: it now describes the direct state update (prepare once, then inference + mutation + report under a single write lock) instead of the old TrainEvent call chain.

Comment thread pkg/agentdrain/miner.go
if err != nil {
return nil, fmt.Errorf("agentdrain: NewMiner: %w", err)
}
detector, err := NewAnomalyDetector(cfg.SimThreshold, cfg.RareClusterThreshold)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d1e95c2: NewAnomalyDetector now rejects math.NaN() explicitly (math.IsNaN(simThreshold) || simThreshold < 0 || simThreshold > 1), with new cases in TestNewAnomalyDetector_ThresholdBoundaries and TestNewMinerRejectsInvalidAnomalyThresholds.

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Ponytail review complete for PR #54977: refactor consolidates duplicated tokenization/detector logic into template.go and adds a shared trainTokens/prepare helper, netting -117/+190... actual diff is a cleanup reducing duplication, no new speculative abstractions, unused flexibility, or hand-rolled stdlib reinventions found. Lean already. Ship.

Generated by Ponytail Reviewer for #54977

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel skipped because pre-fetch PR data was unavailable: unable to fetch test file diff

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

Copy link
Copy Markdown
Contributor

Comment Memory

reviewed_at: 2026-08-23T00:00:00Z
review_event: REQUEST_CHANGES
top_themes:
  - anomaly threshold validation still accepts NaN
files_reviewed:
  - pkg/actionpins/references.go
  - pkg/agentdrain/cluster.go
  - pkg/agentdrain/event.go
  - pkg/agentdrain/miner.go
  - pkg/agentdrain/miner_test.go
  - pkg/agentdrain/persist.go
  - pkg/agentdrain/template.go
comment_count: 1

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 10.1 AIC · ⌖ 7.82 AIC · ⊞ 7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes

This refactor is moving in the right direction, but the new anomaly-detector validation is still incomplete: NaN thresholds are accepted, which undercuts the correctness of the new constructor/load-time guardrails.

Blocking theme
  • NewAnomalyDetector only checks < 0 and > 1, so math.NaN() passes validation.
  • Because this PR now eagerly constructs detectors in both NewMiner and LoadJSON, that bug directly weakens the exact safety improvement this change is trying to introduce.

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 10.1 AIC · ⌖ 7.82 AIC · ⊞ 7K
Comment /review to run again

Comment thread pkg/agentdrain/miner.go
if err != nil {
return nil, fmt.Errorf("agentdrain: NewMiner: %w", err)
}
detector, err := NewAnomalyDetector(cfg.SimThreshold, cfg.RareClusterThreshold)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This threshold validation still accepts math.NaN() because both range comparisons are false, so an invalid persisted or constructed SimThreshold can slip through and make all matching/reporting decisions behave unpredictably.

💡 Reject NaN explicitly

simThreshold < 0 || simThreshold > 1 does not catch NaN in Go. That means this refactor's new constructor-time validation is still porous, and LoadJSON can successfully hydrate a detector with a non-numeric threshold.

A direct guard fixes it:

if math.IsNaN(simThreshold) || simThreshold < 0 || simThreshold > 1 {
    return nil, fmt.Errorf("agentdrain: NewAnomalyDetector: simThreshold must be in [0,1], got %g", simThreshold)
}

Please add a test that covers math.NaN() on both NewMiner and LoadJSON, because the new validation path is the whole point of this change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied the suggested guard in d1e95c2 and added NaN coverage to TestNewAnomalyDetector_ThresholdBoundaries and TestNewMinerRejectsInvalidAnomalyThresholds. No LoadJSON NaN case was added because NaN cannot round-trip through JSON — encoding/json refuses to marshal it and there is no NaN literal to unmarshal — so the LoadJSON test uses an out-of-range value (1.1) with a comment explaining why.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Impeccable Review — distill + extract modes

This refactor moves computeSimilarity, mergeTemplate, extractParams, and Tokenize into a dedicated template.go file, promotes AnomalyDetector to a long-lived Miner field, and collapses the double-lock pattern in AnalyzeEvent into a single write-lock critical section. The structural goals are sound.

Two pre-existing comments flag the blocking issues

  1. NaN threshold bypassNewAnomalyDetector does not reject math.NaN() because range comparisons with NaN are always false. Constructor-time validation can be silently bypassed.
  2. Stale AnalyzeEvent doc comment — the comment still says it calls TrainEvent, but the refactor replaced that with a direct trainTokens call.

Non-blocking observations

  • prepare() cleanly deduplicates the flatten→mask→tokenize sequence across TrainEvent and AnalyzeEvent. Good extraction.
  • The single m.mu.Lock() in AnalyzeEvent (replacing two separate RLock + Lock acquisitions) is correct — findBestMatchingCluster and trainTokens are now atomic. No TOCTOU race.
  • trainTokens stage-propagation uses first-write-wins semantics (c.Stage set only when empty). A brief inline comment would improve readability.
  • TestLoadJSONRefreshesAndValidatesAnomalyThresholds directly accesses m.detector.threshold — this tightly couples the test to an unexported field. Consider asserting through observable behavior or a small Threshold() accessor.

Address the two flagged blocking issues before merging.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 21.7 AIC · ⌖ 9.1 AIC · ⊞ 6.2K

@github-actions github-actions Bot mentioned this pull request Aug 23, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /codebase-design and /tdd — requesting changes on a concurrency concern and two test-quality issues.

📋 Key Themes & Highlights

Key Themes

  • Lock scope widening (miner.go line 158): AnalyzeEvent now holds a write lock over both inference and training. The old code used RLock for the read-only inference pass; the new code serialises all callers entirely. For the current single-drain use case this is fine, but the intent should be documented or the two-phase approach restored.
  • Internal field access in test (miner_test.go line 72): m.detector.threshold is unexported. The test is in package agentdrain (white-box), so it compiles, but a behaviour-driven assertion would be less brittle.
  • Missing NaN edge case (miner_test.go line 43): an existing comment on NewAnomalyDetector notes that NaN slips through the range guard; the new table-driven test doesn't cover this.
  • Tokenize export (template.go line 13): all sibling helpers (computeSimilarity, mergeTemplate, extractParams) are unexported; Tokenize is the odd one out — worth confirming whether any external callers remain.

Positive Highlights

  • ✅ Eliminating the per-call NewAnomalyDetector allocation is a clear correctness win (and a nice perf win).
  • ✅ The prepare helper cleanly centralises flatten-mask-tokenize so neither TrainEvent nor AnalyzeEvent duplicate it.
  • LoadJSON now validates thresholds before swapping state, so an invalid snapshot file fails safely.
  • ✅ Moving template algebra into template.go improves navigability — the file name makes the contents immediately findable.
  • ✅ New tests cover both construction-time and load-time validation, which were previously untested paths.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 38.7 AIC · ⌖ 10.4 AIC · ⊞ 7.6K
Comment /matt to run again

Comment thread pkg/agentdrain/miner.go Outdated
defer m.mu.Unlock()
inferResult, _ := m.findBestMatchingCluster(tokens)
isNew := inferResult == nil
result := m.trainTokens(tokens, evt.Stage)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/codebase-design] AnalyzeEvent now holds a write lock for both inference and training, where the old code used RLock for the inference phase. This serializes all concurrent callers for the entire operation, eliminating previous read parallelism.

💡 Consider retaining RLock for the read-only inference pass

If concurrent callers matter, the inference pass can stay under RLock since findBestMatchingCluster is read-only:

m.mu.RLock()
inferResult, _ := m.findBestMatchingCluster(tokens)
m.mu.RUnlock()
isNew := inferResult == nil

m.mu.Lock()
defer m.mu.Unlock()
result := m.trainTokens(tokens, evt.Stage)
cluster, _ := m.store.get(result.ClusterID)
report := m.detector.Analyze(result, isNew, cluster)

The trade-off: isNew may rarely be stale when two goroutines race on a new cluster — which was also true in the previous implementation. If single-goroutine use is the intended invariant, a comment to that effect would prevent future misuse.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Kept the single write lock and documented why in d1e95c2. The inference result is now reused for the mutation (applyMatch(tokens, stage, inferResult)), which removes the second cluster search that trainTokens used to perform and makes isNew consistent with the cluster the event is trained into. Splitting the phases across RLock/Lock would reintroduce that duplicate search and a stale-isNew window, so the doc comment now states the rationale explicitly.

data, err = json.Marshal(snapshot)
require.NoError(t, err)
require.NoError(t, m.LoadJSON(data))
assert.InDelta(t, 0.8, m.detector.threshold, 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The test accesses the unexported m.detector.threshold field directly — this couples the test to internal struct layout. A behaviour-driven assertion (e.g., re-running inference after load and checking the resulting Similarity) would be less brittle and would also verify that the detector actually works with the new threshold.

💡 Suggested behavioural assertion

After LoadJSON succeeds, drive a pair of Train calls and assert that the similarity score is computed using the updated threshold rather than reading the private field directly:

// Instead of:
assert.InDelta(t, 0.8, m.detector.threshold, 0)

// Consider:
result1, _ := m.Train("foo bar baz")
result2, _ := m.Train("foo bar qux")
// similarity should reflect the reloaded threshold in cluster matching
assert.NotNil(t, result2)

Or expose a SimThreshold() float64 accessor if the field value truly needs to be asserted.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaving the direct field assertion. miner_test.go is in the same package and other tests in the package (for example TestNewAnomalyDetector_ThresholdBoundaries) already assert on detector.threshold directly; the point of this test is specifically that LoadJSON rebuilds the detector from the snapshot config, which a similarity-based assertion would only verify indirectly. Adding a public SimThreshold() accessor just for a test would grow the exported API for no production caller.

cfg.RareClusterThreshold = -1
},
},
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] TestNewMinerRejectsInvalidAnomalyThresholds doesn't cover math.NaN() for SimThreshold, which the existing comment on NewAnomalyDetector (line 32) notes is still accepted because NaN comparison returns false for both range checks. Adding this case to the table-driven test would lock in the fix explicitly.

💡 Add NaN test case
{
    name: "NaN similarity threshold",
    mutate: func(cfg *Config) {
        cfg.SimThreshold = math.NaN()
    },
},

Import "math" and add it to the existing table. This also serves as a regression test if NewAnomalyDetector is later tightened to reject NaN.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added the NaN case to TestNewMinerRejectsInvalidAnomalyThresholds and to TestNewAnomalyDetector_ThresholdBoundaries, and NewAnomalyDetector now rejects NaN (d1e95c2).


// Tokenize splits a log line on whitespace and returns the individual tokens.
func Tokenize(line string) []string {
return strings.Fields(line)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/codebase-design] prepare is a new unexported method but Tokenize remains exported. Since Tokenize is now only called from prepare (moved from event.go to template.go), it's worth confirming whether external callers still exist — if not, unexport it to keep the surface area minimal.

💡 Quick check
grep -r 'agentdrain\.Tokenize' .

If there are no external callers, rename to tokenize (lowercase) to match computeSimilarity, mergeTemplate, and extractParams — all unexported in the same file.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tokenize stays exported: it is part of the package's documented public API (pkg/agentdrain/README.md lists it under the utility API) and is covered by TestSpec_PublicAPI_Utility_Tokenize in spec_test.go, which calls it through the external agentdrain package. Unexporting it would be a breaking API change outside the scope of this refactor.

…le lock

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (190 new lines in pkg/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/54977-consolidate-agentdrain-event-pipeline-under-single-lock.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI couldn't infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-54977: Consolidate agentdrain Event Pipeline Under a Single Write Lock

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 0042-use-postgresql.md for PR #42).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 · 76.9 AIC · ⌖ 19.2 AIC · ⊞ 9.5K ·
Comment /review to run again

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

…eEvent

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan
pelikhan merged commit ff05d76 into main Aug 23, 2026
32 checks passed
@pelikhan
pelikhan deleted the copilot/refactor-duplicated-event-pipeline branch August 23, 2026 05:33
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.87.5

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.

[refactor] Duplicated event pipeline in agentdrain.Miner.AnalyzeEvent + file-organization outliers

3 participants