@astralform/js 6.0.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/dist/index.cjs +137 -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 +137 -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,19 @@ 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
|
-
const firstLinked =
|
|
2262
|
+
const firstLinked = jobs.findIndex((j) => linkOf(j) !== void 0);
|
|
2262
2263
|
if (firstLinked === -1) {
|
|
2263
|
-
return positional(
|
|
2264
|
+
return positional(jobs, userMessages);
|
|
2264
2265
|
}
|
|
2265
|
-
const cutover = linkOf(
|
|
2266
|
+
const cutover = linkOf(jobs[firstLinked]);
|
|
2266
2267
|
const steps = positional(
|
|
2267
|
-
|
|
2268
|
+
jobs.slice(0, firstLinked),
|
|
2268
2269
|
userMessages.slice(0, cutover)
|
|
2269
2270
|
);
|
|
2270
2271
|
let cursor = cutover;
|
|
@@ -2278,7 +2279,7 @@ function planRestore(args) {
|
|
|
2278
2279
|
}
|
|
2279
2280
|
cursor = stopAt + 1;
|
|
2280
2281
|
};
|
|
2281
|
-
for (const job of
|
|
2282
|
+
for (const job of jobs.slice(firstLinked)) {
|
|
2282
2283
|
const at = linkOf(job);
|
|
2283
2284
|
if (at !== void 0) {
|
|
2284
2285
|
drainTo(at);
|
|
@@ -2360,6 +2361,13 @@ var StreamManager = class {
|
|
|
2360
2361
|
* one the user is waiting on — see ``restore``.
|
|
2361
2362
|
*/
|
|
2362
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;
|
|
2363
2371
|
this.session = session;
|
|
2364
2372
|
this.attach();
|
|
2365
2373
|
}
|
|
@@ -2428,6 +2436,7 @@ var StreamManager = class {
|
|
|
2428
2436
|
target = await this.session.createNewConversation();
|
|
2429
2437
|
this.setActiveConversation(target);
|
|
2430
2438
|
}
|
|
2439
|
+
this.turnCounter++;
|
|
2431
2440
|
this.setState("streaming");
|
|
2432
2441
|
try {
|
|
2433
2442
|
await this.session.send(content, {
|
|
@@ -2464,6 +2473,7 @@ var StreamManager = class {
|
|
|
2464
2473
|
);
|
|
2465
2474
|
const lastUserMsg = userMsgs[userMsgs.length - 1];
|
|
2466
2475
|
if (!lastUserMsg) return;
|
|
2476
|
+
this.turnCounter++;
|
|
2467
2477
|
this.setState("streaming");
|
|
2468
2478
|
try {
|
|
2469
2479
|
await this.session.resendFromCheckpoint(
|
|
@@ -2664,7 +2674,9 @@ var StreamManager = class {
|
|
|
2664
2674
|
async restore(conversationId, gen) {
|
|
2665
2675
|
const superseded = () => gen !== this.generation;
|
|
2666
2676
|
if (superseded()) return;
|
|
2667
|
-
|
|
2677
|
+
const turn = this.turnCounter;
|
|
2678
|
+
const announcedRestoring = !this.session.isStreaming;
|
|
2679
|
+
if (announcedRestoring) this.setState("restoring");
|
|
2668
2680
|
let activeJobId = null;
|
|
2669
2681
|
try {
|
|
2670
2682
|
const res = await this.session.client.getActiveJob(conversationId);
|
|
@@ -2672,9 +2684,12 @@ var StreamManager = class {
|
|
|
2672
2684
|
} catch {
|
|
2673
2685
|
}
|
|
2674
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;
|
|
2675
2692
|
if (activeJobId) {
|
|
2676
|
-
await this.session.loadConversation(conversationId);
|
|
2677
|
-
if (superseded()) return;
|
|
2678
2693
|
this.setState("streaming");
|
|
2679
2694
|
try {
|
|
2680
2695
|
await this.session.reconnectToJob(activeJobId);
|
|
@@ -2685,76 +2700,125 @@ var StreamManager = class {
|
|
|
2685
2700
|
this.setState("idle");
|
|
2686
2701
|
}
|
|
2687
2702
|
} else {
|
|
2688
|
-
await this.session.loadConversation(conversationId);
|
|
2689
2703
|
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
|
-
|
|
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") {
|
|
2738
2795
|
this.session.replayTurn(
|
|
2739
2796
|
conversationId,
|
|
2740
|
-
|
|
2797
|
+
[],
|
|
2741
2798
|
step.content,
|
|
2742
|
-
step.messageId
|
|
2799
|
+
step.messageId,
|
|
2800
|
+
true
|
|
2743
2801
|
);
|
|
2802
|
+
continue;
|
|
2744
2803
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
});
|
|
2752
|
-
}
|
|
2753
|
-
} catch {
|
|
2804
|
+
this.session.replayTurn(
|
|
2805
|
+
conversationId,
|
|
2806
|
+
eventsByJobId.get(step.jobId) ?? [],
|
|
2807
|
+
step.content,
|
|
2808
|
+
step.messageId
|
|
2809
|
+
);
|
|
2754
2810
|
}
|
|
2755
|
-
if (
|
|
2756
|
-
|
|
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 {
|
|
2757
2820
|
}
|
|
2821
|
+
return !stopReplay();
|
|
2758
2822
|
}
|
|
2759
2823
|
// ── Internal: set active conversation ─────────────────────────
|
|
2760
2824
|
setActiveConversation(id) {
|