@agent-team-foundation/first-tree-hub 0.12.6 → 0.12.8

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.
@@ -45,9 +45,9 @@ function scanMentionTokens(content) {
45
45
  return tokens;
46
46
  }
47
47
  /**
48
- * Derive the `chat_participants.mode` that a freshly inserted row MUST get,
49
- * given the chat's `type` and the joining agent's `type`. This is the single
50
- * authoritative rule for the invariant
48
+ * Derive the `chat_membership.mode` that a freshly inserted speaker row MUST
49
+ * get, given the chat's `type` and the joining agent's `type`. This is the
50
+ * single authoritative rule for the invariant
51
51
  *
52
52
  * `(chat.type === 'group' && agent.type !== 'human') ⇒ mode === 'mention_only'`
53
53
  *
@@ -68,9 +68,9 @@ function scanMentionTokens(content) {
68
68
  * agent should listen to every message in this 1:1 line)
69
69
  *
70
70
  * `peerAgentTypes` is read only in the `direct` branch; callers may pass
71
- * an empty array (or omit it) for `group` chats — it's ignored. Watcher /
72
- * subscription-side `chat_subscriptions` rows are unaffected; the helper
73
- * only governs the "speaking" mode column.
71
+ * an empty array (or omit it) for `group` chats — it's ignored. Watcher
72
+ * rows (`chat_membership.access_mode = 'watcher'`) are unaffected; the
73
+ * helper only governs the "speaking" mode column.
74
74
  */
75
75
  function defaultParticipantMode(chatType, agentType, peerAgentTypes = []) {
76
76
  if (agentType === "human") return "full";
@@ -412,10 +412,15 @@ const runtimeStateSchema = z.enum([
412
412
  z.enum([
413
413
  "active",
414
414
  "suspended",
415
- "evicted"
415
+ "evicted",
416
+ "errored"
416
417
  ]);
417
418
  /** Wire-level states a client may report. `evicted` from a stale client is rejected. */
418
- const clientSessionStateSchema = z.enum(["active", "suspended"]);
419
+ const clientSessionStateSchema = z.enum([
420
+ "active",
421
+ "suspended",
422
+ "errored"
423
+ ]);
419
424
  const sessionStateMessageSchema = z.object({
420
425
  chatId: z.string().min(1),
421
426
  state: clientSessionStateSchema
@@ -627,6 +632,27 @@ const chatTypeSchema = z.enum([
627
632
  "group",
628
633
  "thread"
629
634
  ]);
635
+ /**
636
+ * Per-(chat, user) engagement state. Stored on `chat_user_state` so each
637
+ * user manages their own view independently of structural membership.
638
+ *
639
+ * active — default; chat is in the user's active conversation list.
640
+ * archived — user-snoozed; auto-revives to `active` when a new message
641
+ * lands in the chat (see `services/chat-projection.ts`).
642
+ * deleted — user-removed; never auto-revives. Restorable only by the
643
+ * user from the chat detail page.
644
+ */
645
+ const CHAT_ENGAGEMENT_STATUSES = {
646
+ ACTIVE: "active",
647
+ ARCHIVED: "archived",
648
+ DELETED: "deleted"
649
+ };
650
+ const chatEngagementStatusSchema = z.enum([
651
+ "active",
652
+ "archived",
653
+ "deleted"
654
+ ]);
655
+ const patchChatEngagementSchema = z.object({ status: chatEngagementStatusSchema });
630
656
  const createChatSchema = z.object({
631
657
  type: chatTypeSchema,
632
658
  topic: z.string().max(500).optional(),
@@ -656,7 +682,8 @@ z.object({
656
682
  }).extend({
657
683
  participants: z.array(chatParticipantSchema),
658
684
  title: z.string(),
659
- firstMessagePreview: z.string().nullable()
685
+ firstMessagePreview: z.string().nullable(),
686
+ engagementStatus: chatEngagementStatusSchema
660
687
  });
661
688
  const updateChatSchema = z.object({ topic: z.string().trim().max(500).nullable() });
662
689
  /**
@@ -1124,10 +1151,16 @@ const meChatFilterSchema = z.enum([
1124
1151
  "watching"
1125
1152
  ]);
1126
1153
  const meChatMembershipKindSchema = z.enum(["participant", "watching"]);
1154
+ const chatEngagementViewSchema = z.enum([
1155
+ "active",
1156
+ "archived",
1157
+ "all"
1158
+ ]);
1127
1159
  const listMeChatsQuerySchema = z.object({
1128
1160
  cursor: z.string().optional(),
1129
1161
  limit: z.coerce.number().int().min(1).max(200).default(50),
1130
- filter: meChatFilterSchema.default("all")
1162
+ filter: meChatFilterSchema.default("all"),
1163
+ engagement: chatEngagementViewSchema.default("active")
1131
1164
  });
1132
1165
  const meChatParticipantSchema = z.object({
1133
1166
  agentId: z.string(),
@@ -1145,7 +1178,8 @@ const meChatRowSchema = z.object({
1145
1178
  lastMessageAt: z.string().nullable(),
1146
1179
  lastMessagePreview: z.string().nullable(),
1147
1180
  unreadMentionCount: z.number().int(),
1148
- canReply: z.boolean()
1181
+ canReply: z.boolean(),
1182
+ engagementStatus: chatEngagementStatusSchema
1149
1183
  });
1150
1184
  z.object({
1151
1185
  rows: z.array(meChatRowSchema),
@@ -1720,4 +1754,4 @@ z.object({
1720
1754
  capabilities: serverCapabilitiesSchema.optional()
1721
1755
  }).passthrough();
1722
1756
  //#endregion
1723
- export { questionMessageContentSchema as $, delegateFeishuUserSchema as A, inboxPollQuerySchema as B, createAgentSchema as C, createOrgFromMeSchema as D, createMemberSchema as E, githubDevCallbackQuerySchema as F, listMeChatsQuerySchema as G, isRedactedEnvValue as H, githubStartQuerySchema as I, notificationQuerySchema as J, loginSchema as K, imageInlineContentSchema as L, extractMentions as M, githubAppInstallationClaimBodySchema as N, defaultParticipantMode as O, githubCallbackQuerySchema as P, questionAnswerMessageContentSchema as Q, inboxAckFrameSchema as R, createAdapterMappingSchema as S, wsAuthFrameSchema as St, createMeChatSchema as T, isReservedAgentName as U, isOrgSettingNamespace as V, joinByInvitationSchema as W, paginationQuerySchema as X, onboardingEventSchema as Y, patchOnboardingSchema as Z, clientCapabilitiesSchema as _, updateAgentSchema as _t, AGENT_VISIBILITY as a, selfServiceFeishuBotSchema as at, contextTreeSnapshotSchema as b, updateMemberSchema as bt, ORG_SETTINGS_NAMESPACES as c, sessionCompletionMessageSchema as ct, addParticipantSchema as d, sessionReconcileRequestSchema as dt, rebindAgentSchema as et, agentBindRequestSchema as f, sessionStateMessageSchema as ft, chatMetadataSchema as g, updateAgentRuntimeConfigSchema as gt, agentTypeSchema as h, updateAdapterConfigSchema as ht, AGENT_STATUSES as i, scanMentionTokens as it, dryRunAgentRuntimeConfigSchema as j, defaultRuntimeConfigPayload as k, WS_AUTH_FRAME_TIMEOUT_MS as l, sessionEventMessageSchema as lt, agentRuntimeConfigPayloadSchema as m, submitQuestionAnswerSchema as mt, AGENT_NAME_REGEX as n, runtimeStateMessageSchema as nt, DEFAULT_RUNTIME_PROVIDER as o, sendMessageSchema as ot, agentPinnedMessageSchema as p, stripCode as pt, messageSourceSchema as q, AGENT_SELECTOR_HEADER as r, safeRedirectPath as rt, MENTION_REGEX as s, sendToAgentSchema as st, AGENT_BIND_REJECT_REASONS as t, refreshTokenSchema as tt, addMeChatParticipantsSchema as u, sessionEventSchema as ut, clientRegisterSchema as v, updateChatSchema as vt, createChatSchema as w, createAdapterConfigSchema as x, updateOrganizationSchema as xt, connectTokenExchangeSchema as y, updateClientCapabilitiesSchema as yt, inboxDeliverFrameSchema as z };
1757
+ export { patchOnboardingSchema as $, defaultRuntimeConfigPayload as A, inboxDeliverFrameSchema as B, createAdapterMappingSchema as C, updateOrganizationSchema as Ct, createMemberSchema as D, createMeChatSchema as E, githubCallbackQuerySchema as F, joinByInvitationSchema as G, isOrgSettingNamespace as H, githubDevCallbackQuerySchema as I, messageSourceSchema as J, listMeChatsQuerySchema as K, githubStartQuerySchema as L, dryRunAgentRuntimeConfigSchema as M, extractMentions as N, createOrgFromMeSchema as O, githubAppInstallationClaimBodySchema as P, patchChatEngagementSchema as Q, imageInlineContentSchema as R, createAdapterConfigSchema as S, updateMemberSchema as St, createChatSchema as T, isRedactedEnvValue as U, inboxPollQuerySchema as V, isReservedAgentName as W, onboardingEventSchema as X, notificationQuerySchema as Y, paginationQuerySchema as Z, chatMetadataSchema as _, updateAdapterConfigSchema as _t, AGENT_VISIBILITY as a, safeRedirectPath as at, connectTokenExchangeSchema as b, updateChatSchema as bt, MENTION_REGEX as c, sendMessageSchema as ct, addMeChatParticipantsSchema as d, sessionEventMessageSchema as dt, questionAnswerMessageContentSchema as et, addParticipantSchema as f, sessionEventSchema as ft, agentTypeSchema as g, submitQuestionAnswerSchema as gt, agentRuntimeConfigPayloadSchema as h, stripCode as ht, AGENT_STATUSES as i, runtimeStateMessageSchema as it, delegateFeishuUserSchema as j, defaultParticipantMode as k, ORG_SETTINGS_NAMESPACES as l, sendToAgentSchema as lt, agentPinnedMessageSchema as m, sessionStateMessageSchema as mt, AGENT_NAME_REGEX as n, rebindAgentSchema as nt, CHAT_ENGAGEMENT_STATUSES as o, scanMentionTokens as ot, agentBindRequestSchema as p, sessionReconcileRequestSchema as pt, loginSchema as q, AGENT_SELECTOR_HEADER as r, refreshTokenSchema as rt, DEFAULT_RUNTIME_PROVIDER as s, selfServiceFeishuBotSchema as st, AGENT_BIND_REJECT_REASONS as t, questionMessageContentSchema as tt, WS_AUTH_FRAME_TIMEOUT_MS as u, sessionCompletionMessageSchema as ut, clientCapabilitiesSchema as v, updateAgentRuntimeConfigSchema as vt, createAgentSchema as w, wsAuthFrameSchema as wt, contextTreeSnapshotSchema as x, updateClientCapabilitiesSchema as xt, clientRegisterSchema as y, updateAgentSchema as yt, inboxAckFrameSchema as z };
@@ -0,0 +1,223 @@
1
+ -- Chat data model restructure — Step 2 (migration N).
2
+ -- See proposals/chat-data-model-restructure.20260512.md §8 (schema)
3
+ -- and §9 (migration path).
4
+ --
5
+ -- Replaces the chat_participants / chat_subscriptions split with a
6
+ -- three-layer model: chats (entity) + chat_membership (structure) +
7
+ -- chat_user_state (per-user private state). This migration creates
8
+ -- the two new tables and back-fills them from the legacy two; the
9
+ -- legacy tables stay in place. A follow-up migration (0038, separate
10
+ -- PR) drops them once the new code stabilises ≥1 workday in prod —
11
+ -- see §9.2 step 6 for why those are split.
12
+ --
13
+ -- Pre-flight collision probe (§9.1) MUST be run against staging and
14
+ -- prod read-replicas before this PR is opened. If
15
+ -- SELECT chat_id, agent_id
16
+ -- FROM chat_participants p JOIN chat_subscriptions s USING (chat_id, agent_id)
17
+ -- returns any rows, the cutover preference (speaker row wins) applies
18
+ -- automatically via the insert ordering below, but the surprise should
19
+ -- be investigated first.
20
+ --
21
+ -- Service-layer integrity (no FK / CHECK / trigger): consistent with
22
+ -- messages / inbox_entries / notifications. The DB-level
23
+ -- `ON DELETE CASCADE` from chats.id is intentionally NOT carried over
24
+ -- — chat hard-delete paths must explicitly clean these tables (see
25
+ -- §8.5, §11.7 chat-delete integration test).
26
+ --
27
+ -- Migration 0037 is hand-written. drizzle-kit generate refuses to
28
+ -- diff against the pre-0019 snapshot; we have followed this convention
29
+ -- for every migration since 0019 (see 0036's header).
30
+
31
+ CREATE TABLE IF NOT EXISTS "chat_membership" (
32
+ "chat_id" text NOT NULL,
33
+ "agent_id" text NOT NULL,
34
+ "role" text NOT NULL DEFAULT 'member',
35
+ "access_mode" text NOT NULL,
36
+ "mode" text NOT NULL DEFAULT 'full',
37
+ "source" text NOT NULL DEFAULT 'manual',
38
+ "joined_at" timestamp with time zone NOT NULL DEFAULT now(),
39
+ CONSTRAINT "chat_membership_pkey" PRIMARY KEY ("chat_id", "agent_id")
40
+ );
41
+
42
+ --> statement-breakpoint
43
+ CREATE INDEX IF NOT EXISTS "idx_membership_agent"
44
+ ON "chat_membership" ("agent_id");
45
+
46
+ --> statement-breakpoint
47
+ -- Index name matches §8.2 of the proposal (`idx_membership_chat_role`).
48
+ -- The trailing token is `role` (the legacy speaker/watcher *role* concept)
49
+ -- rather than `access_mode` (the literal column name) to align with the
50
+ -- design checklist.
51
+ CREATE INDEX IF NOT EXISTS "idx_membership_chat_role"
52
+ ON "chat_membership" ("chat_id", "access_mode");
53
+
54
+ --> statement-breakpoint
55
+ CREATE TABLE IF NOT EXISTS "chat_user_state" (
56
+ "chat_id" text NOT NULL,
57
+ "agent_id" text NOT NULL,
58
+ "last_read_at" timestamp with time zone,
59
+ "unread_mention_count" integer NOT NULL DEFAULT 0,
60
+ CONSTRAINT "chat_user_state_pkey" PRIMARY KEY ("chat_id", "agent_id")
61
+ );
62
+
63
+ --> statement-breakpoint
64
+ CREATE INDEX IF NOT EXISTS "idx_user_state_agent"
65
+ ON "chat_user_state" ("agent_id");
66
+
67
+ --> statement-breakpoint
68
+ -- Partial index for the unread-badge / ?filter=unread lookup. Most rows
69
+ -- have unread_mention_count = 0; bounding the index by the actual
70
+ -- unread row count keeps the scan cheap regardless of table size.
71
+ CREATE INDEX IF NOT EXISTS "idx_user_state_unread"
72
+ ON "chat_user_state" ("agent_id")
73
+ WHERE unread_mention_count > 0;
74
+
75
+ --> statement-breakpoint
76
+ -- Back-fill chat_membership from chat_participants. These are the
77
+ -- "speaker" rows — they retain their owner/member role from the legacy
78
+ -- table and gain access_mode = 'speaker'. `joined_at` carries over
79
+ -- verbatim. `source = 'manual'` is the conservative default; we do not
80
+ -- attempt to reconstruct the original add-path retroactively.
81
+ -- Speaker-wins merge policy per proposal §9.2 step 3 and §11.7 checklist.
82
+ -- Since `chat_participants` is loaded BEFORE `chat_subscriptions`, the
83
+ -- INSERT below cannot produce a conflict in a clean cutover. The
84
+ -- ON CONFLICT DO UPDATE is the explicit double-protection requested by
85
+ -- the proposal: if a dirty write between the §9.1 probe and the
86
+ -- maintenance window has somehow planted a watcher row first (e.g. a
87
+ -- partial / aborted prior migration), upgrade it in place to speaker
88
+ -- rather than silently leaving it as a watcher (which DO NOTHING would
89
+ -- have done). `joined_at` keeps the earliest known timestamp so the
90
+ -- audit trail is not flattened.
91
+ INSERT INTO "chat_membership"
92
+ ("chat_id", "agent_id", "role", "access_mode", "mode", "source", "joined_at")
93
+ SELECT
94
+ cp."chat_id",
95
+ cp."agent_id",
96
+ COALESCE(cp."role", 'member'),
97
+ 'speaker',
98
+ COALESCE(cp."mode", 'full'),
99
+ 'manual',
100
+ COALESCE(cp."joined_at", now())
101
+ FROM "chat_participants" cp
102
+ ON CONFLICT ("chat_id", "agent_id") DO UPDATE SET
103
+ "access_mode" = 'speaker',
104
+ "role" = EXCLUDED."role",
105
+ "mode" = EXCLUDED."mode",
106
+ "source" = EXCLUDED."source",
107
+ "joined_at" = LEAST("chat_membership"."joined_at", EXCLUDED."joined_at");
108
+
109
+ --> statement-breakpoint
110
+ -- Back-fill chat_membership from chat_subscriptions. These are the
111
+ -- "watcher" rows.
112
+ --
113
+ -- Speaker-wins on collision: if a row already exists for this
114
+ -- (chat, agent) pair it is by construction a speaker (the previous
115
+ -- INSERT just wrote it). The guarded UPDATE makes this a no-op for
116
+ -- existing speaker rows (the `WHERE chat_membership.access_mode =
117
+ -- 'watcher'` guard is never satisfied), so the speaker is preserved
118
+ -- untouched. We use ON CONFLICT DO UPDATE rather than DO NOTHING per
119
+ -- proposal §9.2 step 3 — the merge policy lives in the SQL itself,
120
+ -- not implicitly in the INSERT ordering.
121
+ --
122
+ -- source = 'auto_manager' captures that watcher rows historically came
123
+ -- from recomputeChatWatchers' anchor-based set rebuild. This default is
124
+ -- harmless even for the rare manually-attached watcher rows.
125
+ INSERT INTO "chat_membership"
126
+ ("chat_id", "agent_id", "role", "access_mode", "mode", "source", "joined_at")
127
+ SELECT
128
+ cs."chat_id",
129
+ cs."agent_id",
130
+ 'member',
131
+ 'watcher',
132
+ 'full',
133
+ 'auto_manager',
134
+ COALESCE(cs."created_at", now())
135
+ FROM "chat_subscriptions" cs
136
+ ON CONFLICT ("chat_id", "agent_id") DO UPDATE SET
137
+ "joined_at" = LEAST("chat_membership"."joined_at", EXCLUDED."joined_at")
138
+ WHERE "chat_membership"."access_mode" = 'watcher';
139
+
140
+ --> statement-breakpoint
141
+ -- Back-fill chat_user_state from chat_participants. Only rows whose
142
+ -- read state was actually touched (lastReadAt non-null OR
143
+ -- unreadMentionCount > 0) are materialised — the rest can be served
144
+ -- via COALESCE-defaults at read time and would only bloat the table.
145
+ --
146
+ -- Speaker-wins: no prior row can exist for this (chat, agent) pair in
147
+ -- a clean cutover. The ON CONFLICT DO UPDATE is defensive against a
148
+ -- partial / aborted prior migration leaving a stale row behind — we
149
+ -- overwrite with the authoritative speaker-side read state per §9.2.
150
+ INSERT INTO "chat_user_state"
151
+ ("chat_id", "agent_id", "last_read_at", "unread_mention_count")
152
+ SELECT
153
+ cp."chat_id",
154
+ cp."agent_id",
155
+ cp."last_read_at",
156
+ cp."unread_mention_count"
157
+ FROM "chat_participants" cp
158
+ WHERE cp."last_read_at" IS NOT NULL
159
+ OR cp."unread_mention_count" > 0
160
+ ON CONFLICT ("chat_id", "agent_id") DO UPDATE SET
161
+ "last_read_at" = EXCLUDED."last_read_at",
162
+ "unread_mention_count" = EXCLUDED."unread_mention_count";
163
+
164
+ --> statement-breakpoint
165
+ -- Same back-fill from chat_subscriptions. Speaker-wins: if a row was
166
+ -- written above from chat_participants, that participant-side row
167
+ -- represents the speaker's authoritative read state and must not be
168
+ -- clobbered. The guarded UPDATE has WHERE FALSE so the conflict path
169
+ -- is a true no-op; we use DO UPDATE (over DO NOTHING) to keep the
170
+ -- merge policy explicit in the SQL itself per §9.2 step 3.
171
+ INSERT INTO "chat_user_state"
172
+ ("chat_id", "agent_id", "last_read_at", "unread_mention_count")
173
+ SELECT
174
+ cs."chat_id",
175
+ cs."agent_id",
176
+ cs."last_read_at",
177
+ cs."unread_mention_count"
178
+ FROM "chat_subscriptions" cs
179
+ WHERE cs."last_read_at" IS NOT NULL
180
+ OR cs."unread_mention_count" > 0
181
+ ON CONFLICT ("chat_id", "agent_id") DO UPDATE SET
182
+ "last_read_at" = "chat_user_state"."last_read_at"
183
+ WHERE FALSE;
184
+
185
+ --> statement-breakpoint
186
+ -- Row-count assertions. Fail the migration loudly if the back-fills
187
+ -- did not materialise the expected number of rows. UNION (deduping by
188
+ -- (chat_id, agent_id)) matches the speaker-wins merge policy above.
189
+ DO $$
190
+ DECLARE
191
+ expected_membership int;
192
+ actual_membership int;
193
+ expected_state int;
194
+ actual_state int;
195
+ BEGIN
196
+ SELECT COUNT(*) INTO expected_membership FROM (
197
+ SELECT "chat_id", "agent_id" FROM "chat_participants"
198
+ UNION
199
+ SELECT "chat_id", "agent_id" FROM "chat_subscriptions"
200
+ ) sub;
201
+
202
+ SELECT COUNT(*) INTO actual_membership FROM "chat_membership";
203
+
204
+ IF expected_membership <> actual_membership THEN
205
+ RAISE EXCEPTION 'chat_membership row count mismatch: expected % got %',
206
+ expected_membership, actual_membership;
207
+ END IF;
208
+
209
+ SELECT COUNT(*) INTO expected_state FROM (
210
+ SELECT "chat_id", "agent_id" FROM "chat_participants"
211
+ WHERE "last_read_at" IS NOT NULL OR "unread_mention_count" > 0
212
+ UNION
213
+ SELECT "chat_id", "agent_id" FROM "chat_subscriptions"
214
+ WHERE "last_read_at" IS NOT NULL OR "unread_mention_count" > 0
215
+ ) sub;
216
+
217
+ SELECT COUNT(*) INTO actual_state FROM "chat_user_state";
218
+
219
+ IF expected_state <> actual_state THEN
220
+ RAISE EXCEPTION 'chat_user_state row count mismatch: expected % got %',
221
+ expected_state, actual_state;
222
+ END IF;
223
+ END $$;
@@ -0,0 +1,26 @@
1
+ -- Chat data model restructure — Step 3 (drop legacy).
2
+ -- See proposals/chat-data-model-restructure.20260512.md §9.2 step 6
3
+ -- and §12.2 (catastrophic rollback boundary).
4
+ --
5
+ -- PR-B of the two-PR cutover. PR-A (#325, migration 0038) created
6
+ -- `chat_membership` + `chat_user_state` and back-filled them from
7
+ -- `chat_participants` + `chat_subscriptions`. The service layer has
8
+ -- been cutover to read/write only the new tables since PR-A. This
9
+ -- migration drops the legacy tables now that ≥1 workday of post-deploy
10
+ -- observation has passed without anomalies.
11
+ --
12
+ -- ROLLBACK is catastrophic post-this-migration: the legacy tables are
13
+ -- gone and require a backup-restore + re-run of 0038 to recover. This
14
+ -- is the explicit reason PR-A and PR-B were split — PR-A's reverse
15
+ -- migration is loss-free, PR-B's is not (§12.2). Do NOT merge this
16
+ -- before ops confirms PR-A stability.
17
+ --
18
+ -- Service-layer dependency check (run before merging):
19
+ -- git grep 'chat_participants\|chat_subscriptions' packages/server/src/
20
+ -- should return only doc / comment hits — any live query reference is
21
+ -- a blocker.
22
+
23
+ DROP TABLE IF EXISTS "chat_subscriptions";
24
+
25
+ --> statement-breakpoint
26
+ DROP TABLE IF EXISTS "chat_participants";
@@ -0,0 +1,24 @@
1
+ -- Per-(chat, user) engagement state on `chat_user_state`.
2
+ -- Replaces the design of closed PR #316, which originally tried to add
3
+ -- this column to both `chat_participants` and `chat_subscriptions` and
4
+ -- was rejected for forcing double-writes + state-carry across the
5
+ -- speaker/watcher boundary. After the data-model restructure
6
+ -- (proposals/chat-data-model-restructure.20260512.md, migrations
7
+ -- 0038/0039), the natural home is `chat_user_state` — sitting next to
8
+ -- the other per-user private columns (`last_read_at`,
9
+ -- `unread_mention_count`).
10
+ --
11
+ -- Values: 'active' (default) | 'archived' | 'deleted'. Auto-revive
12
+ -- archived → active happens on new message in `applyAfterFanOut`;
13
+ -- `deleted` is sticky and reachable only via the chat detail page +
14
+ -- Restore button.
15
+ --
16
+ -- `chat_user_state` rows are lazy-materialised (row only created on
17
+ -- first markRead / mention / engagement write). The service layer
18
+ -- reads via `COALESCE(engagement_status, 'active')`, so existing rows
19
+ -- without an explicit value (and rows that don't yet exist) both
20
+ -- resolve to `'active'` — no back-fill needed and the NOT NULL DEFAULT
21
+ -- handles new INSERTs.
22
+
23
+ ALTER TABLE "chat_user_state"
24
+ ADD COLUMN "engagement_status" text NOT NULL DEFAULT 'active';
@@ -267,6 +267,27 @@
267
267
  "when": 1778803200000,
268
268
  "tag": "0037_github_app_installations",
269
269
  "breakpoints": true
270
+ },
271
+ {
272
+ "idx": 38,
273
+ "version": "7",
274
+ "when": 1778889600000,
275
+ "tag": "0038_chat_membership_user_state",
276
+ "breakpoints": true
277
+ },
278
+ {
279
+ "idx": 39,
280
+ "version": "7",
281
+ "when": 1779148800000,
282
+ "tag": "0039_drop_chat_participants_subscriptions",
283
+ "breakpoints": true
284
+ },
285
+ {
286
+ "idx": 40,
287
+ "version": "7",
288
+ "when": 1779235200000,
289
+ "tag": "0040_chat_user_state_engagement",
290
+ "breakpoints": true
270
291
  }
271
292
  ]
272
293
  }
@@ -1,6 +1,6 @@
1
1
  import { r as __exportAll } from "./chunk-BSw8zbkd.mjs";
2
2
  import { t as cliFetch } from "./cli-fetch--tiwKm5S.mjs";
3
- import { r as AGENT_SELECTOR_HEADER } from "./dist-xP6NpdMp.mjs";
3
+ import { r as AGENT_SELECTOR_HEADER } from "./dist-CnjqakXS.mjs";
4
4
  //#region src/core/feishu.ts
5
5
  var feishu_exports = /* @__PURE__ */ __exportAll({
6
6
  bindFeishuBot: () => bindFeishuBot,
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import "./observability-BAScT_5S-BcW9HgkG.mjs";
2
- import { A as checkDocker, B as isServiceSupported, E as checkAgentConfigs, F as checkWebSocket, G as uninstallClientService, H as restartClientService, I as printResults, J as stopPostgres, K as ensurePostgres, M as checkServerConfig, N as checkServerHealth, O as checkClientConfig, P as checkServerReachable, R as getClientServiceStatus, S as runHomeMigration, T as runMigrations, U as startClientService, V as resolveCliInvocation, W as stopClientService, X as handleClientOrgMismatch, Y as ClientRuntime, Z as rotateClientIdWithBackup, _ as formatCheckReport, b as onboardCreate, d as startServer, g as promptMissingFields, h as promptAddAgent, j as checkNodeVersion, k as checkDatabase, lt as FirstTreeHubSDK, m as isInteractive, n as deriveHubUrlFromToken, nt as hasUser, q as isDockerAvailable, t as HubUrlDerivationError, tt as createOwner, ut as SdkError, y as onboardCheck, z as installClientService } from "./saas-connect-RCN8zL5e.mjs";
2
+ import { A as checkDocker, B as isServiceSupported, E as checkAgentConfigs, F as checkWebSocket, G as uninstallClientService, H as restartClientService, I as printResults, J as stopPostgres, K as ensurePostgres, M as checkServerConfig, N as checkServerHealth, O as checkClientConfig, P as checkServerReachable, R as getClientServiceStatus, S as runHomeMigration, T as runMigrations, U as startClientService, V as resolveCliInvocation, W as stopClientService, X as handleClientOrgMismatch, Y as ClientRuntime, Z as rotateClientIdWithBackup, _ as formatCheckReport, b as onboardCreate, d as startServer, g as promptMissingFields, h as promptAddAgent, j as checkNodeVersion, k as checkDatabase, lt as FirstTreeHubSDK, m as isInteractive, n as deriveHubUrlFromToken, nt as hasUser, q as isDockerAvailable, t as HubUrlDerivationError, tt as createOwner, ut as SdkError, y as onboardCheck, z as installClientService } from "./saas-connect-CLcon-De.mjs";
3
3
  import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
4
4
  import { a as ensureFreshAdminToken, c as resolveServerUrl, i as ensureFreshAccessToken, n as AuthRefreshRateLimitedError, s as resolveAccessToken, t as AuthRefreshFailedError } from "./bootstrap-BCZC1ki6.mjs";
5
5
  import { i as blank, s as status } from "./cli-fetch--tiwKm5S.mjs";
6
- import "./dist-xP6NpdMp.mjs";
7
- import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-CsfadBKa.mjs";
6
+ import "./dist-CnjqakXS.mjs";
7
+ import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-DrnBbl8T.mjs";
8
8
  import "./errors-CF5evtJt-B0NTIVPt.mjs";
9
9
  import "./src-DNBS5Yjj.mjs";
10
- import "./client-B89AKi3Q-DAyGdQSq.mjs";
10
+ import "./client-bR8nwHaV-OxnjyKOk.mjs";
11
11
  import "./invitation-Bg0TRiyx-BsZH4GCS.mjs";
12
12
  export { AuthRefreshFailedError, AuthRefreshRateLimitedError, ClientRuntime, FirstTreeHubSDK, HubUrlDerivationError, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, deriveHubUrlFromToken, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, restartClientService, rotateClientIdWithBackup, runHomeMigration, runMigrations, startClientService, startServer, status, stopClientService, stopPostgres, uninstallClientService };
@@ -1,4 +1,4 @@
1
- import "./dist-xP6NpdMp.mjs";
1
+ import "./dist-CnjqakXS.mjs";
2
2
  import "./errors-CF5evtJt-B0NTIVPt.mjs";
3
3
  import { s as previewInvitation } from "./invitation-Bg0TRiyx-BsZH4GCS.mjs";
4
4
  export { previewInvitation };