@coffer-org/plugin-webchat 7.0.1 → 7.1.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/index.js CHANGED
@@ -5,6 +5,8 @@ export default definePlugin({
5
5
  id: 'webchat',
6
6
  version: '1.0.0',
7
7
  dependsOn: [],
8
+ label: 'webchat.plugin.label',
9
+ description: 'webchat.plugin.description',
8
10
  settings: defineSettings({
9
11
  label: 'webchat.settings.label',
10
12
  fields: {
@@ -1,6 +1,6 @@
1
1
  import { type ActionCaller } from '@coffer-org/server/plugin-hooks';
2
2
  import { type AgentDescriptor, type ConnectorCapabilities } from '@coffer-org/server/orchestrator';
3
- import { type ThreadSelection } from './chain-store.ts';
3
+ import { type Conversation as ThreadSelection } from '@coffer-org/server/conversation-store';
4
4
  export interface StreamCtx {
5
5
  caller: ActionCaller;
6
6
  emit: (event: string, data: unknown) => void;
@@ -1,9 +1,10 @@
1
1
  import { HttpError } from '@coffer-org/server/plugin-hooks';
2
- import { SIDECAR_ROLES } from '@coffer-org/server/thread-store';
2
+ import { SIDECAR_ROLES } from '@coffer-org/server/conversation-store';
3
3
  import { listUsers } from '@coffer-org/server/auth-store';
4
4
  import { abortTurn } from '@coffer-org/server/turn-gate';
5
5
  import { listAgentCatalog, getDefaultAgentId, listRegisteredAgents, getConversationStarters, mayBePrivate, mayRead, } from '@coffer-org/server/orchestrator';
6
- import { openConversation, conversations, history, getSelection, setSelection, } from "./chain-store.js";
6
+ import { getConversation, setConversation, listConversations, readConversation, } from '@coffer-org/server/conversation-store';
7
+ import { mayWrite } from '@coffer-org/server/orchestrator';
7
8
  import { CAPABILITIES } from "./send.js";
8
9
  import { subscribe, evictWhere } from "./channel-registry.js";
9
10
  const TITLE_MAX = 60;
@@ -21,7 +22,7 @@ function title(firstUserText) {
21
22
  return t.length <= TITLE_MAX ? t : `${t.slice(0, TITLE_MAX - 1)}…`;
22
23
  }
23
24
  export async function threadsAction(_body, caller) {
24
- const chats = await conversations(caller.id);
25
+ const chats = await listConversations(caller.id).then((rows) => rows.map((r) => ({ ...r, convId: r.id })));
25
26
  const users = await listUsers();
26
27
  const nameById = new Map(users.map((u) => [String(u.id), u.displayName]));
27
28
  return {
@@ -42,13 +43,21 @@ function requireConvId(body) {
42
43
  throw new HttpError(400, 'missing required field: convId');
43
44
  return convId;
44
45
  }
45
- const EMPTY_SELECTION = { agentId: null, presetId: null, title: null, owner: null, visibility: null };
46
+ const EMPTY_SELECTION = {
47
+ id: '',
48
+ agentId: null,
49
+ presetId: null,
50
+ title: null,
51
+ owner: null,
52
+ visibility: null,
53
+ updatedAt: '',
54
+ };
46
55
  export async function historyAction(body, caller) {
47
56
  const convId = requireConvId(body);
48
- const chatId = await openConversation(convId, caller.id, 'read');
49
- if (chatId === null)
57
+ const conversationId = await openConversation(convId, caller.id, 'read');
58
+ if (conversationId === null)
50
59
  return { convId, messages: [], headMsgId: null };
51
- const rows = await history(chatId);
60
+ const rows = await readConversation(conversationId);
52
61
  const reasoningByBot = new Map(rows.filter((m) => m.role === 'reasoning').map((m) => [m.msgId.slice(0, -2), m.text]));
53
62
  const suggestionsByBot = new Map(rows
54
63
  .filter((m) => m.role === 'suggestions')
@@ -71,18 +80,18 @@ export async function historyAction(body, caller) {
71
80
  }
72
81
  export async function agentsAction(body, caller) {
73
82
  const convId = requireConvId(body);
74
- const chatId = await openConversation(convId, caller.id, 'read');
83
+ const conversationId = await openConversation(convId, caller.id, 'read');
75
84
  return {
76
85
  agents: await listAgentCatalog(),
77
86
  defaultAgentId: getDefaultAgentId() ?? null,
78
- selection: chatId === null ? EMPTY_SELECTION : await getSelection(chatId),
87
+ selection: conversationId === null ? EMPTY_SELECTION : await getConversation(conversationId),
79
88
  starters: await getConversationStarters(),
80
89
  };
81
90
  }
82
91
  export async function selectAgentAction(body, caller) {
83
92
  const convId = requireConvId(body);
84
- const chatId = await openConversation(convId, caller.id, 'write');
85
- if (chatId === null)
93
+ const conversationId = await openConversation(convId, caller.id, 'write');
94
+ if (conversationId === null)
86
95
  return { selection: EMPTY_SELECTION };
87
96
  const patch = {};
88
97
  if ('agentId' in body) {
@@ -93,19 +102,19 @@ export async function selectAgentAction(body, caller) {
93
102
  }
94
103
  if ('presetId' in body)
95
104
  patch.presetId = typeof body['presetId'] === 'string' && body['presetId'] ? body['presetId'] : null;
96
- await setSelection(chatId, patch);
97
- return { selection: await getSelection(chatId) };
105
+ await setConversation(conversationId, patch);
106
+ return { selection: await getConversation(conversationId) };
98
107
  }
99
108
  export async function channelAction(body, ctx) {
100
109
  const convId = requireConvId(body);
101
- const chatId = await openConversation(convId, ctx.caller.id, 'read');
102
- if (chatId === null)
110
+ const conversationId = await openConversation(convId, ctx.caller.id, 'read');
111
+ if (conversationId === null)
103
112
  return;
104
113
  let resolveHeld;
105
114
  const held = new Promise((resolve) => {
106
115
  resolveHeld = resolve;
107
116
  });
108
- const off = subscribe(chatId, {
117
+ const off = subscribe(conversationId, {
109
118
  viewerId: ctx.caller.id,
110
119
  emit: ctx.emit,
111
120
  close: () => resolveHeld(),
@@ -123,23 +132,30 @@ export async function channelAction(body, ctx) {
123
132
  export async function setVisibilityAction(body, caller, deps = {}) {
124
133
  const convId = requireConvId(body);
125
134
  const makePrivate = body['private'] === true;
126
- const chatId = await openConversation(convId, caller.id, 'write');
127
- if (chatId === null)
135
+ const conversationId = await openConversation(convId, caller.id, 'write');
136
+ if (conversationId === null)
128
137
  return { visibility: null };
129
- const selection = await getSelection(chatId);
138
+ const selection = await getConversation(conversationId);
130
139
  if (selection.owner !== caller.id)
131
140
  return { visibility: selection.visibility };
132
141
  if (makePrivate && !mayBePrivate(deps.capabilities ?? CAPABILITIES))
133
142
  return { visibility: selection.visibility };
134
143
  const visibility = makePrivate ? 'private' : null;
135
- await setSelection(chatId, { visibility });
136
- evictWhere(chatId, (viewerId) => !mayRead({ owner: selection.owner, visibility }, viewerId));
144
+ await setConversation(conversationId, { visibility });
145
+ evictWhere(conversationId, (viewerId) => !mayRead({ owner: selection.owner, visibility }, viewerId));
137
146
  return { visibility };
138
147
  }
139
148
  export async function stopAction(body, caller) {
140
149
  const convId = requireConvId(body);
141
- const chatId = await openConversation(convId, caller.id, 'write');
142
- if (chatId === null)
150
+ const conversationId = await openConversation(convId, caller.id, 'write');
151
+ if (conversationId === null)
143
152
  return;
144
- abortTurn('webchat', chatId);
153
+ abortTurn('webchat', conversationId);
154
+ }
155
+ async function openConversation(convId, viewerId, need) {
156
+ if (!convId || convId.includes(':'))
157
+ return null;
158
+ const conv = await getConversation(convId);
159
+ const allowed = need === 'read' ? mayRead(conv, viewerId) : mayWrite(conv, viewerId);
160
+ return allowed ? convId : null;
145
161
  }
@@ -3,7 +3,14 @@ export interface Subscriber {
3
3
  emit(event: string, data: unknown): void;
4
4
  close(): void;
5
5
  }
6
- export declare function subscribe(chatId: string, sub: Subscriber): () => void;
7
- export declare function broadcast(chatId: string, event: string, data: unknown): void;
8
- export declare function evictWhere(chatId: string, drop: (viewerId: string) => boolean): void;
9
- export declare function subscriberCount(chatId: string): number;
6
+ export type ChannelCallback = (frame: {
7
+ event: string;
8
+ data: unknown;
9
+ }) => void;
10
+ export declare function subscribe(conversationId: string, sub: Subscriber | ChannelCallback): () => void;
11
+ export declare function subscribeAs(conversationId: string, viewerId: string, cb: ChannelCallback): Promise<() => void>;
12
+ export declare function broadcast(conversationId: string, event: string, data: unknown): void;
13
+ export declare function evictWhere(conversationId: string, drop: (viewerId: string) => boolean): void;
14
+ export declare function subscriberCount(conversationId: string): number;
15
+ export declare function initFanout(): () => void;
16
+ export declare function teardownFanout(): void;
@@ -1,24 +1,50 @@
1
1
  import { getLogger } from '@coffer-org/sdk/logger';
2
+ import { getConversation, onMessage, conversationEmitter, HIDDEN_ROLES } from '@coffer-org/server/conversation-store';
3
+ import { mayRead } from '@coffer-org/server/orchestrator';
2
4
  const log = getLogger('webchat');
3
5
  const channels = new Map();
4
- export function subscribe(chatId, sub) {
5
- let set = channels.get(chatId);
6
+ export function subscribe(conversationId, sub) {
7
+ if (typeof sub === 'function') {
8
+ return subscribe(conversationId, {
9
+ viewerId: '*',
10
+ emit: (event, data) => sub({ event, data }),
11
+ close: () => { },
12
+ });
13
+ }
14
+ let set = channels.get(conversationId);
6
15
  if (!set) {
7
16
  set = new Set();
8
- channels.set(chatId, set);
17
+ channels.set(conversationId, set);
9
18
  }
10
19
  set.add(sub);
11
20
  return () => {
12
- const current = channels.get(chatId);
21
+ const current = channels.get(conversationId);
13
22
  if (!current)
14
23
  return;
15
24
  current.delete(sub);
16
25
  if (current.size === 0)
17
- channels.delete(chatId);
26
+ channels.delete(conversationId);
18
27
  };
19
28
  }
20
- export function broadcast(chatId, event, data) {
21
- const set = channels.get(chatId);
29
+ export async function subscribeAs(conversationId, viewerId, cb) {
30
+ if (viewerId !== '*') {
31
+ try {
32
+ const conv = await getConversation(conversationId);
33
+ if (conv && !mayRead(conv, viewerId)) {
34
+ return () => { };
35
+ }
36
+ }
37
+ catch {
38
+ }
39
+ }
40
+ return subscribe(conversationId, {
41
+ viewerId,
42
+ emit: (event, data) => cb({ event, data }),
43
+ close: () => { },
44
+ });
45
+ }
46
+ export function broadcast(conversationId, event, data) {
47
+ const set = channels.get(conversationId);
22
48
  if (!set)
23
49
  return;
24
50
  for (const sub of set) {
@@ -26,12 +52,12 @@ export function broadcast(chatId, event, data) {
26
52
  sub.emit(event, data);
27
53
  }
28
54
  catch (err) {
29
- log.warn(`channel emit failed for viewer ${sub.viewerId} on ${chatId}: ${err instanceof Error ? err.message : String(err)}`);
55
+ log.warn(`channel emit failed for viewer ${sub.viewerId} on ${conversationId}: ${err instanceof Error ? err.message : String(err)}`);
30
56
  }
31
57
  }
32
58
  }
33
- export function evictWhere(chatId, drop) {
34
- const set = channels.get(chatId);
59
+ export function evictWhere(conversationId, drop) {
60
+ const set = channels.get(conversationId);
35
61
  if (!set)
36
62
  return;
37
63
  for (const sub of set) {
@@ -41,8 +67,60 @@ export function evictWhere(chatId, drop) {
41
67
  sub.close();
42
68
  }
43
69
  if (set.size === 0)
44
- channels.delete(chatId);
70
+ channels.delete(conversationId);
71
+ }
72
+ export function subscriberCount(conversationId) {
73
+ return channels.get(conversationId)?.size ?? 0;
74
+ }
75
+ let unsubscribeMessage;
76
+ let unsubscribeConversation;
77
+ export function initFanout() {
78
+ if (!unsubscribeMessage) {
79
+ unsubscribeMessage = onMessage((m) => {
80
+ if (HIDDEN_ROLES.includes(m.role))
81
+ return;
82
+ if (m.role === 'assistant') {
83
+ broadcast(m.conversationId, 'message', { msgId: m.msgId, text: m.text });
84
+ }
85
+ else if (m.role === 'user') {
86
+ broadcast(m.conversationId, 'user', {
87
+ msgId: m.msgId,
88
+ text: m.text,
89
+ ts: m.ts,
90
+ ...(m.attachments?.length ? { attachments: m.attachments } : {}),
91
+ });
92
+ }
93
+ });
94
+ }
95
+ if (!unsubscribeConversation) {
96
+ const handler = (e) => {
97
+ if (e instanceof CustomEvent) {
98
+ const { id, patch } = e.detail;
99
+ if (patch && (patch.visibility !== undefined || patch.owner !== undefined)) {
100
+ void (async () => {
101
+ try {
102
+ const conv = await getConversation(id);
103
+ evictWhere(id, (viewerId) => !mayRead(conv, viewerId));
104
+ }
105
+ catch {
106
+ }
107
+ })();
108
+ }
109
+ }
110
+ };
111
+ conversationEmitter.addEventListener('conversation', handler);
112
+ unsubscribeConversation = () => {
113
+ conversationEmitter.removeEventListener('conversation', handler);
114
+ };
115
+ }
116
+ return () => {
117
+ teardownFanout();
118
+ };
45
119
  }
46
- export function subscriberCount(chatId) {
47
- return channels.get(chatId)?.size ?? 0;
120
+ export function teardownFanout() {
121
+ unsubscribeMessage?.();
122
+ unsubscribeMessage = undefined;
123
+ unsubscribeConversation?.();
124
+ unsubscribeConversation = undefined;
48
125
  }
126
+ initFanout();
@@ -1,5 +1,5 @@
1
1
  import type { Connector } from '@coffer-org/server/orchestrator';
2
- import { recordAssistant as storeAssistant, recordReasoning as storeReasoning, recordSuggestions as storeSuggestions, recordContext as storeContext, setSelection as storeSelection } from './chain-store.ts';
2
+ import { setConversation as storeSelection } from '@coffer-org/server/conversation-store';
3
3
  import type { ReasoningDisplay } from './settings.ts';
4
4
  export interface StreamingConnector {
5
5
  connector: Connector;
@@ -7,13 +7,9 @@ export interface StreamingConnector {
7
7
  suggestions(): string[] | null;
8
8
  }
9
9
  export interface StreamingConnectorOpts {
10
- chatId: string;
11
- botMsgId: string;
10
+ conversationId?: string;
11
+ botMsgId?: string;
12
12
  display: ReasoningDisplay;
13
- record?: typeof storeAssistant;
14
- recordReasoning?: typeof storeReasoning;
15
- recordSuggestions?: typeof storeSuggestions;
16
- recordContext?: typeof storeContext;
17
13
  setSelection?: typeof storeSelection;
18
14
  }
19
15
  export declare function makeStreamingConnector(opts: StreamingConnectorOpts): StreamingConnector;
@@ -1,111 +1,70 @@
1
1
  import { makeLiveSink, plainRender } from '@coffer-org/server/orchestrator';
2
- import { recordAssistant as storeAssistant, recordReasoning as storeReasoning, recordSuggestions as storeSuggestions, recordContext as storeContext, setSelection as storeSelection, } from "./chain-store.js";
2
+ import { setConversation as storeSelection } from '@coffer-org/server/conversation-store';
3
3
  import { broadcast } from "./channel-registry.js";
4
4
  import { getLogger } from '@coffer-org/sdk/logger';
5
5
  const log = getLogger('webchat');
6
6
  const WEBCHAT_THROTTLE_MS = 250;
7
7
  const WEBCHAT_MAX = 100_000;
8
8
  export function makeStreamingConnector(opts) {
9
- const record = opts.record ?? storeAssistant;
10
- const recordReasoningFn = opts.recordReasoning ?? storeReasoning;
11
- const recordSuggestionsFn = opts.recordSuggestions ?? storeSuggestions;
12
- const recordContextFn = opts.recordContext ?? storeContext;
13
9
  const setSelectionFn = opts.setSelection ?? storeSelection;
14
10
  let recordedId = null;
15
11
  let lastSuggestions = null;
12
+ let hasAnswer = false;
16
13
  const connector = {
17
14
  id: 'webchat',
18
- enrolmentNotice: 'unreachable: a web-chat turn is always opened by an already-authenticated session, so ' +
19
- 'the identity gate never refuses one this connector has no unidentified sender to say it to.',
20
- open(envelope) {
15
+ open(envelope, msgId) {
16
+ const activeBotMsgId = msgId ?? opts.botMsgId ?? '';
17
+ const conversationId = envelope.conversationId ?? opts.conversationId ?? '';
21
18
  const inner = makeLiveSink({
22
19
  ops: {
23
20
  send: async (text) => {
24
- broadcast(opts.chatId, 'message', { msgId: opts.botMsgId, text });
25
- return opts.botMsgId;
21
+ broadcast(conversationId, 'delta', { msgId: activeBotMsgId, text });
22
+ return activeBotMsgId;
26
23
  },
27
- edit: async (msgId, text) => {
28
- broadcast(opts.chatId, 'delta', { msgId, text });
24
+ edit: async (id, text) => {
25
+ broadcast(conversationId, 'delta', { msgId: id, text });
29
26
  },
30
27
  },
31
28
  throttleMs: WEBCHAT_THROTTLE_MS,
32
29
  maxLength: WEBCHAT_MAX,
33
30
  render: plainRender(WEBCHAT_MAX),
34
31
  });
35
- let answerText = null;
36
- let answerReasoning = null;
37
- let hasAnswer = false;
38
- let persisted = false;
39
32
  let afterwordWork = Promise.resolve();
40
33
  return {
41
34
  emit(e) {
42
35
  if (e.kind === 'reasoning') {
43
36
  if (opts.display !== 'off')
44
- broadcast(opts.chatId, 'reasoning', { msgId: opts.botMsgId, text: e.text });
37
+ broadcast(conversationId, 'reasoning', { msgId: activeBotMsgId, text: e.text });
45
38
  return;
46
39
  }
47
40
  if (e.kind === 'suggestions') {
48
41
  lastSuggestions = e.items;
49
- if (recordedId && e.items.length) {
50
- void recordSuggestionsFn({
51
- chatId: opts.chatId,
52
- botMsgId: opts.botMsgId,
53
- parentMsgId: envelope.turnId,
54
- suggestions: e.items,
55
- now: Math.floor(Date.now() / 1000),
56
- }).catch((err) => {
57
- log.warn(`recordSuggestions failed: ${err instanceof Error ? err.message : String(err)}`);
58
- });
59
- }
60
42
  return;
61
43
  }
62
44
  if (e.kind === 'title') {
63
- afterwordWork = afterwordWork.then(() => setSelectionFn(opts.chatId, { title: e.text }).catch((err) => {
45
+ afterwordWork = afterwordWork.then(() => setSelectionFn(conversationId, { title: e.text }).catch((err) => {
64
46
  log.warn(`setSelection(title) failed: ${err instanceof Error ? err.message : String(err)}`);
65
47
  }));
66
48
  return;
67
49
  }
68
50
  if (e.kind === 'answer') {
69
- answerText = e.text;
70
- answerReasoning = e.reasoning;
71
51
  hasAnswer = true;
52
+ recordedId = activeBotMsgId;
72
53
  }
73
54
  inner.emit(e);
74
55
  },
75
56
  async done() {
76
57
  await inner.done();
77
- if (hasAnswer && !persisted) {
78
- persisted = true;
79
- const now = Math.floor(Date.now() / 1000);
80
- const wrote = await record({
81
- chatId: opts.chatId,
82
- parentMsgId: envelope.turnId,
83
- botMsgId: opts.botMsgId,
84
- text: answerText ?? '',
85
- now,
86
- });
87
- recordedId = wrote ? opts.botMsgId : null;
88
- if (wrote && opts.display !== 'off' && answerReasoning) {
89
- await recordReasoningFn({
90
- chatId: opts.chatId,
91
- botMsgId: opts.botMsgId,
92
- parentMsgId: envelope.turnId,
93
- text: answerReasoning,
94
- now,
95
- });
96
- }
97
- }
98
58
  await afterwordWork;
99
59
  },
100
60
  };
101
61
  },
102
- async recordContext(m) {
103
- await recordContextFn({ ...m, chatId: opts.chatId });
62
+ async bindMessage(_envelope, _msgId, _externalId) {
104
63
  },
105
64
  };
106
65
  return {
107
66
  connector,
108
- recorded: () => recordedId,
67
+ recorded: () => (hasAnswer ? recordedId : null),
109
68
  suggestions: () => lastSuggestions,
110
69
  };
111
70
  }
@@ -1,2 +1 @@
1
- export declare const WEB_FORMAT: string;
2
1
  export declare function webChannelSystem(): Promise<string>;
@@ -1,15 +1,6 @@
1
1
  import { frontendInstructions } from '@coffer-org/server/frontend-agent';
2
- export const WEB_FORMAT = [
3
- 'CRITICAL OUTPUT FORMAT — MARKDOWN ONLY. Format the reply strictly as Markdown (GFM). NEVER emit raw HTML tags — they are stripped before rendering and their content may be lost.',
4
- 'Inline: **bold**, *italic*, `inline code`, ~~strikethrough~~. Links: [text](https://…).',
5
- 'Blocks: blank-line-separated paragraphs, `#`–`###` headings, `>` blockquotes, `---` separators.',
6
- 'Lists: `- item` for bullets, `1. item` for numbered steps. Nest with two spaces.',
7
- 'Tables render as real tables — use GFM pipe syntax with a header separator row.',
8
- 'Code blocks: triple backticks with a language tag (```python) — highlighted on render.',
9
- 'The reply is read in a narrow chat panel (~360px) — keep it short and skimmable: short paragraphs, lists over prose, narrow tables.',
10
- 'Use Unicode symbols and emoji to structure replies: arrows (→), status marks (✅ ⚠️ ❌ ⏳), topic emoji (📅 💰 🏥 🛒 🌱 …).',
11
- ].join('\n');
2
+ import { NEUTRAL_FORMAT } from '@coffer-org/markdown';
12
3
  export async function webChannelSystem() {
13
4
  const site = await frontendInstructions('');
14
- return [WEB_FORMAT, site].filter(Boolean).join('\n\n');
5
+ return [NEUTRAL_FORMAT, site].filter(Boolean).join('\n\n');
15
6
  }
@@ -1,7 +1,6 @@
1
1
  import type { PluginHooks } from '@coffer-org/server/plugin-hooks';
2
2
  export type { ThreadSummary, HistoryMsg } from './actions.ts';
3
- export { WEB_FORMAT } from './format.ts';
4
3
  export { makeStreamingConnector } from './connector.ts';
5
- export { chatIdFor, recordUser, recordAssistant, buildChain, getSelection, setSelection } from './chain-store.ts';
4
+ export { getConversation as getSelection, setConversation as setSelection, } from '@coffer-org/server/conversation-store';
6
5
  export { sendAction, pageContext } from './send.ts';
7
6
  export declare const serverHooks: PluginHooks;
@@ -1,22 +1,22 @@
1
- import { pruneThreadMessages } from '@coffer-org/server/thread-store';
2
- import { pruneThreadState } from '@coffer-org/server/thread-state';
1
+ import { pruneConversations } from '@coffer-org/server/conversation-store';
3
2
  import { threadsAction, historyAction, agentsAction, selectAgentAction, setVisibilityAction, channelAction, stopAction, } from "./actions.js";
4
3
  import { sendAction } from "./send.js";
5
- import { CONNECTOR } from "./chain-store.js";
6
4
  import { registerConnector } from '@coffer-org/server/orchestrator';
5
+ import { initFanout, teardownFanout } from "./channel-registry.js";
7
6
  const THREAD_TTL_MS = Number(process.env['WEBCHAT_THREAD_TTL_MS'] ?? 30 * 86_400_000);
8
7
  let unregisterConnector;
9
- export { WEB_FORMAT } from "./format.js";
10
8
  export { makeStreamingConnector } from "./connector.js";
11
- export { chatIdFor, recordUser, recordAssistant, buildChain, getSelection, setSelection } from "./chain-store.js";
9
+ export { getConversation as getSelection, setConversation as setSelection, } from '@coffer-org/server/conversation-store';
12
10
  export { sendAction, pageContext } from "./send.js";
13
11
  export const serverHooks = {
14
12
  init: () => {
15
13
  unregisterConnector = registerConnector({ id: 'webchat' });
14
+ initFanout();
16
15
  },
17
16
  teardown: () => {
18
17
  unregisterConnector?.();
19
18
  unregisterConnector = undefined;
19
+ teardownFanout();
20
20
  },
21
21
  userActions: {
22
22
  threads: threadsAction,
@@ -36,8 +36,7 @@ export const serverHooks = {
36
36
  intervalMs: 86_400_000,
37
37
  run: async () => {
38
38
  const cutoff = new Date(Date.now() - THREAD_TTL_MS);
39
- await pruneThreadMessages(CONNECTOR, Math.floor(cutoff.getTime() / 1000));
40
- await pruneThreadState(CONNECTOR, cutoff.toISOString());
39
+ await pruneConversations(Math.floor(cutoff.getTime() / 1000), cutoff.toISOString());
41
40
  },
42
41
  },
43
42
  ],
@@ -1,6 +1,8 @@
1
1
  import { handleIncoming } from '@coffer-org/server/orchestrator';
2
2
  import { type ActionCaller } from '@coffer-org/server/plugin-hooks';
3
+ import { conversationEmitter } from '@coffer-org/server/conversation-store';
3
4
  import type { ConnectorCapabilities, ContextFact } from '@coffer-org/server/orchestrator';
5
+ export { conversationEmitter };
4
6
  export declare const CAPABILITIES: ConnectorCapabilities;
5
7
  export interface SendDeps {
6
8
  handleIncoming?: typeof handleIncoming;