@automagik/omni 2.260430.12 → 2.260430.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ -- Backfill: chats that auto-promoted to a hard terminal under the old
2
+ -- `redirected_sac` escalation rule (escalationThreshold=2 over 7 days) get
3
+ -- `chats.settings.closed` flipped back to false. Companion to the config
4
+ -- fix in `packages/api/src/routes/v2/_close-contact-config.ts` which drops
5
+ -- the escalation going forward — an active customer ending every
6
+ -- conversation with a support-channel redirect is normal account activity,
7
+ -- not a signal worth silencing the reactive agent over.
8
+ --
9
+ -- `closeUntil`, `closeOutcome`, and `close_contact_logs` are deliberately
10
+ -- left intact so reporting and downstream BI consumers see no change.
11
+ -- Hard terminals from `won`/`lost` outcomes are not touched.
12
+ --
13
+ -- Hand-written (not `drizzle-kit generate`) because this is a JSONB merge
14
+ -- with a filter predicate that `drizzle-kit` cannot emit directly.
15
+ --
16
+ -- Performance note: there is no index on `chats.settings->>'closed'` or
17
+ -- `closeOutcome`, so the predicate is evaluated via a sequential scan.
18
+ -- The expected blast radius is small (only chats that previously hit
19
+ -- `redirected_sac` twice within a 7-day window), but on very large
20
+ -- `chats` tables a single UPDATE would still hold row locks for the
21
+ -- entire matched set in one transaction. Batched into 1000-row chunks so
22
+ -- each iteration acquires a bounded number of row locks; once a chat is
23
+ -- flipped to `closed: false` it stops matching the predicate, so
24
+ -- subsequent iterations naturally narrow.
25
+
26
+ DO $$
27
+ DECLARE
28
+ rows_updated INT;
29
+ BEGIN
30
+ LOOP
31
+ WITH candidates AS (
32
+ SELECT "id"
33
+ FROM "chats"
34
+ WHERE ("settings"->>'closed')::boolean IS TRUE
35
+ AND "settings"->>'closeOutcome' = 'redirected_sac'
36
+ LIMIT 1000
37
+ )
38
+ UPDATE "chats" AS c
39
+ SET "settings" = c."settings" - 'closed' || jsonb_build_object('closed', false)
40
+ FROM candidates
41
+ WHERE c."id" = candidates."id";
42
+
43
+ GET DIAGNOSTICS rows_updated = ROW_COUNT;
44
+ EXIT WHEN rows_updated = 0;
45
+ END LOOP;
46
+ END
47
+ $$;
@@ -246,6 +246,13 @@
246
246
  "when": 1777680000000,
247
247
  "tag": "0034_instances_require_genie_signature",
248
248
  "breakpoints": true
249
+ },
250
+ {
251
+ "idx": 35,
252
+ "version": "7",
253
+ "when": 1777770000000,
254
+ "tag": "0035_unstick_redirected_sac_terminals",
255
+ "breakpoints": true
249
256
  }
250
257
  ]
251
258
  }
package/dist/index.js CHANGED
@@ -114079,7 +114079,7 @@ import { fileURLToPath } from "url";
114079
114079
  // package.json
114080
114080
  var package_default = {
114081
114081
  name: "@automagik/omni",
114082
- version: "2.260430.12",
114082
+ version: "2.260430.13",
114083
114083
  description: "LLM-optimized CLI for Omni",
114084
114084
  type: "module",
114085
114085
  bin: {
@@ -224556,7 +224556,7 @@ var init_sentry_scrub = __esm(() => {
224556
224556
  var require_package8 = __commonJS((exports, module) => {
224557
224557
  module.exports = {
224558
224558
  name: "@omni/api",
224559
- version: "2.260430.12",
224559
+ version: "2.260430.13",
224560
224560
  type: "module",
224561
224561
  exports: {
224562
224562
  ".": {
@@ -280868,6 +280868,21 @@ var init_events2 = __esm(() => {
280868
280868
  init_drizzle_orm();
280869
280869
  });
280870
280870
 
280871
+ // ../api/src/lib/close-contact-state.ts
280872
+ function isChatInActiveCloseState(settings) {
280873
+ if (!settings)
280874
+ return false;
280875
+ if (settings.closed === true)
280876
+ return true;
280877
+ const closeUntil = settings.closeUntil;
280878
+ if (typeof closeUntil === "string" && closeUntil.length > 0) {
280879
+ const ms = new Date(closeUntil).getTime();
280880
+ if (Number.isFinite(ms) && Date.now() < ms)
280881
+ return true;
280882
+ }
280883
+ return false;
280884
+ }
280885
+
280871
280886
  // ../api/src/services/follow-up-lifecycle.ts
280872
280887
  function readAgentFollowUpConfig(row) {
280873
280888
  return row?.followUpConfig;
@@ -280907,6 +280922,8 @@ class FollowUpLifecycleService {
280907
280922
  async armForOutbound(input) {
280908
280923
  if (!this.eventBus)
280909
280924
  return;
280925
+ if (await this.isInActiveCloseState(input.chatId, input.instanceId))
280926
+ return;
280910
280927
  const config4 = input.config ?? await this.resolveConfig(input.chatId, input.instanceId, input.agentId ?? null);
280911
280928
  if (!config4 || config4.enabled === false)
280912
280929
  return;
@@ -280925,22 +280942,8 @@ class FollowUpLifecycleService {
280925
280942
  return;
280926
280943
  }
280927
280944
  }
280928
- const existing = await this.readExistingRow(input.chatId, input.instanceId);
280929
- if (existing?.disarmReason && TERMINAL_DISARM_REASONS.has(existing.disarmReason) && existing.disarmedAt) {
280930
- const lastInbound = existing.lastInboundCustomerMessageAt?.getTime() ?? 0;
280931
- const disarmedAt = existing.disarmedAt.getTime();
280932
- if (lastInbound <= disarmedAt) {
280933
- this.logger.info("follow-up lifecycle: refusing to arm \u2014 terminal disarm awaiting customer return", {
280934
- chatId: input.chatId,
280935
- instanceId: input.instanceId,
280936
- disarmReason: existing.disarmReason,
280937
- disarmedAt: existing.disarmedAt.toISOString(),
280938
- lastAgentMessageAt: input.lastAgentMessageAt.toISOString(),
280939
- lastInboundCustomerMessageAt: existing.lastInboundCustomerMessageAt?.toISOString() ?? null
280940
- });
280941
- return;
280942
- }
280943
- }
280945
+ if (await this.shouldRefuseForTerminalDisarm(input))
280946
+ return;
280944
280947
  try {
280945
280948
  await armSequence({ repo: this.repo, eventBus: this.eventBus, logger: this.logger }, {
280946
280949
  chatId: input.chatId,
@@ -280982,6 +280985,40 @@ class FollowUpLifecycleService {
280982
280985
  });
280983
280986
  }
280984
280987
  }
280988
+ async shouldRefuseForTerminalDisarm(input) {
280989
+ const existing = await this.readExistingRow(input.chatId, input.instanceId);
280990
+ if (!existing?.disarmReason || !existing.disarmedAt)
280991
+ return false;
280992
+ if (!TERMINAL_DISARM_REASONS.has(existing.disarmReason))
280993
+ return false;
280994
+ const lastInbound = existing.lastInboundCustomerMessageAt?.getTime() ?? 0;
280995
+ if (lastInbound > existing.disarmedAt.getTime())
280996
+ return false;
280997
+ this.logger.info("follow-up lifecycle: refusing to arm \u2014 terminal disarm awaiting customer return", {
280998
+ chatId: input.chatId,
280999
+ instanceId: input.instanceId,
281000
+ disarmReason: existing.disarmReason,
281001
+ disarmedAt: existing.disarmedAt.toISOString(),
281002
+ lastAgentMessageAt: input.lastAgentMessageAt.toISOString(),
281003
+ lastInboundCustomerMessageAt: existing.lastInboundCustomerMessageAt?.toISOString() ?? null
281004
+ });
281005
+ return true;
281006
+ }
281007
+ async isInActiveCloseState(chatId, instanceId) {
281008
+ const [chatRow] = await this.db.select({ settings: chats.settings }).from(chats).where(eq(chats.id, chatId)).limit(1);
281009
+ const settings = chatRow?.settings;
281010
+ if (!isChatInActiveCloseState(settings))
281011
+ return false;
281012
+ const closeOutcome = settings?.closeOutcome;
281013
+ this.logger.info("follow-up lifecycle: refusing to arm \u2014 chat in active close-contact state", {
281014
+ chatId,
281015
+ instanceId,
281016
+ closed: settings?.closed === true,
281017
+ closeUntil: settings?.closeUntil ?? null,
281018
+ closeOutcome: closeOutcome ?? null
281019
+ });
281020
+ return true;
281021
+ }
280985
281022
  async upsertArmed(input) {
280986
281023
  const values2 = {
280987
281024
  chatId: input.chatId,
@@ -300283,7 +300320,7 @@ var init__close_contact_config = __esm(() => {
300283
300320
  DEFAULT_CLOSE_CONTACT_CONFIG = {
300284
300321
  won: { cooldownMs: null, escalationThreshold: null, escalationWindowMs: null },
300285
300322
  lost: { cooldownMs: null, escalationThreshold: null, escalationWindowMs: null },
300286
- redirected_sac: { cooldownMs: 24 * HOUR, escalationThreshold: 2, escalationWindowMs: 7 * DAY },
300323
+ redirected_sac: { cooldownMs: 24 * HOUR, escalationThreshold: null, escalationWindowMs: null },
300287
300324
  unqualified: { cooldownMs: 7 * DAY, escalationThreshold: 3, escalationWindowMs: 30 * DAY },
300288
300325
  no_response: { cooldownMs: 48 * HOUR, escalationThreshold: 3, escalationWindowMs: 30 * DAY },
300289
300326
  other: { cooldownMs: 24 * HOUR, escalationThreshold: 2, escalationWindowMs: 14 * DAY }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/omni",
3
- "version": "2.260430.12",
3
+ "version": "2.260430.13",
4
4
  "description": "LLM-optimized CLI for Omni",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,15 +51,15 @@
51
51
  "qrcode-terminal": "^0.12.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@omni/api": "2.260430.11",
55
- "@omni/channel-discord": "2.260430.11",
56
- "@omni/channel-gupshup": "2.260430.11",
57
- "@omni/channel-sdk": "2.260430.11",
58
- "@omni/channel-slack": "2.260430.11",
59
- "@omni/channel-telegram": "2.260430.11",
60
- "@omni/channel-whatsapp": "2.260430.11",
61
- "@omni/core": "2.260430.11",
62
- "@omni/sdk": "2.260430.11",
54
+ "@omni/api": "2.260430.12",
55
+ "@omni/channel-discord": "2.260430.12",
56
+ "@omni/channel-gupshup": "2.260430.12",
57
+ "@omni/channel-sdk": "2.260430.12",
58
+ "@omni/channel-slack": "2.260430.12",
59
+ "@omni/channel-telegram": "2.260430.12",
60
+ "@omni/channel-whatsapp": "2.260430.12",
61
+ "@omni/core": "2.260430.12",
62
+ "@omni/sdk": "2.260430.12",
63
63
  "@types/node": "^22.10.3",
64
64
  "@types/qrcode-terminal": "^0.12.2",
65
65
  "typescript": "^5.7.3"