@agent-team-foundation/first-tree-hub 0.10.14 → 0.11.0
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.
- package/dist/{bootstrap-B2x4TTyJ.mjs → bootstrap-DUeYbwm-.mjs} +60 -4
- package/dist/cli/index.mjs +6 -6
- package/dist/{dist-D6AOiyNg.mjs → dist-BoHl9HwW.mjs} +66 -2
- package/dist/drizzle/0030_chat_first_workspace.sql +129 -0
- package/dist/drizzle/meta/_journal.json +7 -0
- package/dist/{feishu-DQ1l18Ah.mjs → feishu-Dxk6ArOK.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/{invitation-CBnQyB7o-Bulf3Sl7.mjs → invitation-CBnQyB7o-TmnIj3kx.mjs} +1 -1
- package/dist/{saas-connect-DWcxHtjX.mjs → saas-connect-DLSyrQcC.mjs} +1394 -395
- package/dist/web/assets/index-BxGzfDTS.js +383 -0
- package/dist/web/assets/{index-BQda2sqe.js → index-COflQOwF.js} +1 -1
- package/dist/web/assets/{index-CKoTjI0J.css → index-DDqPt6PI.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-C7yW7sWI.js +0 -388
|
@@ -3,7 +3,7 @@ import { o as logFormatSchema, s as logLevelSchema } from "./logger-core-BTmvdfl
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
-
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { parse, stringify } from "yaml";
|
|
9
9
|
//#region ../shared/dist/config/index.mjs
|
|
@@ -650,6 +650,7 @@ const serverConfigSchema = defineConfig({
|
|
|
650
650
|
//#region src/core/bootstrap.ts
|
|
651
651
|
var bootstrap_exports = /* @__PURE__ */ __exportAll({
|
|
652
652
|
AuthRefreshFailedError: () => AuthRefreshFailedError,
|
|
653
|
+
AuthRefreshRateLimitedError: () => AuthRefreshRateLimitedError,
|
|
653
654
|
ensureFreshAccessToken: () => ensureFreshAccessToken,
|
|
654
655
|
ensureFreshAdminToken: () => ensureFreshAdminToken,
|
|
655
656
|
loadCredentials: () => loadCredentials,
|
|
@@ -706,6 +707,39 @@ var AuthRefreshFailedError = class extends Error {
|
|
|
706
707
|
}
|
|
707
708
|
};
|
|
708
709
|
/**
|
|
710
|
+
* Thrown when `/auth/refresh` returns 429. Carries the server-suggested
|
|
711
|
+
* retry-after (or a sane default) so the WS reconnect loop can wait at
|
|
712
|
+
* least that long instead of pounding the limiter inside the same window
|
|
713
|
+
* with its default 1/2/4/8s exponential backoff — which would just keep
|
|
714
|
+
* the rate-limit bucket full and stretch the outage. Defaults to 30s when
|
|
715
|
+
* the server omits the header.
|
|
716
|
+
*/
|
|
717
|
+
var AuthRefreshRateLimitedError = class extends Error {
|
|
718
|
+
retryAfterMs;
|
|
719
|
+
constructor(retryAfterMs, message) {
|
|
720
|
+
super(message ?? `Refresh request rate-limited; retry after ${Math.round(retryAfterMs / 1e3)}s.`);
|
|
721
|
+
this.name = "AuthRefreshRateLimitedError";
|
|
722
|
+
this.retryAfterMs = retryAfterMs;
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
/**
|
|
726
|
+
* Parse an HTTP `Retry-After` header. Accepts either an integer seconds
|
|
727
|
+
* value (the form fastify-rate-limit emits) or an RFC 7231 HTTP-date.
|
|
728
|
+
* Returns ms, or `null` when the header is absent / malformed.
|
|
729
|
+
*/
|
|
730
|
+
function parseRetryAfterMs(header) {
|
|
731
|
+
if (!header) return null;
|
|
732
|
+
const trimmed = header.trim();
|
|
733
|
+
const seconds = Number(trimmed);
|
|
734
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
735
|
+
const date = Date.parse(trimmed);
|
|
736
|
+
if (Number.isFinite(date)) {
|
|
737
|
+
const delta = date - Date.now();
|
|
738
|
+
return delta > 0 ? delta : 0;
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
709
743
|
* In-flight refresh promise. Multiple callers (WS handshake, proactive
|
|
710
744
|
* refresh timer, every SDK request) can see an expired token within the same
|
|
711
745
|
* millisecond — without dedupe each would fire an independent `/auth/refresh`
|
|
@@ -747,6 +781,7 @@ async function ensureFreshAccessToken(opts) {
|
|
|
747
781
|
signal: AbortSignal.timeout(1e4)
|
|
748
782
|
});
|
|
749
783
|
if (res.status === 401) throw new AuthRefreshFailedError("Refresh token rejected by server. Re-run `first-tree-hub connect <token>` (get a fresh token from the Web Computers page → New Connection).");
|
|
784
|
+
if (res.status === 429) throw new AuthRefreshRateLimitedError(parseRetryAfterMs(res.headers.get("retry-after")) ?? 3e4);
|
|
750
785
|
if (!res.ok) throw new Error(`Refresh request failed with status ${res.status}.`);
|
|
751
786
|
const data = await res.json();
|
|
752
787
|
saveCredentials({
|
|
@@ -775,13 +810,34 @@ function isTokenStale(token, minValidityMs) {
|
|
|
775
810
|
return true;
|
|
776
811
|
}
|
|
777
812
|
}
|
|
778
|
-
/**
|
|
813
|
+
/**
|
|
814
|
+
* Persist credentials to disk atomically.
|
|
815
|
+
*
|
|
816
|
+
* Plain `writeFileSync` opens with `O_TRUNC` then writes — between those
|
|
817
|
+
* calls the file is empty, and a concurrent `loadCredentials()` (e.g. a
|
|
818
|
+
* background daemon refreshing while the user runs a foreground CLI command)
|
|
819
|
+
* reads "" → `JSON.parse` throws → we fall back to "no credentials" and
|
|
820
|
+
* surface a misleading "run `client connect` again" error. write-to-temp +
|
|
821
|
+
* rename gives readers an all-or-nothing view: they see the old file or the
|
|
822
|
+
* new file, never a half-written one. Server-side the sliding-window design
|
|
823
|
+
* already accepts last-writer-wins semantics for the refresh token itself
|
|
824
|
+
* (see auth service comment), so atomicity at the file level is enough.
|
|
825
|
+
*/
|
|
779
826
|
function saveCredentials(creds) {
|
|
780
827
|
mkdirSync(dirname(CREDENTIALS_PATH), {
|
|
781
828
|
recursive: true,
|
|
782
829
|
mode: 448
|
|
783
830
|
});
|
|
784
|
-
|
|
831
|
+
const tmp = `${CREDENTIALS_PATH}.tmp.${process.pid}`;
|
|
832
|
+
try {
|
|
833
|
+
writeFileSync(tmp, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
834
|
+
renameSync(tmp, CREDENTIALS_PATH);
|
|
835
|
+
} catch (err) {
|
|
836
|
+
try {
|
|
837
|
+
unlinkSync(tmp);
|
|
838
|
+
} catch {}
|
|
839
|
+
throw err;
|
|
840
|
+
}
|
|
785
841
|
}
|
|
786
842
|
/**
|
|
787
843
|
* Load persisted credentials saved by the `connect` command.
|
|
@@ -808,4 +864,4 @@ function saveAgentConfig(agentName, agentId, runtime) {
|
|
|
808
864
|
return agentDir;
|
|
809
865
|
}
|
|
810
866
|
//#endregion
|
|
811
|
-
export {
|
|
867
|
+
export { resetConfigMeta as C, setConfigValue as E, resetConfig as S, serverConfigSchema as T, getConfigValue as _, ensureFreshAdminToken as a, migrateLegacyHome as b, resolveServerUrl as c, DEFAULT_CONFIG_DIR as d, DEFAULT_DATA_DIR as f, collectMissingPrompts as g, clientConfigSchema as h, ensureFreshAccessToken as i, saveAgentConfig as l, agentConfigSchema as m, AuthRefreshRateLimitedError as n, loadCredentials as o, DEFAULT_HOME_DIR as p, bootstrap_exports as r, resolveAccessToken as s, AuthRefreshFailedError as t, saveCredentials as u, initConfig as v, resolveConfigReadonly as w, readConfigFile as x, loadAgents as y };
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "../observability-DPyf745N-BSc8QNcR.mjs";
|
|
3
|
-
import { $ as findStaleAliases, A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, I as checkWebSocket, L as printResults, M as checkNodeVersion, N as checkServerConfig, O as checkBackgroundService, P as checkServerHealth, R as reconcileAgentConfigs, S as saveOnboardState, T as migrateLocalAgentDirs, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, _t as probeCapabilities, a as declineUpdate, at as fail, b as onboardCheck, c as detectInstallMode, ct as print, d as startServer, dt as ClientOrgMismatchError, et as formatStaleReason, f as COMMAND_VERSION, ft as ClientUserMismatchError, g as promptAddAgent, gt as cleanWorkspaces, h as isInteractive, ht as SessionRegistry, i as createExecuteUpdate, it as resolveReplyToFromEnv, j as checkDocker, k as checkClientConfig, l as fetchLatestVersion, lt as setJsonMode, m as uploadClientCapabilities, mt as SdkError, nt as createOwner, o as promptUpdate, ot as success, p as reconcileLocalRuntimeProviders, pt as FirstTreeHubSDK, r as registerSaaSConnectCommand, s as PACKAGE_NAME, tt as removeLocalAgent, u as installGlobalLatest, v as formatCheckReport, vt as applyClientLoggerConfig, w as createApiNameResolver, x as onboardCreate, y as loadOnboardState, yt as configureClientLoggerForService, z as getClientServiceStatus } from "../saas-connect-
|
|
3
|
+
import { $ as findStaleAliases, A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, I as checkWebSocket, L as printResults, M as checkNodeVersion, N as checkServerConfig, O as checkBackgroundService, P as checkServerHealth, R as reconcileAgentConfigs, S as saveOnboardState, T as migrateLocalAgentDirs, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, _t as probeCapabilities, a as declineUpdate, at as fail, b as onboardCheck, c as detectInstallMode, ct as print, d as startServer, dt as ClientOrgMismatchError, et as formatStaleReason, f as COMMAND_VERSION, ft as ClientUserMismatchError, g as promptAddAgent, gt as cleanWorkspaces, h as isInteractive, ht as SessionRegistry, i as createExecuteUpdate, it as resolveReplyToFromEnv, j as checkDocker, k as checkClientConfig, l as fetchLatestVersion, lt as setJsonMode, m as uploadClientCapabilities, mt as SdkError, nt as createOwner, o as promptUpdate, ot as success, p as reconcileLocalRuntimeProviders, pt as FirstTreeHubSDK, r as registerSaaSConnectCommand, s as PACKAGE_NAME, tt as removeLocalAgent, u as installGlobalLatest, v as formatCheckReport, vt as applyClientLoggerConfig, w as createApiNameResolver, x as onboardCreate, y as loadOnboardState, yt as configureClientLoggerForService, z as getClientServiceStatus } from "../saas-connect-DLSyrQcC.mjs";
|
|
4
4
|
import "../logger-core-BTmvdflj-DjW8FM4T.mjs";
|
|
5
|
-
import { C as
|
|
6
|
-
import "../dist-
|
|
7
|
-
import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-
|
|
5
|
+
import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, _ as getConfigValue, a as ensureFreshAdminToken, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, x as readConfigFile, y as loadAgents } from "../bootstrap-DUeYbwm-.mjs";
|
|
6
|
+
import "../dist-BoHl9HwW.mjs";
|
|
7
|
+
import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-Dxk6ArOK.mjs";
|
|
8
8
|
import "../invitation-B1pjAyOz-BaCA9PII.mjs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
@@ -1608,13 +1608,13 @@ function isSecretField(schema, dotPath) {
|
|
|
1608
1608
|
//#region src/commands/onboard.ts
|
|
1609
1609
|
async function promptMissing(args) {
|
|
1610
1610
|
if (!args.server) try {
|
|
1611
|
-
const { resolveServerUrl } = await import("../bootstrap-
|
|
1611
|
+
const { resolveServerUrl } = await import("../bootstrap-DUeYbwm-.mjs").then((n) => n.r);
|
|
1612
1612
|
resolveServerUrl();
|
|
1613
1613
|
} catch {
|
|
1614
1614
|
args.server = await input({ message: "Hub server URL:" });
|
|
1615
1615
|
saveOnboardState(args);
|
|
1616
1616
|
}
|
|
1617
|
-
const { loadCredentials } = await import("../bootstrap-
|
|
1617
|
+
const { loadCredentials } = await import("../bootstrap-DUeYbwm-.mjs").then((n) => n.r);
|
|
1618
1618
|
if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
|
|
1619
1619
|
if (!args.id) {
|
|
1620
1620
|
args.id = await input({ message: "Agent ID:" });
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
//#region ../shared/dist/index.mjs
|
|
3
3
|
const MENTION_REGEX = /(?<![A-Za-z0-9_.@-])@([A-Za-z0-9][A-Za-z0-9_-]{0,63})\b/g;
|
|
4
|
+
/**
|
|
5
|
+
* Strip Markdown code regions (fenced + inline) so identifier-shaped
|
|
6
|
+
* tokens inside code (`@param`, `@staticmethod`, etc.) don't get
|
|
7
|
+
* misclassified as mentions. Shared between `extractMentions` (routing)
|
|
8
|
+
* and `extractSummary` (auto-title) so they agree on what counts as a
|
|
9
|
+
* "real" mention vs a code reference.
|
|
10
|
+
*/
|
|
4
11
|
function stripCode(content) {
|
|
5
12
|
return content.replace(/```[\s\S]*?```/g, "").replace(/~~~[\s\S]*?~~~/g, "").replace(/`[^`\n]+`/g, "");
|
|
6
13
|
}
|
|
@@ -577,7 +584,11 @@ z.object({
|
|
|
577
584
|
metadata: z.record(z.string(), z.unknown()),
|
|
578
585
|
createdAt: z.string(),
|
|
579
586
|
updatedAt: z.string()
|
|
580
|
-
}).extend({
|
|
587
|
+
}).extend({
|
|
588
|
+
participants: z.array(chatParticipantSchema),
|
|
589
|
+
title: z.string(),
|
|
590
|
+
firstMessagePreview: z.string().nullable()
|
|
591
|
+
});
|
|
581
592
|
const updateChatSchema = z.object({ topic: z.string().trim().max(500).nullable() });
|
|
582
593
|
const addParticipantSchema = z.object({
|
|
583
594
|
agentId: z.string().min(1),
|
|
@@ -876,6 +887,59 @@ z.object({
|
|
|
876
887
|
/** Body for joining via invite token. */
|
|
877
888
|
const joinByInvitationSchema = z.object({ token: z.string().min(1) });
|
|
878
889
|
z.object({}).optional();
|
|
890
|
+
const meChatFilterSchema = z.enum([
|
|
891
|
+
"all",
|
|
892
|
+
"unread",
|
|
893
|
+
"watching"
|
|
894
|
+
]);
|
|
895
|
+
const meChatMembershipKindSchema = z.enum(["participant", "watching"]);
|
|
896
|
+
const listMeChatsQuerySchema = z.object({
|
|
897
|
+
cursor: z.string().optional(),
|
|
898
|
+
limit: z.coerce.number().int().min(1).max(200).default(50),
|
|
899
|
+
filter: meChatFilterSchema.default("all")
|
|
900
|
+
});
|
|
901
|
+
const meChatParticipantSchema = z.object({
|
|
902
|
+
agentId: z.string(),
|
|
903
|
+
displayName: z.string(),
|
|
904
|
+
type: z.string()
|
|
905
|
+
});
|
|
906
|
+
const meChatRowSchema = z.object({
|
|
907
|
+
chatId: z.string(),
|
|
908
|
+
type: z.string(),
|
|
909
|
+
membershipKind: meChatMembershipKindSchema,
|
|
910
|
+
title: z.string(),
|
|
911
|
+
topic: z.string().nullable(),
|
|
912
|
+
participants: z.array(meChatParticipantSchema),
|
|
913
|
+
participantCount: z.number().int(),
|
|
914
|
+
lastMessageAt: z.string().nullable(),
|
|
915
|
+
lastMessagePreview: z.string().nullable(),
|
|
916
|
+
unreadMentionCount: z.number().int(),
|
|
917
|
+
canReply: z.boolean(),
|
|
918
|
+
taskId: z.string().nullable(),
|
|
919
|
+
taskStatus: z.string().nullable()
|
|
920
|
+
});
|
|
921
|
+
z.object({
|
|
922
|
+
rows: z.array(meChatRowSchema),
|
|
923
|
+
nextCursor: z.string().nullable()
|
|
924
|
+
});
|
|
925
|
+
const createMeChatSchema = z.object({
|
|
926
|
+
participantIds: z.array(z.string().min(1)).min(1),
|
|
927
|
+
topic: z.string().trim().max(500).optional().nullable()
|
|
928
|
+
});
|
|
929
|
+
const addMeChatParticipantsSchema = z.object({ participantIds: z.array(z.string().min(1)).min(1) });
|
|
930
|
+
z.object({
|
|
931
|
+
chatId: z.string(),
|
|
932
|
+
lastReadAt: z.string(),
|
|
933
|
+
unreadMentionCount: z.number().int()
|
|
934
|
+
});
|
|
935
|
+
z.object({
|
|
936
|
+
chatId: z.string(),
|
|
937
|
+
membershipKind: meChatMembershipKindSchema.nullable()
|
|
938
|
+
});
|
|
939
|
+
z.object({
|
|
940
|
+
type: z.literal("chat:message"),
|
|
941
|
+
chatId: z.string()
|
|
942
|
+
});
|
|
879
943
|
z.enum([
|
|
880
944
|
"connect",
|
|
881
945
|
"create_agent",
|
|
@@ -1325,4 +1389,4 @@ z.object({
|
|
|
1325
1389
|
capabilities: serverCapabilitiesSchema.optional()
|
|
1326
1390
|
}).passthrough();
|
|
1327
1391
|
//#endregion
|
|
1328
|
-
export {
|
|
1392
|
+
export { messageSourceSchema as $, createChatSchema as A, githubCallbackQuerySchema as B, agentTypeSchema as C, updateMemberSchema as Ct, createAdapterConfigSchema as D, wsAuthFrameSchema as Dt, connectTokenExchangeSchema as E, updateTaskStatusSchema as Et, createTaskSchema as F, inboxDeliverFrameSchema as G, githubStartQuerySchema as H, defaultRuntimeConfigPayload as I, isReservedAgentName as J, inboxPollQuerySchema as K, delegateFeishuUserSchema as L, createMemberSchema as M, createOrgFromMeSchema as N, createAdapterMappingSchema as O, createOrganizationSchema as P, loginSchema as Q, dryRunAgentRuntimeConfigSchema as R, agentRuntimeConfigPayloadSchema as S, updateClientCapabilitiesSchema as St, clientRegisterSchema as T, updateSystemConfigSchema as Tt, imageInlineContentSchema as U, githubDevCallbackQuerySchema as V, inboxAckFrameSchema as W, linkTaskChatSchema as X, joinByInvitationSchema as Y, listMeChatsQuerySchema as Z, addParticipantSchema as _, taskListQuerySchema as _t, AGENT_STATUSES as a, safeRedirectPath as at, agentBindRequestSchema as b, updateAgentSchema as bt, DEFAULT_RUNTIME_PROVIDER as c, sendMessageSchema as ct, TASK_CREATOR_TYPES as d, sessionEventMessageSchema as dt, notificationQuerySchema as et, TASK_HEALTH_SIGNALS as f, sessionEventSchema as ft, addMeChatParticipantsSchema as g, switchOrgSchema as gt, WS_AUTH_FRAME_TIMEOUT_MS as h, stripCode as ht, AGENT_SOURCES as i, runtimeStateMessageSchema as it, createMeChatSchema as j, createAgentSchema as k, MENTION_REGEX as l, sendToAgentSchema as lt, TASK_TERMINAL_STATUSES as m, sessionStateMessageSchema as mt, AGENT_NAME_REGEX as n, rebindAgentSchema as nt, AGENT_TYPES as o, scanMentionTokens as ot, TASK_STATUSES as p, sessionReconcileRequestSchema as pt, isRedactedEnvValue as q, AGENT_SELECTOR_HEADER as r, refreshTokenSchema as rt, AGENT_VISIBILITY as s, selfServiceFeishuBotSchema as st, AGENT_BIND_REJECT_REASONS as t, paginationQuerySchema as tt, SYSTEM_CONFIG_DEFAULTS as u, sessionCompletionMessageSchema as ut, adminCreateTaskSchema as v, updateAdapterConfigSchema as vt, clientCapabilitiesSchema as w, updateOrganizationSchema as wt, agentPinnedMessageSchema as x, updateChatSchema as xt, adminUpdateTaskSchema as y, updateAgentRuntimeConfigSchema as yt, extractMentions as z };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
-- Chat-first workspace foundation. See docs/chat-first-workspace-product-design.md
|
|
2
|
+
-- for the contract this migration implements.
|
|
3
|
+
--
|
|
4
|
+
-- Three structural changes + one data backfill:
|
|
5
|
+
-- 1. chats: add last_message_at + last_message_preview projection columns
|
|
6
|
+
-- and (organization_id, last_message_at DESC) index. Powers GET /me/chats
|
|
7
|
+
-- cursor pagination + sort.
|
|
8
|
+
-- 2. chat_participants: add last_read_at + unread_mention_count columns.
|
|
9
|
+
-- The chat-first workspace per-user read cursor and red-dot counter
|
|
10
|
+
-- live with the participation row that owns them; no separate read-state
|
|
11
|
+
-- table.
|
|
12
|
+
-- 3. chat_subscriptions (NEW): non-speaking observers ("watchers"). Stays
|
|
13
|
+
-- strictly disjoint from chat_participants — invariant 1 in the design.
|
|
14
|
+
-- ON DELETE CASCADE so dropping a chat tears down its watchers too.
|
|
15
|
+
-- 4. Backfill (single statement each):
|
|
16
|
+
-- - chats projection from messages, using DISTINCT ON to avoid the
|
|
17
|
+
-- per-row correlated subquery that would lock messages for minutes
|
|
18
|
+
-- on large tables.
|
|
19
|
+
-- - chat_subscriptions for every active manager whose managed non-human
|
|
20
|
+
-- agent already participates in a chat the manager themselves does
|
|
21
|
+
-- not speak in. Exactly the rows recomputeChatWatchers would create
|
|
22
|
+
-- on first run, but in one bulk INSERT.
|
|
23
|
+
--
|
|
24
|
+
-- chat_participants.last_read_at + unread_mention_count default to NULL/0,
|
|
25
|
+
-- which is the desired "treat all existing chats as already read" behavior
|
|
26
|
+
-- on the workspace upgrade.
|
|
27
|
+
|
|
28
|
+
ALTER TABLE "chats"
|
|
29
|
+
ADD COLUMN IF NOT EXISTS "last_message_at" timestamp with time zone,
|
|
30
|
+
ADD COLUMN IF NOT EXISTS "last_message_preview" text;
|
|
31
|
+
|
|
32
|
+
--> statement-breakpoint
|
|
33
|
+
CREATE INDEX IF NOT EXISTS "idx_chats_org_last_message"
|
|
34
|
+
ON "chats" ("organization_id", "last_message_at" DESC);
|
|
35
|
+
|
|
36
|
+
--> statement-breakpoint
|
|
37
|
+
ALTER TABLE "chat_participants"
|
|
38
|
+
ADD COLUMN IF NOT EXISTS "last_read_at" timestamp with time zone,
|
|
39
|
+
ADD COLUMN IF NOT EXISTS "unread_mention_count" integer NOT NULL DEFAULT 0;
|
|
40
|
+
|
|
41
|
+
--> statement-breakpoint
|
|
42
|
+
CREATE TABLE IF NOT EXISTS "chat_subscriptions" (
|
|
43
|
+
"chat_id" text NOT NULL,
|
|
44
|
+
"agent_id" text NOT NULL,
|
|
45
|
+
"kind" text NOT NULL DEFAULT 'watching',
|
|
46
|
+
"last_read_at" timestamp with time zone,
|
|
47
|
+
"unread_mention_count" integer NOT NULL DEFAULT 0,
|
|
48
|
+
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
|
|
49
|
+
CONSTRAINT "chat_subscriptions_chat_id_fkey"
|
|
50
|
+
FOREIGN KEY ("chat_id") REFERENCES "chats"("id") ON DELETE CASCADE,
|
|
51
|
+
-- Intentionally NO ON DELETE clause on the agent FK. `services/agent.ts:
|
|
52
|
+
-- deleteAgent` is soft-only (UPDATE status='deleted', name=NULL — never
|
|
53
|
+
-- DELETE), so the row stays. Adding CASCADE would be dead code today and
|
|
54
|
+
-- silently legitimise a future hard-delete path; default RESTRICT instead
|
|
55
|
+
-- pins the soft-delete convention at the schema layer — any future caller
|
|
56
|
+
-- that tries a hard DELETE FROM agents will be forced to clean up
|
|
57
|
+
-- subscriptions explicitly. (asymmetry with chat_id FK is intentional —
|
|
58
|
+
-- chats can be hard-deleted by admin, agents cannot.)
|
|
59
|
+
CONSTRAINT "chat_subscriptions_agent_id_fkey"
|
|
60
|
+
FOREIGN KEY ("agent_id") REFERENCES "agents"("uuid"),
|
|
61
|
+
CONSTRAINT "chat_subscriptions_pkey"
|
|
62
|
+
PRIMARY KEY ("chat_id", "agent_id")
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
--> statement-breakpoint
|
|
66
|
+
CREATE INDEX IF NOT EXISTS "idx_chat_subscriptions_agent"
|
|
67
|
+
ON "chat_subscriptions" ("agent_id");
|
|
68
|
+
|
|
69
|
+
--> statement-breakpoint
|
|
70
|
+
-- Backfill projection: one INSERT-shaped UPDATE driven by a DISTINCT ON
|
|
71
|
+
-- subquery so messages is touched once. Avoids the correlated subquery
|
|
72
|
+
-- variant that runs two scans per chat row.
|
|
73
|
+
--
|
|
74
|
+
-- Preview must match `chat-projection.ts:applyAfterFanOut`'s live-write
|
|
75
|
+
-- semantics: a clean unquoted string for text messages, NULL for
|
|
76
|
+
-- structured content (file / image / etc.). `content::text` would
|
|
77
|
+
-- serialize JSONB to wire form (with quotes for strings, `{...}` for
|
|
78
|
+
-- objects), producing visible inconsistency between backfilled and
|
|
79
|
+
-- live-written rows. `jsonb_typeof` + `#>> '{}'` extracts the bare
|
|
80
|
+
-- string only when content is a JSON string; otherwise NULL (B3 in
|
|
81
|
+
-- PR review). `trim()` mirrors `outboundContent.trim()` in
|
|
82
|
+
-- `chat-projection.ts:applyAfterFanOut` so leading whitespace doesn't
|
|
83
|
+
-- visually jump on the first live overwrite.
|
|
84
|
+
WITH last_msg AS (
|
|
85
|
+
SELECT DISTINCT ON ("chat_id")
|
|
86
|
+
"chat_id",
|
|
87
|
+
"created_at",
|
|
88
|
+
CASE
|
|
89
|
+
WHEN jsonb_typeof("content") = 'string'
|
|
90
|
+
THEN LEFT(trim("content" #>> '{}'), 200)
|
|
91
|
+
ELSE NULL
|
|
92
|
+
END AS "preview"
|
|
93
|
+
FROM "messages"
|
|
94
|
+
ORDER BY "chat_id", "created_at" DESC
|
|
95
|
+
)
|
|
96
|
+
UPDATE "chats" c
|
|
97
|
+
SET "last_message_at" = lm."created_at",
|
|
98
|
+
"last_message_preview" = lm."preview"
|
|
99
|
+
FROM last_msg lm
|
|
100
|
+
WHERE c."id" = lm."chat_id";
|
|
101
|
+
|
|
102
|
+
--> statement-breakpoint
|
|
103
|
+
-- Watcher backfill: every active member whose managed (non-human) agent
|
|
104
|
+
-- participates in a chat where the member's own human agent is NOT a
|
|
105
|
+
-- speaking participant. Idempotent via ON CONFLICT.
|
|
106
|
+
--
|
|
107
|
+
-- The explicit NULL casts are required: PostgreSQL infers a bare NULL in a
|
|
108
|
+
-- VALUES/SELECT list as `text`, which then fails to coerce to the target
|
|
109
|
+
-- columns (timestamptz / integer respectively).
|
|
110
|
+
INSERT INTO "chat_subscriptions"
|
|
111
|
+
("chat_id", "agent_id", "kind", "last_read_at", "unread_mention_count", "created_at")
|
|
112
|
+
SELECT DISTINCT
|
|
113
|
+
cp."chat_id",
|
|
114
|
+
m."agent_id",
|
|
115
|
+
'watching',
|
|
116
|
+
NULL::timestamp with time zone,
|
|
117
|
+
0,
|
|
118
|
+
now()
|
|
119
|
+
FROM "chat_participants" cp
|
|
120
|
+
JOIN "agents" a ON a."uuid" = cp."agent_id"
|
|
121
|
+
JOIN "members" m ON m."id" = a."manager_id"
|
|
122
|
+
WHERE m."status" = 'active'
|
|
123
|
+
AND a."type" <> 'human'
|
|
124
|
+
AND NOT EXISTS (
|
|
125
|
+
SELECT 1 FROM "chat_participants" cp2
|
|
126
|
+
WHERE cp2."chat_id" = cp."chat_id"
|
|
127
|
+
AND cp2."agent_id" = m."agent_id"
|
|
128
|
+
)
|
|
129
|
+
ON CONFLICT ("chat_id", "agent_id") DO NOTHING;
|
|
@@ -211,6 +211,13 @@
|
|
|
211
211
|
"when": 1777766400000,
|
|
212
212
|
"tag": "0029_direct_agent_only_mention_only",
|
|
213
213
|
"breakpoints": true
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"idx": 30,
|
|
217
|
+
"version": "7",
|
|
218
|
+
"when": 1777852800000,
|
|
219
|
+
"tag": "0030_chat_first_workspace",
|
|
220
|
+
"breakpoints": true
|
|
214
221
|
}
|
|
215
222
|
]
|
|
216
223
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { d as __exportAll } from "./esm-CYu4tXXn.mjs";
|
|
2
|
-
import { r as AGENT_SELECTOR_HEADER } from "./dist-
|
|
2
|
+
import { r as AGENT_SELECTOR_HEADER } from "./dist-BoHl9HwW.mjs";
|
|
3
3
|
//#region src/core/feishu.ts
|
|
4
4
|
var feishu_exports = /* @__PURE__ */ __exportAll({
|
|
5
5
|
bindFeishuBot: () => bindFeishuBot,
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "./observability-DPyf745N-BSc8QNcR.mjs";
|
|
2
|
-
import { A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, H as resolveCliInvocation, I as checkWebSocket, J as isDockerAvailable, K as uninstallClientService, L as printResults, M as checkNodeVersion, N as checkServerConfig, P as checkServerHealth, Q as rotateClientIdWithBackup, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, b as onboardCheck, d as startServer, g as promptAddAgent, h as isInteractive, j as checkDocker, k as checkClientConfig, mt as SdkError, n as deriveHubUrlFromToken, nt as createOwner, pt as FirstTreeHubSDK, q as ensurePostgres, rt as hasUser, st as blank, t as HubUrlDerivationError, ut as status, v as formatCheckReport, x as onboardCreate, z as getClientServiceStatus } from "./saas-connect-
|
|
2
|
+
import { A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, H as resolveCliInvocation, I as checkWebSocket, J as isDockerAvailable, K as uninstallClientService, L as printResults, M as checkNodeVersion, N as checkServerConfig, P as checkServerHealth, Q as rotateClientIdWithBackup, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, b as onboardCheck, d as startServer, g as promptAddAgent, h as isInteractive, j as checkDocker, k as checkClientConfig, mt as SdkError, n as deriveHubUrlFromToken, nt as createOwner, pt as FirstTreeHubSDK, q as ensurePostgres, rt as hasUser, st as blank, t as HubUrlDerivationError, ut as status, v as formatCheckReport, x as onboardCreate, z as getClientServiceStatus } from "./saas-connect-DLSyrQcC.mjs";
|
|
3
3
|
import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import "./dist-
|
|
6
|
-
import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-
|
|
4
|
+
import { a as ensureFreshAdminToken, c as resolveServerUrl, i as ensureFreshAccessToken, n as AuthRefreshRateLimitedError, s as resolveAccessToken, t as AuthRefreshFailedError } from "./bootstrap-DUeYbwm-.mjs";
|
|
5
|
+
import "./dist-BoHl9HwW.mjs";
|
|
6
|
+
import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-Dxk6ArOK.mjs";
|
|
7
7
|
import "./invitation-B1pjAyOz-BaCA9PII.mjs";
|
|
8
|
-
export { AuthRefreshFailedError, 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 };
|
|
8
|
+
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 };
|