@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.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,26 @@ 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
|
-
|
|
2215
|
+
if (jobs.length === 0) {
|
|
2216
|
+
return userMessages.map((m) => ({
|
|
2217
|
+
kind: "steer",
|
|
2218
|
+
content: m.content,
|
|
2219
|
+
messageId: m.id
|
|
2220
|
+
}));
|
|
2221
|
+
}
|
|
2222
|
+
const firstLinked = jobs.findIndex((j) => linkOf(j) !== void 0);
|
|
2215
2223
|
if (firstLinked === -1) {
|
|
2216
|
-
return positional(
|
|
2224
|
+
return positional(jobs, userMessages);
|
|
2217
2225
|
}
|
|
2218
|
-
const cutover = linkOf(
|
|
2226
|
+
const cutover = linkOf(jobs[firstLinked]);
|
|
2219
2227
|
const steps = positional(
|
|
2220
|
-
|
|
2228
|
+
jobs.slice(0, firstLinked),
|
|
2221
2229
|
userMessages.slice(0, cutover)
|
|
2222
2230
|
);
|
|
2223
2231
|
let cursor = cutover;
|
|
@@ -2231,7 +2239,7 @@ function planRestore(args) {
|
|
|
2231
2239
|
}
|
|
2232
2240
|
cursor = stopAt + 1;
|
|
2233
2241
|
};
|
|
2234
|
-
for (const job of
|
|
2242
|
+
for (const job of jobs.slice(firstLinked)) {
|
|
2235
2243
|
const at = linkOf(job);
|
|
2236
2244
|
if (at !== void 0) {
|
|
2237
2245
|
drainTo(at);
|
|
@@ -2313,6 +2321,13 @@ var StreamManager = class {
|
|
|
2313
2321
|
* one the user is waiting on — see ``restore``.
|
|
2314
2322
|
*/
|
|
2315
2323
|
this.generation = 0;
|
|
2324
|
+
/**
|
|
2325
|
+
* Bumped every time a turn STARTS. `generation` does not move for a send
|
|
2326
|
+
* (only a pointer move does), and the streaming state returns to idle when a
|
|
2327
|
+
* turn ends — so neither can tell a restore that a turn ran inside one of
|
|
2328
|
+
* its awaits. This can.
|
|
2329
|
+
*/
|
|
2330
|
+
this.turnCounter = 0;
|
|
2316
2331
|
this.session = session;
|
|
2317
2332
|
this.attach();
|
|
2318
2333
|
}
|
|
@@ -2381,6 +2396,7 @@ var StreamManager = class {
|
|
|
2381
2396
|
target = await this.session.createNewConversation();
|
|
2382
2397
|
this.setActiveConversation(target);
|
|
2383
2398
|
}
|
|
2399
|
+
this.turnCounter++;
|
|
2384
2400
|
this.setState("streaming");
|
|
2385
2401
|
try {
|
|
2386
2402
|
await this.session.send(content, {
|
|
@@ -2417,6 +2433,7 @@ var StreamManager = class {
|
|
|
2417
2433
|
);
|
|
2418
2434
|
const lastUserMsg = userMsgs[userMsgs.length - 1];
|
|
2419
2435
|
if (!lastUserMsg) return;
|
|
2436
|
+
this.turnCounter++;
|
|
2420
2437
|
this.setState("streaming");
|
|
2421
2438
|
try {
|
|
2422
2439
|
await this.session.resendFromCheckpoint(
|
|
@@ -2617,7 +2634,9 @@ var StreamManager = class {
|
|
|
2617
2634
|
async restore(conversationId, gen) {
|
|
2618
2635
|
const superseded = () => gen !== this.generation;
|
|
2619
2636
|
if (superseded()) return;
|
|
2620
|
-
|
|
2637
|
+
const turn = this.turnCounter;
|
|
2638
|
+
const announcedRestoring = !this.session.isStreaming;
|
|
2639
|
+
if (announcedRestoring) this.setState("restoring");
|
|
2621
2640
|
let activeJobId = null;
|
|
2622
2641
|
try {
|
|
2623
2642
|
const res = await this.session.client.getActiveJob(conversationId);
|
|
@@ -2625,9 +2644,12 @@ var StreamManager = class {
|
|
|
2625
2644
|
} catch {
|
|
2626
2645
|
}
|
|
2627
2646
|
if (superseded()) return;
|
|
2647
|
+
await this.session.loadConversation(conversationId);
|
|
2648
|
+
if (superseded()) return;
|
|
2649
|
+
if (announcedRestoring && this.viewTakenOverByLiveTurn()) return;
|
|
2650
|
+
if (announcedRestoring && !await this.replayHistory(conversationId, gen, activeJobId, turn))
|
|
2651
|
+
return;
|
|
2628
2652
|
if (activeJobId) {
|
|
2629
|
-
await this.session.loadConversation(conversationId);
|
|
2630
|
-
if (superseded()) return;
|
|
2631
2653
|
this.setState("streaming");
|
|
2632
2654
|
try {
|
|
2633
2655
|
await this.session.reconnectToJob(activeJobId);
|
|
@@ -2638,76 +2660,125 @@ var StreamManager = class {
|
|
|
2638
2660
|
this.setState("idle");
|
|
2639
2661
|
}
|
|
2640
2662
|
} else {
|
|
2641
|
-
await this.session.loadConversation(conversationId);
|
|
2642
2663
|
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
|
-
|
|
2664
|
+
this.settleIdle();
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Has a live turn taken the block view over?
|
|
2669
|
+
*
|
|
2670
|
+
* ``_state`` is the SYNCHRONOUS authority and ``session.isStreaming`` lags it
|
|
2671
|
+
* by an await: ``send`` sets ``_state = "streaming"`` before its first await,
|
|
2672
|
+
* while the session only raises its flag inside ``processStream``, behind the
|
|
2673
|
+
* ``storage.addMessage`` write. For that whole window a send is underway —
|
|
2674
|
+
* composer cleared, optimistic bubble drawn — and the session flag still
|
|
2675
|
+
* reads false. Reading both closes the window from either end, since
|
|
2676
|
+
* ``reconnectToJob`` is the mirror case: it raises the session flag without
|
|
2677
|
+
* ever moving ``_state``.
|
|
2678
|
+
*/
|
|
2679
|
+
viewTakenOverByLiveTurn() {
|
|
2680
|
+
return this._state === "streaming" || this.session.isStreaming;
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Has a turn STARTED since ``turn`` was captured?
|
|
2684
|
+
*
|
|
2685
|
+
* ``viewTakenOverByLiveTurn`` reads the current state, so it cannot see a
|
|
2686
|
+
* turn that both started and ENDED inside one of the restore's awaits — a
|
|
2687
|
+
* send that fails fast (auth, rate limit) resolves in about the time the job
|
|
2688
|
+
* list takes, and leaves `_state` back at idle with its blocks already
|
|
2689
|
+
* rendered. A monotonic count is the only thing that survives a state that
|
|
2690
|
+
* has returned to where it started.
|
|
2691
|
+
*/
|
|
2692
|
+
turnStarted(since) {
|
|
2693
|
+
return this.turnCounter !== since;
|
|
2694
|
+
}
|
|
2695
|
+
/**
|
|
2696
|
+
* Replay a conversation's persisted history into the consumer's block view.
|
|
2697
|
+
*
|
|
2698
|
+
* Returns false when this restore lost the right to finish — a newer switch
|
|
2699
|
+
* superseded it, or a send took the view over — in which case the caller
|
|
2700
|
+
* must stop rather than finish. See ``restore``.
|
|
2701
|
+
*
|
|
2702
|
+
* ``activeJobId`` names the turn that is still running, if any. Its events
|
|
2703
|
+
* are NOT fetched here: they are the live stream the caller reconnects to
|
|
2704
|
+
* straight after. It is passed so ``planRestore`` can pair it with the prompt
|
|
2705
|
+
* that started it, which is emitted as a bubble with no events — the whole
|
|
2706
|
+
* reason a conversation reopened mid-turn now shows the message that started
|
|
2707
|
+
* that turn.
|
|
2708
|
+
*/
|
|
2709
|
+
async replayHistory(conversationId, gen, activeJobId, turn) {
|
|
2710
|
+
const stopReplay = () => gen !== this.generation || this.viewTakenOverByLiveTurn() || this.turnStarted(turn);
|
|
2711
|
+
try {
|
|
2712
|
+
const jobs = await this.session.client.get(`/v1/conversations/${encodeURIComponent(conversationId)}/jobs`);
|
|
2713
|
+
if (stopReplay()) return false;
|
|
2714
|
+
const completedJobs = jobs.filter(
|
|
2715
|
+
(j) => j.status === "completed" && j.job_id !== activeJobId
|
|
2716
|
+
);
|
|
2717
|
+
const userMessages = this.session.messages.filter(
|
|
2718
|
+
(m) => m.role === "user"
|
|
2719
|
+
);
|
|
2720
|
+
const runningJob = activeJobId ? jobs.find((j) => j.job_id === activeJobId) : void 0;
|
|
2721
|
+
const plan = planRestore({
|
|
2722
|
+
completedJobs: completedJobs.map((j) => ({
|
|
2723
|
+
job_id: j.job_id,
|
|
2724
|
+
message_id: j.message_id
|
|
2725
|
+
})),
|
|
2726
|
+
runningJob: runningJob && {
|
|
2727
|
+
job_id: runningJob.job_id,
|
|
2728
|
+
message_id: runningJob.message_id
|
|
2729
|
+
},
|
|
2730
|
+
// Completed jobs PLUS the ones still going. A send landing in the
|
|
2731
|
+
// probe window has a running job, so its prompt is claimed and does
|
|
2732
|
+
// not read as a steer replayed over the bubble the live send already
|
|
2733
|
+
// rendered. Failed and cancelled jobs are deliberately NOT claimed:
|
|
2734
|
+
// they produce no `turn` step, so claiming them would delete the
|
|
2735
|
+
// user's prompt from the restore entirely rather than show it as a
|
|
2736
|
+
// steer.
|
|
2737
|
+
claimedMessageIds: jobs.filter((j) => j.status !== "failed" && j.status !== "cancelled").map((j) => j.message_id),
|
|
2738
|
+
userMessages: userMessages.map((m) => ({
|
|
2739
|
+
id: m.id,
|
|
2740
|
+
content: m.content
|
|
2741
|
+
}))
|
|
2742
|
+
});
|
|
2743
|
+
const eventLists = await Promise.all(
|
|
2744
|
+
completedJobs.map(
|
|
2745
|
+
(job) => this.session.client.getConversationEvents(conversationId, job.job_id).catch(() => [])
|
|
2746
|
+
)
|
|
2747
|
+
);
|
|
2748
|
+
if (stopReplay()) return false;
|
|
2749
|
+
const eventsByJobId = new Map(
|
|
2750
|
+
completedJobs.map((job, i) => [job.job_id, eventLists[i] ?? []])
|
|
2751
|
+
);
|
|
2752
|
+
for (const step of plan) {
|
|
2753
|
+
if (stopReplay()) return false;
|
|
2754
|
+
if (step.kind === "steer") {
|
|
2691
2755
|
this.session.replayTurn(
|
|
2692
2756
|
conversationId,
|
|
2693
|
-
|
|
2757
|
+
[],
|
|
2694
2758
|
step.content,
|
|
2695
|
-
step.messageId
|
|
2759
|
+
step.messageId,
|
|
2760
|
+
true
|
|
2696
2761
|
);
|
|
2762
|
+
continue;
|
|
2697
2763
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
});
|
|
2705
|
-
}
|
|
2706
|
-
} catch {
|
|
2764
|
+
this.session.replayTurn(
|
|
2765
|
+
conversationId,
|
|
2766
|
+
eventsByJobId.get(step.jobId) ?? [],
|
|
2767
|
+
step.content,
|
|
2768
|
+
step.messageId
|
|
2769
|
+
);
|
|
2707
2770
|
}
|
|
2708
|
-
if (
|
|
2709
|
-
|
|
2771
|
+
if (stopReplay()) return false;
|
|
2772
|
+
if (completedJobs.length > 0) {
|
|
2773
|
+
this.emit({
|
|
2774
|
+
type: "versionsReady",
|
|
2775
|
+
conversationId,
|
|
2776
|
+
count: completedJobs.length
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
} catch {
|
|
2710
2780
|
}
|
|
2781
|
+
return !stopReplay();
|
|
2711
2782
|
}
|
|
2712
2783
|
// ── Internal: set active conversation ─────────────────────────
|
|
2713
2784
|
setActiveConversation(id) {
|