mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +08:00
fix(goal): sync completion memory after transaction commit
This commit is contained in:
parent
542f075759
commit
52a2843bdf
@ -11,6 +11,11 @@ import org.springframework.dao.DuplicateKeyException;
|
|||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
import org.springframework.transaction.TransactionDefinition;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
import vip.mate.audit.service.AuditEventService;
|
import vip.mate.audit.service.AuditEventService;
|
||||||
import vip.mate.exception.MateClawException;
|
import vip.mate.exception.MateClawException;
|
||||||
import vip.mate.goal.config.GoalProperties;
|
import vip.mate.goal.config.GoalProperties;
|
||||||
@ -57,6 +62,7 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
private final AuditEventService auditEventService;
|
private final AuditEventService auditEventService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private ApplicationEventPublisher applicationEventPublisher;
|
private ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
private PlatformTransactionManager transactionManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional — only set when the memory subsystem is wired. On goal
|
* Optional — only set when the memory subsystem is wired. On goal
|
||||||
@ -89,6 +95,11 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
this.applicationEventPublisher = publisher;
|
this.applicationEventPublisher = publisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
public void setTransactionManager(PlatformTransactionManager manager) {
|
||||||
|
this.transactionManager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== CRUD ====================
|
// ==================== CRUD ====================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -431,24 +442,45 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
writeEvent(id, GoalEventType.COMPLETED, null, detail);
|
writeEvent(id, GoalEventType.COMPLETED, null, detail);
|
||||||
recordAudit("goal.completed", g, detail);
|
recordAudit("goal.completed", g, detail);
|
||||||
|
|
||||||
// Forward to long-term memory on completion. Best-effort: a failing
|
syncCompletionMemoryAfterCommit(g, result);
|
||||||
// memory pipeline must not roll back the DB transition.
|
return g;
|
||||||
if (memoryManager != null) {
|
}
|
||||||
|
|
||||||
|
private void syncCompletionMemoryAfterCommit(GoalEntity goal, GoalEvaluationResult result) {
|
||||||
|
var target = memoryManager;
|
||||||
|
if (target == null) return;
|
||||||
|
// Snapshot values before returning the mutable entity to the caller.
|
||||||
|
Long agentId = goal.getAgentId();
|
||||||
|
String conversationId = goal.getConversationId();
|
||||||
|
String subject = "[goal completed] " + goal.getTitle();
|
||||||
|
String summary = goal.getProgressSummary() != null && !goal.getProgressSummary().isBlank()
|
||||||
|
? goal.getProgressSummary() : "Final score: " + (result != null ? result.score() : "—");
|
||||||
|
Runnable sync = () -> {
|
||||||
try {
|
try {
|
||||||
String summary = g.getProgressSummary() != null && !g.getProgressSummary().isBlank()
|
if (transactionManager != null) {
|
||||||
? g.getProgressSummary()
|
// afterCommit still has the old transaction's resources bound.
|
||||||
: "Final score: " + (result != null ? result.score() : "—");
|
// Adapter DB writes need their own transaction to commit reliably.
|
||||||
memoryManager.syncAll(
|
TransactionTemplate independent = new TransactionTemplate(transactionManager);
|
||||||
g.getAgentId(),
|
independent.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||||
g.getConversationId(),
|
independent.executeWithoutResult(status -> target.syncAll(agentId, conversationId, subject, summary));
|
||||||
"[goal completed] " + g.getTitle(),
|
} else {
|
||||||
summary);
|
target.syncAll(agentId, conversationId, subject, summary);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("[GoalService] memory syncAll on goal completion failed: {}", e.getMessage());
|
log.debug("[GoalService] memory syncAll on goal completion failed: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if (TransactionSynchronizationManager.isActualTransactionActive()) {
|
||||||
|
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||||
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
|
@Override public void afterCommit() { sync.run(); }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.debug("[GoalService] skipped completion memory: no commit synchronization available");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sync.run(); // Direct/non-transactional callers retain best-effort behavior.
|
||||||
}
|
}
|
||||||
|
|
||||||
return g;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -229,6 +229,68 @@ class GoalPersistenceIntegrationTest {
|
|||||||
org.mockito.Mockito.verifyNoInteractions(memory);
|
org.mockito.Mockito.verifyNoInteractions(memory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GoalEntity readyForCompletion(String conversation, String title) {
|
||||||
|
GoalEntity goal = goalService.create(req(conversation, title), "alice");
|
||||||
|
goalService.appendCriterion(goal.getId(), "report", "alice");
|
||||||
|
var passed = new vip.mate.goal.model.GoalEvaluationResult(1.0, "", "completed", true, "fixture", 1, 0,
|
||||||
|
java.util.List.of(new vip.mate.goal.model.GoalChecklistVerdict.CriterionVerdict("C1", true, "report evidence")), null);
|
||||||
|
goalService.recordEvaluation(goal.getId(), passed, 0, 1);
|
||||||
|
return goalService.getById(goal.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rolledBackCompletionDoesNotSyncMemory() {
|
||||||
|
GoalEntity goal = readyForCompletion("completion-memory-rollback", "report");
|
||||||
|
new TransactionTemplate(transactionManager).executeWithoutResult(status -> {
|
||||||
|
goalService.markCompleted(goal.getId(), null);
|
||||||
|
status.setRollbackOnly();
|
||||||
|
});
|
||||||
|
assertEquals(GoalStatus.ACTIVE, goalService.getById(goal.getId()).getStatus());
|
||||||
|
assertEquals(0L, goalService.listEvents(goal.getId(), 30).stream()
|
||||||
|
.filter(event -> "completed".equals(event.getEventType())).count());
|
||||||
|
org.mockito.Mockito.verifyNoInteractions(memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void completionMemoryRunsAfterCommitAndItsDatabaseWritesCommitIndependently() {
|
||||||
|
GoalEntity goal = readyForCompletion("completion-memory-after-commit", "original title");
|
||||||
|
jdbc.execute("CREATE TABLE IF NOT EXISTS goal_memory_callback_probe(goal_id BIGINT PRIMARY KEY)");
|
||||||
|
org.mockito.Mockito.doAnswer(call -> {
|
||||||
|
inIndependentTransaction(() -> assertEquals(GoalStatus.COMPLETED,
|
||||||
|
goalService.getById(goal.getId()).getStatus()));
|
||||||
|
assertEquals("[goal completed] original title", call.getArgument(2));
|
||||||
|
jdbc.update("INSERT INTO goal_memory_callback_probe(goal_id) VALUES(?)", goal.getId());
|
||||||
|
return null;
|
||||||
|
}).when(memory).syncAll(org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyString(),
|
||||||
|
org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString());
|
||||||
|
new TransactionTemplate(transactionManager).executeWithoutResult(status -> {
|
||||||
|
GoalEntity returned = goalService.markCompleted(goal.getId(), null);
|
||||||
|
returned.setTitle("mutated after return");
|
||||||
|
org.mockito.Mockito.verifyNoInteractions(memory);
|
||||||
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
|
@Override public void afterCommit() {
|
||||||
|
inIndependentTransaction(() -> assertEquals(1, jdbc.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM goal_memory_callback_probe WHERE goal_id=?", Integer.class, goal.getId())));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
org.mockito.Mockito.verify(memory, org.mockito.Mockito.times(1)).syncAll(
|
||||||
|
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyString(),
|
||||||
|
org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void memoryFailureCannotUndoCommittedCompletion() {
|
||||||
|
GoalEntity goal = readyForCompletion("completion-memory-failure", "report");
|
||||||
|
org.mockito.Mockito.doThrow(new IllegalStateException("fixture failure")).when(memory).syncAll(
|
||||||
|
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyString(),
|
||||||
|
org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString());
|
||||||
|
goalService.markCompleted(goal.getId(), null);
|
||||||
|
assertEquals(GoalStatus.COMPLETED, goalService.getById(goal.getId()).getStatus());
|
||||||
|
assertEquals(1L, goalService.listEvents(goal.getId(), 30).stream()
|
||||||
|
.filter(event -> "completed".equals(event.getEventType())).count());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("GoalStatus values persist as lowercase literals — load-bearing for uk_agent_goal_active_conv")
|
@DisplayName("GoalStatus values persist as lowercase literals — load-bearing for uk_agent_goal_active_conv")
|
||||||
void status_persistsAsLowercaseString() {
|
void status_persistsAsLowercaseString() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user