@trigger.dev/sdk 0.0.0-prerelease-20260909121655 → 0.0.0-prerelease-20260911153544
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/commonjs/v3/ai.d.ts +36 -0
- package/dist/commonjs/v3/ai.js +260 -55
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/auth.d.ts +10 -4
- package/dist/commonjs/v3/auth.js.map +1 -1
- package/dist/commonjs/v3/chatSnapshotIo.js +6 -7
- package/dist/commonjs/v3/chatSnapshotIo.js.map +1 -1
- package/dist/commonjs/v3/retry.d.ts +4 -0
- package/dist/commonjs/v3/retry.js +22 -13
- package/dist/commonjs/v3/retry.js.map +1 -1
- package/dist/commonjs/v3/test/transcript-storage-tests.js.map +1 -1
- package/dist/commonjs/v3/transcriptStorage.d.ts +43 -6
- package/dist/commonjs/v3/transcriptStorage.js +87 -8
- package/dist/commonjs/v3/transcriptStorage.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +36 -0
- package/dist/esm/v3/ai.js +260 -55
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/auth.d.ts +10 -4
- package/dist/esm/v3/auth.js.map +1 -1
- package/dist/esm/v3/chatSnapshotIo.js +7 -8
- package/dist/esm/v3/chatSnapshotIo.js.map +1 -1
- package/dist/esm/v3/retry.d.ts +4 -0
- package/dist/esm/v3/retry.js +14 -8
- package/dist/esm/v3/retry.js.map +1 -1
- package/dist/esm/v3/test/transcript-storage-tests.js.map +1 -1
- package/dist/esm/v3/transcriptStorage.d.ts +43 -6
- package/dist/esm/v3/transcriptStorage.js +85 -8
- package/dist/esm/v3/transcriptStorage.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/background-injection.mdx +54 -1
- package/docs/ai-chat/how-it-works.mdx +1 -1
- package/docs/ai-chat/migrating-from-hydrate-messages.mdx +299 -0
- package/docs/ai-chat/patterns/database-persistence.mdx +18 -7
- package/docs/ai-chat/reference.mdx +2 -1
- package/docs/ai-chat/side-channels.mdx +1 -1
- package/docs/ai-chat/transcript-storage.mdx +21 -7
- package/docs/self-hosting/security.mdx +12 -0
- package/package.json +2 -2
- package/skills/trigger-chat-agent-advanced/SKILL.md +31 -15
package/dist/commonjs/v3/ai.d.ts
CHANGED
|
@@ -149,6 +149,41 @@ export declare const ai: {
|
|
|
149
149
|
* ```
|
|
150
150
|
*/
|
|
151
151
|
declare function createChatAccessToken<TTask extends AnyTask>(taskId: TaskIdentifier<TTask>): Promise<string>;
|
|
152
|
+
/**
|
|
153
|
+
* Register work that must land before anything from this turn reaches the
|
|
154
|
+
* frontend.
|
|
155
|
+
*
|
|
156
|
+
* Like {@link chatDefer} the work starts immediately and is never awaited by
|
|
157
|
+
* the hook that registered it, so it runs alongside the model and costs no
|
|
158
|
+
* time to first token. Unlike `chat.defer`, the output stream waits for it:
|
|
159
|
+
* no chunk of the answer is written to the session until it settles. That
|
|
160
|
+
* makes it the right home for a write the next page load has to see (a
|
|
161
|
+
* conversation row, a message insert), because a reader that can see the
|
|
162
|
+
* answer can also see what the write persisted.
|
|
163
|
+
*
|
|
164
|
+
* Reach for `chat.defer` instead when the timing does not matter for a
|
|
165
|
+
* reload: analytics, audit logs, search-index updates.
|
|
166
|
+
*
|
|
167
|
+
* This is not a consistency barrier for the turn. The work is still in flight
|
|
168
|
+
* while the model runs, so a tool, a `prepareStep`, or anything else executing
|
|
169
|
+
* during the turn can still read the state as it was before the write. It
|
|
170
|
+
* orders the write against what the frontend can see, nothing more. When the
|
|
171
|
+
* turn's own code has to read the write back, `await` it instead and accept
|
|
172
|
+
* the cost.
|
|
173
|
+
*
|
|
174
|
+
* A write registered here that fails, or outlasts the internal timeout, lets
|
|
175
|
+
* the stream through rather than stalling the conversation.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* onTurnStart: async ({ chatId, uiMessages }) => {
|
|
180
|
+
* chat.deferBeforeOutput(
|
|
181
|
+
* db.chat.update({ where: { id: chatId }, data: { messages: uiMessages } })
|
|
182
|
+
* );
|
|
183
|
+
* },
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
declare function chatDeferBeforeOutput(promiseOrFn: Promise<unknown> | (() => Promise<unknown>)): void;
|
|
152
187
|
/**
|
|
153
188
|
* A stream writer passed to chat lifecycle callbacks (`onPreload`, `onChatStart`,
|
|
154
189
|
* `onTurnStart`, `onTurnComplete`, `onCompacted`).
|
|
@@ -3382,6 +3417,7 @@ export declare const chat: {
|
|
|
3382
3417
|
cleanupAbortedParts: typeof cleanupAbortedParts;
|
|
3383
3418
|
/** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
|
|
3384
3419
|
defer: typeof chatDefer;
|
|
3420
|
+
deferBeforeOutput: typeof chatDeferBeforeOutput;
|
|
3385
3421
|
/** Queue model messages for injection at the next `prepareStep` boundary. See {@link injectBackgroundContext}. */
|
|
3386
3422
|
inject: typeof injectBackgroundContext;
|
|
3387
3423
|
/** Typed chat output stream for writing custom chunks or piping from subtasks. */
|
package/dist/commonjs/v3/ai.js
CHANGED
|
@@ -683,31 +683,143 @@ exports.ai = {
|
|
|
683
683
|
function createChatAccessToken(taskId) {
|
|
684
684
|
return auth_js_1.auth.createTriggerPublicToken(taskId, { expirationTime: "24h" });
|
|
685
685
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
686
|
+
function createChatOutGate() {
|
|
687
|
+
let resolveOpened;
|
|
688
|
+
const opened = new Promise((resolve) => {
|
|
689
|
+
resolveOpened = resolve;
|
|
690
|
+
});
|
|
691
|
+
const gate = {
|
|
692
|
+
pending: new Set(),
|
|
693
|
+
open: false,
|
|
694
|
+
opened,
|
|
695
|
+
failOpen() {
|
|
696
|
+
if (gate.open)
|
|
697
|
+
return;
|
|
698
|
+
gate.open = true;
|
|
699
|
+
resolveOpened();
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
return gate;
|
|
703
|
+
}
|
|
704
|
+
const chatOutGateKey = locals_js_1.locals.create("chat.outGate");
|
|
705
|
+
/**
|
|
706
|
+
* How long a write waits on the gate before giving up. A storage that hangs
|
|
707
|
+
* degrades to an ungated write rather than stalling the conversation.
|
|
708
|
+
* @internal
|
|
709
|
+
*/
|
|
710
|
+
const CHAT_OUT_GATE_TIMEOUT_MS = 10_000;
|
|
711
|
+
async function awaitChatOutGate() {
|
|
712
|
+
const gate = locals_js_1.locals.get(chatOutGateKey);
|
|
713
|
+
if (!gate || gate.open)
|
|
714
|
+
return;
|
|
715
|
+
const deadline = Date.now() + CHAT_OUT_GATE_TIMEOUT_MS;
|
|
716
|
+
while (gate.pending.size > 0) {
|
|
717
|
+
const remaining = deadline - Date.now();
|
|
718
|
+
if (remaining <= 0) {
|
|
719
|
+
gate.failOpen();
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const waitingOn = [...gate.pending];
|
|
723
|
+
let timedOut = false;
|
|
724
|
+
let timer;
|
|
725
|
+
try {
|
|
726
|
+
await Promise.race([
|
|
727
|
+
Promise.allSettled(waitingOn),
|
|
728
|
+
gate.opened,
|
|
729
|
+
new Promise((resolve) => {
|
|
730
|
+
timer = setTimeout(() => {
|
|
731
|
+
timedOut = true;
|
|
732
|
+
resolve();
|
|
733
|
+
}, remaining);
|
|
734
|
+
}),
|
|
735
|
+
]);
|
|
736
|
+
}
|
|
737
|
+
finally {
|
|
738
|
+
if (timer)
|
|
739
|
+
clearTimeout(timer);
|
|
740
|
+
}
|
|
741
|
+
if (gate.open)
|
|
742
|
+
return;
|
|
743
|
+
if (timedOut) {
|
|
744
|
+
gate.failOpen();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
for (const settled of waitingOn)
|
|
748
|
+
gate.pending.delete(settled);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
689
751
|
/**
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
752
|
+
* Register work that must land before anything from this turn reaches the
|
|
753
|
+
* frontend.
|
|
754
|
+
*
|
|
755
|
+
* Like {@link chatDefer} the work starts immediately and is never awaited by
|
|
756
|
+
* the hook that registered it, so it runs alongside the model and costs no
|
|
757
|
+
* time to first token. Unlike `chat.defer`, the output stream waits for it:
|
|
758
|
+
* no chunk of the answer is written to the session until it settles. That
|
|
759
|
+
* makes it the right home for a write the next page load has to see (a
|
|
760
|
+
* conversation row, a message insert), because a reader that can see the
|
|
761
|
+
* answer can also see what the write persisted.
|
|
762
|
+
*
|
|
763
|
+
* Reach for `chat.defer` instead when the timing does not matter for a
|
|
764
|
+
* reload: analytics, audit logs, search-index updates.
|
|
693
765
|
*
|
|
694
|
-
*
|
|
766
|
+
* This is not a consistency barrier for the turn. The work is still in flight
|
|
767
|
+
* while the model runs, so a tool, a `prepareStep`, or anything else executing
|
|
768
|
+
* during the turn can still read the state as it was before the write. It
|
|
769
|
+
* orders the write against what the frontend can see, nothing more. When the
|
|
770
|
+
* turn's own code has to read the write back, `await` it instead and accept
|
|
771
|
+
* the cost.
|
|
772
|
+
*
|
|
773
|
+
* A write registered here that fails, or outlasts the internal timeout, lets
|
|
774
|
+
* the stream through rather than stalling the conversation.
|
|
775
|
+
*
|
|
776
|
+
* @example
|
|
695
777
|
* ```ts
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
* },
|
|
702
|
-
* });
|
|
703
|
-
* await waitUntilComplete();
|
|
778
|
+
* onTurnStart: async ({ chatId, uiMessages }) => {
|
|
779
|
+
* chat.deferBeforeOutput(
|
|
780
|
+
* db.chat.update({ where: { id: chatId }, data: { messages: uiMessages } })
|
|
781
|
+
* );
|
|
782
|
+
* },
|
|
704
783
|
* ```
|
|
705
|
-
*
|
|
706
|
-
* Backed by the Session primitive so a chat's output outlives any single
|
|
707
|
-
* run — subscribers (browser transport, server-side `ChatStream`) read
|
|
708
|
-
* the session's `.out`, not a per-run stream. Run-scoped `target`
|
|
709
|
-
* options on `.pipe()` are honoured as no-ops; the session is the target.
|
|
710
784
|
*/
|
|
785
|
+
function chatDeferBeforeOutput(promiseOrFn) {
|
|
786
|
+
const gate = locals_js_1.locals.get(chatOutGateKey);
|
|
787
|
+
const work = typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
|
|
788
|
+
if (!gate || gate.open)
|
|
789
|
+
return;
|
|
790
|
+
gate.pending.add(work);
|
|
791
|
+
}
|
|
792
|
+
function gateWriterOptions(options) {
|
|
793
|
+
return {
|
|
794
|
+
...options,
|
|
795
|
+
execute: async (api) => {
|
|
796
|
+
await awaitChatOutGate();
|
|
797
|
+
return await options.execute(api);
|
|
798
|
+
},
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function gateOutStream(value) {
|
|
802
|
+
return (async function* () {
|
|
803
|
+
await awaitChatOutGate();
|
|
804
|
+
if (isReadableStream(value)) {
|
|
805
|
+
const reader = value.getReader();
|
|
806
|
+
try {
|
|
807
|
+
while (true) {
|
|
808
|
+
const { done, value: chunk } = await reader.read();
|
|
809
|
+
if (done)
|
|
810
|
+
break;
|
|
811
|
+
yield chunk;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
finally {
|
|
815
|
+
reader.releaseLock();
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
yield* value;
|
|
820
|
+
}
|
|
821
|
+
})();
|
|
822
|
+
}
|
|
711
823
|
const chatStream = {
|
|
712
824
|
// Stable opaque label for the run-scoped `RealtimeDefinedStream` shape.
|
|
713
825
|
// `chatStream` is backed by the Session's `.out` channel — this id is
|
|
@@ -717,7 +829,7 @@ const chatStream = {
|
|
|
717
829
|
id: "chat",
|
|
718
830
|
pipe(value, options) {
|
|
719
831
|
const { target: _target, ...sessionOptions } = (options ?? {});
|
|
720
|
-
return getChatSession().out.pipe(value, sessionOptions);
|
|
832
|
+
return getChatSession().out.pipe(gateOutStream(value), sessionOptions);
|
|
721
833
|
},
|
|
722
834
|
async read(_runId, options) {
|
|
723
835
|
// Session channels don't need a runId — the session is the address.
|
|
@@ -727,10 +839,11 @@ const chatStream = {
|
|
|
727
839
|
},
|
|
728
840
|
async append(value, options) {
|
|
729
841
|
const { target: _target, ...sessionOptions } = (options ?? {});
|
|
842
|
+
await awaitChatOutGate();
|
|
730
843
|
return getChatSession().out.append(value, sessionOptions);
|
|
731
844
|
},
|
|
732
845
|
writer(options) {
|
|
733
|
-
return getChatSession().out.writer(options);
|
|
846
|
+
return getChatSession().out.writer(gateWriterOptions(options));
|
|
734
847
|
},
|
|
735
848
|
};
|
|
736
849
|
// ---------------------------------------------------------------------------
|
|
@@ -780,9 +893,13 @@ function createLazyChatWriter() {
|
|
|
780
893
|
let mergeImpl = null;
|
|
781
894
|
let waitPromise = null;
|
|
782
895
|
let resolveExecute = null;
|
|
896
|
+
let started = false;
|
|
897
|
+
const bufferedParts = [];
|
|
898
|
+
const bufferedStreams = [];
|
|
783
899
|
function ensureInitialized() {
|
|
784
|
-
if (
|
|
900
|
+
if (started)
|
|
785
901
|
return;
|
|
902
|
+
started = true;
|
|
786
903
|
const executePromise = new Promise((resolve) => {
|
|
787
904
|
resolveExecute = resolve;
|
|
788
905
|
});
|
|
@@ -792,7 +909,11 @@ function createLazyChatWriter() {
|
|
|
792
909
|
execute: ({ write, merge }) => {
|
|
793
910
|
writeImpl = write;
|
|
794
911
|
mergeImpl = merge;
|
|
795
|
-
|
|
912
|
+
for (const part of bufferedParts.splice(0))
|
|
913
|
+
write(part);
|
|
914
|
+
for (const stream of bufferedStreams.splice(0))
|
|
915
|
+
merge(stream);
|
|
916
|
+
return executePromise;
|
|
796
917
|
},
|
|
797
918
|
});
|
|
798
919
|
waitPromise = waitUntilComplete;
|
|
@@ -802,11 +923,17 @@ function createLazyChatWriter() {
|
|
|
802
923
|
write(part) {
|
|
803
924
|
ensureInitialized();
|
|
804
925
|
queueResponsePart(part);
|
|
805
|
-
writeImpl
|
|
926
|
+
if (writeImpl)
|
|
927
|
+
writeImpl(part);
|
|
928
|
+
else
|
|
929
|
+
bufferedParts.push(part);
|
|
806
930
|
},
|
|
807
931
|
merge(stream) {
|
|
808
932
|
ensureInitialized();
|
|
809
|
-
mergeImpl
|
|
933
|
+
if (mergeImpl)
|
|
934
|
+
mergeImpl(stream);
|
|
935
|
+
else
|
|
936
|
+
bufferedStreams.push(stream);
|
|
810
937
|
},
|
|
811
938
|
},
|
|
812
939
|
async flush() {
|
|
@@ -1615,15 +1742,11 @@ async function installChatInputRouter(chatId, options) {
|
|
|
1615
1742
|
checkpoint.appliedThrough = Math.max(checkpoint.appliedThrough ?? replayWindowEnd, replayWindowEnd);
|
|
1616
1743
|
}
|
|
1617
1744
|
}
|
|
1618
|
-
// A boot that replayed `.in` itself has already answered everything up to
|
|
1619
|
-
// `recoveredThrough`, so the floor has to cover it before the tail opens.
|
|
1620
|
-
if (options?.recoveredThrough !== undefined) {
|
|
1621
|
-
const recovered = options.recoveredThrough;
|
|
1622
|
-
checkpoint.resumeFrom = Math.max(checkpoint.resumeFrom ?? recovered, recovered);
|
|
1623
|
-
checkpoint.appliedThrough = Math.max(checkpoint.appliedThrough ?? checkpoint.resumeFrom, checkpoint.resumeFrom);
|
|
1624
|
-
}
|
|
1625
1745
|
const router = entry.router;
|
|
1626
1746
|
router.restore(checkpoint);
|
|
1747
|
+
if (options?.recoveredSeqNums && options.recoveredSeqNums.length > 0) {
|
|
1748
|
+
router.markRecovered(options.recoveredSeqNums);
|
|
1749
|
+
}
|
|
1627
1750
|
const floor = router.resumeFrom();
|
|
1628
1751
|
if (floor !== undefined) {
|
|
1629
1752
|
v3_1.sessionStreams.setLastSeqNum(chatId, "in", floor);
|
|
@@ -3959,6 +4082,17 @@ function chatAgent(options) {
|
|
|
3959
4082
|
* keeps an action's write cursor-neutral.
|
|
3960
4083
|
*/
|
|
3961
4084
|
let lastSnapshotOutEventId;
|
|
4085
|
+
/**
|
|
4086
|
+
* The `lastInEventId` the most recent snapshot carried.
|
|
4087
|
+
*
|
|
4088
|
+
* A turn-start save happens after the incoming message has been handed to
|
|
4089
|
+
* the turn loop, so the router's live resume floor has already advanced
|
|
4090
|
+
* past it. Persisting that floor before the turn runs would let the next
|
|
4091
|
+
* boot resume past a message this run never answered, which is exactly
|
|
4092
|
+
* what a deferred or recovered message depends on. Turn-start carries
|
|
4093
|
+
* this instead.
|
|
4094
|
+
*/
|
|
4095
|
+
let lastSnapshotInEventId;
|
|
3962
4096
|
const storageTrigger = (trigger) => trigger === "regenerate-message"
|
|
3963
4097
|
? "regenerate-message"
|
|
3964
4098
|
: trigger === "action" || trigger === "action-turn"
|
|
@@ -3972,7 +4106,8 @@ function chatAgent(options) {
|
|
|
3972
4106
|
*/
|
|
3973
4107
|
/** The runtime's opaque state as of the last save; carried on every changeset's transcript. */
|
|
3974
4108
|
let transcriptState = null;
|
|
3975
|
-
|
|
4109
|
+
let transcriptSaveChain = Promise.resolve();
|
|
4110
|
+
const runSaveTranscript = async (opts) => {
|
|
3976
4111
|
const { changes, shadow } = (0, transcriptStorage_js_1.diffTranscript)(transcriptShadow, opts.messages, {
|
|
3977
4112
|
nonFinalIds: opts.nonFinalIds,
|
|
3978
4113
|
});
|
|
@@ -3996,8 +4131,15 @@ function chatAgent(options) {
|
|
|
3996
4131
|
if (runtimeState !== null || persistedStateSet) {
|
|
3997
4132
|
changes.push({ op: "state", value: runtimeState });
|
|
3998
4133
|
}
|
|
4134
|
+
if (opts.skipIfUnchanged && changes.length === 0)
|
|
4135
|
+
return;
|
|
3999
4136
|
transcriptState = runtimeState;
|
|
4000
|
-
const
|
|
4137
|
+
const liveInCursor = chatInputRouter().resumeFloor();
|
|
4138
|
+
const inCursor = opts.carryInCursor
|
|
4139
|
+
? lastSnapshotInEventId
|
|
4140
|
+
: liveInCursor !== undefined
|
|
4141
|
+
? String(liveInCursor)
|
|
4142
|
+
: undefined;
|
|
4001
4143
|
await transcriptStorage.save({
|
|
4002
4144
|
chatId: payload.chatId,
|
|
4003
4145
|
clientData: opts.clientData,
|
|
@@ -4018,12 +4160,28 @@ function chatAgent(options) {
|
|
|
4018
4160
|
},
|
|
4019
4161
|
cursors: {
|
|
4020
4162
|
lastOutEventId: opts.lastOutEventId,
|
|
4021
|
-
lastInEventId: inCursor
|
|
4163
|
+
lastInEventId: inCursor,
|
|
4022
4164
|
},
|
|
4023
4165
|
});
|
|
4024
4166
|
transcriptShadow = shadow;
|
|
4167
|
+
lastSnapshotInEventId = inCursor;
|
|
4025
4168
|
persistedStateSet = runtimeState !== null;
|
|
4026
4169
|
};
|
|
4170
|
+
/**
|
|
4171
|
+
* Serialise every save onto one chain. `runSaveTranscript` derives its
|
|
4172
|
+
* changeset from `transcriptShadow` and only advances it once the write
|
|
4173
|
+
* lands, so two overlapping saves would diff against stale state. The
|
|
4174
|
+
* message list is copied on the way in because the accumulator keeps
|
|
4175
|
+
* mutating while a queued save waits its turn. A rejection is handed to
|
|
4176
|
+
* the caller but never poisons the chain.
|
|
4177
|
+
*/
|
|
4178
|
+
const saveTranscript = (opts) => {
|
|
4179
|
+
const queued = { ...opts, messages: [...opts.messages] };
|
|
4180
|
+
const run = () => runSaveTranscript(queued);
|
|
4181
|
+
const next = transcriptSaveChain.then(run, run);
|
|
4182
|
+
transcriptSaveChain = next.then(() => undefined, () => undefined);
|
|
4183
|
+
return next;
|
|
4184
|
+
};
|
|
4027
4185
|
/**
|
|
4028
4186
|
* Persist the accumulator outside a turn.
|
|
4029
4187
|
*
|
|
@@ -4074,6 +4232,13 @@ function chatAgent(options) {
|
|
|
4074
4232
|
// default, `inFlightUsers`). The turn-loop checks this queue ahead of
|
|
4075
4233
|
// `messagesInput.waitWithIdleTimeout` so recovered turns fire first.
|
|
4076
4234
|
const bootInjectedQueue = [];
|
|
4235
|
+
const recoveredSeqByPayload = new WeakMap();
|
|
4236
|
+
const dispatchBootInjected = () => bootInjectedQueue.shift();
|
|
4237
|
+
const settleRecoveredTurn = (wirePayload) => {
|
|
4238
|
+
const settledSeq = recoveredSeqByPayload.get(wirePayload);
|
|
4239
|
+
if (settledSeq !== undefined)
|
|
4240
|
+
chatInputRouter().settleRecovered(settledSeq);
|
|
4241
|
+
};
|
|
4077
4242
|
const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1;
|
|
4078
4243
|
// `.in` resume cursor, computed at most once per boot. The boot
|
|
4079
4244
|
// block below resolves it (snapshot field or records scan) and the
|
|
@@ -4123,6 +4288,7 @@ function chatAgent(options) {
|
|
|
4123
4288
|
// turn (chain self-bootstraps from turn 2), so this is purely an
|
|
4124
4289
|
// optimization to keep continuation runs bounded from the first turn.
|
|
4125
4290
|
lastSnapshotOutEventId = bootSnapshot?.lastOutEventId;
|
|
4291
|
+
lastSnapshotInEventId = bootSnapshot?.lastInEventId;
|
|
4126
4292
|
if (bootSnapshot?.lastOutEventId !== undefined) {
|
|
4127
4293
|
const seeded = Number.parseInt(bootSnapshot.lastOutEventId, 10);
|
|
4128
4294
|
if (Number.isFinite(seeded)) {
|
|
@@ -4214,16 +4380,10 @@ function chatAgent(options) {
|
|
|
4214
4380
|
}
|
|
4215
4381
|
// ── session.in router ──────────────────────────────────────────
|
|
4216
4382
|
//
|
|
4217
|
-
|
|
4218
|
-
// only a fallback: the boot block above may already have resolved a
|
|
4219
|
-
// cursor from the snapshot, which is used when the boundary itself
|
|
4220
|
-
// carries none. Everything the boot replayed off `.in` is dispatched from
|
|
4221
|
-
// `bootInjectedQueue` below, so it goes into the floor here — folded in
|
|
4222
|
-
// after the subscription opens, the live tail re-delivers it as a turn.
|
|
4223
|
-
const lastRecoveredInSeq = replayedInTail.length > 0 ? replayedInTail[replayedInTail.length - 1].seqNum : undefined;
|
|
4383
|
+
const recoveredSeqNums = replayedInTail.map((r) => r.seqNum);
|
|
4224
4384
|
await installChatInputRouter(payload.chatId, {
|
|
4225
4385
|
fallbackResumeFrom: bootInCursorResolved ? bootInCursor : undefined,
|
|
4226
|
-
|
|
4386
|
+
recoveredSeqNums,
|
|
4227
4387
|
resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
|
|
4228
4388
|
});
|
|
4229
4389
|
// ── Recovery boot + chain reconstruction ────────────────────────
|
|
@@ -4306,7 +4466,7 @@ function chatAgent(options) {
|
|
|
4306
4466
|
// branches: at n=1 the orphan partial is dropped and the interrupted
|
|
4307
4467
|
// user is re-dispatched as a fresh turn instead.
|
|
4308
4468
|
let seedChain;
|
|
4309
|
-
let
|
|
4469
|
+
let recoveredEntries;
|
|
4310
4470
|
if (hookChain !== undefined) {
|
|
4311
4471
|
seedChain = hookChain;
|
|
4312
4472
|
}
|
|
@@ -4317,13 +4477,26 @@ function chatAgent(options) {
|
|
|
4317
4477
|
seedChain = settledMessages;
|
|
4318
4478
|
}
|
|
4319
4479
|
if (hookRecoveredTurns !== undefined) {
|
|
4320
|
-
|
|
4480
|
+
const seqNumsByRecoveredId = new Map();
|
|
4481
|
+
for (const entry of replayedInTail) {
|
|
4482
|
+
const existing = seqNumsByRecoveredId.get(entry.message.id);
|
|
4483
|
+
if (existing)
|
|
4484
|
+
existing.push(entry.seqNum);
|
|
4485
|
+
else
|
|
4486
|
+
seqNumsByRecoveredId.set(entry.message.id, [entry.seqNum]);
|
|
4487
|
+
}
|
|
4488
|
+
recoveredEntries = hookRecoveredTurns.map((message) => ({
|
|
4489
|
+
message,
|
|
4490
|
+
seqNum: seqNumsByRecoveredId.get(message.id)?.shift(),
|
|
4491
|
+
}));
|
|
4321
4492
|
}
|
|
4322
4493
|
else if (partialAssistant !== undefined && inFlightUsers.length > 1) {
|
|
4323
|
-
|
|
4494
|
+
recoveredEntries = replayedInTail
|
|
4495
|
+
.slice(1)
|
|
4496
|
+
.map((r) => ({ message: r.message, seqNum: r.seqNum }));
|
|
4324
4497
|
}
|
|
4325
4498
|
else {
|
|
4326
|
-
|
|
4499
|
+
recoveredEntries = replayedInTail.map((r) => ({ message: r.message, seqNum: r.seqNum }));
|
|
4327
4500
|
}
|
|
4328
4501
|
// `beforeBoot` errors bubble — the customer opted into blocking
|
|
4329
4502
|
// persistence and a failure there should fail the run rather than
|
|
@@ -4353,13 +4526,14 @@ function chatAgent(options) {
|
|
|
4353
4526
|
for (const entry of replayedInTail) {
|
|
4354
4527
|
metadataById.set(entry.message.id, entry.metadata);
|
|
4355
4528
|
}
|
|
4356
|
-
|
|
4529
|
+
const dispatchedRecoveredSeqs = new Set();
|
|
4530
|
+
for (const { message: msg, seqNum } of recoveredEntries) {
|
|
4357
4531
|
if (wireMessageId && msg.id === wireMessageId)
|
|
4358
4532
|
continue;
|
|
4359
4533
|
const recoveredMetadata = metadataById.has(msg.id)
|
|
4360
4534
|
? metadataById.get(msg.id)
|
|
4361
4535
|
: payload.metadata;
|
|
4362
|
-
|
|
4536
|
+
const injectedPayload = {
|
|
4363
4537
|
chatId: payload.chatId,
|
|
4364
4538
|
sessionId: payload.sessionId,
|
|
4365
4539
|
metadata: recoveredMetadata,
|
|
@@ -4368,7 +4542,17 @@ function chatAgent(options) {
|
|
|
4368
4542
|
messageId: msg.id,
|
|
4369
4543
|
continuation: payload.continuation,
|
|
4370
4544
|
previousRunId: payload.previousRunId,
|
|
4371
|
-
}
|
|
4545
|
+
};
|
|
4546
|
+
bootInjectedQueue.push(injectedPayload);
|
|
4547
|
+
if (seqNum !== undefined) {
|
|
4548
|
+
recoveredSeqByPayload.set(injectedPayload, seqNum);
|
|
4549
|
+
dispatchedRecoveredSeqs.add(seqNum);
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
for (const entry of replayedInTail) {
|
|
4553
|
+
if (!dispatchedRecoveredSeqs.has(entry.seqNum)) {
|
|
4554
|
+
chatInputRouter().settleRecovered(entry.seqNum);
|
|
4555
|
+
}
|
|
4372
4556
|
}
|
|
4373
4557
|
accumulatedUIMessages = seedChain;
|
|
4374
4558
|
// ── Head-start bootstrap ─────────────────────────────────────
|
|
@@ -4528,7 +4712,7 @@ function chatAgent(options) {
|
|
|
4528
4712
|
*/
|
|
4529
4713
|
let dispatchedRecoveredFirstTurn = false;
|
|
4530
4714
|
if (preloaded && bootInjectedQueue.length > 0) {
|
|
4531
|
-
currentWirePayload =
|
|
4715
|
+
currentWirePayload = dispatchBootInjected();
|
|
4532
4716
|
dispatchedRecoveredFirstTurn = true;
|
|
4533
4717
|
}
|
|
4534
4718
|
// Handle preloaded runs — fire onPreload, then wait for the first real message
|
|
@@ -4736,7 +4920,7 @@ function chatAgent(options) {
|
|
|
4736
4920
|
// waiting on the live session.in. Subsequent recovered turns
|
|
4737
4921
|
// get drained by the end-of-turn picker below.
|
|
4738
4922
|
if (bootInjectedQueue.length > 0) {
|
|
4739
|
-
currentWirePayload =
|
|
4923
|
+
currentWirePayload = dispatchBootInjected();
|
|
4740
4924
|
}
|
|
4741
4925
|
else {
|
|
4742
4926
|
const effectiveIdleTimeout = idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds;
|
|
@@ -4854,6 +5038,7 @@ function chatAgent(options) {
|
|
|
4854
5038
|
// (errors are caught by the outer try/catch which writes an error chunk)
|
|
4855
5039
|
locals_js_1.locals.set(chatPipeCountKey, 0);
|
|
4856
5040
|
locals_js_1.locals.set(chatDeferKey, new Set());
|
|
5041
|
+
locals_js_1.locals.set(chatOutGateKey, createChatOutGate());
|
|
4857
5042
|
locals_js_1.locals.set(chatCompactionStateKey, undefined);
|
|
4858
5043
|
locals_js_1.locals.set(chatSteeringQueueKey, []);
|
|
4859
5044
|
locals_js_1.locals.set(chatPendingBackgroundKey, []);
|
|
@@ -5300,6 +5485,7 @@ function chatAgent(options) {
|
|
|
5300
5485
|
chatId: currentWirePayload.chatId,
|
|
5301
5486
|
messageId: currentWirePayload.messageId,
|
|
5302
5487
|
});
|
|
5488
|
+
settleRecoveredTurn(currentWirePayload);
|
|
5303
5489
|
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
5304
5490
|
// Not a turn — don't consume an iteration.
|
|
5305
5491
|
turn--;
|
|
@@ -5341,6 +5527,23 @@ function chatAgent(options) {
|
|
|
5341
5527
|
// A no-op turn skips this block, and with it `followSessionPin`:
|
|
5342
5528
|
// there is nothing to answer, so nothing to hand over.
|
|
5343
5529
|
if ((!isAction || actionTurn) && !isNoOpTurn) {
|
|
5530
|
+
if (!hydrateMessages) {
|
|
5531
|
+
chatDeferBeforeOutput(saveTranscript({
|
|
5532
|
+
reason: "turn-start",
|
|
5533
|
+
messages: accumulatedUIMessages,
|
|
5534
|
+
turn,
|
|
5535
|
+
trigger: storageTrigger(currentWirePayload.trigger),
|
|
5536
|
+
clientData,
|
|
5537
|
+
lastOutEventId: lastSnapshotOutEventId,
|
|
5538
|
+
skipIfUnchanged: true,
|
|
5539
|
+
carryInCursor: true,
|
|
5540
|
+
}).catch((error) => {
|
|
5541
|
+
v3_1.logger.warn("chat.agent: turn-start transcript write failed; a reload mid-answer may not show the message being answered", {
|
|
5542
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5543
|
+
sessionId: sessionIdForSnapshot,
|
|
5544
|
+
});
|
|
5545
|
+
}));
|
|
5546
|
+
}
|
|
5344
5547
|
// Mint a scoped public access token once per turn, reused for
|
|
5345
5548
|
// onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk.
|
|
5346
5549
|
const currentRunId = ctx.run.id;
|
|
@@ -6015,6 +6218,7 @@ function chatAgent(options) {
|
|
|
6015
6218
|
}
|
|
6016
6219
|
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
6017
6220
|
}
|
|
6221
|
+
settleRecoveredTurn(currentWirePayload);
|
|
6018
6222
|
// Write turn-complete control chunk — closes the frontend stream.
|
|
6019
6223
|
const turnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId, turnAccessToken);
|
|
6020
6224
|
// Fire onTurnComplete — stream is closed, use for persistence.
|
|
@@ -6126,7 +6330,7 @@ function chatAgent(options) {
|
|
|
6126
6330
|
// produced these from in-flight user messages on session.in
|
|
6127
6331
|
// that the dead predecessor never acknowledged.
|
|
6128
6332
|
if (bootInjectedQueue.length > 0) {
|
|
6129
|
-
currentWirePayload =
|
|
6333
|
+
currentWirePayload = dispatchBootInjected();
|
|
6130
6334
|
return "continue";
|
|
6131
6335
|
}
|
|
6132
6336
|
// chat.requestUpgrade() was called — exit the loop; the handover
|
|
@@ -6446,7 +6650,7 @@ function chatAgent(options) {
|
|
|
6446
6650
|
// recovered turn shouldn't strand the rest of the boot queue
|
|
6447
6651
|
// until an unrelated live message arrives.
|
|
6448
6652
|
if (bootInjectedQueue.length > 0) {
|
|
6449
|
-
currentWirePayload =
|
|
6653
|
+
currentWirePayload = dispatchBootInjected();
|
|
6450
6654
|
continue;
|
|
6451
6655
|
}
|
|
6452
6656
|
// Wait for the next message — same as after a successful turn
|
|
@@ -8816,6 +9020,7 @@ exports.chat = {
|
|
|
8816
9020
|
cleanupAbortedParts,
|
|
8817
9021
|
/** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
|
|
8818
9022
|
defer: chatDefer,
|
|
9023
|
+
deferBeforeOutput: chatDeferBeforeOutput,
|
|
8819
9024
|
/** Queue model messages for injection at the next `prepareStep` boundary. See {@link injectBackgroundContext}. */
|
|
8820
9025
|
inject: injectBackgroundContext,
|
|
8821
9026
|
/** Typed chat output stream for writing custom chunks or piping from subtasks. */
|