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>
58 lines
2.3 KiB
Go
58 lines
2.3 KiB
Go
package daemon
|
|
|
|
import "strings"
|
|
|
|
// FailureReason values for tasks that "completed" with output but the
|
|
// output is actually a known agent fallback marker — i.e. the agent gave
|
|
// up and emitted a meta message instead of a real result. Listed here so
|
|
// the server-side query GetLastTaskSession can filter them out and a
|
|
// rerun starts from a fresh agent session instead of resuming the same
|
|
// poisoned conversation.
|
|
const (
|
|
FailureReasonIterationLimit = "iteration_limit"
|
|
FailureReasonAgentFallbackMsg = "agent_fallback_message"
|
|
)
|
|
|
|
// poisonedOutputMaxLen caps how long an output can be and still be
|
|
// classified as a poisoned fallback. Real fallback messages are short,
|
|
// one-sentence affairs; a long output that happens to mention a marker
|
|
// is almost certainly a real conclusion (e.g. a code-review reply
|
|
// quoting these strings, like the one currently quoting them in
|
|
// MUL-1630). The cap intentionally errs on the side of NOT classifying
|
|
// — a missed poisoned task gets retried by user action, but a
|
|
// false-positive turns a successful task into a failure and a system
|
|
// comment.
|
|
const poisonedOutputMaxLen = 320
|
|
|
|
// poisonedMarkers maps a substring fingerprint of a known agent fallback
|
|
// terminal message to its failure_reason classifier. Match is case-
|
|
// insensitive and substring-based; the cap above prevents long outputs
|
|
// that quote a marker from being misclassified.
|
|
var poisonedMarkers = []struct {
|
|
Substring string
|
|
Reason string
|
|
}{
|
|
{"i reached the iteration limit", FailureReasonIterationLimit},
|
|
{"put your final update inside the content string", FailureReasonAgentFallbackMsg},
|
|
}
|
|
|
|
// classifyPoisonedOutput reports whether output matches a known agent
|
|
// fallback terminal message and, if so, returns the failure_reason that
|
|
// should be persisted on the task row. Long outputs are never
|
|
// classified: a real fallback is the agent's only utterance for the
|
|
// turn, so anything beyond ~one paragraph is treated as a real result
|
|
// even if it contains a marker substring.
|
|
func classifyPoisonedOutput(output string) (string, bool) {
|
|
trimmed := strings.TrimSpace(output)
|
|
if trimmed == "" || len(trimmed) > poisonedOutputMaxLen {
|
|
return "", false
|
|
}
|
|
lowered := strings.ToLower(trimmed)
|
|
for _, m := range poisonedMarkers {
|
|
if strings.Contains(lowered, m.Substring) {
|
|
return m.Reason, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|