perf(ai): kontiguitas batch budget + max_tokens dinamis + urutan kronologis RETURNING

- pickBatchWithinBudget: stop di overflow pertama (break), bukan skip —
  batch tetap prefix kronologis tanpa gap analisis di tengah timeline.
  Diekstrak ke batchBudget.ts (pure, estimator di-inject) + regression test.
- callModerationLLM: param opsional maxTokens; text/media caller menghitung
  ceiling dari estimasi prompt (floor 2048, cap 16384) — batch kecil tak
  lagi reserve window completion 16k.
- getPending/IncompleteMessagesByConversation: sort hasil UPDATE..RETURNING
  by created_at ASC — Postgres tak menjamin urutan, konsumen (anchor konteks
  messages[0], prefix batch) bergantung pada urutan kronologis.
This commit is contained in:
asepharyana
2026-08-22 17:19:41 +07:00
parent 1397380fe9
commit 16becd5340
8 changed files with 230 additions and 19 deletions
@@ -246,6 +246,14 @@ export class MessagesAnalysis {
.returning();
});
// UPDATE..RETURNING has no guaranteed row order (Postgres returns rows
// in physical update order). Consumers rely on chronological order:
// batchScheduler/pickBatchWithinBudget treat the array as a created_at
// ASC prefix, and the context anchor uses messages[0].created_at.
rows.sort(
(a, b) =>
(a as MessageRecord).created_at - (b as MessageRecord).created_at,
);
return rows as MessageRecord[];
} catch (error) {
this.logger.error(
@@ -386,6 +394,13 @@ export class MessagesAnalysis {
.returning();
});
// Same UPDATE..RETURNING ordering guarantee as above: re-sort to
// created_at ASC so the individual fallback path also sees a
// chronological array.
rows.sort(
(a, b) =>
(a as MessageRecord).created_at - (b as MessageRecord).created_at,
);
return rows as MessageRecord[];
} catch (error) {
this.logger.error(