Files
multica/server/pkg/db/generated/channel_typing.sql.go
T
苗大 3bacc41f84 MUL-7573 fix(lark): clear the Typing reaction without trusting in-process state (#8656)
* fix(lark): persist and reconcile typing cleanup across replicas

- preserve terminal trigger targets and per-input cleanup ownership
- reconcile late Adds and source deletion without delaying replies
- recover interrupted ledger indexes and cover lifecycle boundaries

Refs: #8655

* fix(lark): restrict typing cleanup to the current application

- match both app operator type and the current App ID
- preserve foreign-app, human and unknown-owner reactions
- cover ownership filtering in fast and durable cleanup paths

Refs: #8655

* fix(lark): bound typing recovery and credential retention

- recover taskless indicators after lost debounce timers
- cap workspace ledger admission and redact terminal credentials
- isolate maintenance budgets and test retention and quota boundaries

Refs: #8655

* docs(lark): document typing cleanup recovery and rollout limits

- explain ownership, retention, quota and observable terminal outcomes
- document migration upgrades and safe application rollback

Refs: #8655

* fix(deps): require patched Go 1.26.9 and x/net 0.60

- Require the patched Go standard library to clear reachable vulnerabilities.
- Upgrade x/net to the matching HTTP/2 security release.
- Align local development prerequisites with the minimum Go version.
2026-10-08 20:20:15 -07:00

339 lines
13 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: channel_typing.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const acknowledgeChannelTypingReactionCleanup = `-- name: AcknowledgeChannelTypingReactionCleanup :exec
UPDATE channel_typing_reaction
SET cleaned_at = CASE WHEN add_finished AND reaction_id <> '' THEN now() ELSE NULL END,
retry_after = GREATEST(retry_after, now() + interval '30 seconds'),
installation_snapshot = CASE WHEN add_finished AND reaction_id <> '' THEN '{}'::jsonb ELSE installation_snapshot END
WHERE id = $1 AND reaction_id = $2 AND cleanup_required AND abandoned_at IS NULL
`
type AcknowledgeChannelTypingReactionCleanupParams struct {
ID pgtype.UUID `json:"id"`
ReactionID string `json:"reaction_id"`
}
// An unfinished Add may still create a reaction after an empty sweep. Retain
// its anchor; only a known successful Add response can close cleanup forever.
func (q *Queries) AcknowledgeChannelTypingReactionCleanup(ctx context.Context, arg AcknowledgeChannelTypingReactionCleanupParams) error {
_, err := q.db.Exec(ctx, acknowledgeChannelTypingReactionCleanup, arg.ID, arg.ReactionID)
return err
}
const claimChannelTypingReactionCleanup = `-- name: ClaimChannelTypingReactionCleanup :many
WITH candidates AS (
SELECT r.id FROM channel_typing_reaction r
WHERE r.cleaned_at IS NULL AND r.abandoned_at IS NULL AND r.retry_after <= now()
AND r.created_at > now() - interval '7 days'
AND ($1::uuid IS NULL OR r.chat_session_id = $1)
AND (r.cleanup_required OR (NOT r.add_finished AND r.created_at < now() - interval '1 minute') OR NOT EXISTS (
SELECT 1 FROM chat_message m
JOIN chat_session s ON s.id = m.chat_session_id
JOIN channel_installation i ON i.id = r.installation_id
JOIN channel_chat_session_binding b ON b.chat_session_id = s.id AND b.installation_id = i.id
LEFT JOIN agent_task_queue t ON t.id = m.task_id
LEFT JOIN agent a ON a.id = t.agent_id
WHERE m.id = r.chat_message_id AND s.id = r.chat_session_id
AND s.workspace_id = r.workspace_id AND i.workspace_id = r.workspace_id
AND i.channel_type = 'feishu' AND b.channel_type = 'feishu'
AND s.status = 'active' AND i.status = 'active' AND b.retired_at IS NULL
AND m.channel_ingested AND m.role = 'user' AND NOT m.channel_typing_settled
-- A taskless badge has a bounded visual lifetime even if a crashed
-- debouncer never flushes. Do not settle the input or cancel a future run.
AND ((m.task_id IS NULL AND r.created_at > now() - interval '2 minutes') OR (t.chat_session_id = s.id AND a.workspace_id = r.workspace_id
AND t.status IN ('queued','dispatched','running','waiting_local_directory','deferred')))
))
ORDER BY r.retry_after, r.id LIMIT 100
FOR UPDATE OF r SKIP LOCKED
)
UPDATE channel_typing_reaction r SET cleanup_required = true,
attempts = attempts + 1,
retry_after = now() + LEAST(3600, 30 * power(2, LEAST(r.attempts, 7))) * interval '1 second'
FROM candidates c WHERE r.id = c.id RETURNING r.id, r.workspace_id, r.chat_session_id, r.chat_message_id, r.installation_id, r.channel_message_id, r.installation_snapshot, r.reaction_id, r.add_finished, r.cleanup_required, r.cleaned_at, r.retry_after, r.attempts, r.created_at, r.abandoned_at, r.quota_slot
`
// The DB is the shared source of eligibility. Missing source rows after a
// committed deletion are terminal too. No transaction takes remote I/O locks.
func (q *Queries) ClaimChannelTypingReactionCleanup(ctx context.Context, chatSessionID pgtype.UUID) ([]ChannelTypingReaction, error) {
rows, err := q.db.Query(ctx, claimChannelTypingReactionCleanup, chatSessionID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ChannelTypingReaction{}
for rows.Next() {
var i ChannelTypingReaction
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.ChatSessionID,
&i.ChatMessageID,
&i.InstallationID,
&i.ChannelMessageID,
&i.InstallationSnapshot,
&i.ReactionID,
&i.AddFinished,
&i.CleanupRequired,
&i.CleanedAt,
&i.RetryAfter,
&i.Attempts,
&i.CreatedAt,
&i.AbandonedAt,
&i.QuotaSlot,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const expireChannelTypingReactionCleanup = `-- name: ExpireChannelTypingReactionCleanup :many
WITH candidates AS (
SELECT id FROM channel_typing_reaction
WHERE cleaned_at IS NULL AND abandoned_at IS NULL AND created_at <= now() - interval '7 days'
ORDER BY created_at, id LIMIT 1000 FOR UPDATE SKIP LOCKED
)
UPDATE channel_typing_reaction r SET abandoned_at = now(), cleanup_required = true,
installation_snapshot = '{}'::jsonb
FROM candidates c WHERE r.id = c.id
RETURNING r.id, r.workspace_id
`
type ExpireChannelTypingReactionCleanupRow struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// Independent maintenance, including active/uncertain Adds: the cosmetic badge
// has a seven-day maximum lifetime. Remote failure is NOT reported as success.
func (q *Queries) ExpireChannelTypingReactionCleanup(ctx context.Context) ([]ExpireChannelTypingReactionCleanupRow, error) {
rows, err := q.db.Query(ctx, expireChannelTypingReactionCleanup)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ExpireChannelTypingReactionCleanupRow{}
for rows.Next() {
var i ExpireChannelTypingReactionCleanupRow
if err := rows.Scan(&i.ID, &i.WorkspaceID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const finishChannelTypingReactionAdd = `-- name: FinishChannelTypingReactionAdd :one
UPDATE channel_typing_reaction SET reaction_id = $1, add_finished = true,
cleanup_required = cleanup_required OR $2::boolean,
cleaned_at = NULL, retry_after = now()
WHERE id = $3 AND abandoned_at IS NULL AND created_at > now() - interval '7 days'
RETURNING id, workspace_id, chat_session_id, chat_message_id, installation_id, channel_message_id, installation_snapshot, reaction_id, add_finished, cleanup_required, cleaned_at, retry_after, attempts, created_at, abandoned_at, quota_slot
`
type FinishChannelTypingReactionAddParams struct {
ReactionID string `json:"reaction_id"`
CleanupRequired bool `json:"cleanup_required"`
ID pgtype.UUID `json:"id"`
}
// cleanup_required is monotonic. A stale active read cannot undo another
// replica's terminal decision; late HTTP completion re-arms compensation.
func (q *Queries) FinishChannelTypingReactionAdd(ctx context.Context, arg FinishChannelTypingReactionAddParams) (ChannelTypingReaction, error) {
row := q.db.QueryRow(ctx, finishChannelTypingReactionAdd, arg.ReactionID, arg.CleanupRequired, arg.ID)
var i ChannelTypingReaction
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.ChatSessionID,
&i.ChatMessageID,
&i.InstallationID,
&i.ChannelMessageID,
&i.InstallationSnapshot,
&i.ReactionID,
&i.AddFinished,
&i.CleanupRequired,
&i.CleanedAt,
&i.RetryAfter,
&i.Attempts,
&i.CreatedAt,
&i.AbandonedAt,
&i.QuotaSlot,
)
return i, err
}
const listRequestedChannelTypingReactions = `-- name: ListRequestedChannelTypingReactions :many
SELECT id FROM channel_typing_reaction WHERE id = ANY($1::uuid[]) AND cleanup_required
`
func (q *Queries) ListRequestedChannelTypingReactions(ctx context.Context, ids []pgtype.UUID) ([]pgtype.UUID, error) {
rows, err := q.db.Query(ctx, listRequestedChannelTypingReactions, ids)
if err != nil {
return nil, err
}
defer rows.Close()
items := []pgtype.UUID{}
for rows.Next() {
var id pgtype.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const pruneChannelTypingReactionCleanup = `-- name: PruneChannelTypingReactionCleanup :exec
WITH candidates AS (
SELECT id FROM channel_typing_reaction
WHERE cleaned_at < now() - interval '7 days' OR abandoned_at < now() - interval '7 days'
LIMIT 1000 FOR UPDATE SKIP LOCKED
)
DELETE FROM channel_typing_reaction r USING candidates c WHERE r.id = c.id
`
// Each terminal outcome remains inspectable for seven days without credentials.
func (q *Queries) PruneChannelTypingReactionCleanup(ctx context.Context) error {
_, err := q.db.Exec(ctx, pruneChannelTypingReactionCleanup)
return err
}
const registerChannelTypingReaction = `-- name: RegisterChannelTypingReaction :one
INSERT INTO channel_typing_reaction (
id, workspace_id, chat_session_id, chat_message_id, installation_id,
channel_message_id, installation_snapshot, cleanup_required, quota_slot
)
SELECT $1, $2, $3, $4, $5,
$6, $7, message.channel_typing_settled, slot.value
FROM chat_message AS message
JOIN chat_session AS session ON session.id = message.chat_session_id
JOIN channel_installation AS installation ON installation.id = $5
CROSS JOIN LATERAL (
SELECT value FROM generate_series(1, 1000) AS value
WHERE NOT EXISTS (SELECT 1 FROM channel_typing_reaction r
WHERE r.workspace_id = $2 AND r.quota_slot = value
AND r.cleaned_at IS NULL AND r.abandoned_at IS NULL)
ORDER BY value LIMIT 1
) slot
WHERE message.id = $4
AND session.id = $3 AND session.workspace_id = $2
AND installation.workspace_id = $2 AND installation.channel_type = 'feishu'
AND message.channel_ingested AND message.role = 'user'
ON CONFLICT DO NOTHING RETURNING id, workspace_id, chat_session_id, chat_message_id, installation_id, channel_message_id, installation_snapshot, reaction_id, add_finished, cleanup_required, cleaned_at, retry_after, attempts, created_at, abandoned_at, quota_slot
`
type RegisterChannelTypingReactionParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
ChatSessionID pgtype.UUID `json:"chat_session_id"`
ChatMessageID pgtype.UUID `json:"chat_message_id"`
InstallationID pgtype.UUID `json:"installation_id"`
ChannelMessageID string `json:"channel_message_id"`
InstallationSnapshot []byte `json:"installation_snapshot"`
}
// Commit the complete external anchor BEFORE issuing Add. A terminal observer
// can enumerate every input, independently of the one delivery trigger.
// A concurrent registration can take the same slot. Fail closed: a cosmetic
// badge may be skipped, but an untracked Add or a quota overrun is never safe.
func (q *Queries) RegisterChannelTypingReaction(ctx context.Context, arg RegisterChannelTypingReactionParams) (ChannelTypingReaction, error) {
row := q.db.QueryRow(ctx, registerChannelTypingReaction,
arg.ID,
arg.WorkspaceID,
arg.ChatSessionID,
arg.ChatMessageID,
arg.InstallationID,
arg.ChannelMessageID,
arg.InstallationSnapshot,
)
var i ChannelTypingReaction
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.ChatSessionID,
&i.ChatMessageID,
&i.InstallationID,
&i.ChannelMessageID,
&i.InstallationSnapshot,
&i.ReactionID,
&i.AddFinished,
&i.CleanupRequired,
&i.CleanedAt,
&i.RetryAfter,
&i.Attempts,
&i.CreatedAt,
&i.AbandonedAt,
&i.QuotaSlot,
)
return i, err
}
const settleChannelTypingInputs = `-- name: SettleChannelTypingInputs :exec
UPDATE chat_message AS message SET channel_typing_settled = true
FROM chat_message AS cutoff, chat_session AS session, channel_installation AS installation
WHERE cutoff.id = $1 AND cutoff.chat_session_id = $2
AND session.id = $2 AND session.workspace_id = $3
AND installation.id = $4 AND installation.workspace_id = $3
AND installation.channel_type = 'feishu'
AND EXISTS (SELECT 1 FROM channel_chat_session_binding b WHERE b.chat_session_id = session.id
AND b.installation_id = installation.id AND b.channel_type = 'feishu')
AND message.chat_session_id = session.id AND message.task_id IS NULL
AND message.channel_ingested AND message.role = 'user'
AND COALESCE(message.channel_context_revision, 1) = $5
AND (message.created_at, message.id) <= (cutoff.created_at, cutoff.id)
`
type SettleChannelTypingInputsParams struct {
ThroughMessageID pgtype.UUID `json:"through_message_id"`
ChatSessionID pgtype.UUID `json:"chat_session_id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
InstallationID pgtype.UUID `json:"installation_id"`
ContextRevision pgtype.Int8 `json:"context_revision"`
}
// The flush owns only this context's unowned inputs up to its captured cutoff.
// Later inputs, even in the same session, are not settled by this failed run.
func (q *Queries) SettleChannelTypingInputs(ctx context.Context, arg SettleChannelTypingInputsParams) error {
_, err := q.db.Exec(ctx, settleChannelTypingInputs,
arg.ThroughMessageID,
arg.ChatSessionID,
arg.WorkspaceID,
arg.InstallationID,
arg.ContextRevision,
)
return err
}
const skipChannelTypingReactionAdd = `-- name: SkipChannelTypingReactionAdd :exec
UPDATE channel_typing_reaction SET add_finished = true, cleanup_required = true,
cleaned_at = now(), installation_snapshot = '{}'::jsonb WHERE id = $1 AND abandoned_at IS NULL
`
// No HTTP Add was issued for an already-settled input.
func (q *Queries) SkipChannelTypingReactionAdd(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, skipChannelTypingReactionAdd, id)
return err
}