@astralform/js 5.1.0 → 6.0.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/README.md +8 -3
- package/dist/index.cjs +169 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -6
- package/dist/index.d.ts +67 -6
- package/dist/index.js +169 -132
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -265,8 +265,13 @@ const id = await session.createNewConversation();
|
|
|
265
265
|
// Switch to an existing conversation (loads messages from backend)
|
|
266
266
|
await session.switchConversation("conversation-id");
|
|
267
267
|
|
|
268
|
-
// Delete a conversation
|
|
269
|
-
|
|
268
|
+
// Delete a conversation. Rejects if the server refused it — a 404 counts as
|
|
269
|
+
// already deleted and resolves, anything else leaves the conversation in place.
|
|
270
|
+
try {
|
|
271
|
+
await session.deleteConversation("conversation-id");
|
|
272
|
+
} catch (err) {
|
|
273
|
+
// Still there: tell the user rather than removing it from your own UI.
|
|
274
|
+
}
|
|
270
275
|
|
|
271
276
|
// Edit and resend from a checkpoint
|
|
272
277
|
await session.resendFromCheckpoint("message-id", "Updated message");
|
|
@@ -357,7 +362,7 @@ import {
|
|
|
357
362
|
AuthenticationError, // 401 — invalid API key
|
|
358
363
|
RateLimitError, // 429 — rate limit exceeded
|
|
359
364
|
LLMNotConfiguredError, // LLM provider not set up
|
|
360
|
-
ServerError, // 5xx or unexpected errors
|
|
365
|
+
ServerError, // 5xx or unexpected errors — carries `status` when it came from a response
|
|
361
366
|
ConnectionError, // Network failures
|
|
362
367
|
StreamAbortedError, // Stream cancelled via disconnect()
|
|
363
368
|
} from "@astralform/js";
|
package/dist/index.cjs
CHANGED
|
@@ -73,9 +73,10 @@ var LLMNotConfiguredError = class extends AstralformError {
|
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
75
|
var ServerError = class extends AstralformError {
|
|
76
|
-
constructor(message = "Internal server error") {
|
|
76
|
+
constructor(message = "Internal server error", status) {
|
|
77
77
|
super(message, "server_error");
|
|
78
78
|
this.name = "ServerError";
|
|
79
|
+
if (status !== void 0) Object.assign(this, { status });
|
|
79
80
|
}
|
|
80
81
|
};
|
|
81
82
|
var ConnectionError = class extends AstralformError {
|
|
@@ -105,6 +106,16 @@ function generateId() {
|
|
|
105
106
|
return v.toString(16);
|
|
106
107
|
});
|
|
107
108
|
}
|
|
109
|
+
function snakeToCamel(str) {
|
|
110
|
+
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
111
|
+
}
|
|
112
|
+
function camelizeKeys(obj) {
|
|
113
|
+
const result = {};
|
|
114
|
+
for (const key of Object.keys(obj)) {
|
|
115
|
+
result[snakeToCamel(key)] = obj[key];
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
108
119
|
|
|
109
120
|
// src/rate-limit.ts
|
|
110
121
|
var DEFAULT_MESSAGE = "Rate limit exceeded";
|
|
@@ -255,7 +266,7 @@ async function* streamJobSSE(options) {
|
|
|
255
266
|
case 429:
|
|
256
267
|
throw createRateLimitErrorFromHttp(response, rawText);
|
|
257
268
|
default:
|
|
258
|
-
throw new ServerError(text || `HTTP ${response.status}
|
|
269
|
+
throw new ServerError(text || `HTTP ${response.status}`, response.status);
|
|
259
270
|
}
|
|
260
271
|
}
|
|
261
272
|
if (!response.body) {
|
|
@@ -526,7 +537,10 @@ var AstralformClient = class {
|
|
|
526
537
|
throw createRateLimitErrorFromHttp(response, text);
|
|
527
538
|
default: {
|
|
528
539
|
const safeText = text ? sanitizeErrorText(text) : "";
|
|
529
|
-
throw new ServerError(
|
|
540
|
+
throw new ServerError(
|
|
541
|
+
safeText || `HTTP ${response.status}`,
|
|
542
|
+
response.status
|
|
543
|
+
);
|
|
530
544
|
}
|
|
531
545
|
}
|
|
532
546
|
}
|
|
@@ -563,13 +577,7 @@ var AstralformClient = class {
|
|
|
563
577
|
const safeLimit = Math.max(1, Math.min(200, Math.floor(Number(limit))));
|
|
564
578
|
const safeOffset = Math.max(0, Math.floor(Number(offset)));
|
|
565
579
|
const raw = await this.get(`/v1/conversations?limit=${safeLimit}&offset=${safeOffset}`);
|
|
566
|
-
return raw.map((c) => (
|
|
567
|
-
id: c.id,
|
|
568
|
-
title: c.title,
|
|
569
|
-
messageCount: c.message_count,
|
|
570
|
-
createdAt: c.created_at,
|
|
571
|
-
updatedAt: c.updated_at
|
|
572
|
-
}));
|
|
580
|
+
return raw.map((c) => camelizeKeys(c));
|
|
573
581
|
}
|
|
574
582
|
async getMessages(conversationId) {
|
|
575
583
|
const raw = await this.get(`/v1/conversations/${encodeURIComponent(conversationId)}/messages`);
|
|
@@ -594,13 +602,7 @@ var AstralformClient = class {
|
|
|
594
602
|
*/
|
|
595
603
|
async renameConversation(id, title) {
|
|
596
604
|
const c = await this.patch(`/v1/conversations/${encodeURIComponent(id)}`, { title });
|
|
597
|
-
return
|
|
598
|
-
id: c.id,
|
|
599
|
-
title: c.title,
|
|
600
|
-
messageCount: c.message_count,
|
|
601
|
-
createdAt: c.created_at,
|
|
602
|
-
updatedAt: c.updated_at
|
|
603
|
-
};
|
|
605
|
+
return camelizeKeys(c);
|
|
604
606
|
}
|
|
605
607
|
async deleteConversation(id) {
|
|
606
608
|
await this.del(`/v1/conversations/${encodeURIComponent(id)}`);
|
|
@@ -613,14 +615,7 @@ var AstralformClient = class {
|
|
|
613
615
|
*/
|
|
614
616
|
async getAgents() {
|
|
615
617
|
const raw = await this.get("/v1/agents");
|
|
616
|
-
return raw.map((a) => (
|
|
617
|
-
name: a.name,
|
|
618
|
-
displayName: a.display_name,
|
|
619
|
-
description: a.description,
|
|
620
|
-
isOrchestrator: a.is_orchestrator,
|
|
621
|
-
isEnabled: a.is_enabled,
|
|
622
|
-
avatarUrl: a.avatar_url
|
|
623
|
-
}));
|
|
618
|
+
return raw.map((a) => camelizeKeys(a));
|
|
624
619
|
}
|
|
625
620
|
/**
|
|
626
621
|
* List the models the caller may pick this turn — expanded from the curated
|
|
@@ -647,12 +642,7 @@ var AstralformClient = class {
|
|
|
647
642
|
}
|
|
648
643
|
async getSkills() {
|
|
649
644
|
const raw = await this.get("/v1/skills");
|
|
650
|
-
return raw.map((s) => (
|
|
651
|
-
name: s.name,
|
|
652
|
-
displayName: s.display_name,
|
|
653
|
-
description: s.description,
|
|
654
|
-
isEnabled: s.is_enabled
|
|
655
|
-
}));
|
|
645
|
+
return raw.map((s) => camelizeKeys(s));
|
|
656
646
|
}
|
|
657
647
|
async getConversationEvents(conversationId, jobId) {
|
|
658
648
|
let url = `/v1/conversations/${encodeURIComponent(conversationId)}/events`;
|
|
@@ -769,13 +759,7 @@ var AstralformClient = class {
|
|
|
769
759
|
// sending them in API-key mode yields 401.
|
|
770
760
|
async listTeams() {
|
|
771
761
|
const raw = await this.get("/v1/teams");
|
|
772
|
-
return raw.map((t) => (
|
|
773
|
-
id: t.id,
|
|
774
|
-
name: t.name,
|
|
775
|
-
slug: t.slug,
|
|
776
|
-
isDefault: t.is_default,
|
|
777
|
-
role: t.role
|
|
778
|
-
}));
|
|
762
|
+
return raw.map((t) => camelizeKeys(t));
|
|
779
763
|
}
|
|
780
764
|
/**
|
|
781
765
|
* List the team-level agents (formerly "projects") the signed-in user can
|
|
@@ -784,14 +768,7 @@ var AstralformClient = class {
|
|
|
784
768
|
*/
|
|
785
769
|
async listAgents(teamId) {
|
|
786
770
|
const raw = await this.get(`/v1/teams/${encodeURIComponent(teamId)}/agents`);
|
|
787
|
-
return raw.map((a) => (
|
|
788
|
-
id: a.id,
|
|
789
|
-
name: a.name,
|
|
790
|
-
teamId: a.team_id,
|
|
791
|
-
createdAt: a.created_at,
|
|
792
|
-
updatedAt: a.updated_at,
|
|
793
|
-
avatarUrl: a.avatar_url ?? null
|
|
794
|
-
}));
|
|
771
|
+
return raw.map((a) => camelizeKeys(a));
|
|
795
772
|
}
|
|
796
773
|
// --- Jobs API ---
|
|
797
774
|
async createJob(request) {
|
|
@@ -828,13 +805,7 @@ var AstralformClient = class {
|
|
|
828
805
|
};
|
|
829
806
|
if (request.comment != null) body.comment = request.comment;
|
|
830
807
|
const raw = await this.post(`/v1/jobs/${encodeURIComponent(jobId)}/feedback`, body);
|
|
831
|
-
return
|
|
832
|
-
id: raw.id,
|
|
833
|
-
jobId: raw.job_id,
|
|
834
|
-
rating: raw.rating,
|
|
835
|
-
comment: raw.comment,
|
|
836
|
-
createdAt: raw.created_at
|
|
837
|
-
};
|
|
808
|
+
return camelizeKeys(raw);
|
|
838
809
|
}
|
|
839
810
|
async getActiveJob(conversationId) {
|
|
840
811
|
const raw = await this.get(`/v1/conversations/${encodeURIComponent(conversationId)}/active-job`);
|
|
@@ -2222,11 +2193,12 @@ var ChatSession = class {
|
|
|
2222
2193
|
/**
|
|
2223
2194
|
* Rename a conversation, server first.
|
|
2224
2195
|
*
|
|
2225
|
-
*
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2229
|
-
*
|
|
2196
|
+
* Server first, like the delete below: a failed rename written locally would
|
|
2197
|
+
* leave the sidebar showing a title the server never accepted, and nothing
|
|
2198
|
+
* refetches a conversation that is already in the loaded list. (The delete
|
|
2199
|
+
* used to be the counter-example here — it dropped the row whatever the
|
|
2200
|
+
* server said, on the theory that a failed one was self-correcting. It was
|
|
2201
|
+
* not: the row stayed deleted locally and alive on the server.)
|
|
2230
2202
|
*
|
|
2231
2203
|
* Mirrors the `title_generated` path: the entry in `conversations` is
|
|
2232
2204
|
* mutated in place, which is what every consumer of the list reads.
|
|
@@ -2242,7 +2214,8 @@ var ChatSession = class {
|
|
|
2242
2214
|
async deleteConversation(id) {
|
|
2243
2215
|
try {
|
|
2244
2216
|
await this.client.deleteConversation(id);
|
|
2245
|
-
} catch {
|
|
2217
|
+
} catch (err) {
|
|
2218
|
+
if (!(err instanceof ServerError) || err.status !== 404) throw err;
|
|
2246
2219
|
}
|
|
2247
2220
|
await this.storage.deleteConversation(id);
|
|
2248
2221
|
if (this.serverConversationIds.delete(id)) {
|
|
@@ -2268,9 +2241,10 @@ var ChatSession = class {
|
|
|
2268
2241
|
|
|
2269
2242
|
// src/restore-plan.ts
|
|
2270
2243
|
function planRestore(args) {
|
|
2271
|
-
const { completedJobs, userMessages } = args;
|
|
2244
|
+
const { completedJobs, runningJob, userMessages } = args;
|
|
2245
|
+
const jobs = runningJob ? [...completedJobs, runningJob] : completedJobs;
|
|
2272
2246
|
const claimed = new Set(
|
|
2273
|
-
(args.claimedMessageIds ??
|
|
2247
|
+
(args.claimedMessageIds ?? jobs.map((j) => j.message_id)).filter(
|
|
2274
2248
|
(id) => !!id
|
|
2275
2249
|
)
|
|
2276
2250
|
);
|
|
@@ -2279,19 +2253,19 @@ function planRestore(args) {
|
|
|
2279
2253
|
if (m.id) byId.set(m.id, i);
|
|
2280
2254
|
});
|
|
2281
2255
|
const linkOf = (j) => j.message_id ? byId.get(j.message_id) : void 0;
|
|
2282
|
-
const positional = (
|
|
2256
|
+
const positional = (slice, msgs) => slice.map((job, i) => ({
|
|
2283
2257
|
kind: "turn",
|
|
2284
2258
|
jobId: job.job_id,
|
|
2285
2259
|
content: msgs[i]?.content,
|
|
2286
2260
|
messageId: msgs[i]?.id
|
|
2287
2261
|
}));
|
|
2288
|
-
const firstLinked =
|
|
2262
|
+
const firstLinked = jobs.findIndex((j) => linkOf(j) !== void 0);
|
|
2289
2263
|
if (firstLinked === -1) {
|
|
2290
|
-
return positional(
|
|
2264
|
+
return positional(jobs, userMessages);
|
|
2291
2265
|
}
|
|
2292
|
-
const cutover = linkOf(
|
|
2266
|
+
const cutover = linkOf(jobs[firstLinked]);
|
|
2293
2267
|
const steps = positional(
|
|
2294
|
-
|
|
2268
|
+
jobs.slice(0, firstLinked),
|
|
2295
2269
|
userMessages.slice(0, cutover)
|
|
2296
2270
|
);
|
|
2297
2271
|
let cursor = cutover;
|
|
@@ -2305,7 +2279,7 @@ function planRestore(args) {
|
|
|
2305
2279
|
}
|
|
2306
2280
|
cursor = stopAt + 1;
|
|
2307
2281
|
};
|
|
2308
|
-
for (const job of
|
|
2282
|
+
for (const job of jobs.slice(firstLinked)) {
|
|
2309
2283
|
const at = linkOf(job);
|
|
2310
2284
|
if (at !== void 0) {
|
|
2311
2285
|
drainTo(at);
|
|
@@ -2387,6 +2361,13 @@ var StreamManager = class {
|
|
|
2387
2361
|
* one the user is waiting on — see ``restore``.
|
|
2388
2362
|
*/
|
|
2389
2363
|
this.generation = 0;
|
|
2364
|
+
/**
|
|
2365
|
+
* Bumped every time a turn STARTS. `generation` does not move for a send
|
|
2366
|
+
* (only a pointer move does), and the streaming state returns to idle when a
|
|
2367
|
+
* turn ends — so neither can tell a restore that a turn ran inside one of
|
|
2368
|
+
* its awaits. This can.
|
|
2369
|
+
*/
|
|
2370
|
+
this.turnCounter = 0;
|
|
2390
2371
|
this.session = session;
|
|
2391
2372
|
this.attach();
|
|
2392
2373
|
}
|
|
@@ -2455,6 +2436,7 @@ var StreamManager = class {
|
|
|
2455
2436
|
target = await this.session.createNewConversation();
|
|
2456
2437
|
this.setActiveConversation(target);
|
|
2457
2438
|
}
|
|
2439
|
+
this.turnCounter++;
|
|
2458
2440
|
this.setState("streaming");
|
|
2459
2441
|
try {
|
|
2460
2442
|
await this.session.send(content, {
|
|
@@ -2491,6 +2473,7 @@ var StreamManager = class {
|
|
|
2491
2473
|
);
|
|
2492
2474
|
const lastUserMsg = userMsgs[userMsgs.length - 1];
|
|
2493
2475
|
if (!lastUserMsg) return;
|
|
2476
|
+
this.turnCounter++;
|
|
2494
2477
|
this.setState("streaming");
|
|
2495
2478
|
try {
|
|
2496
2479
|
await this.session.resendFromCheckpoint(
|
|
@@ -2691,7 +2674,9 @@ var StreamManager = class {
|
|
|
2691
2674
|
async restore(conversationId, gen) {
|
|
2692
2675
|
const superseded = () => gen !== this.generation;
|
|
2693
2676
|
if (superseded()) return;
|
|
2694
|
-
|
|
2677
|
+
const turn = this.turnCounter;
|
|
2678
|
+
const announcedRestoring = !this.session.isStreaming;
|
|
2679
|
+
if (announcedRestoring) this.setState("restoring");
|
|
2695
2680
|
let activeJobId = null;
|
|
2696
2681
|
try {
|
|
2697
2682
|
const res = await this.session.client.getActiveJob(conversationId);
|
|
@@ -2699,9 +2684,12 @@ var StreamManager = class {
|
|
|
2699
2684
|
} catch {
|
|
2700
2685
|
}
|
|
2701
2686
|
if (superseded()) return;
|
|
2687
|
+
await this.session.loadConversation(conversationId);
|
|
2688
|
+
if (superseded()) return;
|
|
2689
|
+
if (announcedRestoring && this.viewTakenOverByLiveTurn()) return;
|
|
2690
|
+
if (announcedRestoring && !await this.replayHistory(conversationId, gen, activeJobId, turn))
|
|
2691
|
+
return;
|
|
2702
2692
|
if (activeJobId) {
|
|
2703
|
-
await this.session.loadConversation(conversationId);
|
|
2704
|
-
if (superseded()) return;
|
|
2705
2693
|
this.setState("streaming");
|
|
2706
2694
|
try {
|
|
2707
2695
|
await this.session.reconnectToJob(activeJobId);
|
|
@@ -2712,76 +2700,125 @@ var StreamManager = class {
|
|
|
2712
2700
|
this.setState("idle");
|
|
2713
2701
|
}
|
|
2714
2702
|
} else {
|
|
2715
|
-
await this.session.loadConversation(conversationId);
|
|
2716
2703
|
if (superseded()) return;
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2704
|
+
this.settleIdle();
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Has a live turn taken the block view over?
|
|
2709
|
+
*
|
|
2710
|
+
* ``_state`` is the SYNCHRONOUS authority and ``session.isStreaming`` lags it
|
|
2711
|
+
* by an await: ``send`` sets ``_state = "streaming"`` before its first await,
|
|
2712
|
+
* while the session only raises its flag inside ``processStream``, behind the
|
|
2713
|
+
* ``storage.addMessage`` write. For that whole window a send is underway —
|
|
2714
|
+
* composer cleared, optimistic bubble drawn — and the session flag still
|
|
2715
|
+
* reads false. Reading both closes the window from either end, since
|
|
2716
|
+
* ``reconnectToJob`` is the mirror case: it raises the session flag without
|
|
2717
|
+
* ever moving ``_state``.
|
|
2718
|
+
*/
|
|
2719
|
+
viewTakenOverByLiveTurn() {
|
|
2720
|
+
return this._state === "streaming" || this.session.isStreaming;
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Has a turn STARTED since ``turn`` was captured?
|
|
2724
|
+
*
|
|
2725
|
+
* ``viewTakenOverByLiveTurn`` reads the current state, so it cannot see a
|
|
2726
|
+
* turn that both started and ENDED inside one of the restore's awaits — a
|
|
2727
|
+
* send that fails fast (auth, rate limit) resolves in about the time the job
|
|
2728
|
+
* list takes, and leaves `_state` back at idle with its blocks already
|
|
2729
|
+
* rendered. A monotonic count is the only thing that survives a state that
|
|
2730
|
+
* has returned to where it started.
|
|
2731
|
+
*/
|
|
2732
|
+
turnStarted(since) {
|
|
2733
|
+
return this.turnCounter !== since;
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Replay a conversation's persisted history into the consumer's block view.
|
|
2737
|
+
*
|
|
2738
|
+
* Returns false when this restore lost the right to finish — a newer switch
|
|
2739
|
+
* superseded it, or a send took the view over — in which case the caller
|
|
2740
|
+
* must stop rather than finish. See ``restore``.
|
|
2741
|
+
*
|
|
2742
|
+
* ``activeJobId`` names the turn that is still running, if any. Its events
|
|
2743
|
+
* are NOT fetched here: they are the live stream the caller reconnects to
|
|
2744
|
+
* straight after. It is passed so ``planRestore`` can pair it with the prompt
|
|
2745
|
+
* that started it, which is emitted as a bubble with no events — the whole
|
|
2746
|
+
* reason a conversation reopened mid-turn now shows the message that started
|
|
2747
|
+
* that turn.
|
|
2748
|
+
*/
|
|
2749
|
+
async replayHistory(conversationId, gen, activeJobId, turn) {
|
|
2750
|
+
const stopReplay = () => gen !== this.generation || this.viewTakenOverByLiveTurn() || this.turnStarted(turn);
|
|
2751
|
+
try {
|
|
2752
|
+
const jobs = await this.session.client.get(`/v1/conversations/${encodeURIComponent(conversationId)}/jobs`);
|
|
2753
|
+
if (stopReplay()) return false;
|
|
2754
|
+
const completedJobs = jobs.filter(
|
|
2755
|
+
(j) => j.status === "completed" && j.job_id !== activeJobId
|
|
2756
|
+
);
|
|
2757
|
+
const userMessages = this.session.messages.filter(
|
|
2758
|
+
(m) => m.role === "user"
|
|
2759
|
+
);
|
|
2760
|
+
const runningJob = activeJobId ? jobs.find((j) => j.job_id === activeJobId) : void 0;
|
|
2761
|
+
const plan = planRestore({
|
|
2762
|
+
completedJobs: completedJobs.map((j) => ({
|
|
2763
|
+
job_id: j.job_id,
|
|
2764
|
+
message_id: j.message_id
|
|
2765
|
+
})),
|
|
2766
|
+
runningJob: runningJob && {
|
|
2767
|
+
job_id: runningJob.job_id,
|
|
2768
|
+
message_id: runningJob.message_id
|
|
2769
|
+
},
|
|
2770
|
+
// Completed jobs PLUS the ones still going. A send landing in the
|
|
2771
|
+
// probe window has a running job, so its prompt is claimed and does
|
|
2772
|
+
// not read as a steer replayed over the bubble the live send already
|
|
2773
|
+
// rendered. Failed and cancelled jobs are deliberately NOT claimed:
|
|
2774
|
+
// they produce no `turn` step, so claiming them would delete the
|
|
2775
|
+
// user's prompt from the restore entirely rather than show it as a
|
|
2776
|
+
// steer.
|
|
2777
|
+
claimedMessageIds: jobs.filter((j) => j.status !== "failed" && j.status !== "cancelled").map((j) => j.message_id),
|
|
2778
|
+
userMessages: userMessages.map((m) => ({
|
|
2779
|
+
id: m.id,
|
|
2780
|
+
content: m.content
|
|
2781
|
+
}))
|
|
2782
|
+
});
|
|
2783
|
+
const eventLists = await Promise.all(
|
|
2784
|
+
completedJobs.map(
|
|
2785
|
+
(job) => this.session.client.getConversationEvents(conversationId, job.job_id).catch(() => [])
|
|
2786
|
+
)
|
|
2787
|
+
);
|
|
2788
|
+
if (stopReplay()) return false;
|
|
2789
|
+
const eventsByJobId = new Map(
|
|
2790
|
+
completedJobs.map((job, i) => [job.job_id, eventLists[i] ?? []])
|
|
2791
|
+
);
|
|
2792
|
+
for (const step of plan) {
|
|
2793
|
+
if (stopReplay()) return false;
|
|
2794
|
+
if (step.kind === "steer") {
|
|
2765
2795
|
this.session.replayTurn(
|
|
2766
2796
|
conversationId,
|
|
2767
|
-
|
|
2797
|
+
[],
|
|
2768
2798
|
step.content,
|
|
2769
|
-
step.messageId
|
|
2799
|
+
step.messageId,
|
|
2800
|
+
true
|
|
2770
2801
|
);
|
|
2802
|
+
continue;
|
|
2771
2803
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
});
|
|
2779
|
-
}
|
|
2780
|
-
} catch {
|
|
2804
|
+
this.session.replayTurn(
|
|
2805
|
+
conversationId,
|
|
2806
|
+
eventsByJobId.get(step.jobId) ?? [],
|
|
2807
|
+
step.content,
|
|
2808
|
+
step.messageId
|
|
2809
|
+
);
|
|
2781
2810
|
}
|
|
2782
|
-
if (
|
|
2783
|
-
|
|
2811
|
+
if (stopReplay()) return false;
|
|
2812
|
+
if (completedJobs.length > 0) {
|
|
2813
|
+
this.emit({
|
|
2814
|
+
type: "versionsReady",
|
|
2815
|
+
conversationId,
|
|
2816
|
+
count: completedJobs.length
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
} catch {
|
|
2784
2820
|
}
|
|
2821
|
+
return !stopReplay();
|
|
2785
2822
|
}
|
|
2786
2823
|
// ── Internal: set active conversation ─────────────────────────
|
|
2787
2824
|
setActiveConversation(id) {
|