mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
* fix(task): rerun starts a fresh session, skip poisoned resume
When a task ended in a known agent fallback ("I reached the iteration
limit and couldn't generate a summary.", "Put your final update inside
the content string. Keep it concise.") the (agent_id, issue_id) resume
lookup would still pick that session, so a manual rerun inherited the
poisoned state and reproduced the same bad output.
Two complementary guards:
1. Daemon classifies poisoned terminal output and routes it through the
blocked path with failure_reason set ('iteration_limit' /
'agent_fallback_message'). GetLastTaskSession excludes failed tasks
with those reasons, so even comment-triggered tasks no longer resume
them. Tasks that failed mid-flight (timeout, runtime_recovery, etc.)
are still resumable, preserving MUL-1128's auto-retry contract.
2. Manual rerun marks the new task force_fresh_session=true. The daemon
claim handler skips the resume lookup entirely when the flag is set,
capturing the user-intent signal that "the prior output was bad" even
when poisoned classification misses a future fallback wording.
Auto-retry of orphaned mid-flight failures (MaybeRetryFailedTask →
CreateRetryTask) does not take this path, so it keeps resuming.
Tests: classifyPoisonedOutput unit test; integration tests assert the
SQL filter excludes poisoned classifiers, RerunIssue flips the flag,
and the normal enqueue path leaves it false.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): cap poisoned-output matcher to short trimmed text
GPT-Boy review on MUL-1630: the previous strings.Contains match would
classify any output that quoted the marker substring — including a
review/analysis that simply discussed the marker itself. Real fallback
messages are short single-sentence affairs, so cap the candidate at
~one paragraph and trim whitespace before matching. Adds regression
tests covering a long quoting review and a marker buried in a long
real conclusion; both must stay classified as completed.
Co-authored-by: multica-agent <github@multica.ai>
* fix(migrations): rename 065 force_fresh_session → 066 to clear collision
main introduced 065_project_resources after this branch was cut, so
both files shared the 065_ prefix. The readiness check
(server/cmd/server/health.go → migrations.LatestVersion) takes the
last entry by lexical order, which is 065_project_resources, leaving
this branch's 065_force_fresh_session unguarded — a deploy that
applied project_resources but not force_fresh_session would still
report ready, and the next enqueue / rerun / claim would crash on
"column force_fresh_session does not exist".
Renaming to 066_force_fresh_session puts it strictly after
project_resources so readiness blocks until it's applied.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
87 lines
2.6 KiB
Go
87 lines
2.6 KiB
Go
package daemon
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestClassifyPoisonedOutput(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
output string
|
|
wantOK bool
|
|
wantReason string
|
|
}{
|
|
{
|
|
name: "iteration limit canonical",
|
|
output: "I reached the iteration limit and couldn't generate a summary.",
|
|
wantOK: true,
|
|
wantReason: FailureReasonIterationLimit,
|
|
},
|
|
{
|
|
name: "iteration limit case insensitive",
|
|
output: "I REACHED THE ITERATION LIMIT and stopped",
|
|
wantOK: true,
|
|
wantReason: FailureReasonIterationLimit,
|
|
},
|
|
{
|
|
name: "fallback meta message",
|
|
output: "Put your final update inside the content string. Keep it concise.",
|
|
wantOK: true,
|
|
wantReason: FailureReasonAgentFallbackMsg,
|
|
},
|
|
{
|
|
name: "real conclusion is not poisoned",
|
|
output: "Fixed the bug in auth.go and pushed PR #42.",
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "empty output",
|
|
output: "",
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "mentions iteration but not the marker",
|
|
output: "Each iteration of the loop processes one record.",
|
|
wantOK: false,
|
|
},
|
|
{
|
|
// Regression guard for the GPT-Boy review on MUL-1630:
|
|
// a real review/analysis that quotes both markers must not
|
|
// be misclassified. Without the length cap, this entire
|
|
// PR's review thread would tank as a poisoned failure.
|
|
name: "long review quoting both markers is not poisoned",
|
|
output: `Review for the rerun fix.
|
|
|
|
Detection markers under consideration:
|
|
- "I reached the iteration limit and couldn't generate a summary."
|
|
- "Put your final update inside the content string. Keep it concise."
|
|
|
|
The implementation looks correct: the daemon classifies these as
|
|
fallback output, persists a dedicated failure_reason, and the SQL
|
|
filter excludes them from the resume lookup. Auto-retry of mid-flight
|
|
orphans still keeps the resume contract because CreateRetryTask does
|
|
not set force_fresh_session. Approving with a follow-up note about
|
|
the matcher being too permissive on long outputs.`,
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "marker buried inside a long agent conclusion",
|
|
output: strings.Repeat("All checks passed and the bug is fixed. ", 10) + "i reached the iteration limit while debugging earlier.",
|
|
wantOK: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
reason, ok := classifyPoisonedOutput(tc.output)
|
|
if ok != tc.wantOK {
|
|
t.Fatalf("classifyPoisonedOutput(%q) ok=%v, want %v", tc.output, ok, tc.wantOK)
|
|
}
|
|
if ok && reason != tc.wantReason {
|
|
t.Fatalf("classifyPoisonedOutput(%q) reason=%q, want %q", tc.output, reason, tc.wantReason)
|
|
}
|
|
})
|
|
}
|
|
}
|