@automagik/omni 2.260429.4 → 2.260430.1

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
+ -- Close-contact endpoint audit table.
2
+ --
3
+ -- Records every agent→close-contact event with full payload. Written
4
+ -- synchronously in the /messages/send/close-contact route so no data is
5
+ -- lost. The route also reads this table at close-time to count recent
6
+ -- rows for the same (chat_uuid, outcome) within the configured escalation
7
+ -- window — that count drives the auto-promotion of soft outcomes to hard
8
+ -- terminal (recorded back as `escalated = TRUE` on the new row). See the
9
+ -- design doc at .genie/wishes/559-close-contact/design.md §6 + §8.1 for
10
+ -- the loop-bound proof.
11
+ --
12
+ -- Hot path is the recent-count query, indexed on
13
+ -- (chat_uuid, outcome, sent_at). Per-instance BI feed is indexed on
14
+ -- (instance_id, sent_at). chat_id and agent_id get their own indexes for
15
+ -- support drilldowns.
16
+ --
17
+ -- The `outcome` column is a varchar(32) free string at the DB level; the
18
+ -- TypeScript layer constrains it to the `closeContactOutcomes` const array
19
+ -- in `packages/db/src/schema.ts` (kept in sync with `CloseContactOutcome`
20
+ -- in `@omni/core/events`). No CHECK constraint here so the BI/CRM tooling
21
+ -- can extend the taxonomy without a migration.
22
+
23
+ CREATE TABLE IF NOT EXISTS "close_contact_logs" (
24
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
25
+ "instance_id" uuid REFERENCES "instances"("id") ON DELETE SET NULL,
26
+ "chat_uuid" uuid REFERENCES "chats"("id") ON DELETE SET NULL,
27
+ "chat_id" varchar(255) NOT NULL,
28
+ "to_phone" varchar(100) NOT NULL,
29
+ "text" text NOT NULL,
30
+ "outcome" varchar(32) NOT NULL,
31
+ "reason" text,
32
+ "close_fields" jsonb,
33
+ "agent_id" uuid REFERENCES "agents"("id") ON DELETE SET NULL,
34
+ "external_message_id" varchar(255),
35
+ "escalated" boolean NOT NULL DEFAULT false,
36
+ "sent_at" timestamp with time zone DEFAULT now() NOT NULL,
37
+ "metadata" jsonb
38
+ );
39
+
40
+ CREATE INDEX IF NOT EXISTS "close_contact_logs_chat_outcome_sent_at_idx"
41
+ ON "close_contact_logs" ("chat_uuid", "outcome", "sent_at");
42
+ CREATE INDEX IF NOT EXISTS "close_contact_logs_instance_sent_at_idx"
43
+ ON "close_contact_logs" ("instance_id", "sent_at");
44
+ CREATE INDEX IF NOT EXISTS "close_contact_logs_chat_id_idx"
45
+ ON "close_contact_logs" ("chat_id");
46
+ CREATE INDEX IF NOT EXISTS "close_contact_logs_agent_idx"
47
+ ON "close_contact_logs" ("agent_id");
@@ -232,6 +232,13 @@
232
232
  "when": 1777500000000,
233
233
  "tag": "0032_genie_hosts",
234
234
  "breakpoints": true
235
+ },
236
+ {
237
+ "idx": 33,
238
+ "version": "7",
239
+ "when": 1777590000000,
240
+ "tag": "0033_close_contact_logs",
241
+ "breakpoints": true
235
242
  }
236
243
  ]
237
244
  }
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AA8M5C,wBAAgB,qBAAqB,IAAI,OAAO,CA2S/C"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AA8M5C,wBAAgB,qBAAqB,IAAI,OAAO,CA4T/C"}
package/dist/index.js CHANGED
@@ -5755,6 +5755,7 @@ var init_types = __esm(() => {
5755
5755
  "chat.idle_timeout",
5756
5756
  "chat.handoff_activated",
5757
5757
  "chat.archived",
5758
+ "chat.closed",
5758
5759
  "follow_up.armed",
5759
5760
  "follow_up.fired",
5760
5761
  "follow_up.skipped",
@@ -27730,7 +27731,8 @@ var init_follow_up = __esm(() => {
27730
27731
  "sequence_complete",
27731
27732
  "agent_error",
27732
27733
  "send_failed",
27733
- "session_cleared"
27734
+ "session_cleared",
27735
+ "contact_closed"
27734
27736
  ]);
27735
27737
  FixedScheduleSchema = exports_external.object({
27736
27738
  kind: exports_external.literal("fixed"),
@@ -58589,6 +58591,8 @@ __export(exports_schema, {
58589
58591
  contentTypes: () => contentTypes,
58590
58592
  consumerOffsets: () => consumerOffsets,
58591
58593
  conditionOperators: () => conditionOperators,
58594
+ closeContactOutcomes: () => closeContactOutcomes,
58595
+ closeContactLogs: () => closeContactLogs,
58592
58596
  chatsRelations: () => chatsRelations,
58593
58597
  chats: () => chats,
58594
58598
  chatTypes: () => chatTypes,
@@ -58632,7 +58636,7 @@ __export(exports_schema, {
58632
58636
  accessRules: () => accessRules,
58633
58637
  accessModes: () => accessModes
58634
58638
  });
58635
- var channelTypes, agentTypes, agentSystems, agentEntityTypes, debounceMode, splitDelayMode, replyFilterMode, agentSessionStrategies, ruleTypes, accessModes, settingValueTypes, apiKeyStatuses, apiKeyProfiles, eventTypes, contentTypes, chatTypes, messageSources, messageTypes, messageStatuses, deliveryStatuses, jobStatuses, providerSchemas, agentProviders, agents, agentRoutes, agentSessions, apiKeys, apiKeyAuditLogs, apiKeysRelations, apiKeyAuditLogsRelations, instances, persons, platformIdentities, conversations, chats, chatParticipants, omniGroups, messages, omniEvents, handoffLogs, accessRules, globalSettings, settingChangeHistory, batchJobs, syncJobTypes, syncJobs, mediaContent, chatIdMappings, pluginStorage, agentProvidersRelations, agentsRelations, instancesRelations, syncJobsRelations, personsRelations, platformIdentitiesRelations, conversationsRelations, chatsRelations, chatParticipantsRelations, messagesRelations, omniEventsRelations, accessRulesRelations, globalSettingsRelations, settingChangeHistoryRelations, batchJobsRelations, mediaContentRelations, chatIdMappingsRelations, deadLetterStatuses, deadLetterEvents, payloadStorageConfig, payloadStages, eventPayloads, webhookSources, conditionOperators, actionTypes, automationDebounceModes, automations2, automationLogStatuses, automationLogs, consumerOffsets, automationsRelations, automationLogsRelations, triggerLogs, triggerLogsRelations, agentRoutesRelations, agentTaskStatuses, agentTasks, agentTasksRelations, turnStatuses, turnActions, turns, turnsRelations, followUpDisarmReasons, chatFollowUpState, chatFollowUpStateRelations, processedEvents, genieHosts;
58639
+ var channelTypes, agentTypes, agentSystems, agentEntityTypes, debounceMode, splitDelayMode, replyFilterMode, agentSessionStrategies, ruleTypes, accessModes, settingValueTypes, apiKeyStatuses, apiKeyProfiles, eventTypes, contentTypes, chatTypes, messageSources, messageTypes, messageStatuses, deliveryStatuses, jobStatuses, providerSchemas, agentProviders, agents, agentRoutes, agentSessions, apiKeys, apiKeyAuditLogs, apiKeysRelations, apiKeyAuditLogsRelations, instances, persons, platformIdentities, conversations, chats, chatParticipants, omniGroups, messages, omniEvents, handoffLogs, closeContactOutcomes, closeContactLogs, accessRules, globalSettings, settingChangeHistory, batchJobs, syncJobTypes, syncJobs, mediaContent, chatIdMappings, pluginStorage, agentProvidersRelations, agentsRelations, instancesRelations, syncJobsRelations, personsRelations, platformIdentitiesRelations, conversationsRelations, chatsRelations, chatParticipantsRelations, messagesRelations, omniEventsRelations, accessRulesRelations, globalSettingsRelations, settingChangeHistoryRelations, batchJobsRelations, mediaContentRelations, chatIdMappingsRelations, deadLetterStatuses, deadLetterEvents, payloadStorageConfig, payloadStages, eventPayloads, webhookSources, conditionOperators, actionTypes, automationDebounceModes, automations2, automationLogStatuses, automationLogs, consumerOffsets, automationsRelations, automationLogsRelations, triggerLogs, triggerLogsRelations, agentRoutesRelations, agentTaskStatuses, agentTasks, agentTasksRelations, turnStatuses, turnActions, turns, turnsRelations, followUpDisarmReasons, chatFollowUpState, chatFollowUpStateRelations, processedEvents, genieHosts;
58636
58640
  var init_schema2 = __esm(() => {
58637
58641
  init_events();
58638
58642
  init_drizzle_orm();
@@ -59258,6 +59262,28 @@ var init_schema2 = __esm(() => {
59258
59262
  sentAtIdx: index("handoff_logs_sent_at_idx").on(table3.sentAt),
59259
59263
  agentIdx: index("handoff_logs_agent_idx").on(table3.agentId)
59260
59264
  }));
59265
+ closeContactOutcomes = ["won", "lost", "redirected_sac", "unqualified", "no_response", "other"];
59266
+ closeContactLogs = pgTable("close_contact_logs", {
59267
+ id: uuid("id").primaryKey().defaultRandom(),
59268
+ instanceId: uuid("instance_id").references(() => instances.id, { onDelete: "set null" }),
59269
+ chatUuid: uuid("chat_uuid").references(() => chats.id, { onDelete: "set null" }),
59270
+ chatId: varchar("chat_id", { length: 255 }).notNull(),
59271
+ toPhone: varchar("to_phone", { length: 100 }).notNull(),
59272
+ text: text("text").notNull(),
59273
+ outcome: varchar("outcome", { length: 32 }).notNull().$type(),
59274
+ reason: text("reason"),
59275
+ closeFields: jsonb("close_fields").$type(),
59276
+ agentId: uuid("agent_id").references(() => agents.id, { onDelete: "set null" }),
59277
+ externalMessageId: varchar("external_message_id", { length: 255 }),
59278
+ escalated: boolean("escalated").notNull().default(false),
59279
+ sentAt: timestamp("sent_at", { withTimezone: true }).notNull().defaultNow(),
59280
+ metadata: jsonb("metadata").$type()
59281
+ }, (table3) => ({
59282
+ chatOutcomeSentAtIdx: index("close_contact_logs_chat_outcome_sent_at_idx").on(table3.chatUuid, table3.outcome, table3.sentAt),
59283
+ instanceSentAtIdx: index("close_contact_logs_instance_sent_at_idx").on(table3.instanceId, table3.sentAt),
59284
+ chatIdIdx: index("close_contact_logs_chat_id_idx").on(table3.chatId),
59285
+ agentIdx: index("close_contact_logs_agent_idx").on(table3.agentId)
59286
+ }));
59261
59287
  accessRules = pgTable("access_rules", {
59262
59288
  id: uuid("id").primaryKey().defaultRandom(),
59263
59289
  instanceId: uuid("instance_id").references(() => instances.id, { onDelete: "cascade" }),
@@ -59892,7 +59918,8 @@ var init_schema2 = __esm(() => {
59892
59918
  "sequence_complete",
59893
59919
  "agent_error",
59894
59920
  "send_failed",
59895
- "session_cleared"
59921
+ "session_cleared",
59922
+ "contact_closed"
59896
59923
  ];
59897
59924
  chatFollowUpState = pgTable("chat_follow_up_state", {
59898
59925
  id: uuid("id").primaryKey().defaultRandom(),
@@ -113953,7 +113980,7 @@ import { fileURLToPath } from "url";
113953
113980
  // package.json
113954
113981
  var package_default = {
113955
113982
  name: "@automagik/omni",
113956
- version: "2.260429.4",
113983
+ version: "2.260430.1",
113957
113984
  description: "LLM-optimized CLI for Omni",
113958
113985
  type: "module",
113959
113986
  bin: {
@@ -122464,8 +122491,77 @@ function createMessagesCommand() {
122464
122491
  error(`Failed to edit message: ${message2}`);
122465
122492
  }
122466
122493
  });
122494
+ messages3.command("close-contact").description("Close a chat terminally (won/lost) or with a soft cooldown (redirected_sac/unqualified/no_response/other)").requiredOption("--instance <id>", "Instance ID").requiredOption("--chat <chatId>", "Chat DB UUID to close").requiredOption("--to <recipient>", "Recipient phone or platform ID").requiredOption("--text <text>", "Farewell message shown to the lead").requiredOption("--outcome <outcome>", "Outcome: won | lost | redirected_sac | unqualified | no_response | other").option("--reason <reason>", "Free-text rationale persisted in close_contact_logs").option("--close-fields <jsonOrPath>", "Structured BI/CRM payload \u2014 inline JSON or path to a JSON file").action(handleCloseContact);
122467
122495
  return messages3;
122468
122496
  }
122497
+ var VALID_CLOSE_OUTCOMES = ["won", "lost", "redirected_sac", "unqualified", "no_response", "other"];
122498
+ async function parseCloseFields(raw2) {
122499
+ const trimmed = raw2.trim();
122500
+ if (trimmed.startsWith("{")) {
122501
+ return JSON.parse(trimmed);
122502
+ }
122503
+ const fs2 = await import("fs/promises");
122504
+ const text3 = await fs2.readFile(trimmed, "utf-8");
122505
+ return JSON.parse(text3);
122506
+ }
122507
+ async function postCloseContact(body) {
122508
+ const _cfg = (await Promise.resolve().then(() => (init_config(), exports_config))).loadConfig();
122509
+ const baseUrl = _cfg.apiUrl ?? "http://localhost:8882";
122510
+ const apiKey = _cfg.apiKey ?? "";
122511
+ const resp = await fetch(`${baseUrl}/api/v2/messages/send/close-contact`, {
122512
+ method: "POST",
122513
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey },
122514
+ body: JSON.stringify(body)
122515
+ });
122516
+ if (!resp.ok) {
122517
+ const err2 = await resp.json().catch(() => ({}));
122518
+ const errMsg = typeof err2.error === "string" ? err2.error : err2.error?.message ?? `API error: ${resp.status}`;
122519
+ throw new Error(errMsg);
122520
+ }
122521
+ const data2 = await resp.json();
122522
+ return data2.data ?? {};
122523
+ }
122524
+ async function buildCloseContactBody(options) {
122525
+ let closeFields;
122526
+ if (options.closeFields) {
122527
+ try {
122528
+ closeFields = await parseCloseFields(options.closeFields);
122529
+ } catch (err2) {
122530
+ error(`Failed to parse --close-fields: ${err2 instanceof Error ? err2.message : "Unknown error"}`);
122531
+ return null;
122532
+ }
122533
+ }
122534
+ const instanceId = await resolveInstanceId(options.instance);
122535
+ const resolvedChatId = await resolveChatId(options.chat);
122536
+ const body = {
122537
+ instanceId,
122538
+ chatId: resolvedChatId,
122539
+ to: options.to,
122540
+ text: options.text,
122541
+ outcome: options.outcome
122542
+ };
122543
+ if (options.reason)
122544
+ body.reason = options.reason;
122545
+ if (closeFields)
122546
+ body.closeFields = closeFields;
122547
+ return body;
122548
+ }
122549
+ async function handleCloseContact(options) {
122550
+ if (!VALID_CLOSE_OUTCOMES.includes(options.outcome)) {
122551
+ error(`Invalid --outcome '${options.outcome}'. Must be one of: ${VALID_CLOSE_OUTCOMES.join(", ")}`);
122552
+ return;
122553
+ }
122554
+ try {
122555
+ const body = await buildCloseContactBody(options);
122556
+ if (!body)
122557
+ return;
122558
+ const result = await postCloseContact(body);
122559
+ const cooldownPart = result.closeUntil ? `, closeUntil=${result.closeUntil}` : "";
122560
+ success(`Chat closed (${options.outcome}): terminal=${result.terminal ?? false}, escalated=${result.escalated ?? false}${cooldownPart}`);
122561
+ } catch (err2) {
122562
+ error(`Failed to close contact: ${err2 instanceof Error ? err2.message : "Unknown error"}`);
122563
+ }
122564
+ }
122469
122565
 
122470
122566
  // src/commands/open.ts
122471
122567
  init_output();