@astralform/js 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -988,10 +988,11 @@ declare class ChatSession {
988
988
  switchConversation(id: string, jobId?: string,
989
989
  /**
990
990
  * User prompt that triggered this job, if known. Emitted as a
991
- * synthetic ``user_message`` ChatEvent right before the first
992
- * ``message_start`` of the replay. User messages aren't persisted
993
- * in ``job_events``, so without this the restored conversation
994
- * would show the agent response with no visible prompt above it.
991
+ * synthetic ``user_message`` ChatEvent at the START of the replay —
992
+ * a completed job maps to exactly one user turn, so every event in it
993
+ * belongs beneath that prompt. User messages aren't persisted in
994
+ * ``job_events``, so without this the restored conversation would show
995
+ * the agent response with no visible prompt above it.
995
996
  */
996
997
  userMessageContent?: string): Promise<void>;
997
998
  deleteConversation(id: string): Promise<void>;
@@ -1145,8 +1146,18 @@ interface RawSseEvent {
1145
1146
  declare function mapSseToChat(raw: RawSseEvent): ChatEvent[];
1146
1147
  /**
1147
1148
  * Replay persisted SSE events through the provided handler, interleaving
1148
- * user messages from session.messages at the first message_start of each
1149
- * turn (user messages aren't persisted in job_events).
1149
+ * user messages from session.messages at the START of each turn (user
1150
+ * messages aren't persisted in job_events).
1151
+ *
1152
+ * The turn boundary is a change of ``job_id``, NOT ``message_start`` or
1153
+ * ``message_stop``. A completed job maps to exactly one user turn — but a
1154
+ * single job can contain several ``message_start``/``message_stop`` pairs (a
1155
+ * tool-use loop: LLM call → tool result → LLM call again), so neither event
1156
+ * reliably delimits turns. And within a job some events precede the first
1157
+ * ``message_start`` (e.g. ``memory_recall``, emitted during prompt prep), so
1158
+ * gating the user block on ``message_start`` would replay them above the
1159
+ * user's own message. Keying off ``job_id`` injects the prompt once per job,
1160
+ * before its first event — matching ``session.ts#switchConversation``.
1150
1161
  */
1151
1162
  declare function replayEvents(sseEvents: RawSseEvent[], userMessages: {
1152
1163
  role: string;
package/dist/index.d.ts CHANGED
@@ -988,10 +988,11 @@ declare class ChatSession {
988
988
  switchConversation(id: string, jobId?: string,
989
989
  /**
990
990
  * User prompt that triggered this job, if known. Emitted as a
991
- * synthetic ``user_message`` ChatEvent right before the first
992
- * ``message_start`` of the replay. User messages aren't persisted
993
- * in ``job_events``, so without this the restored conversation
994
- * would show the agent response with no visible prompt above it.
991
+ * synthetic ``user_message`` ChatEvent at the START of the replay —
992
+ * a completed job maps to exactly one user turn, so every event in it
993
+ * belongs beneath that prompt. User messages aren't persisted in
994
+ * ``job_events``, so without this the restored conversation would show
995
+ * the agent response with no visible prompt above it.
995
996
  */
996
997
  userMessageContent?: string): Promise<void>;
997
998
  deleteConversation(id: string): Promise<void>;
@@ -1145,8 +1146,18 @@ interface RawSseEvent {
1145
1146
  declare function mapSseToChat(raw: RawSseEvent): ChatEvent[];
1146
1147
  /**
1147
1148
  * Replay persisted SSE events through the provided handler, interleaving
1148
- * user messages from session.messages at the first message_start of each
1149
- * turn (user messages aren't persisted in job_events).
1149
+ * user messages from session.messages at the START of each turn (user
1150
+ * messages aren't persisted in job_events).
1151
+ *
1152
+ * The turn boundary is a change of ``job_id``, NOT ``message_start`` or
1153
+ * ``message_stop``. A completed job maps to exactly one user turn — but a
1154
+ * single job can contain several ``message_start``/``message_stop`` pairs (a
1155
+ * tool-use loop: LLM call → tool result → LLM call again), so neither event
1156
+ * reliably delimits turns. And within a job some events precede the first
1157
+ * ``message_start`` (e.g. ``memory_recall``, emitted during prompt prep), so
1158
+ * gating the user block on ``message_start`` would replay them above the
1159
+ * user's own message. Keying off ``job_id`` injects the prompt once per job,
1160
+ * before its first event — matching ``session.ts#switchConversation``.
1150
1161
  */
1151
1162
  declare function replayEvents(sseEvents: RawSseEvent[], userMessages: {
1152
1163
  role: string;
package/dist/index.js CHANGED
@@ -1628,17 +1628,12 @@ var ChatSession = class {
1628
1628
  ]);
1629
1629
  this.messages = messagesResult.status === "fulfilled" ? messagesResult.value : [];
1630
1630
  if (eventsResult.status === "fulfilled") {
1631
- let userMessageEmitted = !userMessageContent;
1631
+ if (userMessageContent) {
1632
+ this.emit({ type: "user_message", content: userMessageContent });
1633
+ }
1632
1634
  for (const ev of eventsResult.value) {
1633
1635
  const type = ev.data.type || ev.event;
1634
1636
  if (!type || type === "done") continue;
1635
- if (!userMessageEmitted && type === "message_start") {
1636
- this.emit({
1637
- type: "user_message",
1638
- content: userMessageContent
1639
- });
1640
- userMessageEmitted = true;
1641
- }
1642
1637
  const wire = { ...ev.data, type };
1643
1638
  try {
1644
1639
  await this.dispatchWireEvent(
@@ -1947,21 +1942,21 @@ function mapSseToChat(raw) {
1947
1942
  function replayEvents(sseEvents, userMessages, handleEvent, addBlock) {
1948
1943
  const userMsgs = userMessages.filter((m) => m.role === "user");
1949
1944
  let userIdx = 0;
1950
- let expectingUserMessage = true;
1945
+ let currentJobId = null;
1951
1946
  for (const raw of sseEvents) {
1952
1947
  const type = raw.data.type || raw.event;
1953
- if (type === "message_stop") {
1954
- expectingUserMessage = true;
1955
- }
1956
- if (type === "message_start" && expectingUserMessage && userIdx < userMsgs.length) {
1957
- const userMsg = userMsgs[userIdx];
1958
- addBlock({
1959
- type: "user",
1960
- id: `replay_user_${userIdx}`,
1961
- content: userMsg.content
1962
- });
1963
- userIdx++;
1964
- expectingUserMessage = false;
1948
+ if (!type || type === "done") continue;
1949
+ const jobId = raw.data.job_id ?? null;
1950
+ if (jobId !== null && jobId !== currentJobId) {
1951
+ currentJobId = jobId;
1952
+ if (userIdx < userMsgs.length) {
1953
+ addBlock({
1954
+ type: "user",
1955
+ id: `replay_user_${userIdx}`,
1956
+ content: userMsgs[userIdx].content
1957
+ });
1958
+ userIdx++;
1959
+ }
1965
1960
  }
1966
1961
  for (const ce of mapSseToChat(raw)) {
1967
1962
  handleEvent(ce);