@astralform/js 6.0.0 → 6.0.2
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.cjs +144 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +144 -73
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2241,9 +2241,10 @@ var ChatSession = class {
|
|
|
2241
2241
|
|
|
2242
2242
|
// src/restore-plan.ts
|
|
2243
2243
|
function planRestore(args) {
|
|
2244
|
-
const { completedJobs, userMessages } = args;
|
|
2244
|
+
const { completedJobs, runningJob, userMessages } = args;
|
|
2245
|
+
const jobs = runningJob ? [...completedJobs, runningJob] : completedJobs;
|
|
2245
2246
|
const claimed = new Set(
|
|
2246
|
-
(args.claimedMessageIds ??
|
|
2247
|
+
(args.claimedMessageIds ?? jobs.map((j) => j.message_id)).filter(
|
|
2247
2248
|
(id) => !!id
|
|
2248
2249
|
)
|
|
2249
2250
|
);
|
|
@@ -2252,19 +2253,26 @@ function planRestore(args) {
|
|
|
2252
2253
|
if (m.id) byId.set(m.id, i);
|
|
2253
2254
|
});
|
|
2254
2255
|
const linkOf = (j) => j.message_id ? byId.get(j.message_id) : void 0;
|
|
2255
|
-
const positional = (
|
|
2256
|
+
const positional = (slice, msgs) => slice.map((job, i) => ({
|
|
2256
2257
|
kind: "turn",
|
|
2257
2258
|
jobId: job.job_id,
|
|
2258
2259
|
content: msgs[i]?.content,
|
|
2259
2260
|
messageId: msgs[i]?.id
|
|
2260
2261
|
}));
|
|
2261
|
-
|
|
2262
|
+
if (jobs.length === 0) {
|
|
2263
|
+
return userMessages.map((m) => ({
|
|
2264
|
+
kind: "steer",
|
|
2265
|
+
content: m.content,
|
|
2266
|
+
messageId: m.id
|
|
2267
|
+
}));
|
|
2268
|
+
}
|
|
2269
|
+
const firstLinked = jobs.findIndex((j) => linkOf(j) !== void 0);
|
|
2262
2270
|
if (firstLinked === -1) {
|
|
2263
|
-
return positional(
|
|
2271
|
+
return positional(jobs, userMessages);
|
|
2264
2272
|
}
|
|
2265
|
-
const cutover = linkOf(
|
|
2273
|
+
const cutover = linkOf(jobs[firstLinked]);
|
|
2266
2274
|
const steps = positional(
|
|
2267
|
-
|
|
2275
|
+
jobs.slice(0, firstLinked),
|
|
2268
2276
|
userMessages.slice(0, cutover)
|
|
2269
2277
|
);
|
|
2270
2278
|
let cursor = cutover;
|
|
@@ -2278,7 +2286,7 @@ function planRestore(args) {
|
|
|
2278
2286
|
}
|
|
2279
2287
|
cursor = stopAt + 1;
|
|
2280
2288
|
};
|
|
2281
|
-
for (const job of
|
|
2289
|
+
for (const job of jobs.slice(firstLinked)) {
|
|
2282
2290
|
const at = linkOf(job);
|
|
2283
2291
|
if (at !== void 0) {
|
|
2284
2292
|
drainTo(at);
|
|
@@ -2360,6 +2368,13 @@ var StreamManager = class {
|
|
|
2360
2368
|
* one the user is waiting on — see ``restore``.
|
|
2361
2369
|
*/
|
|
2362
2370
|
this.generation = 0;
|
|
2371
|
+
/**
|
|
2372
|
+
* Bumped every time a turn STARTS. `generation` does not move for a send
|
|
2373
|
+
* (only a pointer move does), and the streaming state returns to idle when a
|
|
2374
|
+
* turn ends — so neither can tell a restore that a turn ran inside one of
|
|
2375
|
+
* its awaits. This can.
|
|
2376
|
+
*/
|
|
2377
|
+
this.turnCounter = 0;
|
|
2363
2378
|
this.session = session;
|
|
2364
2379
|
this.attach();
|
|
2365
2380
|
}
|
|
@@ -2428,6 +2443,7 @@ var StreamManager = class {
|
|
|
2428
2443
|
target = await this.session.createNewConversation();
|
|
2429
2444
|
this.setActiveConversation(target);
|
|
2430
2445
|
}
|
|
2446
|
+
this.turnCounter++;
|
|
2431
2447
|
this.setState("streaming");
|
|
2432
2448
|
try {
|
|
2433
2449
|
await this.session.send(content, {
|
|
@@ -2464,6 +2480,7 @@ var StreamManager = class {
|
|
|
2464
2480
|
);
|
|
2465
2481
|
const lastUserMsg = userMsgs[userMsgs.length - 1];
|
|
2466
2482
|
if (!lastUserMsg) return;
|
|
2483
|
+
this.turnCounter++;
|
|
2467
2484
|
this.setState("streaming");
|
|
2468
2485
|
try {
|
|
2469
2486
|
await this.session.resendFromCheckpoint(
|
|
@@ -2664,7 +2681,9 @@ var StreamManager = class {
|
|
|
2664
2681
|
async restore(conversationId, gen) {
|
|
2665
2682
|
const superseded = () => gen !== this.generation;
|
|
2666
2683
|
if (superseded()) return;
|
|
2667
|
-
|
|
2684
|
+
const turn = this.turnCounter;
|
|
2685
|
+
const announcedRestoring = !this.session.isStreaming;
|
|
2686
|
+
if (announcedRestoring) this.setState("restoring");
|
|
2668
2687
|
let activeJobId = null;
|
|
2669
2688
|
try {
|
|
2670
2689
|
const res = await this.session.client.getActiveJob(conversationId);
|
|
@@ -2672,9 +2691,12 @@ var StreamManager = class {
|
|
|
2672
2691
|
} catch {
|
|
2673
2692
|
}
|
|
2674
2693
|
if (superseded()) return;
|
|
2694
|
+
await this.session.loadConversation(conversationId);
|
|
2695
|
+
if (superseded()) return;
|
|
2696
|
+
if (announcedRestoring && this.viewTakenOverByLiveTurn()) return;
|
|
2697
|
+
if (announcedRestoring && !await this.replayHistory(conversationId, gen, activeJobId, turn))
|
|
2698
|
+
return;
|
|
2675
2699
|
if (activeJobId) {
|
|
2676
|
-
await this.session.loadConversation(conversationId);
|
|
2677
|
-
if (superseded()) return;
|
|
2678
2700
|
this.setState("streaming");
|
|
2679
2701
|
try {
|
|
2680
2702
|
await this.session.reconnectToJob(activeJobId);
|
|
@@ -2685,76 +2707,125 @@ var StreamManager = class {
|
|
|
2685
2707
|
this.setState("idle");
|
|
2686
2708
|
}
|
|
2687
2709
|
} else {
|
|
2688
|
-
await this.session.loadConversation(conversationId);
|
|
2689
2710
|
if (superseded()) return;
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2711
|
+
this.settleIdle();
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* Has a live turn taken the block view over?
|
|
2716
|
+
*
|
|
2717
|
+
* ``_state`` is the SYNCHRONOUS authority and ``session.isStreaming`` lags it
|
|
2718
|
+
* by an await: ``send`` sets ``_state = "streaming"`` before its first await,
|
|
2719
|
+
* while the session only raises its flag inside ``processStream``, behind the
|
|
2720
|
+
* ``storage.addMessage`` write. For that whole window a send is underway —
|
|
2721
|
+
* composer cleared, optimistic bubble drawn — and the session flag still
|
|
2722
|
+
* reads false. Reading both closes the window from either end, since
|
|
2723
|
+
* ``reconnectToJob`` is the mirror case: it raises the session flag without
|
|
2724
|
+
* ever moving ``_state``.
|
|
2725
|
+
*/
|
|
2726
|
+
viewTakenOverByLiveTurn() {
|
|
2727
|
+
return this._state === "streaming" || this.session.isStreaming;
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* Has a turn STARTED since ``turn`` was captured?
|
|
2731
|
+
*
|
|
2732
|
+
* ``viewTakenOverByLiveTurn`` reads the current state, so it cannot see a
|
|
2733
|
+
* turn that both started and ENDED inside one of the restore's awaits — a
|
|
2734
|
+
* send that fails fast (auth, rate limit) resolves in about the time the job
|
|
2735
|
+
* list takes, and leaves `_state` back at idle with its blocks already
|
|
2736
|
+
* rendered. A monotonic count is the only thing that survives a state that
|
|
2737
|
+
* has returned to where it started.
|
|
2738
|
+
*/
|
|
2739
|
+
turnStarted(since) {
|
|
2740
|
+
return this.turnCounter !== since;
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Replay a conversation's persisted history into the consumer's block view.
|
|
2744
|
+
*
|
|
2745
|
+
* Returns false when this restore lost the right to finish — a newer switch
|
|
2746
|
+
* superseded it, or a send took the view over — in which case the caller
|
|
2747
|
+
* must stop rather than finish. See ``restore``.
|
|
2748
|
+
*
|
|
2749
|
+
* ``activeJobId`` names the turn that is still running, if any. Its events
|
|
2750
|
+
* are NOT fetched here: they are the live stream the caller reconnects to
|
|
2751
|
+
* straight after. It is passed so ``planRestore`` can pair it with the prompt
|
|
2752
|
+
* that started it, which is emitted as a bubble with no events — the whole
|
|
2753
|
+
* reason a conversation reopened mid-turn now shows the message that started
|
|
2754
|
+
* that turn.
|
|
2755
|
+
*/
|
|
2756
|
+
async replayHistory(conversationId, gen, activeJobId, turn) {
|
|
2757
|
+
const stopReplay = () => gen !== this.generation || this.viewTakenOverByLiveTurn() || this.turnStarted(turn);
|
|
2758
|
+
try {
|
|
2759
|
+
const jobs = await this.session.client.get(`/v1/conversations/${encodeURIComponent(conversationId)}/jobs`);
|
|
2760
|
+
if (stopReplay()) return false;
|
|
2761
|
+
const completedJobs = jobs.filter(
|
|
2762
|
+
(j) => j.status === "completed" && j.job_id !== activeJobId
|
|
2763
|
+
);
|
|
2764
|
+
const userMessages = this.session.messages.filter(
|
|
2765
|
+
(m) => m.role === "user"
|
|
2766
|
+
);
|
|
2767
|
+
const runningJob = activeJobId ? jobs.find((j) => j.job_id === activeJobId) : void 0;
|
|
2768
|
+
const plan = planRestore({
|
|
2769
|
+
completedJobs: completedJobs.map((j) => ({
|
|
2770
|
+
job_id: j.job_id,
|
|
2771
|
+
message_id: j.message_id
|
|
2772
|
+
})),
|
|
2773
|
+
runningJob: runningJob && {
|
|
2774
|
+
job_id: runningJob.job_id,
|
|
2775
|
+
message_id: runningJob.message_id
|
|
2776
|
+
},
|
|
2777
|
+
// Completed jobs PLUS the ones still going. A send landing in the
|
|
2778
|
+
// probe window has a running job, so its prompt is claimed and does
|
|
2779
|
+
// not read as a steer replayed over the bubble the live send already
|
|
2780
|
+
// rendered. Failed and cancelled jobs are deliberately NOT claimed:
|
|
2781
|
+
// they produce no `turn` step, so claiming them would delete the
|
|
2782
|
+
// user's prompt from the restore entirely rather than show it as a
|
|
2783
|
+
// steer.
|
|
2784
|
+
claimedMessageIds: jobs.filter((j) => j.status !== "failed" && j.status !== "cancelled").map((j) => j.message_id),
|
|
2785
|
+
userMessages: userMessages.map((m) => ({
|
|
2786
|
+
id: m.id,
|
|
2787
|
+
content: m.content
|
|
2788
|
+
}))
|
|
2789
|
+
});
|
|
2790
|
+
const eventLists = await Promise.all(
|
|
2791
|
+
completedJobs.map(
|
|
2792
|
+
(job) => this.session.client.getConversationEvents(conversationId, job.job_id).catch(() => [])
|
|
2793
|
+
)
|
|
2794
|
+
);
|
|
2795
|
+
if (stopReplay()) return false;
|
|
2796
|
+
const eventsByJobId = new Map(
|
|
2797
|
+
completedJobs.map((job, i) => [job.job_id, eventLists[i] ?? []])
|
|
2798
|
+
);
|
|
2799
|
+
for (const step of plan) {
|
|
2800
|
+
if (stopReplay()) return false;
|
|
2801
|
+
if (step.kind === "steer") {
|
|
2738
2802
|
this.session.replayTurn(
|
|
2739
2803
|
conversationId,
|
|
2740
|
-
|
|
2804
|
+
[],
|
|
2741
2805
|
step.content,
|
|
2742
|
-
step.messageId
|
|
2806
|
+
step.messageId,
|
|
2807
|
+
true
|
|
2743
2808
|
);
|
|
2809
|
+
continue;
|
|
2744
2810
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
});
|
|
2752
|
-
}
|
|
2753
|
-
} catch {
|
|
2811
|
+
this.session.replayTurn(
|
|
2812
|
+
conversationId,
|
|
2813
|
+
eventsByJobId.get(step.jobId) ?? [],
|
|
2814
|
+
step.content,
|
|
2815
|
+
step.messageId
|
|
2816
|
+
);
|
|
2754
2817
|
}
|
|
2755
|
-
if (
|
|
2756
|
-
|
|
2818
|
+
if (stopReplay()) return false;
|
|
2819
|
+
if (completedJobs.length > 0) {
|
|
2820
|
+
this.emit({
|
|
2821
|
+
type: "versionsReady",
|
|
2822
|
+
conversationId,
|
|
2823
|
+
count: completedJobs.length
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
} catch {
|
|
2757
2827
|
}
|
|
2828
|
+
return !stopReplay();
|
|
2758
2829
|
}
|
|
2759
2830
|
// ── Internal: set active conversation ─────────────────────────
|
|
2760
2831
|
setActiveConversation(id) {
|