Skip to content

Commit 82730f1

Browse files
committed
feat: persist status URL across stream boundaries
Introduced lastStatusUrl field to track the most recent URL set via status_set, ensuring it persists even when agentStatus is cleared by new user messages. - URL now persists across stream boundaries until explicitly replaced - Added TDD test to verify URL persistence after status clear - All 15 status tests passing This ensures the URL remains available throughout a conversation, even when status is cleared between user/assistant turns.
1 parent 1ada725 commit 82730f1

File tree

2 files changed

+97
-3
lines changed

2 files changed

+97
-3
lines changed

src/utils/messages/StreamingMessageAggregator.status.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,4 +674,89 @@ describe("StreamingMessageAggregator - Agent Status", () => {
674674
expect(finalStatus?.message).toBe("New PR");
675675
expect(finalStatus?.url).toBe(newUrl); // URL replaced
676676
});
677+
678+
it("should persist URL even after status is cleared by new stream start", () => {
679+
const aggregator = new StreamingMessageAggregator("2024-01-01T00:00:00.000Z");
680+
const messageId1 = "msg1";
681+
682+
// Start first stream
683+
aggregator.handleStreamStart({
684+
type: "stream-start",
685+
workspaceId: "workspace1",
686+
messageId: messageId1,
687+
model: "test-model",
688+
historySequence: 1,
689+
});
690+
691+
// Set status with URL in first stream
692+
const testUrl = "https://github.com/owner/repo/pull/123";
693+
aggregator.handleToolCallStart({
694+
type: "tool-call-start",
695+
workspaceId: "workspace1",
696+
messageId: messageId1,
697+
toolCallId: "tool1",
698+
toolName: "status_set",
699+
args: { emoji: "🔗", message: "PR submitted", url: testUrl },
700+
tokens: 10,
701+
timestamp: Date.now(),
702+
});
703+
704+
aggregator.handleToolCallEnd({
705+
type: "tool-call-end",
706+
workspaceId: "workspace1",
707+
messageId: messageId1,
708+
toolCallId: "tool1",
709+
toolName: "status_set",
710+
result: { success: true, emoji: "🔗", message: "PR submitted", url: testUrl },
711+
});
712+
713+
expect(aggregator.getAgentStatus()?.url).toBe(testUrl);
714+
715+
// User sends a new message, which clears the status
716+
const userMessage = {
717+
id: "user1",
718+
role: "user" as const,
719+
parts: [{ type: "text" as const, text: "Continue" }],
720+
metadata: { timestamp: Date.now(), historySequence: 2 },
721+
};
722+
aggregator.handleMessage(userMessage);
723+
724+
expect(aggregator.getAgentStatus()).toBeUndefined(); // Status cleared
725+
726+
// Start second stream
727+
const messageId2 = "msg2";
728+
aggregator.handleStreamStart({
729+
type: "stream-start",
730+
workspaceId: "workspace1",
731+
messageId: messageId2,
732+
model: "test-model",
733+
historySequence: 2,
734+
});
735+
736+
// Set new status WITHOUT URL - should use the last URL ever seen
737+
aggregator.handleToolCallStart({
738+
type: "tool-call-start",
739+
workspaceId: "workspace1",
740+
messageId: messageId2,
741+
toolCallId: "tool2",
742+
toolName: "status_set",
743+
args: { emoji: "✅", message: "Tests passed" },
744+
tokens: 10,
745+
timestamp: Date.now(),
746+
});
747+
748+
aggregator.handleToolCallEnd({
749+
type: "tool-call-end",
750+
workspaceId: "workspace1",
751+
messageId: messageId2,
752+
toolCallId: "tool2",
753+
toolName: "status_set",
754+
result: { success: true, emoji: "✅", message: "Tests passed" },
755+
});
756+
757+
const finalStatus = aggregator.getAgentStatus();
758+
expect(finalStatus?.emoji).toBe("✅");
759+
expect(finalStatus?.message).toBe("Tests passed");
760+
expect(finalStatus?.url).toBe(testUrl); // URL from previous stream persists!
761+
});
677762
});

src/utils/messages/StreamingMessageAggregator.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ export class StreamingMessageAggregator {
7373
// Unlike todos, this persists after stream completion to show last activity
7474
private agentStatus: { emoji: string; message: string; url?: string } | undefined = undefined;
7575

76+
// Last URL set via status_set - persists even when agentStatus is cleared
77+
// This ensures URL stays available across stream boundaries
78+
private lastStatusUrl: string | undefined = undefined;
79+
7680
// Workspace init hook state (ephemeral, not persisted to history)
7781
private initState: {
7882
status: "running" | "success" | "error";
@@ -523,12 +527,17 @@ export class StreamingMessageAggregator {
523527
// Use output instead of input to get the truncated message
524528
if (toolName === "status_set" && hasSuccessResult(output)) {
525529
const result = output as { success: true; emoji: string; message: string; url?: string };
526-
// Preserve the previous URL if the new status doesn't provide one
527-
const previousUrl = this.agentStatus?.url;
530+
531+
// Update lastStatusUrl if a new URL is provided
532+
if (result.url) {
533+
this.lastStatusUrl = result.url;
534+
}
535+
536+
// Use the provided URL, or fall back to the last URL ever set
528537
this.agentStatus = {
529538
emoji: result.emoji,
530539
message: result.message,
531-
url: result.url ?? previousUrl,
540+
url: result.url ?? this.lastStatusUrl,
532541
};
533542
}
534543
}

0 commit comments

Comments
 (0)