@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.js
CHANGED
|
@@ -676,6 +676,11 @@ var AstralformClient = class {
|
|
|
676
676
|
// undefined so it matches the `url?: string` type and consumers that
|
|
677
677
|
// check `!== undefined` never receive a null.
|
|
678
678
|
url: raw.url ?? void 0,
|
|
679
|
+
// This mapping is an ALLOWLIST — a field the API returns and this
|
|
680
|
+
// function does not name is dropped silently, and no type error says so.
|
|
681
|
+
// `content_url` shipped that way and was invisible to every consumer.
|
|
682
|
+
contentUrl: raw.content_url ?? void 0,
|
|
683
|
+
posterUrl: raw.poster_url ?? void 0,
|
|
679
684
|
createdAt: raw.created_at
|
|
680
685
|
};
|
|
681
686
|
}
|
|
@@ -1329,6 +1334,17 @@ var ChatSession = class {
|
|
|
1329
1334
|
/** True while ``loadMoreConversations`` is in flight. */
|
|
1330
1335
|
this.isLoadingConversations = false;
|
|
1331
1336
|
this.messages = [];
|
|
1337
|
+
/**
|
|
1338
|
+
* Which conversation ``messages`` currently holds.
|
|
1339
|
+
*
|
|
1340
|
+
* Distinct from ``conversationId``, and the distinction is the point:
|
|
1341
|
+
* ``loadConversation`` moves the POINTER synchronously and installs the LIST
|
|
1342
|
+
* an await later, so for the whole duration of every load the two disagree.
|
|
1343
|
+
* Anything pairing a message with a conversation — ``regenerate`` above all —
|
|
1344
|
+
* has to read this one, or it will pair the previous conversation's last
|
|
1345
|
+
* message with the new conversation's id.
|
|
1346
|
+
*/
|
|
1347
|
+
this.messagesConversationId = null;
|
|
1332
1348
|
this.isStreaming = false;
|
|
1333
1349
|
this.agentStatus = null;
|
|
1334
1350
|
this.agents = [];
|
|
@@ -1361,6 +1377,43 @@ var ChatSession = class {
|
|
|
1361
1377
|
* is discarded instead.
|
|
1362
1378
|
*/
|
|
1363
1379
|
this.conversationsGeneration = 0;
|
|
1380
|
+
/**
|
|
1381
|
+
* Bumped by every ``loadConversation`` call, so an out-of-order fetch can
|
|
1382
|
+
* tell it is no longer the newest one and drop its result. Separate from
|
|
1383
|
+
* ``conversationsGeneration``, which guards the conversation LIST.
|
|
1384
|
+
*/
|
|
1385
|
+
this.loadGeneration = 0;
|
|
1386
|
+
/**
|
|
1387
|
+
* Ids of locally-created user messages the server has not acknowledged yet.
|
|
1388
|
+
*
|
|
1389
|
+
* Arrival time cannot answer "could the reply have included this?" on its
|
|
1390
|
+
* own. Every `loadConversation` in `StreamManager` sits behind the
|
|
1391
|
+
* active-job probe, so the ORDINARY ordering is that a send lands BEFORE the
|
|
1392
|
+
* load starts — and an arrival-time rule drops exactly those, losing the
|
|
1393
|
+
* prompt the user just sent while its stream is still running. Membership
|
|
1394
|
+
* here is set by `send` and cleared when a server row turns up carrying the
|
|
1395
|
+
* same turn, so the keep-decision no longer depends on which side of the
|
|
1396
|
+
* fetch the push landed on.
|
|
1397
|
+
*
|
|
1398
|
+
* The value is `serverRowsKnown` as of the `message_stop` that proved the
|
|
1399
|
+
* row committed, or 0 until then — including after the job response, which
|
|
1400
|
+
* hands back the id but starts the loop as a background task and so proves
|
|
1401
|
+
* nothing about the row. That stamp is what separates "the snapshot predates
|
|
1402
|
+
* the row" from "the server does not have this row" — see
|
|
1403
|
+
* `loadConversation`.
|
|
1404
|
+
*
|
|
1405
|
+
* It is an ANNOTATION ON `this.messages`: reconciliation only ever consults
|
|
1406
|
+
* entries of that array, so an id whose message has left it is dead weight.
|
|
1407
|
+
* `setMessages` is the single place the array is replaced, and it prunes.
|
|
1408
|
+
*/
|
|
1409
|
+
this.pendingUserMessages = /* @__PURE__ */ new Map();
|
|
1410
|
+
/**
|
|
1411
|
+
* Bumped once per completed turn, at `message_stop`, so a fetch can record
|
|
1412
|
+
* what was proven when it was ISSUED. A row proven committed before the
|
|
1413
|
+
* fetch went out must appear in its snapshot; one proven after may
|
|
1414
|
+
* legitimately be missing.
|
|
1415
|
+
*/
|
|
1416
|
+
this.serverRowsKnown = 0;
|
|
1364
1417
|
// Minimal in-session accumulation for the assistant message record.
|
|
1365
1418
|
// Only top-level ``text`` blocks contribute; subagent / tool output
|
|
1366
1419
|
// is tracked by the consumer's own block store.
|
|
@@ -1383,6 +1436,19 @@ var ChatSession = class {
|
|
|
1383
1436
|
this.toolRegistry = new ToolRegistry();
|
|
1384
1437
|
this.storage = storage ?? new InMemoryStorage();
|
|
1385
1438
|
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Replace the message list, keeping `pendingUserMessages` an annotation on
|
|
1441
|
+
* it. Every removal from the array goes through here — `push` is the only
|
|
1442
|
+
* other mutation and it cannot orphan an id.
|
|
1443
|
+
*/
|
|
1444
|
+
setMessages(next) {
|
|
1445
|
+
this.messages = next;
|
|
1446
|
+
if (this.pendingUserMessages.size === 0) return;
|
|
1447
|
+
const present = new Set(next.map((m) => m.id));
|
|
1448
|
+
for (const id of this.pendingUserMessages.keys()) {
|
|
1449
|
+
if (!present.has(id)) this.pendingUserMessages.delete(id);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1386
1452
|
on(handler) {
|
|
1387
1453
|
this.handlers.add(handler);
|
|
1388
1454
|
return () => {
|
|
@@ -1431,6 +1497,22 @@ var ChatSession = class {
|
|
|
1431
1497
|
}
|
|
1432
1498
|
if (this.isStreaming) return;
|
|
1433
1499
|
const conversationId = options?.conversationId ?? this.conversationId ?? void 0;
|
|
1500
|
+
let relocatedFrom = null;
|
|
1501
|
+
if (conversationId) {
|
|
1502
|
+
if (conversationId !== this.conversationId) {
|
|
1503
|
+
this.loadGeneration++;
|
|
1504
|
+
relocatedFrom = {
|
|
1505
|
+
messages: this.messages,
|
|
1506
|
+
messagesId: this.messagesConversationId,
|
|
1507
|
+
conversationId: this.conversationId,
|
|
1508
|
+
generation: this.loadGeneration,
|
|
1509
|
+
target: conversationId
|
|
1510
|
+
};
|
|
1511
|
+
this.setMessages([]);
|
|
1512
|
+
this.messagesConversationId = null;
|
|
1513
|
+
}
|
|
1514
|
+
this.conversationId = conversationId;
|
|
1515
|
+
}
|
|
1434
1516
|
const userMessage = {
|
|
1435
1517
|
id: generateId(),
|
|
1436
1518
|
conversationId: conversationId ?? "",
|
|
@@ -1440,9 +1522,11 @@ var ChatSession = class {
|
|
|
1440
1522
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1441
1523
|
};
|
|
1442
1524
|
if (conversationId) {
|
|
1443
|
-
await this.storage.addMessage(userMessage, conversationId)
|
|
1525
|
+
await this.storage.addMessage(userMessage, conversationId).catch(() => {
|
|
1526
|
+
});
|
|
1444
1527
|
}
|
|
1445
1528
|
this.messages.push(userMessage);
|
|
1529
|
+
this.pendingUserMessages.set(userMessage.id, 0);
|
|
1446
1530
|
const request = {
|
|
1447
1531
|
message: content,
|
|
1448
1532
|
conversation_id: conversationId,
|
|
@@ -1462,7 +1546,24 @@ var ChatSession = class {
|
|
|
1462
1546
|
reasoning_effort: options?.reasoningEffort,
|
|
1463
1547
|
temperature: options?.temperature
|
|
1464
1548
|
};
|
|
1465
|
-
|
|
1549
|
+
const wire = { reached: false };
|
|
1550
|
+
await this.processStream(request, wire);
|
|
1551
|
+
if (!wire.reached) {
|
|
1552
|
+
this.pendingUserMessages.delete(userMessage.id);
|
|
1553
|
+
const at = this.messages.indexOf(userMessage);
|
|
1554
|
+
if (at !== -1) this.messages.splice(at, 1);
|
|
1555
|
+
if (conversationId) {
|
|
1556
|
+
await this.storage.deleteMessage(userMessage.id).catch(() => {
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
if (relocatedFrom && wire.reached && this.loadGeneration === relocatedFrom.generation) {
|
|
1561
|
+
this.messagesConversationId = relocatedFrom.target;
|
|
1562
|
+
} else if (relocatedFrom && !wire.reached && this.loadGeneration === relocatedFrom.generation) {
|
|
1563
|
+
this.setMessages(relocatedFrom.messages);
|
|
1564
|
+
this.messagesConversationId = relocatedFrom.messagesId;
|
|
1565
|
+
this.conversationId = relocatedFrom.conversationId;
|
|
1566
|
+
}
|
|
1466
1567
|
}
|
|
1467
1568
|
async resendFromCheckpoint(messageId, newContent) {
|
|
1468
1569
|
if (this.isStreaming) return;
|
|
@@ -1479,12 +1580,13 @@ var ChatSession = class {
|
|
|
1479
1580
|
this.accumulatedText = "";
|
|
1480
1581
|
this.currentTextPath = null;
|
|
1481
1582
|
}
|
|
1482
|
-
async processStream(request) {
|
|
1583
|
+
async processStream(request, wire) {
|
|
1483
1584
|
this.isStreaming = true;
|
|
1484
1585
|
this.resetStreamingState();
|
|
1485
|
-
|
|
1586
|
+
const controller = new AbortController();
|
|
1587
|
+
this.abortController = controller;
|
|
1486
1588
|
try {
|
|
1487
|
-
await this.consumeJobStream(request);
|
|
1589
|
+
await this.consumeJobStream(request, wire);
|
|
1488
1590
|
} catch (err) {
|
|
1489
1591
|
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
1490
1592
|
this.emit({
|
|
@@ -1495,16 +1597,20 @@ var ChatSession = class {
|
|
|
1495
1597
|
});
|
|
1496
1598
|
}
|
|
1497
1599
|
} finally {
|
|
1498
|
-
this.
|
|
1499
|
-
|
|
1600
|
+
if (this.abortController === controller) {
|
|
1601
|
+
this.isStreaming = false;
|
|
1602
|
+
this.abortController = null;
|
|
1603
|
+
}
|
|
1500
1604
|
}
|
|
1501
1605
|
}
|
|
1502
|
-
async consumeJobStream(request) {
|
|
1606
|
+
async consumeJobStream(request, wire) {
|
|
1503
1607
|
const job = await this.client.createJob(request);
|
|
1608
|
+
if (wire) wire.reached = true;
|
|
1504
1609
|
this.currentJobId = job.job_id;
|
|
1505
1610
|
const conversationId = job.conversation_id;
|
|
1506
1611
|
if (!this.conversationId) {
|
|
1507
1612
|
this.conversationId = conversationId;
|
|
1613
|
+
this.messagesConversationId = conversationId;
|
|
1508
1614
|
}
|
|
1509
1615
|
if (!this.conversations.some((c) => c.id === conversationId)) {
|
|
1510
1616
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1525,13 +1631,29 @@ var ChatSession = class {
|
|
|
1525
1631
|
await this.storage.addMessage(lastMsg, conversationId).catch(() => {
|
|
1526
1632
|
});
|
|
1527
1633
|
}
|
|
1528
|
-
const
|
|
1634
|
+
const promptMessageId = job.message_id;
|
|
1635
|
+
if (promptMessageId && lastMsg?.role === "user" && this.pendingUserMessages.has(lastMsg.id)) {
|
|
1636
|
+
this.pendingUserMessages.delete(lastMsg.id);
|
|
1637
|
+
const clientMintedId = lastMsg.id;
|
|
1638
|
+
lastMsg.id = promptMessageId;
|
|
1639
|
+
this.pendingUserMessages.set(promptMessageId, 0);
|
|
1640
|
+
if (conversationId && clientMintedId !== promptMessageId) {
|
|
1641
|
+
await this.storage.deleteMessage(clientMintedId).catch(() => {
|
|
1642
|
+
});
|
|
1643
|
+
await this.storage.addMessage(lastMsg, conversationId).catch(() => {
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
const dupe = this.messages.findIndex(
|
|
1647
|
+
(m) => m !== lastMsg && m.id === promptMessageId
|
|
1648
|
+
);
|
|
1649
|
+
if (dupe !== -1) this.messages.splice(dupe, 1);
|
|
1650
|
+
}
|
|
1529
1651
|
this.lastSeq = -1;
|
|
1530
1652
|
this.submittedToolCallIds.clear();
|
|
1531
1653
|
await this.consumeEventStream(
|
|
1532
1654
|
job.job_id,
|
|
1533
1655
|
conversationId,
|
|
1534
|
-
|
|
1656
|
+
promptMessageId,
|
|
1535
1657
|
true
|
|
1536
1658
|
// executeClientTools
|
|
1537
1659
|
);
|
|
@@ -1540,7 +1662,7 @@ var ChatSession = class {
|
|
|
1540
1662
|
* Shared event consumption loop. Parses each wire event, updates
|
|
1541
1663
|
* minimal session state, and emits typed ChatEvents to consumers.
|
|
1542
1664
|
*/
|
|
1543
|
-
async consumeEventStream(jobId, conversationId,
|
|
1665
|
+
async consumeEventStream(jobId, conversationId, promptMessageId, executeClientTools) {
|
|
1544
1666
|
const signal = this.abortController?.signal;
|
|
1545
1667
|
for (let attempt = 0; ; attempt++) {
|
|
1546
1668
|
if (signal?.aborted) return;
|
|
@@ -1557,7 +1679,7 @@ var ChatSession = class {
|
|
|
1557
1679
|
sawTerminal = await this.pumpStream(
|
|
1558
1680
|
stream,
|
|
1559
1681
|
conversationId,
|
|
1560
|
-
|
|
1682
|
+
promptMessageId,
|
|
1561
1683
|
executeClientTools,
|
|
1562
1684
|
() => attemptController.abort()
|
|
1563
1685
|
);
|
|
@@ -1589,7 +1711,7 @@ var ChatSession = class {
|
|
|
1589
1711
|
* zombie — ``reader.read()`` will never settle — so we kill the fetch and
|
|
1590
1712
|
* throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.
|
|
1591
1713
|
*/
|
|
1592
|
-
async pumpStream(stream, conversationId,
|
|
1714
|
+
async pumpStream(stream, conversationId, promptMessageId, executeClientTools, onStall) {
|
|
1593
1715
|
let sawTerminal = false;
|
|
1594
1716
|
const iterator = stream[Symbol.asyncIterator]();
|
|
1595
1717
|
try {
|
|
@@ -1636,7 +1758,7 @@ var ChatSession = class {
|
|
|
1636
1758
|
await this.dispatchWireEvent(
|
|
1637
1759
|
parsed,
|
|
1638
1760
|
conversationId,
|
|
1639
|
-
|
|
1761
|
+
promptMessageId,
|
|
1640
1762
|
executeClientTools
|
|
1641
1763
|
);
|
|
1642
1764
|
}
|
|
@@ -1678,8 +1800,8 @@ var ChatSession = class {
|
|
|
1678
1800
|
}
|
|
1679
1801
|
}
|
|
1680
1802
|
}
|
|
1681
|
-
async dispatchWireEvent(wire, conversationId,
|
|
1682
|
-
this.applyWireSideEffects(wire, conversationId,
|
|
1803
|
+
async dispatchWireEvent(wire, conversationId, promptMessageId, executeClientTools) {
|
|
1804
|
+
this.applyWireSideEffects(wire, conversationId, promptMessageId, true);
|
|
1683
1805
|
const event = translateWireEvent(wire);
|
|
1684
1806
|
if (event) {
|
|
1685
1807
|
this.emit(event);
|
|
@@ -1697,7 +1819,9 @@ var ChatSession = class {
|
|
|
1697
1819
|
const results = await this.executeClientTools([request]);
|
|
1698
1820
|
await this.submitToolResultWithRetry({
|
|
1699
1821
|
conversation_id: conversationId,
|
|
1700
|
-
|
|
1822
|
+
// The message that TRIGGERED the tool calls, which is what the
|
|
1823
|
+
// backend stores it against — the prompt id, correctly.
|
|
1824
|
+
message_id: promptMessageId,
|
|
1701
1825
|
tool_results: results
|
|
1702
1826
|
});
|
|
1703
1827
|
this.submittedToolCallIds.add(callId);
|
|
@@ -1712,7 +1836,7 @@ var ChatSession = class {
|
|
|
1712
1836
|
* instead of re-typing the whole conversation event by event.
|
|
1713
1837
|
*/
|
|
1714
1838
|
replayWireEvent(wire, conversationId) {
|
|
1715
|
-
this.applyWireSideEffects(wire, conversationId, "");
|
|
1839
|
+
this.applyWireSideEffects(wire, conversationId, "", false);
|
|
1716
1840
|
const event = translateWireEvent(wire);
|
|
1717
1841
|
if (event) {
|
|
1718
1842
|
this.emit(event);
|
|
@@ -1723,37 +1847,52 @@ var ChatSession = class {
|
|
|
1723
1847
|
* the pure wire → ChatEvent mapping can live in translate.ts and be reused
|
|
1724
1848
|
* by the replay path.
|
|
1725
1849
|
*
|
|
1726
|
-
* ``
|
|
1727
|
-
*
|
|
1728
|
-
*
|
|
1850
|
+
* ``promptMessageId`` is the id of the USER turn that started this job —
|
|
1851
|
+
* `POST /v1/jobs` returns it and the backend tags the prompt with it
|
|
1852
|
+
* (`HumanMessage(content=..., id=message_id)`), which is what lets a restore
|
|
1853
|
+
* pair a job with its prompt by id instead of by position. It was previously
|
|
1854
|
+
* documented here as the ASSISTANT's id and used as one; there is no
|
|
1855
|
+
* server-assigned assistant id on the wire, so that row gets a local one.
|
|
1856
|
+
* Empty in the reconnect and conversation-switch replay paths, where the
|
|
1857
|
+
* messages have already been loaded from REST and must not be re-pushed —
|
|
1858
|
+
* so it doubles as the "is this a live send?" gate.
|
|
1729
1859
|
*/
|
|
1730
|
-
applyWireSideEffects(wire, conversationId,
|
|
1860
|
+
applyWireSideEffects(wire, conversationId, promptMessageId, live) {
|
|
1861
|
+
const ownsTurnState = live || !this.isStreaming;
|
|
1731
1862
|
switch (wire.type) {
|
|
1732
1863
|
case "message_start":
|
|
1733
|
-
this.resetStreamingState();
|
|
1864
|
+
if (ownsTurnState) this.resetStreamingState();
|
|
1734
1865
|
if (wire.model) {
|
|
1735
1866
|
this.modelDisplayName = wire.model;
|
|
1736
1867
|
}
|
|
1737
1868
|
return;
|
|
1738
1869
|
case "block_start":
|
|
1739
|
-
if (wire.kind === "text" && (!wire.parent_path || wire.parent_path.length === 0)) {
|
|
1870
|
+
if (ownsTurnState && wire.kind === "text" && (!wire.parent_path || wire.parent_path.length === 0)) {
|
|
1740
1871
|
this.currentTextPath = wire.path;
|
|
1741
1872
|
}
|
|
1742
1873
|
return;
|
|
1743
1874
|
case "block_delta":
|
|
1744
|
-
if (wire.delta.channel === "text" && this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
1875
|
+
if (ownsTurnState && wire.delta.channel === "text" && this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
1745
1876
|
this.accumulatedText += wire.delta.text;
|
|
1746
1877
|
}
|
|
1747
1878
|
return;
|
|
1748
1879
|
case "block_stop":
|
|
1749
|
-
if (this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
1880
|
+
if (ownsTurnState && this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
1750
1881
|
this.currentTextPath = null;
|
|
1751
1882
|
}
|
|
1752
1883
|
return;
|
|
1753
1884
|
case "message_stop":
|
|
1754
|
-
if (
|
|
1885
|
+
if (promptMessageId && this.pendingUserMessages.has(promptMessageId)) {
|
|
1886
|
+
this.pendingUserMessages.set(promptMessageId, ++this.serverRowsKnown);
|
|
1887
|
+
}
|
|
1888
|
+
if (promptMessageId) {
|
|
1755
1889
|
const assistantMessage = {
|
|
1756
|
-
id
|
|
1890
|
+
// NOT `promptMessageId` — that is the USER turn's id, which
|
|
1891
|
+
// `consumeJobStream` stamps onto the user row. Sharing it puts two
|
|
1892
|
+
// rows under one id, and `pendingUserMessages` is id-keyed. No
|
|
1893
|
+
// server-assigned assistant id exists on the wire, so this row is
|
|
1894
|
+
// local until a REST load replaces it.
|
|
1895
|
+
id: generateId(),
|
|
1757
1896
|
conversationId,
|
|
1758
1897
|
role: "assistant",
|
|
1759
1898
|
content: this.accumulatedText,
|
|
@@ -1764,8 +1903,10 @@ var ChatSession = class {
|
|
|
1764
1903
|
this.storage.addMessage(assistantMessage, conversationId).catch(() => {
|
|
1765
1904
|
});
|
|
1766
1905
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1906
|
+
if (ownsTurnState) {
|
|
1907
|
+
this.isStreaming = false;
|
|
1908
|
+
this.currentJobId = null;
|
|
1909
|
+
}
|
|
1769
1910
|
return;
|
|
1770
1911
|
case "custom":
|
|
1771
1912
|
if (wire.name === "title_generated") {
|
|
@@ -1799,9 +1940,27 @@ var ChatSession = class {
|
|
|
1799
1940
|
* Used before reconnectToJob — SSE replay handles event replay.
|
|
1800
1941
|
*/
|
|
1801
1942
|
async loadConversation(id) {
|
|
1943
|
+
const load = ++this.loadGeneration;
|
|
1802
1944
|
this.conversationId = id;
|
|
1803
|
-
this.resetStreamingState();
|
|
1804
|
-
|
|
1945
|
+
if (!this.isStreaming) this.resetStreamingState();
|
|
1946
|
+
const rowsKnownAtIssue = this.serverRowsKnown;
|
|
1947
|
+
const messages = await this.client.getMessages(id).catch(() => this.storage.fetchMessages(id));
|
|
1948
|
+
if (load !== this.loadGeneration) return;
|
|
1949
|
+
const pending = this.messages.filter(
|
|
1950
|
+
(m) => this.pendingUserMessages.has(m.id) && m.conversationId === id
|
|
1951
|
+
);
|
|
1952
|
+
const stillPending = pending.filter((m) => {
|
|
1953
|
+
if (messages.some((f) => f.id === m.id)) return false;
|
|
1954
|
+
const knownAt = this.pendingUserMessages.get(m.id) ?? 0;
|
|
1955
|
+
return knownAt === 0 || knownAt > rowsKnownAtIssue;
|
|
1956
|
+
});
|
|
1957
|
+
for (const m of pending) {
|
|
1958
|
+
if (!stillPending.includes(m)) this.pendingUserMessages.delete(m.id);
|
|
1959
|
+
}
|
|
1960
|
+
this.setMessages(
|
|
1961
|
+
stillPending.length ? [...messages, ...stillPending] : messages
|
|
1962
|
+
);
|
|
1963
|
+
this.messagesConversationId = id;
|
|
1805
1964
|
}
|
|
1806
1965
|
/**
|
|
1807
1966
|
* Reconnect to a running job's SSE stream (e.g. after page reload).
|
|
@@ -1814,7 +1973,8 @@ var ChatSession = class {
|
|
|
1814
1973
|
this.lastSeq = -1;
|
|
1815
1974
|
this.submittedToolCallIds.clear();
|
|
1816
1975
|
this.resetStreamingState();
|
|
1817
|
-
|
|
1976
|
+
const controller = new AbortController();
|
|
1977
|
+
this.abortController = controller;
|
|
1818
1978
|
try {
|
|
1819
1979
|
await this.consumeEventStream(
|
|
1820
1980
|
jobId,
|
|
@@ -1831,8 +1991,10 @@ var ChatSession = class {
|
|
|
1831
1991
|
blockPath: null
|
|
1832
1992
|
});
|
|
1833
1993
|
} finally {
|
|
1834
|
-
this.
|
|
1835
|
-
|
|
1994
|
+
if (this.abortController === controller) {
|
|
1995
|
+
this.isStreaming = false;
|
|
1996
|
+
this.abortController = null;
|
|
1997
|
+
}
|
|
1836
1998
|
}
|
|
1837
1999
|
}
|
|
1838
2000
|
/** Detach from the SSE stream without cancelling the job. */
|
|
@@ -1843,25 +2005,54 @@ var ChatSession = class {
|
|
|
1843
2005
|
this.resetStreamingState();
|
|
1844
2006
|
this.emit({ type: "disconnected" });
|
|
1845
2007
|
}
|
|
1846
|
-
/**
|
|
1847
|
-
|
|
2008
|
+
/**
|
|
2009
|
+
* Cancel the running turn: stop the job server-side and tear the stream
|
|
2010
|
+
* down, WITHOUT ending the session. A turn-level cancel is not a
|
|
2011
|
+
* session-level teardown, so unlike `disconnect()` this leaves the protocol
|
|
2012
|
+
* registry alone — the SDK never auto-registers adapters, so clearing them
|
|
2013
|
+
* on a Stop press would silently kill embedded-resource rendering for the
|
|
2014
|
+
* rest of the session with nothing to re-register it.
|
|
2015
|
+
*/
|
|
2016
|
+
cancelTurn() {
|
|
1848
2017
|
if (this.currentJobId) {
|
|
1849
2018
|
this.client.cancelJob(this.currentJobId).catch(() => {
|
|
1850
2019
|
});
|
|
1851
2020
|
}
|
|
1852
2021
|
this.detach();
|
|
1853
2022
|
this.currentJobId = null;
|
|
2023
|
+
for (const [id, knownAt] of this.pendingUserMessages) {
|
|
2024
|
+
if (knownAt === 0) this.pendingUserMessages.delete(id);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
/** Stop the job and end the session's activity (explicit user action). */
|
|
2028
|
+
disconnect() {
|
|
2029
|
+
this.cancelTurn();
|
|
1854
2030
|
this.protocols.clear();
|
|
1855
2031
|
}
|
|
2032
|
+
/**
|
|
2033
|
+
* A pointer move happened above this layer, so any `loadConversation` in
|
|
2034
|
+
* flight must lose to it. `StreamManager` owns a pointer of its own and
|
|
2035
|
+
* moves it before this one; without this the two halves would gate on
|
|
2036
|
+
* counters that bump at different instants — `generation` synchronously in
|
|
2037
|
+
* `setActiveConversation`, `loadGeneration` only once the switch's own load
|
|
2038
|
+
* actually runs, which is behind the active-job probe.
|
|
2039
|
+
*/
|
|
2040
|
+
invalidateLoadsInFlight() {
|
|
2041
|
+
this.loadGeneration++;
|
|
2042
|
+
}
|
|
1856
2043
|
async createNewConversation() {
|
|
1857
2044
|
const id = generateId();
|
|
2045
|
+
const load = this.loadGeneration;
|
|
1858
2046
|
const conversation = await this.storage.createConversation(
|
|
1859
2047
|
id,
|
|
1860
2048
|
"New Conversation"
|
|
1861
2049
|
);
|
|
1862
2050
|
this.conversations.unshift(conversation);
|
|
2051
|
+
if (load !== this.loadGeneration) return id;
|
|
2052
|
+
this.loadGeneration++;
|
|
1863
2053
|
this.conversationId = id;
|
|
1864
|
-
this.
|
|
2054
|
+
this.setMessages([]);
|
|
2055
|
+
this.messagesConversationId = id;
|
|
1865
2056
|
return id;
|
|
1866
2057
|
}
|
|
1867
2058
|
/**
|
|
@@ -1880,7 +2071,7 @@ var ChatSession = class {
|
|
|
1880
2071
|
*/
|
|
1881
2072
|
replayTurn(id, events, userMessageContent, userMessageId, isSteer = false) {
|
|
1882
2073
|
this.conversationId = id;
|
|
1883
|
-
this.resetStreamingState();
|
|
2074
|
+
if (!this.isStreaming) this.resetStreamingState();
|
|
1884
2075
|
if (userMessageContent) {
|
|
1885
2076
|
this.emit({
|
|
1886
2077
|
type: "user_message",
|
|
@@ -1911,11 +2102,17 @@ var ChatSession = class {
|
|
|
1911
2102
|
* job's events.
|
|
1912
2103
|
*/
|
|
1913
2104
|
async switchConversation(id, jobId) {
|
|
1914
|
-
const
|
|
1915
|
-
|
|
2105
|
+
const loading = this.loadConversation(id);
|
|
2106
|
+
const token = this.loadGeneration;
|
|
2107
|
+
const [loadResult, eventsResult] = await Promise.allSettled([
|
|
2108
|
+
loading,
|
|
1916
2109
|
this.client.getConversationEvents(id, jobId)
|
|
1917
2110
|
]);
|
|
1918
|
-
|
|
2111
|
+
if (token !== this.loadGeneration) return;
|
|
2112
|
+
if (loadResult.status === "rejected") {
|
|
2113
|
+
this.setMessages([]);
|
|
2114
|
+
this.messagesConversationId = id;
|
|
2115
|
+
}
|
|
1919
2116
|
this.replayTurn(
|
|
1920
2117
|
id,
|
|
1921
2118
|
eventsResult.status === "fulfilled" ? eventsResult.value : []
|
|
@@ -2006,8 +2203,10 @@ var ChatSession = class {
|
|
|
2006
2203
|
}
|
|
2007
2204
|
this.conversations = this.conversations.filter((c) => c.id !== id);
|
|
2008
2205
|
if (this.conversationId === id) {
|
|
2206
|
+
this.loadGeneration++;
|
|
2009
2207
|
this.conversationId = null;
|
|
2010
|
-
this.
|
|
2208
|
+
this.setMessages([]);
|
|
2209
|
+
this.messagesConversationId = null;
|
|
2011
2210
|
}
|
|
2012
2211
|
}
|
|
2013
2212
|
toggleClientTool(name) {
|
|
@@ -2023,6 +2222,11 @@ var ChatSession = class {
|
|
|
2023
2222
|
// src/restore-plan.ts
|
|
2024
2223
|
function planRestore(args) {
|
|
2025
2224
|
const { completedJobs, userMessages } = args;
|
|
2225
|
+
const claimed = new Set(
|
|
2226
|
+
(args.claimedMessageIds ?? completedJobs.map((j) => j.message_id)).filter(
|
|
2227
|
+
(id) => !!id
|
|
2228
|
+
)
|
|
2229
|
+
);
|
|
2026
2230
|
const byId = /* @__PURE__ */ new Map();
|
|
2027
2231
|
userMessages.forEach((m, i) => {
|
|
2028
2232
|
if (m.id) byId.set(m.id, i);
|
|
@@ -2044,7 +2248,7 @@ function planRestore(args) {
|
|
|
2044
2248
|
userMessages.slice(0, cutover)
|
|
2045
2249
|
);
|
|
2046
2250
|
let cursor = cutover;
|
|
2047
|
-
const isSteer = (m) => !!m?.id && !
|
|
2251
|
+
const isSteer = (m) => !!m?.id && !claimed.has(m.id);
|
|
2048
2252
|
const drainTo = (stopAt) => {
|
|
2049
2253
|
while (cursor < stopAt) {
|
|
2050
2254
|
const m = userMessages[cursor++];
|
|
@@ -2130,6 +2334,12 @@ var StreamManager = class {
|
|
|
2130
2334
|
this._backgroundJobs = /* @__PURE__ */ new Map();
|
|
2131
2335
|
this.handlers = [];
|
|
2132
2336
|
this.unsub = null;
|
|
2337
|
+
/**
|
|
2338
|
+
* Bumped every time the active conversation moves. An async sequence that
|
|
2339
|
+
* captures it can then tell, at each await boundary, whether it is still the
|
|
2340
|
+
* one the user is waiting on — see ``restore``.
|
|
2341
|
+
*/
|
|
2342
|
+
this.generation = 0;
|
|
2133
2343
|
this.session = session;
|
|
2134
2344
|
this.attach();
|
|
2135
2345
|
}
|
|
@@ -2180,7 +2390,7 @@ var StreamManager = class {
|
|
|
2180
2390
|
event
|
|
2181
2391
|
});
|
|
2182
2392
|
if (event.type === ChatEventType.MessageStop) {
|
|
2183
|
-
if (this._state === "streaming") {
|
|
2393
|
+
if (this._state === "streaming" && !this.session.isStreaming) {
|
|
2184
2394
|
this.setState("idle");
|
|
2185
2395
|
}
|
|
2186
2396
|
}
|
|
@@ -2193,13 +2403,21 @@ var StreamManager = class {
|
|
|
2193
2403
|
);
|
|
2194
2404
|
}
|
|
2195
2405
|
if (this._state === "streaming") return;
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
this.
|
|
2406
|
+
let target = this._activeConversationId;
|
|
2407
|
+
if (!target) {
|
|
2408
|
+
target = await this.session.createNewConversation();
|
|
2409
|
+
this.setActiveConversation(target);
|
|
2199
2410
|
}
|
|
2200
2411
|
this.setState("streaming");
|
|
2201
2412
|
try {
|
|
2202
2413
|
await this.session.send(content, {
|
|
2414
|
+
// Address the send explicitly. `ChatSession.send` otherwise falls back
|
|
2415
|
+
// to `session.conversationId`, which LAGS this pointer: a restore
|
|
2416
|
+
// assigns it synchronously but only reaches the next switch's own
|
|
2417
|
+
// `loadConversation` an await later, so between the two the session
|
|
2418
|
+
// still names the conversation the user left. The manager's pointer
|
|
2419
|
+
// moved the moment the user clicked; it is the authority.
|
|
2420
|
+
conversationId: target ?? void 0,
|
|
2203
2421
|
agentName: options?.agentName,
|
|
2204
2422
|
uploadIds: options?.uploadIds,
|
|
2205
2423
|
planMode: options?.planMode,
|
|
@@ -2218,6 +2436,9 @@ var StreamManager = class {
|
|
|
2218
2436
|
// ── Regenerate ────────────────────────────────────────────────
|
|
2219
2437
|
async regenerate() {
|
|
2220
2438
|
if (this._state === "streaming") return;
|
|
2439
|
+
if (this.session.messagesConversationId !== this._activeConversationId) {
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2221
2442
|
const userMsgs = this.session.messages.filter(
|
|
2222
2443
|
(m) => m.role === "user"
|
|
2223
2444
|
);
|
|
@@ -2254,44 +2475,78 @@ var StreamManager = class {
|
|
|
2254
2475
|
async switchTo(conversationId, opts) {
|
|
2255
2476
|
if (conversationId === this._activeConversationId) return;
|
|
2256
2477
|
const targetHadBackgroundJob = this._backgroundJobs.has(conversationId);
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
if (oldConvId && jobId) {
|
|
2261
|
-
this._backgroundJobs.set(oldConvId, jobId);
|
|
2262
|
-
this.emit({
|
|
2263
|
-
type: "backgroundJobsChanged",
|
|
2264
|
-
jobs: this._backgroundJobs
|
|
2265
|
-
});
|
|
2266
|
-
}
|
|
2267
|
-
this.session.detach();
|
|
2268
|
-
}
|
|
2269
|
-
if (this._backgroundJobs.has(conversationId)) {
|
|
2478
|
+
this.detachStreamingTurn();
|
|
2479
|
+
const parkedJobId = this._backgroundJobs.get(conversationId);
|
|
2480
|
+
if (parkedJobId !== void 0) {
|
|
2270
2481
|
this._backgroundJobs.delete(conversationId);
|
|
2271
2482
|
this.emit({
|
|
2272
2483
|
type: "backgroundJobsChanged",
|
|
2273
2484
|
jobs: this._backgroundJobs
|
|
2274
2485
|
});
|
|
2275
2486
|
}
|
|
2276
|
-
this.setActiveConversation(conversationId);
|
|
2487
|
+
const gen = this.setActiveConversation(conversationId);
|
|
2277
2488
|
if (opts?.skipHistoryReplay && !targetHadBackgroundJob) {
|
|
2278
2489
|
let activeJobId = null;
|
|
2279
2490
|
try {
|
|
2280
2491
|
activeJobId = (await this.session.client.getActiveJob(conversationId)).jobId;
|
|
2281
2492
|
} catch {
|
|
2282
2493
|
}
|
|
2494
|
+
if (gen !== this.generation) return;
|
|
2283
2495
|
if (!activeJobId) {
|
|
2284
|
-
|
|
2285
|
-
|
|
2496
|
+
try {
|
|
2497
|
+
await this.session.loadConversation(conversationId);
|
|
2498
|
+
} finally {
|
|
2499
|
+
if (gen === this.generation) this.settleIdle();
|
|
2500
|
+
}
|
|
2286
2501
|
return;
|
|
2287
2502
|
}
|
|
2288
2503
|
}
|
|
2289
|
-
|
|
2504
|
+
let tookOver = false;
|
|
2505
|
+
try {
|
|
2506
|
+
await this.restore(conversationId, gen);
|
|
2507
|
+
tookOver = gen === this.generation;
|
|
2508
|
+
} finally {
|
|
2509
|
+
const supersededOntoSame = gen !== this.generation && this._activeConversationId === conversationId;
|
|
2510
|
+
const stillExists = this.session.conversations.some(
|
|
2511
|
+
(c) => c.id === conversationId
|
|
2512
|
+
);
|
|
2513
|
+
if (parkedJobId !== void 0 && !tookOver && !supersededOntoSame && stillExists) {
|
|
2514
|
+
this._backgroundJobs.set(conversationId, parkedJobId);
|
|
2515
|
+
this.emit({
|
|
2516
|
+
type: "backgroundJobsChanged",
|
|
2517
|
+
jobs: this._backgroundJobs
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
if (!tookOver && gen === this.generation && this._state === "restoring") {
|
|
2521
|
+
this.settleIdle();
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2290
2524
|
}
|
|
2291
2525
|
// ── Create / rename / delete conversation ─────────────────────
|
|
2526
|
+
/**
|
|
2527
|
+
* Create a conversation and make it active.
|
|
2528
|
+
*
|
|
2529
|
+
* The returned id is NOT guaranteed to be the active conversation: if a
|
|
2530
|
+
* switch lands inside the storage round-trip, this declines the pointer move
|
|
2531
|
+
* so the newer one wins, and no `conversationChanged` fires for the new id.
|
|
2532
|
+
* A caller that routes on the return value should `switchTo(id)` rather than
|
|
2533
|
+
* assume it is current — that call is not a no-op in the declined case.
|
|
2534
|
+
*/
|
|
2292
2535
|
async createConversation() {
|
|
2293
|
-
|
|
2536
|
+
this.detachStreamingTurn();
|
|
2537
|
+
let id;
|
|
2538
|
+
try {
|
|
2539
|
+
id = await this.session.createNewConversation();
|
|
2540
|
+
} catch (err) {
|
|
2541
|
+
this.settleIdle();
|
|
2542
|
+
throw err;
|
|
2543
|
+
}
|
|
2544
|
+
if (this.session.conversationId !== id) {
|
|
2545
|
+
this.settleIdle();
|
|
2546
|
+
return id;
|
|
2547
|
+
}
|
|
2294
2548
|
this.setActiveConversation(id);
|
|
2549
|
+
this.settleIdle();
|
|
2295
2550
|
return id;
|
|
2296
2551
|
}
|
|
2297
2552
|
/**
|
|
@@ -2302,16 +2557,34 @@ var StreamManager = class {
|
|
|
2302
2557
|
await this.session.renameConversation(id, title);
|
|
2303
2558
|
}
|
|
2304
2559
|
async deleteConversation(id) {
|
|
2305
|
-
|
|
2306
|
-
this.
|
|
2560
|
+
const wasActive = this._activeConversationId === id;
|
|
2561
|
+
const cancelled = wasActive && this._state === "streaming";
|
|
2562
|
+
if (cancelled) {
|
|
2563
|
+
this.session.cancelTurn();
|
|
2564
|
+
this._state = "idle";
|
|
2565
|
+
}
|
|
2566
|
+
try {
|
|
2567
|
+
await this.session.deleteConversation(id);
|
|
2568
|
+
} catch (err) {
|
|
2569
|
+
if (cancelled) this.setState("idle");
|
|
2570
|
+
throw err;
|
|
2571
|
+
}
|
|
2307
2572
|
if (this._activeConversationId === id) {
|
|
2308
|
-
this.
|
|
2309
|
-
this.
|
|
2573
|
+
this.setActiveConversation(null);
|
|
2574
|
+
this.settleIdle();
|
|
2575
|
+
}
|
|
2576
|
+
const parkedJobId = this._backgroundJobs.get(id);
|
|
2577
|
+
if (this._backgroundJobs.delete(id)) {
|
|
2578
|
+
if (parkedJobId) {
|
|
2579
|
+
this.session.client.cancelJob(parkedJobId).catch(() => {
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
this.emit({ type: "backgroundJobsChanged", jobs: this._backgroundJobs });
|
|
2310
2583
|
}
|
|
2311
2584
|
}
|
|
2312
2585
|
// ── Stop (explicit cancel) ────────────────────────────────────
|
|
2313
2586
|
stop() {
|
|
2314
|
-
this.session.
|
|
2587
|
+
this.session.cancelTurn();
|
|
2315
2588
|
this.setState("idle");
|
|
2316
2589
|
}
|
|
2317
2590
|
// ── Cleanup ───────────────────────────────────────────────────
|
|
@@ -2323,34 +2596,80 @@ var StreamManager = class {
|
|
|
2323
2596
|
this.handlers = [];
|
|
2324
2597
|
}
|
|
2325
2598
|
// ── Internal: helpers ──────────────────────────────────────────
|
|
2599
|
+
/**
|
|
2600
|
+
* Park a streaming turn as a background job and detach from its SSE stream.
|
|
2601
|
+
*
|
|
2602
|
+
* Every method that relocates the active conversation has to do this before
|
|
2603
|
+
* announcing a new state. Announcing `idle` while `session.isStreaming` is
|
|
2604
|
+
* still true is worse than announcing nothing: `manager.send` no longer bails
|
|
2605
|
+
* on the streaming state, calls `session.send`, and THAT bails on its own
|
|
2606
|
+
* `isStreaming` — so the message is never posted, no error is emitted, and
|
|
2607
|
+
* the composer looks ready the whole time.
|
|
2608
|
+
*/
|
|
2609
|
+
detachStreamingTurn() {
|
|
2610
|
+
if (this._state !== "streaming") return;
|
|
2611
|
+
const oldConvId = this._activeConversationId;
|
|
2612
|
+
const jobId = this.session.currentJobId;
|
|
2613
|
+
if (oldConvId && jobId) {
|
|
2614
|
+
this._backgroundJobs.set(oldConvId, jobId);
|
|
2615
|
+
this.emit({ type: "backgroundJobsChanged", jobs: this._backgroundJobs });
|
|
2616
|
+
}
|
|
2617
|
+
this.session.detach();
|
|
2618
|
+
this.session.currentJobId = null;
|
|
2619
|
+
this._state = "idle";
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2622
|
+
* Announce `idle` unless a turn is actually streaming.
|
|
2623
|
+
*
|
|
2624
|
+
* A `send` can land inside any of the switch paths — the fast path most
|
|
2625
|
+
* easily, since it deliberately stays out of `restoring` and so leaves the
|
|
2626
|
+
* composer live for the whole probe. `send` sets `streaming` and does not
|
|
2627
|
+
* bump the generation, so the path resumes, passes its supersession check,
|
|
2628
|
+
* and would announce a ready composer over a running stream. From there
|
|
2629
|
+
* `finalizeStream` and the `message_stop` branch both no-op (they only act
|
|
2630
|
+
* on `streaming`), so it stays `idle` for the whole turn — and the next send
|
|
2631
|
+
* reaches `session.send`, which bails on its own `isStreaming`: message
|
|
2632
|
+
* never posted, no error, composer ready throughout.
|
|
2633
|
+
*/
|
|
2634
|
+
settleIdle() {
|
|
2635
|
+
if (this._state === "streaming") return;
|
|
2636
|
+
this.setState("idle");
|
|
2637
|
+
}
|
|
2326
2638
|
finalizeStream() {
|
|
2327
2639
|
if (this._state === "streaming") {
|
|
2328
2640
|
this.setState("idle");
|
|
2329
2641
|
}
|
|
2330
2642
|
}
|
|
2331
2643
|
// ── Internal: restore ─────────────────────────────────────────
|
|
2332
|
-
async restore(conversationId) {
|
|
2333
|
-
this.
|
|
2644
|
+
async restore(conversationId, gen) {
|
|
2645
|
+
const superseded = () => gen !== this.generation;
|
|
2646
|
+
if (superseded()) return;
|
|
2647
|
+
if (!this.session.isStreaming) this.setState("restoring");
|
|
2334
2648
|
let activeJobId = null;
|
|
2335
2649
|
try {
|
|
2336
2650
|
const res = await this.session.client.getActiveJob(conversationId);
|
|
2337
2651
|
activeJobId = res.jobId;
|
|
2338
2652
|
} catch {
|
|
2339
2653
|
}
|
|
2654
|
+
if (superseded()) return;
|
|
2340
2655
|
if (activeJobId) {
|
|
2341
2656
|
await this.session.loadConversation(conversationId);
|
|
2657
|
+
if (superseded()) return;
|
|
2342
2658
|
this.setState("streaming");
|
|
2343
2659
|
try {
|
|
2344
2660
|
await this.session.reconnectToJob(activeJobId);
|
|
2345
2661
|
} catch {
|
|
2346
2662
|
}
|
|
2347
|
-
if (
|
|
2663
|
+
if (superseded()) return;
|
|
2664
|
+
if (this._state === "streaming" && !this.session.isStreaming) {
|
|
2348
2665
|
this.setState("idle");
|
|
2349
2666
|
}
|
|
2350
2667
|
} else {
|
|
2351
2668
|
await this.session.loadConversation(conversationId);
|
|
2669
|
+
if (superseded()) return;
|
|
2352
2670
|
try {
|
|
2353
2671
|
const jobs = await this.session.client.get(`/v1/conversations/${encodeURIComponent(conversationId)}/jobs`);
|
|
2672
|
+
if (superseded()) return;
|
|
2354
2673
|
const completedJobs = jobs.filter(
|
|
2355
2674
|
(j) => j.status === "completed"
|
|
2356
2675
|
);
|
|
@@ -2362,6 +2681,14 @@ var StreamManager = class {
|
|
|
2362
2681
|
job_id: j.job_id,
|
|
2363
2682
|
message_id: j.message_id
|
|
2364
2683
|
})),
|
|
2684
|
+
// Completed jobs PLUS the ones still going. A send landing in the
|
|
2685
|
+
// probe window has a running job, so its prompt is claimed and does
|
|
2686
|
+
// not read as a steer replayed over the bubble the live send already
|
|
2687
|
+
// rendered. Failed and cancelled jobs are deliberately NOT claimed:
|
|
2688
|
+
// they produce no `turn` step, so claiming them would delete the
|
|
2689
|
+
// user's prompt from the restore entirely rather than show it as a
|
|
2690
|
+
// steer.
|
|
2691
|
+
claimedMessageIds: jobs.filter((j) => j.status !== "failed" && j.status !== "cancelled").map((j) => j.message_id),
|
|
2365
2692
|
userMessages: userMessages.map((m) => ({
|
|
2366
2693
|
id: m.id,
|
|
2367
2694
|
content: m.content
|
|
@@ -2372,10 +2699,12 @@ var StreamManager = class {
|
|
|
2372
2699
|
(job) => this.session.client.getConversationEvents(conversationId, job.job_id).catch(() => [])
|
|
2373
2700
|
)
|
|
2374
2701
|
);
|
|
2702
|
+
if (superseded()) return;
|
|
2375
2703
|
const eventsByJobId = new Map(
|
|
2376
2704
|
completedJobs.map((job, i) => [job.job_id, eventLists[i] ?? []])
|
|
2377
2705
|
);
|
|
2378
2706
|
for (const step of plan) {
|
|
2707
|
+
if (superseded()) return;
|
|
2379
2708
|
if (step.kind === "steer") {
|
|
2380
2709
|
this.session.replayTurn(
|
|
2381
2710
|
conversationId,
|
|
@@ -2393,6 +2722,7 @@ var StreamManager = class {
|
|
|
2393
2722
|
step.messageId
|
|
2394
2723
|
);
|
|
2395
2724
|
}
|
|
2725
|
+
if (superseded()) return;
|
|
2396
2726
|
if (completedJobs.length > 0) {
|
|
2397
2727
|
this.emit({
|
|
2398
2728
|
type: "versionsReady",
|
|
@@ -2402,13 +2732,17 @@ var StreamManager = class {
|
|
|
2402
2732
|
}
|
|
2403
2733
|
} catch {
|
|
2404
2734
|
}
|
|
2405
|
-
|
|
2735
|
+
if (superseded()) return;
|
|
2736
|
+
this.settleIdle();
|
|
2406
2737
|
}
|
|
2407
2738
|
}
|
|
2408
2739
|
// ── Internal: set active conversation ─────────────────────────
|
|
2409
2740
|
setActiveConversation(id) {
|
|
2410
2741
|
this._activeConversationId = id;
|
|
2742
|
+
this.session.invalidateLoadsInFlight();
|
|
2743
|
+
const claimed = ++this.generation;
|
|
2411
2744
|
this.emit({ type: "conversationChanged", conversationId: id });
|
|
2745
|
+
return claimed;
|
|
2412
2746
|
}
|
|
2413
2747
|
};
|
|
2414
2748
|
|