authorgravatar for 56691628+Mnigos@users.noreply.github.comIgor Makowski <56691628+Mnigos@users.noreply.github.com> 2026-09-22 00:33:44+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-21 19:33:44-03:00
log80d9c181da41ee3890791c9b65018ce093e7bc9e
tree72de3ad43c7b443412b3d98f1e731912187d905a
parentd2a90b921a5843654a58448d0bc59828e1e3ccc4
signature Signed by PGP key B5690EEEBB952194

fix(contracts): old message-sent events without turnId no longer stop the server from starting (#12763)


2 files changed, 33 insertions(+), 1 deletions(-)

packages/contracts/src/orchestration.test.ts+31
...@@ -812,6 +812,37 @@ it.effect("decodes thread pull request links with snapshot and stack", () =>...@@ -812,6 +812,37 @@ it.effect("decodes thread pull request links with snapshot and stack", () =>
812 }),812 }),
813);813);
814814
815// A stored event that fails to decode stops the event store read, and with it
816// server startup, so rows written before `turnId` existed must still load.
817it.effect("decodes a legacy message-sent event persisted without turnId", () =>
818 Effect.gen(function* () {
819 const event = yield* decodeOrchestrationEvent({
820 sequence: 539,
821 eventId: "event-message-legacy-1",
822 aggregateKind: "thread",
823 aggregateId: "thread-1",
824 type: "thread.message-sent",
825 occurredAt: "2026-01-01T00:00:00.000Z",
826 commandId: "cmd-message-legacy-1",
827 causationEventId: null,
828 correlationId: "cmd-message-legacy-1",
829 metadata: {},
830 payload: {
831 threadId: "thread-1",
832 messageId: "message-1",
833 role: "user",
834 text: "written before turn ids were recorded",
835 streaming: false,
836 createdAt: "2026-01-01T00:00:00.000Z",
837 updatedAt: "2026-01-01T00:00:00.000Z",
838 },
839 });
840 assert.strictEqual(event.type, "thread.message-sent");
841 if (event.type !== "thread.message-sent") return;
842 assert.strictEqual(event.payload.turnId, null);
843 }),
844);
845
815it.effect("decodes thread archived and unarchived events", () =>846it.effect("decodes thread archived and unarchived events", () =>
816 Effect.gen(function* () {847 Effect.gen(function* () {
817 const archived = yield* decodeOrchestrationEvent({848 const archived = yield* decodeOrchestrationEvent({
packages/contracts/src/orchestration.ts+2-1
...@@ -1893,7 +1893,8 @@ export const ThreadMessageSentPayload = Schema.Struct({...@@ -1893,7 +1893,8 @@ export const ThreadMessageSentPayload = Schema.Struct({
1893 text: Schema.String,1893 text: Schema.String,
1894 attachments: Schema.optional(Schema.Array(ChatAttachment)),1894 attachments: Schema.optional(Schema.Array(ChatAttachment)),
1895 context: Schema.optional(OrchestrationMessageContext),1895 context: Schema.optional(OrchestrationMessageContext),
1896 turnId: Schema.NullOr(TurnId),1896 // Events persisted before the field existed carry no key at all.
1897 turnId: Schema.NullOr(TurnId).pipe(Schema.withDecodingDefault(Effect.succeed(null))),
1897 streaming: Schema.Boolean,1898 streaming: Schema.Boolean,
1898 createdAt: IsoDateTime,1899 createdAt: IsoDateTime,
1899 updatedAt: IsoDateTime,1900 updatedAt: IsoDateTime,