@automagik/omni 2.260430.11 → 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.11",
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.11",
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,
@@ -284814,13 +284851,13 @@ async function applyCloseContactGate(services, chatRecordId, instanceId, chatId,
284814
284851
  if (!Number.isFinite(closeUntilMs))
284815
284852
  return "pass";
284816
284853
  if (Date.now() < closeUntilMs) {
284817
- log95.debug("Chat in close cooldown, skipping dispatch", {
284854
+ log95.debug("Chat in soft close cooldown, follow-up disarmed but agent reactive", {
284818
284855
  instanceId,
284819
284856
  chatId,
284820
284857
  closeUntil: chatSettings.closeUntil,
284821
284858
  outcome: chatSettings.closeOutcome ?? null
284822
284859
  });
284823
- return "skip";
284860
+ return "pass";
284824
284861
  }
284825
284862
  if (!chatRecordId)
284826
284863
  return "pass";
@@ -284833,7 +284870,7 @@ async function applyCloseContactGate(services, chatRecordId, instanceId, chatId,
284833
284870
  agentResumedAt: new Date().toISOString()
284834
284871
  }
284835
284872
  });
284836
- log95.info("Close cooldown expired, agent reopened", {
284873
+ log95.info("Close cooldown expired, state cleaned", {
284837
284874
  instanceId,
284838
284875
  chatId,
284839
284876
  closeOutcome: chatSettings.closeOutcome ?? null
@@ -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 }
@@ -301417,10 +301454,11 @@ var init_messages5 = __esm(() => {
301417
301454
  }
301418
301455
  }).returning();
301419
301456
  const { terminal, escalated, closeUntil } = await computeCloseContactTerminalState(db2, data.chatId, outcome, auditRow?.id ?? null);
301457
+ const shouldPauseAgent = outcome === "lost";
301420
301458
  const closedAt = new Date;
301421
301459
  await services.chats.update(data.chatId, {
301422
301460
  settings: {
301423
- agentPaused: true,
301461
+ ...shouldPauseAgent ? { agentPaused: true } : {},
301424
301462
  closed: terminal,
301425
301463
  closeUntil: closeUntil?.toISOString() ?? null,
301426
301464
  closeOutcome: outcome
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/omni",
3
- "version": "2.260430.11",
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.10",
55
- "@omni/channel-discord": "2.260430.10",
56
- "@omni/channel-gupshup": "2.260430.10",
57
- "@omni/channel-sdk": "2.260430.10",
58
- "@omni/channel-slack": "2.260430.10",
59
- "@omni/channel-telegram": "2.260430.10",
60
- "@omni/channel-whatsapp": "2.260430.10",
61
- "@omni/core": "2.260430.10",
62
- "@omni/sdk": "2.260430.10",
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"