@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.d.cts
CHANGED
|
@@ -1532,6 +1532,13 @@ declare class StreamManager {
|
|
|
1532
1532
|
* one the user is waiting on — see ``restore``.
|
|
1533
1533
|
*/
|
|
1534
1534
|
private generation;
|
|
1535
|
+
/**
|
|
1536
|
+
* Bumped every time a turn STARTS. `generation` does not move for a send
|
|
1537
|
+
* (only a pointer move does), and the streaming state returns to idle when a
|
|
1538
|
+
* turn ends — so neither can tell a restore that a turn ran inside one of
|
|
1539
|
+
* its awaits. This can.
|
|
1540
|
+
*/
|
|
1541
|
+
private turnCounter;
|
|
1535
1542
|
constructor(session: ChatSession);
|
|
1536
1543
|
get state(): StreamState;
|
|
1537
1544
|
get activeConversationId(): string | null;
|
|
@@ -1608,6 +1615,45 @@ declare class StreamManager {
|
|
|
1608
1615
|
private settleIdle;
|
|
1609
1616
|
private finalizeStream;
|
|
1610
1617
|
private restore;
|
|
1618
|
+
/**
|
|
1619
|
+
* Has a live turn taken the block view over?
|
|
1620
|
+
*
|
|
1621
|
+
* ``_state`` is the SYNCHRONOUS authority and ``session.isStreaming`` lags it
|
|
1622
|
+
* by an await: ``send`` sets ``_state = "streaming"`` before its first await,
|
|
1623
|
+
* while the session only raises its flag inside ``processStream``, behind the
|
|
1624
|
+
* ``storage.addMessage`` write. For that whole window a send is underway —
|
|
1625
|
+
* composer cleared, optimistic bubble drawn — and the session flag still
|
|
1626
|
+
* reads false. Reading both closes the window from either end, since
|
|
1627
|
+
* ``reconnectToJob`` is the mirror case: it raises the session flag without
|
|
1628
|
+
* ever moving ``_state``.
|
|
1629
|
+
*/
|
|
1630
|
+
private viewTakenOverByLiveTurn;
|
|
1631
|
+
/**
|
|
1632
|
+
* Has a turn STARTED since ``turn`` was captured?
|
|
1633
|
+
*
|
|
1634
|
+
* ``viewTakenOverByLiveTurn`` reads the current state, so it cannot see a
|
|
1635
|
+
* turn that both started and ENDED inside one of the restore's awaits — a
|
|
1636
|
+
* send that fails fast (auth, rate limit) resolves in about the time the job
|
|
1637
|
+
* list takes, and leaves `_state` back at idle with its blocks already
|
|
1638
|
+
* rendered. A monotonic count is the only thing that survives a state that
|
|
1639
|
+
* has returned to where it started.
|
|
1640
|
+
*/
|
|
1641
|
+
private turnStarted;
|
|
1642
|
+
/**
|
|
1643
|
+
* Replay a conversation's persisted history into the consumer's block view.
|
|
1644
|
+
*
|
|
1645
|
+
* Returns false when this restore lost the right to finish — a newer switch
|
|
1646
|
+
* superseded it, or a send took the view over — in which case the caller
|
|
1647
|
+
* must stop rather than finish. See ``restore``.
|
|
1648
|
+
*
|
|
1649
|
+
* ``activeJobId`` names the turn that is still running, if any. Its events
|
|
1650
|
+
* are NOT fetched here: they are the live stream the caller reconnects to
|
|
1651
|
+
* straight after. It is passed so ``planRestore`` can pair it with the prompt
|
|
1652
|
+
* that started it, which is emitted as a bubble with no events — the whole
|
|
1653
|
+
* reason a conversation reopened mid-turn now shows the message that started
|
|
1654
|
+
* that turn.
|
|
1655
|
+
*/
|
|
1656
|
+
private replayHistory;
|
|
1611
1657
|
private setActiveConversation;
|
|
1612
1658
|
}
|
|
1613
1659
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1532,6 +1532,13 @@ declare class StreamManager {
|
|
|
1532
1532
|
* one the user is waiting on — see ``restore``.
|
|
1533
1533
|
*/
|
|
1534
1534
|
private generation;
|
|
1535
|
+
/**
|
|
1536
|
+
* Bumped every time a turn STARTS. `generation` does not move for a send
|
|
1537
|
+
* (only a pointer move does), and the streaming state returns to idle when a
|
|
1538
|
+
* turn ends — so neither can tell a restore that a turn ran inside one of
|
|
1539
|
+
* its awaits. This can.
|
|
1540
|
+
*/
|
|
1541
|
+
private turnCounter;
|
|
1535
1542
|
constructor(session: ChatSession);
|
|
1536
1543
|
get state(): StreamState;
|
|
1537
1544
|
get activeConversationId(): string | null;
|
|
@@ -1608,6 +1615,45 @@ declare class StreamManager {
|
|
|
1608
1615
|
private settleIdle;
|
|
1609
1616
|
private finalizeStream;
|
|
1610
1617
|
private restore;
|
|
1618
|
+
/**
|
|
1619
|
+
* Has a live turn taken the block view over?
|
|
1620
|
+
*
|
|
1621
|
+
* ``_state`` is the SYNCHRONOUS authority and ``session.isStreaming`` lags it
|
|
1622
|
+
* by an await: ``send`` sets ``_state = "streaming"`` before its first await,
|
|
1623
|
+
* while the session only raises its flag inside ``processStream``, behind the
|
|
1624
|
+
* ``storage.addMessage`` write. For that whole window a send is underway —
|
|
1625
|
+
* composer cleared, optimistic bubble drawn — and the session flag still
|
|
1626
|
+
* reads false. Reading both closes the window from either end, since
|
|
1627
|
+
* ``reconnectToJob`` is the mirror case: it raises the session flag without
|
|
1628
|
+
* ever moving ``_state``.
|
|
1629
|
+
*/
|
|
1630
|
+
private viewTakenOverByLiveTurn;
|
|
1631
|
+
/**
|
|
1632
|
+
* Has a turn STARTED since ``turn`` was captured?
|
|
1633
|
+
*
|
|
1634
|
+
* ``viewTakenOverByLiveTurn`` reads the current state, so it cannot see a
|
|
1635
|
+
* turn that both started and ENDED inside one of the restore's awaits — a
|
|
1636
|
+
* send that fails fast (auth, rate limit) resolves in about the time the job
|
|
1637
|
+
* list takes, and leaves `_state` back at idle with its blocks already
|
|
1638
|
+
* rendered. A monotonic count is the only thing that survives a state that
|
|
1639
|
+
* has returned to where it started.
|
|
1640
|
+
*/
|
|
1641
|
+
private turnStarted;
|
|
1642
|
+
/**
|
|
1643
|
+
* Replay a conversation's persisted history into the consumer's block view.
|
|
1644
|
+
*
|
|
1645
|
+
* Returns false when this restore lost the right to finish — a newer switch
|
|
1646
|
+
* superseded it, or a send took the view over — in which case the caller
|
|
1647
|
+
* must stop rather than finish. See ``restore``.
|
|
1648
|
+
*
|
|
1649
|
+
* ``activeJobId`` names the turn that is still running, if any. Its events
|
|
1650
|
+
* are NOT fetched here: they are the live stream the caller reconnects to
|
|
1651
|
+
* straight after. It is passed so ``planRestore`` can pair it with the prompt
|
|
1652
|
+
* that started it, which is emitted as a bubble with no events — the whole
|
|
1653
|
+
* reason a conversation reopened mid-turn now shows the message that started
|
|
1654
|
+
* that turn.
|
|
1655
|
+
*/
|
|
1656
|
+
private replayHistory;
|
|
1611
1657
|
private setActiveConversation;
|
|
1612
1658
|
}
|
|
1613
1659
|
|
package/dist/index.js
CHANGED
|
@@ -2194,9 +2194,10 @@ var ChatSession = class {
|
|
|
2194
2194
|
|
|
2195
2195
|
// src/restore-plan.ts
|
|
2196
2196
|
function planRestore(args) {
|
|
2197
|
-
const { completedJobs, userMessages } = args;
|
|
2197
|
+
const { completedJobs, runningJob, userMessages } = args;
|
|
2198
|
+
const jobs = runningJob ? [...completedJobs, runningJob] : completedJobs;
|
|
2198
2199
|
const claimed = new Set(
|
|
2199
|
-
(args.claimedMessageIds ??
|
|
2200
|
+
(args.claimedMessageIds ?? jobs.map((j) => j.message_id)).filter(
|
|
2200
2201
|
(id) => !!id
|
|
2201
2202
|
)
|
|
2202
2203
|
);
|
|
@@ -2205,19 +2206,19 @@ function planRestore(args) {
|
|
|
2205
2206
|
if (m.id) byId.set(m.id, i);
|
|
2206
2207
|
});
|
|
2207
2208
|
const linkOf = (j) => j.message_id ? byId.get(j.message_id) : void 0;
|
|
2208
|
-
const positional = (
|
|
2209
|
+
const positional = (slice, msgs) => slice.map((job, i) => ({
|
|
2209
2210
|
kind: "turn",
|
|
2210
2211
|
jobId: job.job_id,
|
|
2211
2212
|
content: msgs[i]?.content,
|
|
2212
2213
|
messageId: msgs[i]?.id
|
|
2213
2214
|
}));
|
|
2214
|
-
const firstLinked =
|
|
2215
|
+
const firstLinked = jobs.findIndex((j) => linkOf(j) !== void 0);
|
|
2215
2216
|
if (firstLinked === -1) {
|
|
2216
|
-
return positional(
|
|
2217
|
+
return positional(jobs, userMessages);
|
|
2217
2218
|
}
|
|
2218
|
-
const cutover = linkOf(
|
|
2219
|
+
const cutover = linkOf(jobs[firstLinked]);
|
|
2219
2220
|
const steps = positional(
|
|
2220
|
-
|
|
2221
|
+
jobs.slice(0, firstLinked),
|
|
2221
2222
|
userMessages.slice(0, cutover)
|
|
2222
2223
|
);
|
|
2223
2224
|
let cursor = cutover;
|
|
@@ -2231,7 +2232,7 @@ function planRestore(args) {
|
|
|
2231
2232
|
}
|
|
2232
2233
|
cursor = stopAt + 1;
|
|
2233
2234
|
};
|
|
2234
|
-
for (const job of
|
|
2235
|
+
for (const job of jobs.slice(firstLinked)) {
|
|
2235
2236
|
const at = linkOf(job);
|
|
2236
2237
|
if (at !== void 0) {
|
|
2237
2238
|
drainTo(at);
|
|
@@ -2313,6 +2314,13 @@ var StreamManager = class {
|
|
|
2313
2314
|
* one the user is waiting on — see ``restore``.
|
|
2314
2315
|
*/
|
|
2315
2316
|
this.generation = 0;
|
|
2317
|
+
/**
|
|
2318
|
+
* Bumped every time a turn STARTS. `generation` does not move for a send
|
|
2319
|
+
* (only a pointer move does), and the streaming state returns to idle when a
|
|
2320
|
+
* turn ends — so neither can tell a restore that a turn ran inside one of
|
|
2321
|
+
* its awaits. This can.
|
|
2322
|
+
*/
|
|
2323
|
+
this.turnCounter = 0;
|
|
2316
2324
|
this.session = session;
|
|
2317
2325
|
this.attach();
|
|
2318
2326
|
}
|
|
@@ -2381,6 +2389,7 @@ var StreamManager = class {
|
|
|
2381
2389
|
target = await this.session.createNewConversation();
|
|
2382
2390
|
this.setActiveConversation(target);
|
|
2383
2391
|
}
|
|
2392
|
+
this.turnCounter++;
|
|
2384
2393
|
this.setState("streaming");
|
|
2385
2394
|
try {
|
|
2386
2395
|
await this.session.send(content, {
|
|
@@ -2417,6 +2426,7 @@ var StreamManager = class {
|
|
|
2417
2426
|
);
|
|
2418
2427
|
const lastUserMsg = userMsgs[userMsgs.length - 1];
|
|
2419
2428
|
if (!lastUserMsg) return;
|
|
2429
|
+
this.turnCounter++;
|
|
2420
2430
|
this.setState("streaming");
|
|
2421
2431
|
try {
|
|
2422
2432
|
await this.session.resendFromCheckpoint(
|
|
@@ -2617,7 +2627,9 @@ var StreamManager = class {
|
|
|
2617
2627
|
async restore(conversationId, gen) {
|
|
2618
2628
|
const superseded = () => gen !== this.generation;
|
|
2619
2629
|
if (superseded()) return;
|
|
2620
|
-
|
|
2630
|
+
const turn = this.turnCounter;
|
|
2631
|
+
const announcedRestoring = !this.session.isStreaming;
|
|
2632
|
+
if (announcedRestoring) this.setState("restoring");
|
|
2621
2633
|
let activeJobId = null;
|
|
2622
2634
|
try {
|
|
2623
2635
|
const res = await this.session.client.getActiveJob(conversationId);
|
|
@@ -2625,9 +2637,12 @@ var StreamManager = class {
|
|
|
2625
2637
|
} catch {
|
|
2626
2638
|
}
|
|
2627
2639
|
if (superseded()) return;
|
|
2640
|
+
await this.session.loadConversation(conversationId);
|
|
2641
|
+
if (superseded()) return;
|
|
2642
|
+
if (announcedRestoring && this.viewTakenOverByLiveTurn()) return;
|
|
2643
|
+
if (announcedRestoring && !await this.replayHistory(conversationId, gen, activeJobId, turn))
|
|
2644
|
+
return;
|
|
2628
2645
|
if (activeJobId) {
|
|
2629
|
-
await this.session.loadConversation(conversationId);
|
|
2630
|
-
if (superseded()) return;
|
|
2631
2646
|
this.setState("streaming");
|
|
2632
2647
|
try {
|
|
2633
2648
|
await this.session.reconnectToJob(activeJobId);
|
|
@@ -2638,76 +2653,125 @@ var StreamManager = class {
|
|
|
2638
2653
|
this.setState("idle");
|
|
2639
2654
|
}
|
|
2640
2655
|
} else {
|
|
2641
|
-
await this.session.loadConversation(conversationId);
|
|
2642
2656
|
if (superseded()) return;
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2657
|
+
this.settleIdle();
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Has a live turn taken the block view over?
|
|
2662
|
+
*
|
|
2663
|
+
* ``_state`` is the SYNCHRONOUS authority and ``session.isStreaming`` lags it
|
|
2664
|
+
* by an await: ``send`` sets ``_state = "streaming"`` before its first await,
|
|
2665
|
+
* while the session only raises its flag inside ``processStream``, behind the
|
|
2666
|
+
* ``storage.addMessage`` write. For that whole window a send is underway —
|
|
2667
|
+
* composer cleared, optimistic bubble drawn — and the session flag still
|
|
2668
|
+
* reads false. Reading both closes the window from either end, since
|
|
2669
|
+
* ``reconnectToJob`` is the mirror case: it raises the session flag without
|
|
2670
|
+
* ever moving ``_state``.
|
|
2671
|
+
*/
|
|
2672
|
+
viewTakenOverByLiveTurn() {
|
|
2673
|
+
return this._state === "streaming" || this.session.isStreaming;
|
|
2674
|
+
}
|
|
2675
|
+
/**
|
|
2676
|
+
* Has a turn STARTED since ``turn`` was captured?
|
|
2677
|
+
*
|
|
2678
|
+
* ``viewTakenOverByLiveTurn`` reads the current state, so it cannot see a
|
|
2679
|
+
* turn that both started and ENDED inside one of the restore's awaits — a
|
|
2680
|
+
* send that fails fast (auth, rate limit) resolves in about the time the job
|
|
2681
|
+
* list takes, and leaves `_state` back at idle with its blocks already
|
|
2682
|
+
* rendered. A monotonic count is the only thing that survives a state that
|
|
2683
|
+
* has returned to where it started.
|
|
2684
|
+
*/
|
|
2685
|
+
turnStarted(since) {
|
|
2686
|
+
return this.turnCounter !== since;
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Replay a conversation's persisted history into the consumer's block view.
|
|
2690
|
+
*
|
|
2691
|
+
* Returns false when this restore lost the right to finish — a newer switch
|
|
2692
|
+
* superseded it, or a send took the view over — in which case the caller
|
|
2693
|
+
* must stop rather than finish. See ``restore``.
|
|
2694
|
+
*
|
|
2695
|
+
* ``activeJobId`` names the turn that is still running, if any. Its events
|
|
2696
|
+
* are NOT fetched here: they are the live stream the caller reconnects to
|
|
2697
|
+
* straight after. It is passed so ``planRestore`` can pair it with the prompt
|
|
2698
|
+
* that started it, which is emitted as a bubble with no events — the whole
|
|
2699
|
+
* reason a conversation reopened mid-turn now shows the message that started
|
|
2700
|
+
* that turn.
|
|
2701
|
+
*/
|
|
2702
|
+
async replayHistory(conversationId, gen, activeJobId, turn) {
|
|
2703
|
+
const stopReplay = () => gen !== this.generation || this.viewTakenOverByLiveTurn() || this.turnStarted(turn);
|
|
2704
|
+
try {
|
|
2705
|
+
const jobs = await this.session.client.get(`/v1/conversations/${encodeURIComponent(conversationId)}/jobs`);
|
|
2706
|
+
if (stopReplay()) return false;
|
|
2707
|
+
const completedJobs = jobs.filter(
|
|
2708
|
+
(j) => j.status === "completed" && j.job_id !== activeJobId
|
|
2709
|
+
);
|
|
2710
|
+
const userMessages = this.session.messages.filter(
|
|
2711
|
+
(m) => m.role === "user"
|
|
2712
|
+
);
|
|
2713
|
+
const runningJob = activeJobId ? jobs.find((j) => j.job_id === activeJobId) : void 0;
|
|
2714
|
+
const plan = planRestore({
|
|
2715
|
+
completedJobs: completedJobs.map((j) => ({
|
|
2716
|
+
job_id: j.job_id,
|
|
2717
|
+
message_id: j.message_id
|
|
2718
|
+
})),
|
|
2719
|
+
runningJob: runningJob && {
|
|
2720
|
+
job_id: runningJob.job_id,
|
|
2721
|
+
message_id: runningJob.message_id
|
|
2722
|
+
},
|
|
2723
|
+
// Completed jobs PLUS the ones still going. A send landing in the
|
|
2724
|
+
// probe window has a running job, so its prompt is claimed and does
|
|
2725
|
+
// not read as a steer replayed over the bubble the live send already
|
|
2726
|
+
// rendered. Failed and cancelled jobs are deliberately NOT claimed:
|
|
2727
|
+
// they produce no `turn` step, so claiming them would delete the
|
|
2728
|
+
// user's prompt from the restore entirely rather than show it as a
|
|
2729
|
+
// steer.
|
|
2730
|
+
claimedMessageIds: jobs.filter((j) => j.status !== "failed" && j.status !== "cancelled").map((j) => j.message_id),
|
|
2731
|
+
userMessages: userMessages.map((m) => ({
|
|
2732
|
+
id: m.id,
|
|
2733
|
+
content: m.content
|
|
2734
|
+
}))
|
|
2735
|
+
});
|
|
2736
|
+
const eventLists = await Promise.all(
|
|
2737
|
+
completedJobs.map(
|
|
2738
|
+
(job) => this.session.client.getConversationEvents(conversationId, job.job_id).catch(() => [])
|
|
2739
|
+
)
|
|
2740
|
+
);
|
|
2741
|
+
if (stopReplay()) return false;
|
|
2742
|
+
const eventsByJobId = new Map(
|
|
2743
|
+
completedJobs.map((job, i) => [job.job_id, eventLists[i] ?? []])
|
|
2744
|
+
);
|
|
2745
|
+
for (const step of plan) {
|
|
2746
|
+
if (stopReplay()) return false;
|
|
2747
|
+
if (step.kind === "steer") {
|
|
2691
2748
|
this.session.replayTurn(
|
|
2692
2749
|
conversationId,
|
|
2693
|
-
|
|
2750
|
+
[],
|
|
2694
2751
|
step.content,
|
|
2695
|
-
step.messageId
|
|
2752
|
+
step.messageId,
|
|
2753
|
+
true
|
|
2696
2754
|
);
|
|
2755
|
+
continue;
|
|
2697
2756
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
});
|
|
2705
|
-
}
|
|
2706
|
-
} catch {
|
|
2757
|
+
this.session.replayTurn(
|
|
2758
|
+
conversationId,
|
|
2759
|
+
eventsByJobId.get(step.jobId) ?? [],
|
|
2760
|
+
step.content,
|
|
2761
|
+
step.messageId
|
|
2762
|
+
);
|
|
2707
2763
|
}
|
|
2708
|
-
if (
|
|
2709
|
-
|
|
2764
|
+
if (stopReplay()) return false;
|
|
2765
|
+
if (completedJobs.length > 0) {
|
|
2766
|
+
this.emit({
|
|
2767
|
+
type: "versionsReady",
|
|
2768
|
+
conversationId,
|
|
2769
|
+
count: completedJobs.length
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
} catch {
|
|
2710
2773
|
}
|
|
2774
|
+
return !stopReplay();
|
|
2711
2775
|
}
|
|
2712
2776
|
// ── Internal: set active conversation ─────────────────────────
|
|
2713
2777
|
setActiveConversation(id) {
|