@astralform/js 4.9.1 → 5.1.0
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 +409 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -4
- package/dist/index.d.ts +154 -4
- package/dist/index.js +409 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -723,6 +723,11 @@ var AstralformClient = class {
|
|
|
723
723
|
// undefined so it matches the `url?: string` type and consumers that
|
|
724
724
|
// check `!== undefined` never receive a null.
|
|
725
725
|
url: raw.url ?? void 0,
|
|
726
|
+
// This mapping is an ALLOWLIST — a field the API returns and this
|
|
727
|
+
// function does not name is dropped silently, and no type error says so.
|
|
728
|
+
// `content_url` shipped that way and was invisible to every consumer.
|
|
729
|
+
contentUrl: raw.content_url ?? void 0,
|
|
730
|
+
posterUrl: raw.poster_url ?? void 0,
|
|
726
731
|
createdAt: raw.created_at
|
|
727
732
|
};
|
|
728
733
|
}
|
|
@@ -1376,6 +1381,17 @@ var ChatSession = class {
|
|
|
1376
1381
|
/** True while ``loadMoreConversations`` is in flight. */
|
|
1377
1382
|
this.isLoadingConversations = false;
|
|
1378
1383
|
this.messages = [];
|
|
1384
|
+
/**
|
|
1385
|
+
* Which conversation ``messages`` currently holds.
|
|
1386
|
+
*
|
|
1387
|
+
* Distinct from ``conversationId``, and the distinction is the point:
|
|
1388
|
+
* ``loadConversation`` moves the POINTER synchronously and installs the LIST
|
|
1389
|
+
* an await later, so for the whole duration of every load the two disagree.
|
|
1390
|
+
* Anything pairing a message with a conversation — ``regenerate`` above all —
|
|
1391
|
+
* has to read this one, or it will pair the previous conversation's last
|
|
1392
|
+
* message with the new conversation's id.
|
|
1393
|
+
*/
|
|
1394
|
+
this.messagesConversationId = null;
|
|
1379
1395
|
this.isStreaming = false;
|
|
1380
1396
|
this.agentStatus = null;
|
|
1381
1397
|
this.agents = [];
|
|
@@ -1408,6 +1424,43 @@ var ChatSession = class {
|
|
|
1408
1424
|
* is discarded instead.
|
|
1409
1425
|
*/
|
|
1410
1426
|
this.conversationsGeneration = 0;
|
|
1427
|
+
/**
|
|
1428
|
+
* Bumped by every ``loadConversation`` call, so an out-of-order fetch can
|
|
1429
|
+
* tell it is no longer the newest one and drop its result. Separate from
|
|
1430
|
+
* ``conversationsGeneration``, which guards the conversation LIST.
|
|
1431
|
+
*/
|
|
1432
|
+
this.loadGeneration = 0;
|
|
1433
|
+
/**
|
|
1434
|
+
* Ids of locally-created user messages the server has not acknowledged yet.
|
|
1435
|
+
*
|
|
1436
|
+
* Arrival time cannot answer "could the reply have included this?" on its
|
|
1437
|
+
* own. Every `loadConversation` in `StreamManager` sits behind the
|
|
1438
|
+
* active-job probe, so the ORDINARY ordering is that a send lands BEFORE the
|
|
1439
|
+
* load starts — and an arrival-time rule drops exactly those, losing the
|
|
1440
|
+
* prompt the user just sent while its stream is still running. Membership
|
|
1441
|
+
* here is set by `send` and cleared when a server row turns up carrying the
|
|
1442
|
+
* same turn, so the keep-decision no longer depends on which side of the
|
|
1443
|
+
* fetch the push landed on.
|
|
1444
|
+
*
|
|
1445
|
+
* The value is `serverRowsKnown` as of the `message_stop` that proved the
|
|
1446
|
+
* row committed, or 0 until then — including after the job response, which
|
|
1447
|
+
* hands back the id but starts the loop as a background task and so proves
|
|
1448
|
+
* nothing about the row. That stamp is what separates "the snapshot predates
|
|
1449
|
+
* the row" from "the server does not have this row" — see
|
|
1450
|
+
* `loadConversation`.
|
|
1451
|
+
*
|
|
1452
|
+
* It is an ANNOTATION ON `this.messages`: reconciliation only ever consults
|
|
1453
|
+
* entries of that array, so an id whose message has left it is dead weight.
|
|
1454
|
+
* `setMessages` is the single place the array is replaced, and it prunes.
|
|
1455
|
+
*/
|
|
1456
|
+
this.pendingUserMessages = /* @__PURE__ */ new Map();
|
|
1457
|
+
/**
|
|
1458
|
+
* Bumped once per completed turn, at `message_stop`, so a fetch can record
|
|
1459
|
+
* what was proven when it was ISSUED. A row proven committed before the
|
|
1460
|
+
* fetch went out must appear in its snapshot; one proven after may
|
|
1461
|
+
* legitimately be missing.
|
|
1462
|
+
*/
|
|
1463
|
+
this.serverRowsKnown = 0;
|
|
1411
1464
|
// Minimal in-session accumulation for the assistant message record.
|
|
1412
1465
|
// Only top-level ``text`` blocks contribute; subagent / tool output
|
|
1413
1466
|
// is tracked by the consumer's own block store.
|
|
@@ -1430,6 +1483,19 @@ var ChatSession = class {
|
|
|
1430
1483
|
this.toolRegistry = new ToolRegistry();
|
|
1431
1484
|
this.storage = storage ?? new InMemoryStorage();
|
|
1432
1485
|
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Replace the message list, keeping `pendingUserMessages` an annotation on
|
|
1488
|
+
* it. Every removal from the array goes through here — `push` is the only
|
|
1489
|
+
* other mutation and it cannot orphan an id.
|
|
1490
|
+
*/
|
|
1491
|
+
setMessages(next) {
|
|
1492
|
+
this.messages = next;
|
|
1493
|
+
if (this.pendingUserMessages.size === 0) return;
|
|
1494
|
+
const present = new Set(next.map((m) => m.id));
|
|
1495
|
+
for (const id of this.pendingUserMessages.keys()) {
|
|
1496
|
+
if (!present.has(id)) this.pendingUserMessages.delete(id);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1433
1499
|
on(handler) {
|
|
1434
1500
|
this.handlers.add(handler);
|
|
1435
1501
|
return () => {
|
|
@@ -1478,6 +1544,22 @@ var ChatSession = class {
|
|
|
1478
1544
|
}
|
|
1479
1545
|
if (this.isStreaming) return;
|
|
1480
1546
|
const conversationId = options?.conversationId ?? this.conversationId ?? void 0;
|
|
1547
|
+
let relocatedFrom = null;
|
|
1548
|
+
if (conversationId) {
|
|
1549
|
+
if (conversationId !== this.conversationId) {
|
|
1550
|
+
this.loadGeneration++;
|
|
1551
|
+
relocatedFrom = {
|
|
1552
|
+
messages: this.messages,
|
|
1553
|
+
messagesId: this.messagesConversationId,
|
|
1554
|
+
conversationId: this.conversationId,
|
|
1555
|
+
generation: this.loadGeneration,
|
|
1556
|
+
target: conversationId
|
|
1557
|
+
};
|
|
1558
|
+
this.setMessages([]);
|
|
1559
|
+
this.messagesConversationId = null;
|
|
1560
|
+
}
|
|
1561
|
+
this.conversationId = conversationId;
|
|
1562
|
+
}
|
|
1481
1563
|
const userMessage = {
|
|
1482
1564
|
id: generateId(),
|
|
1483
1565
|
conversationId: conversationId ?? "",
|
|
@@ -1487,9 +1569,11 @@ var ChatSession = class {
|
|
|
1487
1569
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1488
1570
|
};
|
|
1489
1571
|
if (conversationId) {
|
|
1490
|
-
await this.storage.addMessage(userMessage, conversationId)
|
|
1572
|
+
await this.storage.addMessage(userMessage, conversationId).catch(() => {
|
|
1573
|
+
});
|
|
1491
1574
|
}
|
|
1492
1575
|
this.messages.push(userMessage);
|
|
1576
|
+
this.pendingUserMessages.set(userMessage.id, 0);
|
|
1493
1577
|
const request = {
|
|
1494
1578
|
message: content,
|
|
1495
1579
|
conversation_id: conversationId,
|
|
@@ -1509,7 +1593,24 @@ var ChatSession = class {
|
|
|
1509
1593
|
reasoning_effort: options?.reasoningEffort,
|
|
1510
1594
|
temperature: options?.temperature
|
|
1511
1595
|
};
|
|
1512
|
-
|
|
1596
|
+
const wire = { reached: false };
|
|
1597
|
+
await this.processStream(request, wire);
|
|
1598
|
+
if (!wire.reached) {
|
|
1599
|
+
this.pendingUserMessages.delete(userMessage.id);
|
|
1600
|
+
const at = this.messages.indexOf(userMessage);
|
|
1601
|
+
if (at !== -1) this.messages.splice(at, 1);
|
|
1602
|
+
if (conversationId) {
|
|
1603
|
+
await this.storage.deleteMessage(userMessage.id).catch(() => {
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
if (relocatedFrom && wire.reached && this.loadGeneration === relocatedFrom.generation) {
|
|
1608
|
+
this.messagesConversationId = relocatedFrom.target;
|
|
1609
|
+
} else if (relocatedFrom && !wire.reached && this.loadGeneration === relocatedFrom.generation) {
|
|
1610
|
+
this.setMessages(relocatedFrom.messages);
|
|
1611
|
+
this.messagesConversationId = relocatedFrom.messagesId;
|
|
1612
|
+
this.conversationId = relocatedFrom.conversationId;
|
|
1613
|
+
}
|
|
1513
1614
|
}
|
|
1514
1615
|
async resendFromCheckpoint(messageId, newContent) {
|
|
1515
1616
|
if (this.isStreaming) return;
|
|
@@ -1526,12 +1627,13 @@ var ChatSession = class {
|
|
|
1526
1627
|
this.accumulatedText = "";
|
|
1527
1628
|
this.currentTextPath = null;
|
|
1528
1629
|
}
|
|
1529
|
-
async processStream(request) {
|
|
1630
|
+
async processStream(request, wire) {
|
|
1530
1631
|
this.isStreaming = true;
|
|
1531
1632
|
this.resetStreamingState();
|
|
1532
|
-
|
|
1633
|
+
const controller = new AbortController();
|
|
1634
|
+
this.abortController = controller;
|
|
1533
1635
|
try {
|
|
1534
|
-
await this.consumeJobStream(request);
|
|
1636
|
+
await this.consumeJobStream(request, wire);
|
|
1535
1637
|
} catch (err) {
|
|
1536
1638
|
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
1537
1639
|
this.emit({
|
|
@@ -1542,16 +1644,20 @@ var ChatSession = class {
|
|
|
1542
1644
|
});
|
|
1543
1645
|
}
|
|
1544
1646
|
} finally {
|
|
1545
|
-
this.
|
|
1546
|
-
|
|
1647
|
+
if (this.abortController === controller) {
|
|
1648
|
+
this.isStreaming = false;
|
|
1649
|
+
this.abortController = null;
|
|
1650
|
+
}
|
|
1547
1651
|
}
|
|
1548
1652
|
}
|
|
1549
|
-
async consumeJobStream(request) {
|
|
1653
|
+
async consumeJobStream(request, wire) {
|
|
1550
1654
|
const job = await this.client.createJob(request);
|
|
1655
|
+
if (wire) wire.reached = true;
|
|
1551
1656
|
this.currentJobId = job.job_id;
|
|
1552
1657
|
const conversationId = job.conversation_id;
|
|
1553
1658
|
if (!this.conversationId) {
|
|
1554
1659
|
this.conversationId = conversationId;
|
|
1660
|
+
this.messagesConversationId = conversationId;
|
|
1555
1661
|
}
|
|
1556
1662
|
if (!this.conversations.some((c) => c.id === conversationId)) {
|
|
1557
1663
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1572,13 +1678,29 @@ var ChatSession = class {
|
|
|
1572
1678
|
await this.storage.addMessage(lastMsg, conversationId).catch(() => {
|
|
1573
1679
|
});
|
|
1574
1680
|
}
|
|
1575
|
-
const
|
|
1681
|
+
const promptMessageId = job.message_id;
|
|
1682
|
+
if (promptMessageId && lastMsg?.role === "user" && this.pendingUserMessages.has(lastMsg.id)) {
|
|
1683
|
+
this.pendingUserMessages.delete(lastMsg.id);
|
|
1684
|
+
const clientMintedId = lastMsg.id;
|
|
1685
|
+
lastMsg.id = promptMessageId;
|
|
1686
|
+
this.pendingUserMessages.set(promptMessageId, 0);
|
|
1687
|
+
if (conversationId && clientMintedId !== promptMessageId) {
|
|
1688
|
+
await this.storage.deleteMessage(clientMintedId).catch(() => {
|
|
1689
|
+
});
|
|
1690
|
+
await this.storage.addMessage(lastMsg, conversationId).catch(() => {
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
const dupe = this.messages.findIndex(
|
|
1694
|
+
(m) => m !== lastMsg && m.id === promptMessageId
|
|
1695
|
+
);
|
|
1696
|
+
if (dupe !== -1) this.messages.splice(dupe, 1);
|
|
1697
|
+
}
|
|
1576
1698
|
this.lastSeq = -1;
|
|
1577
1699
|
this.submittedToolCallIds.clear();
|
|
1578
1700
|
await this.consumeEventStream(
|
|
1579
1701
|
job.job_id,
|
|
1580
1702
|
conversationId,
|
|
1581
|
-
|
|
1703
|
+
promptMessageId,
|
|
1582
1704
|
true
|
|
1583
1705
|
// executeClientTools
|
|
1584
1706
|
);
|
|
@@ -1587,7 +1709,7 @@ var ChatSession = class {
|
|
|
1587
1709
|
* Shared event consumption loop. Parses each wire event, updates
|
|
1588
1710
|
* minimal session state, and emits typed ChatEvents to consumers.
|
|
1589
1711
|
*/
|
|
1590
|
-
async consumeEventStream(jobId, conversationId,
|
|
1712
|
+
async consumeEventStream(jobId, conversationId, promptMessageId, executeClientTools) {
|
|
1591
1713
|
const signal = this.abortController?.signal;
|
|
1592
1714
|
for (let attempt = 0; ; attempt++) {
|
|
1593
1715
|
if (signal?.aborted) return;
|
|
@@ -1604,7 +1726,7 @@ var ChatSession = class {
|
|
|
1604
1726
|
sawTerminal = await this.pumpStream(
|
|
1605
1727
|
stream,
|
|
1606
1728
|
conversationId,
|
|
1607
|
-
|
|
1729
|
+
promptMessageId,
|
|
1608
1730
|
executeClientTools,
|
|
1609
1731
|
() => attemptController.abort()
|
|
1610
1732
|
);
|
|
@@ -1636,7 +1758,7 @@ var ChatSession = class {
|
|
|
1636
1758
|
* zombie — ``reader.read()`` will never settle — so we kill the fetch and
|
|
1637
1759
|
* throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.
|
|
1638
1760
|
*/
|
|
1639
|
-
async pumpStream(stream, conversationId,
|
|
1761
|
+
async pumpStream(stream, conversationId, promptMessageId, executeClientTools, onStall) {
|
|
1640
1762
|
let sawTerminal = false;
|
|
1641
1763
|
const iterator = stream[Symbol.asyncIterator]();
|
|
1642
1764
|
try {
|
|
@@ -1683,7 +1805,7 @@ var ChatSession = class {
|
|
|
1683
1805
|
await this.dispatchWireEvent(
|
|
1684
1806
|
parsed,
|
|
1685
1807
|
conversationId,
|
|
1686
|
-
|
|
1808
|
+
promptMessageId,
|
|
1687
1809
|
executeClientTools
|
|
1688
1810
|
);
|
|
1689
1811
|
}
|
|
@@ -1725,8 +1847,8 @@ var ChatSession = class {
|
|
|
1725
1847
|
}
|
|
1726
1848
|
}
|
|
1727
1849
|
}
|
|
1728
|
-
async dispatchWireEvent(wire, conversationId,
|
|
1729
|
-
this.applyWireSideEffects(wire, conversationId,
|
|
1850
|
+
async dispatchWireEvent(wire, conversationId, promptMessageId, executeClientTools) {
|
|
1851
|
+
this.applyWireSideEffects(wire, conversationId, promptMessageId, true);
|
|
1730
1852
|
const event = translateWireEvent(wire);
|
|
1731
1853
|
if (event) {
|
|
1732
1854
|
this.emit(event);
|
|
@@ -1744,7 +1866,9 @@ var ChatSession = class {
|
|
|
1744
1866
|
const results = await this.executeClientTools([request]);
|
|
1745
1867
|
await this.submitToolResultWithRetry({
|
|
1746
1868
|
conversation_id: conversationId,
|
|
1747
|
-
|
|
1869
|
+
// The message that TRIGGERED the tool calls, which is what the
|
|
1870
|
+
// backend stores it against — the prompt id, correctly.
|
|
1871
|
+
message_id: promptMessageId,
|
|
1748
1872
|
tool_results: results
|
|
1749
1873
|
});
|
|
1750
1874
|
this.submittedToolCallIds.add(callId);
|
|
@@ -1759,7 +1883,7 @@ var ChatSession = class {
|
|
|
1759
1883
|
* instead of re-typing the whole conversation event by event.
|
|
1760
1884
|
*/
|
|
1761
1885
|
replayWireEvent(wire, conversationId) {
|
|
1762
|
-
this.applyWireSideEffects(wire, conversationId, "");
|
|
1886
|
+
this.applyWireSideEffects(wire, conversationId, "", false);
|
|
1763
1887
|
const event = translateWireEvent(wire);
|
|
1764
1888
|
if (event) {
|
|
1765
1889
|
this.emit(event);
|
|
@@ -1770,37 +1894,52 @@ var ChatSession = class {
|
|
|
1770
1894
|
* the pure wire → ChatEvent mapping can live in translate.ts and be reused
|
|
1771
1895
|
* by the replay path.
|
|
1772
1896
|
*
|
|
1773
|
-
* ``
|
|
1774
|
-
*
|
|
1775
|
-
*
|
|
1897
|
+
* ``promptMessageId`` is the id of the USER turn that started this job —
|
|
1898
|
+
* `POST /v1/jobs` returns it and the backend tags the prompt with it
|
|
1899
|
+
* (`HumanMessage(content=..., id=message_id)`), which is what lets a restore
|
|
1900
|
+
* pair a job with its prompt by id instead of by position. It was previously
|
|
1901
|
+
* documented here as the ASSISTANT's id and used as one; there is no
|
|
1902
|
+
* server-assigned assistant id on the wire, so that row gets a local one.
|
|
1903
|
+
* Empty in the reconnect and conversation-switch replay paths, where the
|
|
1904
|
+
* messages have already been loaded from REST and must not be re-pushed —
|
|
1905
|
+
* so it doubles as the "is this a live send?" gate.
|
|
1776
1906
|
*/
|
|
1777
|
-
applyWireSideEffects(wire, conversationId,
|
|
1907
|
+
applyWireSideEffects(wire, conversationId, promptMessageId, live) {
|
|
1908
|
+
const ownsTurnState = live || !this.isStreaming;
|
|
1778
1909
|
switch (wire.type) {
|
|
1779
1910
|
case "message_start":
|
|
1780
|
-
this.resetStreamingState();
|
|
1911
|
+
if (ownsTurnState) this.resetStreamingState();
|
|
1781
1912
|
if (wire.model) {
|
|
1782
1913
|
this.modelDisplayName = wire.model;
|
|
1783
1914
|
}
|
|
1784
1915
|
return;
|
|
1785
1916
|
case "block_start":
|
|
1786
|
-
if (wire.kind === "text" && (!wire.parent_path || wire.parent_path.length === 0)) {
|
|
1917
|
+
if (ownsTurnState && wire.kind === "text" && (!wire.parent_path || wire.parent_path.length === 0)) {
|
|
1787
1918
|
this.currentTextPath = wire.path;
|
|
1788
1919
|
}
|
|
1789
1920
|
return;
|
|
1790
1921
|
case "block_delta":
|
|
1791
|
-
if (wire.delta.channel === "text" && this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
1922
|
+
if (ownsTurnState && wire.delta.channel === "text" && this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
1792
1923
|
this.accumulatedText += wire.delta.text;
|
|
1793
1924
|
}
|
|
1794
1925
|
return;
|
|
1795
1926
|
case "block_stop":
|
|
1796
|
-
if (this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
1927
|
+
if (ownsTurnState && this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
1797
1928
|
this.currentTextPath = null;
|
|
1798
1929
|
}
|
|
1799
1930
|
return;
|
|
1800
1931
|
case "message_stop":
|
|
1801
|
-
if (
|
|
1932
|
+
if (promptMessageId && this.pendingUserMessages.has(promptMessageId)) {
|
|
1933
|
+
this.pendingUserMessages.set(promptMessageId, ++this.serverRowsKnown);
|
|
1934
|
+
}
|
|
1935
|
+
if (promptMessageId) {
|
|
1802
1936
|
const assistantMessage = {
|
|
1803
|
-
id
|
|
1937
|
+
// NOT `promptMessageId` — that is the USER turn's id, which
|
|
1938
|
+
// `consumeJobStream` stamps onto the user row. Sharing it puts two
|
|
1939
|
+
// rows under one id, and `pendingUserMessages` is id-keyed. No
|
|
1940
|
+
// server-assigned assistant id exists on the wire, so this row is
|
|
1941
|
+
// local until a REST load replaces it.
|
|
1942
|
+
id: generateId(),
|
|
1804
1943
|
conversationId,
|
|
1805
1944
|
role: "assistant",
|
|
1806
1945
|
content: this.accumulatedText,
|
|
@@ -1811,8 +1950,10 @@ var ChatSession = class {
|
|
|
1811
1950
|
this.storage.addMessage(assistantMessage, conversationId).catch(() => {
|
|
1812
1951
|
});
|
|
1813
1952
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1953
|
+
if (ownsTurnState) {
|
|
1954
|
+
this.isStreaming = false;
|
|
1955
|
+
this.currentJobId = null;
|
|
1956
|
+
}
|
|
1816
1957
|
return;
|
|
1817
1958
|
case "custom":
|
|
1818
1959
|
if (wire.name === "title_generated") {
|
|
@@ -1846,9 +1987,27 @@ var ChatSession = class {
|
|
|
1846
1987
|
* Used before reconnectToJob — SSE replay handles event replay.
|
|
1847
1988
|
*/
|
|
1848
1989
|
async loadConversation(id) {
|
|
1990
|
+
const load = ++this.loadGeneration;
|
|
1849
1991
|
this.conversationId = id;
|
|
1850
|
-
this.resetStreamingState();
|
|
1851
|
-
|
|
1992
|
+
if (!this.isStreaming) this.resetStreamingState();
|
|
1993
|
+
const rowsKnownAtIssue = this.serverRowsKnown;
|
|
1994
|
+
const messages = await this.client.getMessages(id).catch(() => this.storage.fetchMessages(id));
|
|
1995
|
+
if (load !== this.loadGeneration) return;
|
|
1996
|
+
const pending = this.messages.filter(
|
|
1997
|
+
(m) => this.pendingUserMessages.has(m.id) && m.conversationId === id
|
|
1998
|
+
);
|
|
1999
|
+
const stillPending = pending.filter((m) => {
|
|
2000
|
+
if (messages.some((f) => f.id === m.id)) return false;
|
|
2001
|
+
const knownAt = this.pendingUserMessages.get(m.id) ?? 0;
|
|
2002
|
+
return knownAt === 0 || knownAt > rowsKnownAtIssue;
|
|
2003
|
+
});
|
|
2004
|
+
for (const m of pending) {
|
|
2005
|
+
if (!stillPending.includes(m)) this.pendingUserMessages.delete(m.id);
|
|
2006
|
+
}
|
|
2007
|
+
this.setMessages(
|
|
2008
|
+
stillPending.length ? [...messages, ...stillPending] : messages
|
|
2009
|
+
);
|
|
2010
|
+
this.messagesConversationId = id;
|
|
1852
2011
|
}
|
|
1853
2012
|
/**
|
|
1854
2013
|
* Reconnect to a running job's SSE stream (e.g. after page reload).
|
|
@@ -1861,7 +2020,8 @@ var ChatSession = class {
|
|
|
1861
2020
|
this.lastSeq = -1;
|
|
1862
2021
|
this.submittedToolCallIds.clear();
|
|
1863
2022
|
this.resetStreamingState();
|
|
1864
|
-
|
|
2023
|
+
const controller = new AbortController();
|
|
2024
|
+
this.abortController = controller;
|
|
1865
2025
|
try {
|
|
1866
2026
|
await this.consumeEventStream(
|
|
1867
2027
|
jobId,
|
|
@@ -1878,8 +2038,10 @@ var ChatSession = class {
|
|
|
1878
2038
|
blockPath: null
|
|
1879
2039
|
});
|
|
1880
2040
|
} finally {
|
|
1881
|
-
this.
|
|
1882
|
-
|
|
2041
|
+
if (this.abortController === controller) {
|
|
2042
|
+
this.isStreaming = false;
|
|
2043
|
+
this.abortController = null;
|
|
2044
|
+
}
|
|
1883
2045
|
}
|
|
1884
2046
|
}
|
|
1885
2047
|
/** Detach from the SSE stream without cancelling the job. */
|
|
@@ -1890,25 +2052,54 @@ var ChatSession = class {
|
|
|
1890
2052
|
this.resetStreamingState();
|
|
1891
2053
|
this.emit({ type: "disconnected" });
|
|
1892
2054
|
}
|
|
1893
|
-
/**
|
|
1894
|
-
|
|
2055
|
+
/**
|
|
2056
|
+
* Cancel the running turn: stop the job server-side and tear the stream
|
|
2057
|
+
* down, WITHOUT ending the session. A turn-level cancel is not a
|
|
2058
|
+
* session-level teardown, so unlike `disconnect()` this leaves the protocol
|
|
2059
|
+
* registry alone — the SDK never auto-registers adapters, so clearing them
|
|
2060
|
+
* on a Stop press would silently kill embedded-resource rendering for the
|
|
2061
|
+
* rest of the session with nothing to re-register it.
|
|
2062
|
+
*/
|
|
2063
|
+
cancelTurn() {
|
|
1895
2064
|
if (this.currentJobId) {
|
|
1896
2065
|
this.client.cancelJob(this.currentJobId).catch(() => {
|
|
1897
2066
|
});
|
|
1898
2067
|
}
|
|
1899
2068
|
this.detach();
|
|
1900
2069
|
this.currentJobId = null;
|
|
2070
|
+
for (const [id, knownAt] of this.pendingUserMessages) {
|
|
2071
|
+
if (knownAt === 0) this.pendingUserMessages.delete(id);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
/** Stop the job and end the session's activity (explicit user action). */
|
|
2075
|
+
disconnect() {
|
|
2076
|
+
this.cancelTurn();
|
|
1901
2077
|
this.protocols.clear();
|
|
1902
2078
|
}
|
|
2079
|
+
/**
|
|
2080
|
+
* A pointer move happened above this layer, so any `loadConversation` in
|
|
2081
|
+
* flight must lose to it. `StreamManager` owns a pointer of its own and
|
|
2082
|
+
* moves it before this one; without this the two halves would gate on
|
|
2083
|
+
* counters that bump at different instants — `generation` synchronously in
|
|
2084
|
+
* `setActiveConversation`, `loadGeneration` only once the switch's own load
|
|
2085
|
+
* actually runs, which is behind the active-job probe.
|
|
2086
|
+
*/
|
|
2087
|
+
invalidateLoadsInFlight() {
|
|
2088
|
+
this.loadGeneration++;
|
|
2089
|
+
}
|
|
1903
2090
|
async createNewConversation() {
|
|
1904
2091
|
const id = generateId();
|
|
2092
|
+
const load = this.loadGeneration;
|
|
1905
2093
|
const conversation = await this.storage.createConversation(
|
|
1906
2094
|
id,
|
|
1907
2095
|
"New Conversation"
|
|
1908
2096
|
);
|
|
1909
2097
|
this.conversations.unshift(conversation);
|
|
2098
|
+
if (load !== this.loadGeneration) return id;
|
|
2099
|
+
this.loadGeneration++;
|
|
1910
2100
|
this.conversationId = id;
|
|
1911
|
-
this.
|
|
2101
|
+
this.setMessages([]);
|
|
2102
|
+
this.messagesConversationId = id;
|
|
1912
2103
|
return id;
|
|
1913
2104
|
}
|
|
1914
2105
|
/**
|
|
@@ -1927,7 +2118,7 @@ var ChatSession = class {
|
|
|
1927
2118
|
*/
|
|
1928
2119
|
replayTurn(id, events, userMessageContent, userMessageId, isSteer = false) {
|
|
1929
2120
|
this.conversationId = id;
|
|
1930
|
-
this.resetStreamingState();
|
|
2121
|
+
if (!this.isStreaming) this.resetStreamingState();
|
|
1931
2122
|
if (userMessageContent) {
|
|
1932
2123
|
this.emit({
|
|
1933
2124
|
type: "user_message",
|
|
@@ -1958,11 +2149,17 @@ var ChatSession = class {
|
|
|
1958
2149
|
* job's events.
|
|
1959
2150
|
*/
|
|
1960
2151
|
async switchConversation(id, jobId) {
|
|
1961
|
-
const
|
|
1962
|
-
|
|
2152
|
+
const loading = this.loadConversation(id);
|
|
2153
|
+
const token = this.loadGeneration;
|
|
2154
|
+
const [loadResult, eventsResult] = await Promise.allSettled([
|
|
2155
|
+
loading,
|
|
1963
2156
|
this.client.getConversationEvents(id, jobId)
|
|
1964
2157
|
]);
|
|
1965
|
-
|
|
2158
|
+
if (token !== this.loadGeneration) return;
|
|
2159
|
+
if (loadResult.status === "rejected") {
|
|
2160
|
+
this.setMessages([]);
|
|
2161
|
+
this.messagesConversationId = id;
|
|
2162
|
+
}
|
|
1966
2163
|
this.replayTurn(
|
|
1967
2164
|
id,
|
|
1968
2165
|
eventsResult.status === "fulfilled" ? eventsResult.value : []
|
|
@@ -2053,8 +2250,10 @@ var ChatSession = class {
|
|
|
2053
2250
|
}
|
|
2054
2251
|
this.conversations = this.conversations.filter((c) => c.id !== id);
|
|
2055
2252
|
if (this.conversationId === id) {
|
|
2253
|
+
this.loadGeneration++;
|
|
2056
2254
|
this.conversationId = null;
|
|
2057
|
-
this.
|
|
2255
|
+
this.setMessages([]);
|
|
2256
|
+
this.messagesConversationId = null;
|
|
2058
2257
|
}
|
|
2059
2258
|
}
|
|
2060
2259
|
toggleClientTool(name) {
|
|
@@ -2070,6 +2269,11 @@ var ChatSession = class {
|
|
|
2070
2269
|
// src/restore-plan.ts
|
|
2071
2270
|
function planRestore(args) {
|
|
2072
2271
|
const { completedJobs, userMessages } = args;
|
|
2272
|
+
const claimed = new Set(
|
|
2273
|
+
(args.claimedMessageIds ?? completedJobs.map((j) => j.message_id)).filter(
|
|
2274
|
+
(id) => !!id
|
|
2275
|
+
)
|
|
2276
|
+
);
|
|
2073
2277
|
const byId = /* @__PURE__ */ new Map();
|
|
2074
2278
|
userMessages.forEach((m, i) => {
|
|
2075
2279
|
if (m.id) byId.set(m.id, i);
|
|
@@ -2091,7 +2295,7 @@ function planRestore(args) {
|
|
|
2091
2295
|
userMessages.slice(0, cutover)
|
|
2092
2296
|
);
|
|
2093
2297
|
let cursor = cutover;
|
|
2094
|
-
const isSteer = (m) => !!m?.id && !
|
|
2298
|
+
const isSteer = (m) => !!m?.id && !claimed.has(m.id);
|
|
2095
2299
|
const drainTo = (stopAt) => {
|
|
2096
2300
|
while (cursor < stopAt) {
|
|
2097
2301
|
const m = userMessages[cursor++];
|
|
@@ -2177,6 +2381,12 @@ var StreamManager = class {
|
|
|
2177
2381
|
this._backgroundJobs = /* @__PURE__ */ new Map();
|
|
2178
2382
|
this.handlers = [];
|
|
2179
2383
|
this.unsub = null;
|
|
2384
|
+
/**
|
|
2385
|
+
* Bumped every time the active conversation moves. An async sequence that
|
|
2386
|
+
* captures it can then tell, at each await boundary, whether it is still the
|
|
2387
|
+
* one the user is waiting on — see ``restore``.
|
|
2388
|
+
*/
|
|
2389
|
+
this.generation = 0;
|
|
2180
2390
|
this.session = session;
|
|
2181
2391
|
this.attach();
|
|
2182
2392
|
}
|
|
@@ -2227,7 +2437,7 @@ var StreamManager = class {
|
|
|
2227
2437
|
event
|
|
2228
2438
|
});
|
|
2229
2439
|
if (event.type === ChatEventType.MessageStop) {
|
|
2230
|
-
if (this._state === "streaming") {
|
|
2440
|
+
if (this._state === "streaming" && !this.session.isStreaming) {
|
|
2231
2441
|
this.setState("idle");
|
|
2232
2442
|
}
|
|
2233
2443
|
}
|
|
@@ -2240,13 +2450,21 @@ var StreamManager = class {
|
|
|
2240
2450
|
);
|
|
2241
2451
|
}
|
|
2242
2452
|
if (this._state === "streaming") return;
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
this.
|
|
2453
|
+
let target = this._activeConversationId;
|
|
2454
|
+
if (!target) {
|
|
2455
|
+
target = await this.session.createNewConversation();
|
|
2456
|
+
this.setActiveConversation(target);
|
|
2246
2457
|
}
|
|
2247
2458
|
this.setState("streaming");
|
|
2248
2459
|
try {
|
|
2249
2460
|
await this.session.send(content, {
|
|
2461
|
+
// Address the send explicitly. `ChatSession.send` otherwise falls back
|
|
2462
|
+
// to `session.conversationId`, which LAGS this pointer: a restore
|
|
2463
|
+
// assigns it synchronously but only reaches the next switch's own
|
|
2464
|
+
// `loadConversation` an await later, so between the two the session
|
|
2465
|
+
// still names the conversation the user left. The manager's pointer
|
|
2466
|
+
// moved the moment the user clicked; it is the authority.
|
|
2467
|
+
conversationId: target ?? void 0,
|
|
2250
2468
|
agentName: options?.agentName,
|
|
2251
2469
|
uploadIds: options?.uploadIds,
|
|
2252
2470
|
planMode: options?.planMode,
|
|
@@ -2265,6 +2483,9 @@ var StreamManager = class {
|
|
|
2265
2483
|
// ── Regenerate ────────────────────────────────────────────────
|
|
2266
2484
|
async regenerate() {
|
|
2267
2485
|
if (this._state === "streaming") return;
|
|
2486
|
+
if (this.session.messagesConversationId !== this._activeConversationId) {
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2268
2489
|
const userMsgs = this.session.messages.filter(
|
|
2269
2490
|
(m) => m.role === "user"
|
|
2270
2491
|
);
|
|
@@ -2301,44 +2522,78 @@ var StreamManager = class {
|
|
|
2301
2522
|
async switchTo(conversationId, opts) {
|
|
2302
2523
|
if (conversationId === this._activeConversationId) return;
|
|
2303
2524
|
const targetHadBackgroundJob = this._backgroundJobs.has(conversationId);
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
if (oldConvId && jobId) {
|
|
2308
|
-
this._backgroundJobs.set(oldConvId, jobId);
|
|
2309
|
-
this.emit({
|
|
2310
|
-
type: "backgroundJobsChanged",
|
|
2311
|
-
jobs: this._backgroundJobs
|
|
2312
|
-
});
|
|
2313
|
-
}
|
|
2314
|
-
this.session.detach();
|
|
2315
|
-
}
|
|
2316
|
-
if (this._backgroundJobs.has(conversationId)) {
|
|
2525
|
+
this.detachStreamingTurn();
|
|
2526
|
+
const parkedJobId = this._backgroundJobs.get(conversationId);
|
|
2527
|
+
if (parkedJobId !== void 0) {
|
|
2317
2528
|
this._backgroundJobs.delete(conversationId);
|
|
2318
2529
|
this.emit({
|
|
2319
2530
|
type: "backgroundJobsChanged",
|
|
2320
2531
|
jobs: this._backgroundJobs
|
|
2321
2532
|
});
|
|
2322
2533
|
}
|
|
2323
|
-
this.setActiveConversation(conversationId);
|
|
2534
|
+
const gen = this.setActiveConversation(conversationId);
|
|
2324
2535
|
if (opts?.skipHistoryReplay && !targetHadBackgroundJob) {
|
|
2325
2536
|
let activeJobId = null;
|
|
2326
2537
|
try {
|
|
2327
2538
|
activeJobId = (await this.session.client.getActiveJob(conversationId)).jobId;
|
|
2328
2539
|
} catch {
|
|
2329
2540
|
}
|
|
2541
|
+
if (gen !== this.generation) return;
|
|
2330
2542
|
if (!activeJobId) {
|
|
2331
|
-
|
|
2332
|
-
|
|
2543
|
+
try {
|
|
2544
|
+
await this.session.loadConversation(conversationId);
|
|
2545
|
+
} finally {
|
|
2546
|
+
if (gen === this.generation) this.settleIdle();
|
|
2547
|
+
}
|
|
2333
2548
|
return;
|
|
2334
2549
|
}
|
|
2335
2550
|
}
|
|
2336
|
-
|
|
2551
|
+
let tookOver = false;
|
|
2552
|
+
try {
|
|
2553
|
+
await this.restore(conversationId, gen);
|
|
2554
|
+
tookOver = gen === this.generation;
|
|
2555
|
+
} finally {
|
|
2556
|
+
const supersededOntoSame = gen !== this.generation && this._activeConversationId === conversationId;
|
|
2557
|
+
const stillExists = this.session.conversations.some(
|
|
2558
|
+
(c) => c.id === conversationId
|
|
2559
|
+
);
|
|
2560
|
+
if (parkedJobId !== void 0 && !tookOver && !supersededOntoSame && stillExists) {
|
|
2561
|
+
this._backgroundJobs.set(conversationId, parkedJobId);
|
|
2562
|
+
this.emit({
|
|
2563
|
+
type: "backgroundJobsChanged",
|
|
2564
|
+
jobs: this._backgroundJobs
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
if (!tookOver && gen === this.generation && this._state === "restoring") {
|
|
2568
|
+
this.settleIdle();
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2337
2571
|
}
|
|
2338
2572
|
// ── Create / rename / delete conversation ─────────────────────
|
|
2573
|
+
/**
|
|
2574
|
+
* Create a conversation and make it active.
|
|
2575
|
+
*
|
|
2576
|
+
* The returned id is NOT guaranteed to be the active conversation: if a
|
|
2577
|
+
* switch lands inside the storage round-trip, this declines the pointer move
|
|
2578
|
+
* so the newer one wins, and no `conversationChanged` fires for the new id.
|
|
2579
|
+
* A caller that routes on the return value should `switchTo(id)` rather than
|
|
2580
|
+
* assume it is current — that call is not a no-op in the declined case.
|
|
2581
|
+
*/
|
|
2339
2582
|
async createConversation() {
|
|
2340
|
-
|
|
2583
|
+
this.detachStreamingTurn();
|
|
2584
|
+
let id;
|
|
2585
|
+
try {
|
|
2586
|
+
id = await this.session.createNewConversation();
|
|
2587
|
+
} catch (err) {
|
|
2588
|
+
this.settleIdle();
|
|
2589
|
+
throw err;
|
|
2590
|
+
}
|
|
2591
|
+
if (this.session.conversationId !== id) {
|
|
2592
|
+
this.settleIdle();
|
|
2593
|
+
return id;
|
|
2594
|
+
}
|
|
2341
2595
|
this.setActiveConversation(id);
|
|
2596
|
+
this.settleIdle();
|
|
2342
2597
|
return id;
|
|
2343
2598
|
}
|
|
2344
2599
|
/**
|
|
@@ -2349,16 +2604,34 @@ var StreamManager = class {
|
|
|
2349
2604
|
await this.session.renameConversation(id, title);
|
|
2350
2605
|
}
|
|
2351
2606
|
async deleteConversation(id) {
|
|
2352
|
-
|
|
2353
|
-
this.
|
|
2607
|
+
const wasActive = this._activeConversationId === id;
|
|
2608
|
+
const cancelled = wasActive && this._state === "streaming";
|
|
2609
|
+
if (cancelled) {
|
|
2610
|
+
this.session.cancelTurn();
|
|
2611
|
+
this._state = "idle";
|
|
2612
|
+
}
|
|
2613
|
+
try {
|
|
2614
|
+
await this.session.deleteConversation(id);
|
|
2615
|
+
} catch (err) {
|
|
2616
|
+
if (cancelled) this.setState("idle");
|
|
2617
|
+
throw err;
|
|
2618
|
+
}
|
|
2354
2619
|
if (this._activeConversationId === id) {
|
|
2355
|
-
this.
|
|
2356
|
-
this.
|
|
2620
|
+
this.setActiveConversation(null);
|
|
2621
|
+
this.settleIdle();
|
|
2622
|
+
}
|
|
2623
|
+
const parkedJobId = this._backgroundJobs.get(id);
|
|
2624
|
+
if (this._backgroundJobs.delete(id)) {
|
|
2625
|
+
if (parkedJobId) {
|
|
2626
|
+
this.session.client.cancelJob(parkedJobId).catch(() => {
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
this.emit({ type: "backgroundJobsChanged", jobs: this._backgroundJobs });
|
|
2357
2630
|
}
|
|
2358
2631
|
}
|
|
2359
2632
|
// ── Stop (explicit cancel) ────────────────────────────────────
|
|
2360
2633
|
stop() {
|
|
2361
|
-
this.session.
|
|
2634
|
+
this.session.cancelTurn();
|
|
2362
2635
|
this.setState("idle");
|
|
2363
2636
|
}
|
|
2364
2637
|
// ── Cleanup ───────────────────────────────────────────────────
|
|
@@ -2370,34 +2643,80 @@ var StreamManager = class {
|
|
|
2370
2643
|
this.handlers = [];
|
|
2371
2644
|
}
|
|
2372
2645
|
// ── Internal: helpers ──────────────────────────────────────────
|
|
2646
|
+
/**
|
|
2647
|
+
* Park a streaming turn as a background job and detach from its SSE stream.
|
|
2648
|
+
*
|
|
2649
|
+
* Every method that relocates the active conversation has to do this before
|
|
2650
|
+
* announcing a new state. Announcing `idle` while `session.isStreaming` is
|
|
2651
|
+
* still true is worse than announcing nothing: `manager.send` no longer bails
|
|
2652
|
+
* on the streaming state, calls `session.send`, and THAT bails on its own
|
|
2653
|
+
* `isStreaming` — so the message is never posted, no error is emitted, and
|
|
2654
|
+
* the composer looks ready the whole time.
|
|
2655
|
+
*/
|
|
2656
|
+
detachStreamingTurn() {
|
|
2657
|
+
if (this._state !== "streaming") return;
|
|
2658
|
+
const oldConvId = this._activeConversationId;
|
|
2659
|
+
const jobId = this.session.currentJobId;
|
|
2660
|
+
if (oldConvId && jobId) {
|
|
2661
|
+
this._backgroundJobs.set(oldConvId, jobId);
|
|
2662
|
+
this.emit({ type: "backgroundJobsChanged", jobs: this._backgroundJobs });
|
|
2663
|
+
}
|
|
2664
|
+
this.session.detach();
|
|
2665
|
+
this.session.currentJobId = null;
|
|
2666
|
+
this._state = "idle";
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Announce `idle` unless a turn is actually streaming.
|
|
2670
|
+
*
|
|
2671
|
+
* A `send` can land inside any of the switch paths — the fast path most
|
|
2672
|
+
* easily, since it deliberately stays out of `restoring` and so leaves the
|
|
2673
|
+
* composer live for the whole probe. `send` sets `streaming` and does not
|
|
2674
|
+
* bump the generation, so the path resumes, passes its supersession check,
|
|
2675
|
+
* and would announce a ready composer over a running stream. From there
|
|
2676
|
+
* `finalizeStream` and the `message_stop` branch both no-op (they only act
|
|
2677
|
+
* on `streaming`), so it stays `idle` for the whole turn — and the next send
|
|
2678
|
+
* reaches `session.send`, which bails on its own `isStreaming`: message
|
|
2679
|
+
* never posted, no error, composer ready throughout.
|
|
2680
|
+
*/
|
|
2681
|
+
settleIdle() {
|
|
2682
|
+
if (this._state === "streaming") return;
|
|
2683
|
+
this.setState("idle");
|
|
2684
|
+
}
|
|
2373
2685
|
finalizeStream() {
|
|
2374
2686
|
if (this._state === "streaming") {
|
|
2375
2687
|
this.setState("idle");
|
|
2376
2688
|
}
|
|
2377
2689
|
}
|
|
2378
2690
|
// ── Internal: restore ─────────────────────────────────────────
|
|
2379
|
-
async restore(conversationId) {
|
|
2380
|
-
this.
|
|
2691
|
+
async restore(conversationId, gen) {
|
|
2692
|
+
const superseded = () => gen !== this.generation;
|
|
2693
|
+
if (superseded()) return;
|
|
2694
|
+
if (!this.session.isStreaming) this.setState("restoring");
|
|
2381
2695
|
let activeJobId = null;
|
|
2382
2696
|
try {
|
|
2383
2697
|
const res = await this.session.client.getActiveJob(conversationId);
|
|
2384
2698
|
activeJobId = res.jobId;
|
|
2385
2699
|
} catch {
|
|
2386
2700
|
}
|
|
2701
|
+
if (superseded()) return;
|
|
2387
2702
|
if (activeJobId) {
|
|
2388
2703
|
await this.session.loadConversation(conversationId);
|
|
2704
|
+
if (superseded()) return;
|
|
2389
2705
|
this.setState("streaming");
|
|
2390
2706
|
try {
|
|
2391
2707
|
await this.session.reconnectToJob(activeJobId);
|
|
2392
2708
|
} catch {
|
|
2393
2709
|
}
|
|
2394
|
-
if (
|
|
2710
|
+
if (superseded()) return;
|
|
2711
|
+
if (this._state === "streaming" && !this.session.isStreaming) {
|
|
2395
2712
|
this.setState("idle");
|
|
2396
2713
|
}
|
|
2397
2714
|
} else {
|
|
2398
2715
|
await this.session.loadConversation(conversationId);
|
|
2716
|
+
if (superseded()) return;
|
|
2399
2717
|
try {
|
|
2400
2718
|
const jobs = await this.session.client.get(`/v1/conversations/${encodeURIComponent(conversationId)}/jobs`);
|
|
2719
|
+
if (superseded()) return;
|
|
2401
2720
|
const completedJobs = jobs.filter(
|
|
2402
2721
|
(j) => j.status === "completed"
|
|
2403
2722
|
);
|
|
@@ -2409,6 +2728,14 @@ var StreamManager = class {
|
|
|
2409
2728
|
job_id: j.job_id,
|
|
2410
2729
|
message_id: j.message_id
|
|
2411
2730
|
})),
|
|
2731
|
+
// Completed jobs PLUS the ones still going. A send landing in the
|
|
2732
|
+
// probe window has a running job, so its prompt is claimed and does
|
|
2733
|
+
// not read as a steer replayed over the bubble the live send already
|
|
2734
|
+
// rendered. Failed and cancelled jobs are deliberately NOT claimed:
|
|
2735
|
+
// they produce no `turn` step, so claiming them would delete the
|
|
2736
|
+
// user's prompt from the restore entirely rather than show it as a
|
|
2737
|
+
// steer.
|
|
2738
|
+
claimedMessageIds: jobs.filter((j) => j.status !== "failed" && j.status !== "cancelled").map((j) => j.message_id),
|
|
2412
2739
|
userMessages: userMessages.map((m) => ({
|
|
2413
2740
|
id: m.id,
|
|
2414
2741
|
content: m.content
|
|
@@ -2419,10 +2746,12 @@ var StreamManager = class {
|
|
|
2419
2746
|
(job) => this.session.client.getConversationEvents(conversationId, job.job_id).catch(() => [])
|
|
2420
2747
|
)
|
|
2421
2748
|
);
|
|
2749
|
+
if (superseded()) return;
|
|
2422
2750
|
const eventsByJobId = new Map(
|
|
2423
2751
|
completedJobs.map((job, i) => [job.job_id, eventLists[i] ?? []])
|
|
2424
2752
|
);
|
|
2425
2753
|
for (const step of plan) {
|
|
2754
|
+
if (superseded()) return;
|
|
2426
2755
|
if (step.kind === "steer") {
|
|
2427
2756
|
this.session.replayTurn(
|
|
2428
2757
|
conversationId,
|
|
@@ -2440,6 +2769,7 @@ var StreamManager = class {
|
|
|
2440
2769
|
step.messageId
|
|
2441
2770
|
);
|
|
2442
2771
|
}
|
|
2772
|
+
if (superseded()) return;
|
|
2443
2773
|
if (completedJobs.length > 0) {
|
|
2444
2774
|
this.emit({
|
|
2445
2775
|
type: "versionsReady",
|
|
@@ -2449,13 +2779,17 @@ var StreamManager = class {
|
|
|
2449
2779
|
}
|
|
2450
2780
|
} catch {
|
|
2451
2781
|
}
|
|
2452
|
-
|
|
2782
|
+
if (superseded()) return;
|
|
2783
|
+
this.settleIdle();
|
|
2453
2784
|
}
|
|
2454
2785
|
}
|
|
2455
2786
|
// ── Internal: set active conversation ─────────────────────────
|
|
2456
2787
|
setActiveConversation(id) {
|
|
2457
2788
|
this._activeConversationId = id;
|
|
2789
|
+
this.session.invalidateLoadsInFlight();
|
|
2790
|
+
const claimed = ++this.generation;
|
|
2458
2791
|
this.emit({ type: "conversationChanged", conversationId: id });
|
|
2792
|
+
return claimed;
|
|
2459
2793
|
}
|
|
2460
2794
|
};
|
|
2461
2795
|
|