@love-moon/app-sdk 0.3.2 → 0.4.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.
- package/CHANGELOG.md +19 -3
- package/dist/react/index.d.ts +49 -6
- package/dist/react/index.js +75 -5
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.js +175 -14
- package/package.json +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @love-moon/app-sdk
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
## 0.4.0
|
|
6
|
+
|
|
7
|
+
### Patch Changes
|
|
8
|
+
|
|
9
|
+
- 4ecc359: Publish the chat-web browser runtime and wire it into the CLI and AI SDK for
|
|
10
|
+
ChatGPT and Gemini web sessions, including provider error handling and local
|
|
11
|
+
development installation support.
|
|
12
|
+
|
|
13
|
+
Ship app SDK realtime history catch-up and the CLI/AI SDK goal-mode and custom
|
|
14
|
+
command runtime updates included in this release.
|
|
15
|
+
|
|
3
16
|
## 0.3.2
|
|
4
17
|
|
|
5
18
|
### Changed
|
|
@@ -26,10 +39,13 @@
|
|
|
26
39
|
silent for minutes) can opt out by passing `idleTimeoutMs: 0`:
|
|
27
40
|
|
|
28
41
|
```ts
|
|
29
|
-
for await (const delta of client.tasks.streamReply(taskId, {
|
|
42
|
+
for await (const delta of client.tasks.streamReply(taskId, {
|
|
43
|
+
idleTimeoutMs: 0,
|
|
44
|
+
})) {
|
|
30
45
|
// never time out — caller is responsible for cancelling via signal
|
|
31
46
|
}
|
|
32
47
|
```
|
|
48
|
+
|
|
33
49
|
- **Locked down the public `.d.ts` surface.** Internal transport types —
|
|
34
50
|
`Fetcher`, `FetcherOptions`, `RequestOptions`, `AppWebSocket`,
|
|
35
51
|
`AppWebSocketOptions`, `TasksRestApi` — are now tagged
|
|
@@ -89,7 +105,7 @@ Initial implementation, RFC 0027 milestones M0–M3.
|
|
|
89
105
|
subscribe; yields `text` deltas from `reply_preview` plus a terminal `done`
|
|
90
106
|
on the assistant message (or `error` on `task_status_update=failed`).
|
|
91
107
|
- Unified `ConductorAppError` with named error codes mapped from HTTP status
|
|
92
|
-
|
|
108
|
+
- backend error strings.
|
|
93
109
|
- Custom fetch / WebSocket / bearerToken providers for SSR + test injection.
|
|
94
110
|
|
|
95
111
|
### Added — `/react` (M2)
|
|
@@ -106,7 +122,7 @@ Initial implementation, RFC 0027 milestones M0–M3.
|
|
|
106
122
|
- Optimistic send → server confirm flow with pending-message replacement.
|
|
107
123
|
- Pre-compiled CSS at `@love-moon/app-sdk/react/styles.css`; CSS-variable
|
|
108
124
|
theming via the `theme` prop; mobile/desktop layout via responsive CSS
|
|
109
|
-
|
|
125
|
+
- an explicit `layout` prop override.
|
|
110
126
|
- jsdom integration tests covering hydration, live events, runtime status,
|
|
111
127
|
interrupt button visibility, and optimistic send.
|
|
112
128
|
|
package/dist/react/index.d.ts
CHANGED
|
@@ -43,6 +43,35 @@ interface SendMessageInput {
|
|
|
43
43
|
role?: MessageRole;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Custom content renderer. Receives the full `Message` so the renderer can
|
|
48
|
+
* branch on `role` / `metadata` / `attachments` if it wants to. Return any
|
|
49
|
+
* ReactNode; the SDK still wraps it in `<div class="conductor-bubble">` and
|
|
50
|
+
* applies role-based alignment / pending styling.
|
|
51
|
+
*
|
|
52
|
+
* Returning `null` (or `undefined`) renders an empty bubble — equivalent to
|
|
53
|
+
* a message with empty content. If you want to suppress the bubble entirely,
|
|
54
|
+
* filter the message upstream (e.g. via your own MessageList).
|
|
55
|
+
*/
|
|
56
|
+
type RenderMessageContent = (message: Message) => ReactNode;
|
|
57
|
+
interface MessageListProps {
|
|
58
|
+
labels: ChatViewLabels;
|
|
59
|
+
/**
|
|
60
|
+
* Optional override for how each message's content is rendered inside its
|
|
61
|
+
* bubble. Defaults to plain text (`message.content`). Pass e.g. a
|
|
62
|
+
* `react-markdown` renderer here to enable markdown formatting:
|
|
63
|
+
*
|
|
64
|
+
* <MessageList
|
|
65
|
+
* labels={...}
|
|
66
|
+
* renderMessageContent={(m) => (
|
|
67
|
+
* <ReactMarkdown remarkPlugins={[remarkGfm]}>{m.content}</ReactMarkdown>
|
|
68
|
+
* )}
|
|
69
|
+
* />
|
|
70
|
+
*/
|
|
71
|
+
renderMessageContent?: RenderMessageContent;
|
|
72
|
+
}
|
|
73
|
+
declare function MessageList({ labels, renderMessageContent }: MessageListProps): react_jsx_runtime.JSX.Element;
|
|
74
|
+
|
|
46
75
|
/**
|
|
47
76
|
* Runtime status pushed by the daemon while a task is in progress.
|
|
48
77
|
* Mirrors `task_runtime_status` envelope on /ws/app.
|
|
@@ -169,6 +198,25 @@ interface ChatViewProps {
|
|
|
169
198
|
* "via <self>" self-reference loop.
|
|
170
199
|
*/
|
|
171
200
|
showAppOriginChip?: boolean;
|
|
201
|
+
/**
|
|
202
|
+
* Override how each message's content is rendered inside its bubble.
|
|
203
|
+
* Defaults to plain text. Common use: wire in a markdown renderer.
|
|
204
|
+
*
|
|
205
|
+
* import ReactMarkdown from 'react-markdown';
|
|
206
|
+
* import remarkGfm from 'remark-gfm';
|
|
207
|
+
*
|
|
208
|
+
* <ChatView
|
|
209
|
+
* taskId={taskId}
|
|
210
|
+
* adapter={adapter}
|
|
211
|
+
* renderMessageContent={(m) => (
|
|
212
|
+
* <ReactMarkdown remarkPlugins={[remarkGfm]}>{m.content}</ReactMarkdown>
|
|
213
|
+
* )}
|
|
214
|
+
* />
|
|
215
|
+
*
|
|
216
|
+
* The SDK still owns the bubble container (role-based alignment, pending
|
|
217
|
+
* state, data attributes). Only the content inside is replaced.
|
|
218
|
+
*/
|
|
219
|
+
renderMessageContent?: RenderMessageContent;
|
|
172
220
|
onError?: (error: unknown) => void;
|
|
173
221
|
/** Extra className applied to the root container. */
|
|
174
222
|
className?: string;
|
|
@@ -196,11 +244,6 @@ interface ChatViewTheme {
|
|
|
196
244
|
}
|
|
197
245
|
declare function ChatView(props: ChatViewProps): react_jsx_runtime.JSX.Element;
|
|
198
246
|
|
|
199
|
-
interface MessageListProps {
|
|
200
|
-
labels: ChatViewLabels;
|
|
201
|
-
}
|
|
202
|
-
declare function MessageList({ labels }: MessageListProps): react_jsx_runtime.JSX.Element;
|
|
203
|
-
|
|
204
247
|
interface MessageInputProps {
|
|
205
248
|
labels: ChatViewLabels;
|
|
206
249
|
/** Disable the send button (useful while a turn is in progress). */
|
|
@@ -361,4 +404,4 @@ type ConductorErrorCode = 'unauthorized' | 'forbidden' | 'token_revoked' | 'inva
|
|
|
361
404
|
/** Helper: type-guard for callers that don't want `instanceof`. */
|
|
362
405
|
declare function isConductorAppError(error: unknown): error is ConductorAppError;
|
|
363
406
|
|
|
364
|
-
export { type Attachment, type ChatAdapter, type ChatEvent, ChatProvider, type ChatState, ChatView, type ChatViewLabels, type ChatViewProps, type ChatViewTheme, ConductorAppError, type Message, MessageInput, MessageList, type MessageRole, type RestAdapterOptions, type RuntimeState, type RuntimeStatus, RuntimeStatusBar, type SendMessageInput, type Task, type TaskStatus, createRestAdapter, isConductorAppError, useChat };
|
|
407
|
+
export { type Attachment, type ChatAdapter, type ChatEvent, ChatProvider, type ChatState, ChatView, type ChatViewLabels, type ChatViewProps, type ChatViewTheme, ConductorAppError, type Message, MessageInput, MessageList, type MessageListProps, type MessageRole, type RenderMessageContent, type RestAdapterOptions, type RuntimeState, type RuntimeStatus, RuntimeStatusBar, type SendMessageInput, type Task, type TaskStatus, createRestAdapter, isConductorAppError, useChat };
|
package/dist/react/index.js
CHANGED
|
@@ -152,6 +152,51 @@ function ChatProvider(props) {
|
|
|
152
152
|
const activeAdapter = adapterRef.current;
|
|
153
153
|
let cancelled = false;
|
|
154
154
|
const abort = new AbortController();
|
|
155
|
+
let catchUpTimer = null;
|
|
156
|
+
let catchUpInFlight = false;
|
|
157
|
+
let needAnotherCatchUp = false;
|
|
158
|
+
let catchUpAbort = null;
|
|
159
|
+
let prevReplyInProgress;
|
|
160
|
+
let prevConnectionState;
|
|
161
|
+
const runCatchUp = async () => {
|
|
162
|
+
if (cancelled) return;
|
|
163
|
+
catchUpInFlight = true;
|
|
164
|
+
catchUpAbort = new AbortController();
|
|
165
|
+
try {
|
|
166
|
+
const page = await activeAdapter.fetchHistory(taskId, {
|
|
167
|
+
limit: 20,
|
|
168
|
+
signal: catchUpAbort.signal
|
|
169
|
+
});
|
|
170
|
+
if (cancelled) return;
|
|
171
|
+
for (const m of page.messages) {
|
|
172
|
+
if (!m.id) continue;
|
|
173
|
+
dispatch({ type: "APPEND_MESSAGE", message: m });
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const name = err?.name;
|
|
177
|
+
const code = err?.code;
|
|
178
|
+
if (name === "AbortError" || code === "stream_aborted") return;
|
|
179
|
+
console.warn("[app-sdk] history catch-up failed", err);
|
|
180
|
+
} finally {
|
|
181
|
+
catchUpInFlight = false;
|
|
182
|
+
if (needAnotherCatchUp && !cancelled) {
|
|
183
|
+
needAnotherCatchUp = false;
|
|
184
|
+
void runCatchUp();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const scheduleCatchUp = (delayMs) => {
|
|
189
|
+
if (cancelled) return;
|
|
190
|
+
if (catchUpInFlight) {
|
|
191
|
+
needAnotherCatchUp = true;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (catchUpTimer) clearTimeout(catchUpTimer);
|
|
195
|
+
catchUpTimer = setTimeout(() => {
|
|
196
|
+
catchUpTimer = null;
|
|
197
|
+
void runCatchUp();
|
|
198
|
+
}, delayMs);
|
|
199
|
+
};
|
|
155
200
|
dispatch({ type: "LOADING_HISTORY", loading: true });
|
|
156
201
|
activeAdapter.fetchHistory(taskId, { signal: abort.signal }).then((page) => {
|
|
157
202
|
if (cancelled) return;
|
|
@@ -180,11 +225,18 @@ function ChatProvider(props) {
|
|
|
180
225
|
case "message_updated":
|
|
181
226
|
dispatch({ type: "UPDATE_MESSAGE", message: event.message });
|
|
182
227
|
break;
|
|
183
|
-
case "runtime_status":
|
|
228
|
+
case "runtime_status": {
|
|
184
229
|
dispatch({ type: "SET_RUNTIME", status: event.status });
|
|
230
|
+
const now = event.status.replyInProgress;
|
|
231
|
+
if (prevReplyInProgress === true && now === false) {
|
|
232
|
+
scheduleCatchUp(500);
|
|
233
|
+
}
|
|
234
|
+
prevReplyInProgress = now;
|
|
185
235
|
break;
|
|
236
|
+
}
|
|
186
237
|
case "task_finished":
|
|
187
238
|
dispatch({ type: "TASK_FINISHED" });
|
|
239
|
+
scheduleCatchUp(500);
|
|
188
240
|
break;
|
|
189
241
|
case "task_failed": {
|
|
190
242
|
const taskErr = {
|
|
@@ -194,17 +246,28 @@ function ChatProvider(props) {
|
|
|
194
246
|
if (event.error.details !== void 0) taskErr.details = event.error.details;
|
|
195
247
|
if (event.error.cause !== void 0) taskErr.cause = event.error.cause;
|
|
196
248
|
dispatch({ type: "TASK_FAILED", error: taskErr });
|
|
249
|
+
scheduleCatchUp(500);
|
|
197
250
|
break;
|
|
198
251
|
}
|
|
199
|
-
case "connection_state":
|
|
252
|
+
case "connection_state": {
|
|
200
253
|
dispatch({ type: "SET_CONNECTION", state: event.state });
|
|
254
|
+
if (event.state === "connected" && prevConnectionState === "reconnecting") {
|
|
255
|
+
scheduleCatchUp(0);
|
|
256
|
+
}
|
|
257
|
+
prevConnectionState = event.state;
|
|
201
258
|
break;
|
|
259
|
+
}
|
|
202
260
|
}
|
|
203
261
|
});
|
|
204
262
|
return () => {
|
|
205
263
|
cancelled = true;
|
|
206
264
|
abort.abort();
|
|
207
265
|
sub.unsubscribe();
|
|
266
|
+
if (catchUpTimer) {
|
|
267
|
+
clearTimeout(catchUpTimer);
|
|
268
|
+
catchUpTimer = null;
|
|
269
|
+
}
|
|
270
|
+
catchUpAbort?.abort();
|
|
208
271
|
};
|
|
209
272
|
}, [taskId]);
|
|
210
273
|
const value = useMemo(() => {
|
|
@@ -299,7 +362,7 @@ function extractError(err) {
|
|
|
299
362
|
// src/react/components/MessageList.tsx
|
|
300
363
|
import { useEffect as useEffect2, useLayoutEffect, useRef as useRef2 } from "react";
|
|
301
364
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
302
|
-
function MessageList({ labels }) {
|
|
365
|
+
function MessageList({ labels, renderMessageContent }) {
|
|
303
366
|
const { state, loadEarlier } = useChat();
|
|
304
367
|
const containerRef = useRef2(null);
|
|
305
368
|
const lastMessageCountRef = useRef2(state.messages.length);
|
|
@@ -335,13 +398,14 @@ function MessageList({ labels }) {
|
|
|
335
398
|
state.messages.map((m) => {
|
|
336
399
|
const isUser = m.role === "user" || m.role === "sdk";
|
|
337
400
|
const isPending = m.id.startsWith("pending:");
|
|
401
|
+
const content = renderMessageContent ? renderMessageContent(m) : m.content;
|
|
338
402
|
return /* @__PURE__ */ jsx2(
|
|
339
403
|
"div",
|
|
340
404
|
{
|
|
341
405
|
className: "conductor-message " + (isUser ? "conductor-message--user" : "conductor-message--assistant") + (isPending ? " conductor-message--pending" : ""),
|
|
342
406
|
"data-role": m.role,
|
|
343
407
|
"data-message-id": m.id,
|
|
344
|
-
children: /* @__PURE__ */ jsx2("div", { className: "conductor-bubble", children:
|
|
408
|
+
children: /* @__PURE__ */ jsx2("div", { className: "conductor-bubble", children: content })
|
|
345
409
|
},
|
|
346
410
|
m.id
|
|
347
411
|
);
|
|
@@ -487,7 +551,13 @@ function ChatView(props) {
|
|
|
487
551
|
style: themeStyle,
|
|
488
552
|
children: /* @__PURE__ */ jsxs4(ChatProvider, { taskId: props.taskId, adapter: props.adapter, onError: props.onError, children: [
|
|
489
553
|
/* @__PURE__ */ jsx5(RuntimeStatusBar, { labels }),
|
|
490
|
-
/* @__PURE__ */ jsx5(
|
|
554
|
+
/* @__PURE__ */ jsx5(
|
|
555
|
+
MessageList,
|
|
556
|
+
{
|
|
557
|
+
labels,
|
|
558
|
+
renderMessageContent: props.renderMessageContent
|
|
559
|
+
}
|
|
560
|
+
),
|
|
491
561
|
/* @__PURE__ */ jsx5(MessageInput, { labels, disabled: props.readOnly })
|
|
492
562
|
] })
|
|
493
563
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -329,10 +329,28 @@ declare class TasksApi {
|
|
|
329
329
|
*
|
|
330
330
|
* The first call lazily opens a /ws/app connection; subsequent calls share
|
|
331
331
|
* the same connection.
|
|
332
|
+
*
|
|
333
|
+
* History catch-up (enabled by default): when a terminal event arrives
|
|
334
|
+
* (`task_finished`, `task_failed`, `runtime_status` flipping
|
|
335
|
+
* `replyInProgress` true→false, or `connection_state` recovering from
|
|
336
|
+
* `reconnecting → connected`), the wrapper pulls a window of recent
|
|
337
|
+
* history via REST and injects any missing entries as synthetic
|
|
338
|
+
* `message_appended` events. This compensates for Conductor deployments
|
|
339
|
+
* where the realtime broadcast doesn't always reach the WS connection
|
|
340
|
+
* (multi-instance fan-out without a backplane, idempotent commit retries
|
|
341
|
+
* that bypass projection, momentary WS drops). Disable with
|
|
342
|
+
* `disableHistoryCatchUp: true` for callers that prefer the raw stream.
|
|
332
343
|
*/
|
|
333
344
|
subscribe(taskId: string, opts?: {
|
|
334
345
|
signal?: AbortSignal;
|
|
335
346
|
bufferCap?: number;
|
|
347
|
+
/**
|
|
348
|
+
* Disable the post-terminal history catch-up. Default: catch-up
|
|
349
|
+
* enabled. Set to `true` to receive only events that traveled the
|
|
350
|
+
* realtime path (no synthetic `message_appended` injected from
|
|
351
|
+
* REST).
|
|
352
|
+
*/
|
|
353
|
+
disableHistoryCatchUp?: boolean;
|
|
336
354
|
}): AsyncIterable<ChatEvent>;
|
|
337
355
|
/**
|
|
338
356
|
* Higher-level convenience: yield only AI reply deltas. Internally consumes
|
package/dist/server/index.js
CHANGED
|
@@ -1151,6 +1151,8 @@ function streamReplyForTask(socket, taskId, opts) {
|
|
|
1151
1151
|
}
|
|
1152
1152
|
|
|
1153
1153
|
// src/server/client.ts
|
|
1154
|
+
var CATCH_UP_DELAY_MS = 500;
|
|
1155
|
+
var CATCH_UP_LIMIT = 20;
|
|
1154
1156
|
var AppClient = class {
|
|
1155
1157
|
projects;
|
|
1156
1158
|
tasks;
|
|
@@ -1252,6 +1254,17 @@ var TasksApi = class {
|
|
|
1252
1254
|
*
|
|
1253
1255
|
* The first call lazily opens a /ws/app connection; subsequent calls share
|
|
1254
1256
|
* the same connection.
|
|
1257
|
+
*
|
|
1258
|
+
* History catch-up (enabled by default): when a terminal event arrives
|
|
1259
|
+
* (`task_finished`, `task_failed`, `runtime_status` flipping
|
|
1260
|
+
* `replyInProgress` true→false, or `connection_state` recovering from
|
|
1261
|
+
* `reconnecting → connected`), the wrapper pulls a window of recent
|
|
1262
|
+
* history via REST and injects any missing entries as synthetic
|
|
1263
|
+
* `message_appended` events. This compensates for Conductor deployments
|
|
1264
|
+
* where the realtime broadcast doesn't always reach the WS connection
|
|
1265
|
+
* (multi-instance fan-out without a backplane, idempotent commit retries
|
|
1266
|
+
* that bypass projection, momentary WS drops). Disable with
|
|
1267
|
+
* `disableHistoryCatchUp: true` for callers that prefer the raw stream.
|
|
1255
1268
|
*/
|
|
1256
1269
|
subscribe(taskId, opts) {
|
|
1257
1270
|
if (!taskId) {
|
|
@@ -1262,37 +1275,185 @@ var TasksApi = class {
|
|
|
1262
1275
|
}
|
|
1263
1276
|
this.assertOpen();
|
|
1264
1277
|
const socket = this.getSocket();
|
|
1278
|
+
const rest = this.rest;
|
|
1265
1279
|
const inner = subscribeToTask(socket, taskId, opts);
|
|
1280
|
+
const catchUpEnabled = opts?.disableHistoryCatchUp !== true;
|
|
1266
1281
|
return {
|
|
1282
|
+
// Each `for await` consumer gets its own iterator with its own queue,
|
|
1283
|
+
// pump task, knownIds set, and catch-up state. Multiple concurrent
|
|
1284
|
+
// iterations over the same returned AsyncIterable do not share state
|
|
1285
|
+
// beyond the underlying socket (which is shared by design).
|
|
1267
1286
|
[Symbol.asyncIterator]() {
|
|
1268
1287
|
const innerIter = inner[Symbol.asyncIterator]();
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1288
|
+
const queue = [];
|
|
1289
|
+
let pendingResolve = null;
|
|
1290
|
+
let upstreamDone = false;
|
|
1291
|
+
let consumerReturned = false;
|
|
1292
|
+
const knownIds = /* @__PURE__ */ new Set();
|
|
1293
|
+
let prevReplyInProgress;
|
|
1294
|
+
let prevConnectionState;
|
|
1295
|
+
let catchUpInFlight = 0;
|
|
1296
|
+
let needAnotherCatchUp = false;
|
|
1297
|
+
let activeCatchUpAbort = null;
|
|
1298
|
+
const wakePending = (r) => {
|
|
1299
|
+
const resolver = pendingResolve;
|
|
1300
|
+
pendingResolve = null;
|
|
1301
|
+
resolver?.(r);
|
|
1302
|
+
};
|
|
1303
|
+
const drain = () => {
|
|
1304
|
+
if (!pendingResolve) return;
|
|
1305
|
+
if (queue.length > 0) {
|
|
1306
|
+
wakePending({ value: queue.shift(), done: false });
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (upstreamDone && catchUpInFlight === 0) {
|
|
1310
|
+
wakePending({ value: void 0, done: true });
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
const enqueue = (ev) => {
|
|
1314
|
+
if (consumerReturned) return;
|
|
1315
|
+
queue.push(ev);
|
|
1316
|
+
drain();
|
|
1317
|
+
};
|
|
1318
|
+
const triggerCatchUp = (delayMs) => {
|
|
1319
|
+
if (consumerReturned || !catchUpEnabled) return;
|
|
1320
|
+
if (catchUpInFlight > 0) {
|
|
1321
|
+
needAnotherCatchUp = true;
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
catchUpInFlight += 1;
|
|
1325
|
+
const abort = new AbortController();
|
|
1326
|
+
activeCatchUpAbort = abort;
|
|
1327
|
+
void (async () => {
|
|
1328
|
+
try {
|
|
1329
|
+
if (delayMs > 0) {
|
|
1330
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
1331
|
+
}
|
|
1332
|
+
if (consumerReturned || abort.signal.aborted) return;
|
|
1333
|
+
const page = await rest.history(
|
|
1334
|
+
taskId,
|
|
1335
|
+
{ limit: CATCH_UP_LIMIT },
|
|
1336
|
+
{ signal: abort.signal }
|
|
1337
|
+
);
|
|
1338
|
+
if (consumerReturned || abort.signal.aborted) return;
|
|
1339
|
+
for (const m of page.messages) {
|
|
1340
|
+
if (!m.id) continue;
|
|
1341
|
+
if (knownIds.has(m.id)) continue;
|
|
1342
|
+
knownIds.add(m.id);
|
|
1343
|
+
enqueue({ type: "message_appended", message: m });
|
|
1344
|
+
}
|
|
1345
|
+
} catch (err) {
|
|
1346
|
+
const name = err?.name;
|
|
1347
|
+
const code = err?.code;
|
|
1348
|
+
if (name === "AbortError" || code === "stream_aborted") return;
|
|
1349
|
+
console.warn("[app-sdk] subscribe history catch-up failed", err);
|
|
1350
|
+
} finally {
|
|
1351
|
+
if (activeCatchUpAbort === abort) {
|
|
1352
|
+
activeCatchUpAbort = null;
|
|
1353
|
+
}
|
|
1354
|
+
catchUpInFlight -= 1;
|
|
1355
|
+
if (needAnotherCatchUp && !consumerReturned) {
|
|
1356
|
+
needAnotherCatchUp = false;
|
|
1357
|
+
triggerCatchUp(0);
|
|
1358
|
+
}
|
|
1359
|
+
drain();
|
|
1360
|
+
}
|
|
1361
|
+
})();
|
|
1362
|
+
};
|
|
1363
|
+
let pumpStarted = false;
|
|
1364
|
+
const startPump = () => {
|
|
1365
|
+
if (pumpStarted) return;
|
|
1366
|
+
pumpStarted = true;
|
|
1367
|
+
const connectPromise = socket.connect().then(
|
|
1368
|
+
() => null,
|
|
1369
|
+
(err) => err
|
|
1370
|
+
);
|
|
1371
|
+
void (async () => {
|
|
1372
|
+
try {
|
|
1278
1373
|
const err = await connectPromise;
|
|
1279
1374
|
if (err) {
|
|
1280
1375
|
try {
|
|
1281
1376
|
await innerIter.return?.();
|
|
1282
1377
|
} catch {
|
|
1283
1378
|
}
|
|
1284
|
-
|
|
1379
|
+
enqueue({
|
|
1285
1380
|
type: "task_failed",
|
|
1286
1381
|
taskId,
|
|
1287
1382
|
error: errorToChatError(err, "subscribe_failed")
|
|
1288
|
-
};
|
|
1289
|
-
|
|
1383
|
+
});
|
|
1384
|
+
upstreamDone = true;
|
|
1385
|
+
drain();
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
while (!consumerReturned) {
|
|
1389
|
+
const r = await innerIter.next();
|
|
1390
|
+
if (r.done) {
|
|
1391
|
+
upstreamDone = true;
|
|
1392
|
+
drain();
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
const ev = r.value;
|
|
1396
|
+
if (catchUpEnabled) {
|
|
1397
|
+
if (ev.type === "message_appended") {
|
|
1398
|
+
if (ev.message.id) knownIds.add(ev.message.id);
|
|
1399
|
+
} else if (ev.type === "task_finished" || ev.type === "task_failed") {
|
|
1400
|
+
triggerCatchUp(CATCH_UP_DELAY_MS);
|
|
1401
|
+
} else if (ev.type === "runtime_status") {
|
|
1402
|
+
const now = ev.status.replyInProgress;
|
|
1403
|
+
if (prevReplyInProgress === true && now === false) {
|
|
1404
|
+
triggerCatchUp(CATCH_UP_DELAY_MS);
|
|
1405
|
+
}
|
|
1406
|
+
prevReplyInProgress = now;
|
|
1407
|
+
} else if (ev.type === "connection_state") {
|
|
1408
|
+
if (ev.state === "connected" && prevConnectionState === "reconnecting") {
|
|
1409
|
+
triggerCatchUp(0);
|
|
1410
|
+
}
|
|
1411
|
+
prevConnectionState = ev.state;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
enqueue(ev);
|
|
1415
|
+
}
|
|
1416
|
+
} catch (err) {
|
|
1417
|
+
if (!consumerReturned) {
|
|
1418
|
+
enqueue({
|
|
1419
|
+
type: "task_failed",
|
|
1420
|
+
taskId,
|
|
1421
|
+
error: errorToChatError(err, "subscribe_failed")
|
|
1422
|
+
});
|
|
1423
|
+
upstreamDone = true;
|
|
1424
|
+
drain();
|
|
1290
1425
|
}
|
|
1291
1426
|
}
|
|
1292
|
-
|
|
1427
|
+
})();
|
|
1428
|
+
};
|
|
1429
|
+
return {
|
|
1430
|
+
async next() {
|
|
1431
|
+
if (consumerReturned) {
|
|
1432
|
+
return { value: void 0, done: true };
|
|
1433
|
+
}
|
|
1434
|
+
startPump();
|
|
1435
|
+
if (queue.length > 0) {
|
|
1436
|
+
return { value: queue.shift(), done: false };
|
|
1437
|
+
}
|
|
1438
|
+
if (upstreamDone && catchUpInFlight === 0) {
|
|
1439
|
+
return { value: void 0, done: true };
|
|
1440
|
+
}
|
|
1441
|
+
return new Promise((resolve) => {
|
|
1442
|
+
pendingResolve = resolve;
|
|
1443
|
+
});
|
|
1293
1444
|
},
|
|
1294
1445
|
async return() {
|
|
1295
|
-
|
|
1446
|
+
consumerReturned = true;
|
|
1447
|
+
activeCatchUpAbort?.abort();
|
|
1448
|
+
activeCatchUpAbort = null;
|
|
1449
|
+
try {
|
|
1450
|
+
await innerIter.return?.();
|
|
1451
|
+
} catch {
|
|
1452
|
+
}
|
|
1453
|
+
if (pendingResolve) {
|
|
1454
|
+
wakePending({ value: void 0, done: true });
|
|
1455
|
+
}
|
|
1456
|
+
return { value: void 0, done: true };
|
|
1296
1457
|
}
|
|
1297
1458
|
};
|
|
1298
1459
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/app-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Conductor App SDK: third-party backend SDK + React chat widget for embedding Conductor AI tools",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,8 +60,12 @@
|
|
|
60
60
|
"react-dom": ">=18.0.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
63
|
-
"react": {
|
|
64
|
-
|
|
63
|
+
"react": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"react-dom": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
65
69
|
},
|
|
66
70
|
"devDependencies": {
|
|
67
71
|
"@types/node": "^22.10.2",
|