feat: apply terminal reconciliation cleanup

This commit is contained in:
2026-09-06 07:22:17 +00:00
parent 7e902d1103
commit 0a0b6f9707
5 changed files with 202 additions and 0 deletions
+77
View File
@@ -345,6 +345,83 @@ func (store *Store) CleanupTerminal(ctx context.Context, issueUUID domain.UUID,
return tx.Commit()
}
// DiscardTerminal removes a server-confirmed terminal command even when its
// local event rows have not been acknowledged. The compact tombstone is kept so
// a late duplicate dispatch cannot execute it again. It is intentionally only
// for a ReconcileResult that authoritatively tells the client to discard local
// terminal state; ordinary terminal cleanup must use CleanupTerminal.
func (store *Store) DiscardTerminal(ctx context.Context, issueUUID domain.UUID, acknowledgedAt time.Time) error {
if !validUUID(issueUUID) || acknowledgedAt.IsZero() {
return errors.New("invalid terminal discard")
}
tx, err := store.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
var hash []byte
var revision uint64
var phase uint32
var terminal int
var totalCharged uint64
err = tx.QueryRowContext(ctx, `SELECT immutable_sha256, command_revision, phase, terminal, total_charged_bytes FROM commands WHERE issue_uuid = ?`, issueUUID[:]).Scan(&hash, &revision, &phase, &terminal, &totalCharged)
if errors.Is(err, sql.ErrNoRows) {
var tombstoneHash []byte
err = tx.QueryRowContext(ctx, `SELECT immutable_sha256 FROM command_tombstones WHERE issue_uuid = ?`, issueUUID[:]).Scan(&tombstoneHash)
if errors.Is(err, sql.ErrNoRows) {
return ErrUnknownCommand
}
if err != nil {
return err
}
if len(tombstoneHash) != 32 {
return fmt.Errorf("invalid terminal tombstone")
}
return tx.Commit()
}
if err != nil {
return err
}
if terminal == 0 {
return errors.New("command is not terminal")
}
clientTotal, err := clientTotalCharge(ctx, tx)
if err != nil {
return err
}
if totalCharged > clientTotal {
return fmt.Errorf("client spool aggregate charge counter mismatch")
}
var existingHash []byte
err = tx.QueryRowContext(ctx, `SELECT immutable_sha256 FROM command_tombstones WHERE issue_uuid = ?`, issueUUID[:]).Scan(&existingHash)
if err == nil && (len(existingHash) != len(hash) || string(existingHash) != string(hash)) {
return ErrCommandConflict
}
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
if errors.Is(err, sql.ErrNoRows) {
if _, err := tx.ExecContext(ctx, `INSERT INTO command_tombstones(issue_uuid, immutable_sha256, command_revision, terminal_lifecycle, acknowledged_at) VALUES (?, ?, ?, ?, ?)`, issueUUID[:], hash, revision, phase, acknowledgedAt.UnixNano()); err != nil {
return err
}
}
if _, err := tx.ExecContext(ctx, `DELETE FROM commands WHERE issue_uuid = ?`, issueUUID[:]); err != nil {
return err
}
if err := updateClientTotalCharge(ctx, tx, clientTotal-totalCharged); err != nil {
return err
}
trim, err := tombstonesToTrim(ctx, tx, store.tombstoneLimit)
if err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM command_tombstones WHERE issue_uuid IN (SELECT issue_uuid FROM command_tombstones ORDER BY acknowledged_at, issue_uuid LIMIT ?)`, trim); err != nil {
return err
}
return tx.Commit()
}
type queryer interface {
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
}