From 998f96cb7dd0efca915daa19f0892c7d919fbd9c Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 31 Aug 2026 16:25:24 -0700 Subject: [PATCH 1/5] Send every reply as the sender the prefill resolved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every reply path filled acting_sender_id from the SDK's account default, ignoring the sender HEY resolved for the thread. The reply prefill picks its sender by inspecting the entry's own to and from addresses — on a shared or alternate address (a HEY for Work support address, an extension, a forwarded external account) that is not the account default — and the create path trusts whatever it is handed. So hey reply, hey compose --thread-id, and the TUI reply all delivered as the account owner: the wrong identity on every reply from a shared address. Keep the prefill's sender alongside its subject and recipients — replyPrefillFromServer now answers a replyPrefill struct — and hand it to CreateReply and CreateReplyDraft as the acting sender, on the delivery and draft paths of both commands and the TUI form alike. Like the subject, the sender survives the recipient fallback: only the recipients needed the local computation, which cannot name a sender and leaves the SDK on the account default, exactly as before. Follows the SDK signature change in basecamp/hey-sdk#135; go.mod pins that branch head until it merges. The vendored MCP model snapshot and Nix vendorHash ride along with the pin. --- go.mod | 2 +- go.sum | 4 +- internal/cmd/compose.go | 4 +- internal/cmd/reply.go | 4 +- internal/cmd/thread_reply.go | 94 +++++++++++++--------- internal/cmd/thread_reply_test.go | 33 ++++++++ internal/mcpserver/model/PROVENANCE.json | 4 +- internal/tui/compose.go | 99 +++++++++++++----------- internal/tui/compose_test.go | 31 ++++++-- nix/package.nix | 2 +- 10 files changed, 180 insertions(+), 97 deletions(-) diff --git a/go.mod b/go.mod index cafaf14c..8d5212b1 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( charm.land/glamour/v2 v2.0.1 charm.land/lipgloss/v2 v2.0.6 github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655 - github.com/basecamp/hey-sdk/go v0.28.2-0.20260831223614-ed5cf6fadf55 + github.com/basecamp/hey-sdk/go v0.28.2-0.20260831231248-da9d3ca760d5 github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d github.com/charmbracelet/x/ansi v0.11.8 github.com/fsnotify/fsnotify v1.10.1 diff --git a/go.sum b/go.sum index d0451ebe..2d9a78be 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655 h1:zz0WUSEmjURj0T+soXuTtgX291nYouqa+UoyYY3Xxk8= github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655/go.mod h1:ezaV5z1GXQAsqyejqTs6wCFl2D8Wj+COLQkHc/kwoRs= -github.com/basecamp/hey-sdk/go v0.28.2-0.20260831223614-ed5cf6fadf55 h1:aBprx230epAZVi6by7FkrTTn0kN/etiVXTRz9YR7+6k= -github.com/basecamp/hey-sdk/go v0.28.2-0.20260831223614-ed5cf6fadf55/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk= +github.com/basecamp/hey-sdk/go v0.28.2-0.20260831231248-da9d3ca760d5 h1:GGGWJe4Vuhk2IrpEhMIEUcYIoLYr51qh4q7AZGvPYBI= +github.com/basecamp/hey-sdk/go v0.28.2-0.20260831231248-da9d3ca760d5/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk= github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d h1:zEQVGq1x1nhKMZ2TudFAcSJ32CHT8richI1vQakIKz4= github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d/go.mod h1:Ee2c/q1/pg+5T5741PIuA3s6VJMQC7I0XBNXIHIujzA= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= diff --git a/internal/cmd/compose.go b/internal/cmd/compose.go index d43436f0..3b1f75d6 100644 --- a/internal/cmd/compose.go +++ b/internal/cmd/compose.go @@ -113,14 +113,14 @@ func (c *composeCommand) run(cmd *cobra.Command, args []string) error { return attachErr } if c.draft { - draftID, draftErr := replySDK.Entries().CreateReplyDraft(ctx, target.EntryID, target.Subject, messageWithAttachments, + draftID, draftErr := replySDK.Entries().CreateReplyDraft(ctx, target.EntryID, target.ActingSenderID, target.Subject, messageWithAttachments, target.Addressed.To, target.Addressed.CC, target.Addressed.BCC) if draftErr != nil { return apierr.FromSDK(draftErr) } return writeDraftSaved(cmd, draftID, len(c.attachments)) } - if err := replySDK.Entries().CreateReply(ctx, target.EntryID, target.Subject, messageWithAttachments, + if err := replySDK.Entries().CreateReply(ctx, target.EntryID, target.ActingSenderID, target.Subject, messageWithAttachments, target.Addressed.To, target.Addressed.CC, target.Addressed.BCC); err != nil { return apierr.FromSDK(err) } diff --git a/internal/cmd/reply.go b/internal/cmd/reply.go index 78b8c1a2..a6b3ef84 100644 --- a/internal/cmd/reply.go +++ b/internal/cmd/reply.go @@ -96,14 +96,14 @@ func (c *replyCommand) run(cmd *cobra.Command, args []string) error { return err } if c.draft { - draftID, draftErr := replySDK.Entries().CreateReplyDraft(ctx, target.EntryID, target.Subject, message, + draftID, draftErr := replySDK.Entries().CreateReplyDraft(ctx, target.EntryID, target.ActingSenderID, target.Subject, message, target.Addressed.To, target.Addressed.CC, target.Addressed.BCC) if draftErr != nil { return apierr.FromSDK(draftErr) } return writeDraftSaved(cmd, draftID, len(c.attachments)) } - if err = replySDK.Entries().CreateReply(ctx, target.EntryID, target.Subject, message, target.Addressed.To, target.Addressed.CC, target.Addressed.BCC); err != nil { + if err = replySDK.Entries().CreateReply(ctx, target.EntryID, target.ActingSenderID, target.Subject, message, target.Addressed.To, target.Addressed.CC, target.Addressed.BCC); err != nil { return apierr.FromSDK(err) } diff --git a/internal/cmd/thread_reply.go b/internal/cmd/thread_reply.go index 1e1f0c3e..ae85d507 100644 --- a/internal/cmd/thread_reply.go +++ b/internal/cmd/thread_reply.go @@ -19,17 +19,21 @@ type replyRecipients struct { BCC []string } -// threadReplyTarget carries the entry a reply answers, its subject and recipients, and -// an immutable client bound to the thread's mail account. HEY saves an unaddressed -// reply as a draft, so the recipients are not optional. The subject is not optional -// either: HEY never derives one, so a reply sent without it saves drafts that read -// "No subject" in Drafts. +// threadReplyTarget carries the entry a reply answers, its subject, sender and +// recipients, and an immutable client bound to the thread's mail account. HEY saves an +// unaddressed reply as a draft, so the recipients are not optional. The subject is not +// optional either: HEY never derives one, so a reply sent without it saves drafts that +// read "No subject" in Drafts. ActingSenderID is the identity the reply goes out as — +// the sender HEY resolved for the thread, which on a shared or alternate address is +// not the account default; zero (the prefill named none, or was unreachable) leaves +// the SDK on the account default. type threadReplyTarget struct { - EntryID int64 - AccountID int64 - Subject string - Addressed replyRecipients - client *hey.Client + EntryID int64 + AccountID int64 + ActingSenderID int64 + Subject string + Addressed replyRecipients + client *hey.Client } // resolveThreadReply returns the thread's latest entry, linked account, and the @@ -54,10 +58,11 @@ func resolveThreadReply(ctx context.Context, threadID int64) (*threadReplyTarget AccountID: topic.AccountId, client: threadSDK, } - subject, addressed, ok := replyPrefillFromServer(ctx, threadSDK, entryID) + prefill, ok := replyPrefillFromServer(ctx, threadSDK, entryID) + target.ActingSenderID = prefill.ActingSenderID + target.Subject = prefill.Subject if ok { - target.Subject = subject - target.Addressed = addressed + target.Addressed = prefill.Addressed return target, nil } @@ -69,44 +74,59 @@ func resolveThreadReply(ctx context.Context, threadID int64) (*threadReplyTarget return nil, apierr.ErrNotFound("message", fmt.Sprintf("%d", entryID)) } - addressed = recipientsForReplyTo(*message) + addressed := recipientsForReplyTo(*message) if len(addressed.To) == 0 && len(addressed.CC) == 0 && len(addressed.BCC) == 0 { return nil, apierr.ErrUsage("could not determine thread recipients") } // The prefill's subject survives an empty recipient list: only the recipients // needed the local computation. - if subject == "" { - subject = replySubject(message.Subject) + if target.Subject == "" { + target.Subject = replySubject(message.Subject) } - target.Subject = subject target.Addressed = addressed return target, nil } +// replyPrefill is how a reply starts out, as HEY prefills it: the "Re: …" subject it +// goes out under, the sender it goes out as, and who it goes out to. +type replyPrefill struct { + Subject string + ActingSenderID int64 + Addressed replyRecipients +} + // replyPrefillFromServer asks HEY how a reply to the entry starts out -// (GET /entries/{id}/replies/new): the "Re: …" subject the reply carries, and its -// recipients — the entry's sender moved onto the To line and the acting user's own -// addresses, aliases and catch-alls excluded — the exclusion this CLI cannot compute -// locally, and the reason a reply used to be able to CC its writer back to themselves. -// A failed read falls back to the local computation, and so does an empty answer: on a -// thread with yourself, everyone HEY excludes is everyone there is, and the local list -// is what keeps that reply addressable. The subject is answered even when the -// recipients are not — only they need the fallback, not the subject HEY supplied. -func replyPrefillFromServer(ctx context.Context, client *hey.Client, entryID int64) (string, replyRecipients, bool) { +// (GET /entries/{id}/replies/new): the "Re: …" subject the reply carries; the sender +// it goes out as — resolved from the entry's own to and from addresses, so a thread on +// a shared or alternate address answers as that address, not the account default, and +// named only when it differs from the acting user; and its recipients — the entry's +// sender moved onto the To line and the acting user's own addresses, aliases and +// catch-alls excluded — the exclusion this CLI cannot compute locally, and the reason +// a reply used to be able to CC its writer back to themselves. A failed read falls +// back to the local computation, and so does an empty answer: on a thread with +// yourself, everyone HEY excludes is everyone there is, and the local list is what +// keeps that reply addressable. The subject and sender are answered even when the +// recipients are not — only they need the fallback, not what HEY already supplied. +func replyPrefillFromServer(ctx context.Context, client *hey.Client, entryID int64) (replyPrefill, bool) { prefilled, err := client.Entries().NewReply(ctx, entryID) if err != nil || prefilled == nil { - return "", replyRecipients{}, false - } - addressed := replyRecipients{ - To: addressEmails(prefilled.Addressed.Directly), - CC: addressEmails(prefilled.Addressed.Copied), - BCC: addressEmails(prefilled.Addressed.Blindcopied), - } - if len(addressed.To)+len(addressed.CC)+len(addressed.BCC) == 0 { - return prefilled.Subject, replyRecipients{}, false - } - return prefilled.Subject, addressed, true + return replyPrefill{}, false + } + prefill := replyPrefill{ + Subject: prefilled.Subject, + ActingSenderID: prefilled.Sender.Id, + Addressed: replyRecipients{ + To: addressEmails(prefilled.Addressed.Directly), + CC: addressEmails(prefilled.Addressed.Copied), + BCC: addressEmails(prefilled.Addressed.Blindcopied), + }, + } + if len(prefill.Addressed.To)+len(prefill.Addressed.CC)+len(prefill.Addressed.BCC) == 0 { + prefill.Addressed = replyRecipients{} + return prefill, false + } + return prefill, true } // replySubject answers the subject a reply to the given subject carries, the way HEY diff --git a/internal/cmd/thread_reply_test.go b/internal/cmd/thread_reply_test.go index 262e5910..0deccfe4 100644 --- a/internal/cmd/thread_reply_test.go +++ b/internal/cmd/thread_reply_test.go @@ -388,6 +388,39 @@ func TestReplyPrefersTheServersComputedRecipients(t *testing.T) { } } +// The prefill also names the sender the reply goes out as — the identity HEY resolved +// from the entry's own to and from addresses. On a shared or alternate address that is +// not the account default, and sending as the default delivers the reply from the +// wrong identity. The prefill's sender wins. +func TestReplySendsAsThePrefillsSender(t *testing.T) { + server, sent := threadReplyServer(t, messageAddressedToJane, 11, 12) + sent.ReplyNewJSON = `{"subject":"Re: Weekly sync","content":"
quoted
","is_reply":true, + "sender":{"id":215,"name":"Support","email_address":"support@example.com"}, + "addressed":{"directly":[{"id":31,"name":"Rick Ramirez","email_address":"rick@example.com"}]}}` + + if err := runCLI(t, server, "--account", "8", "reply", "7", "-m", "sounds good"); err != nil { + t.Fatalf("reply: %v", err) + } + if sent.ActingSenderID != 215 { + t.Errorf("acting_sender_id = %d, want the prefill's sender 215, not the account default", sent.ActingSenderID) + } +} + +// A prefill that names no sender means the acting user replies as themselves, and the +// account's default sender is the right identity. +func TestReplyWithoutAPrefillSenderActsAsTheAccountDefault(t *testing.T) { + server, sent := threadReplyServer(t, messageAddressedToJane, 11, 12) + sent.ReplyNewJSON = `{"subject":"Re: Weekly sync","content":"
quoted
","is_reply":true, + "addressed":{"directly":[{"id":31,"name":"Rick Ramirez","email_address":"rick@example.com"}]}}` + + if err := runCLI(t, server, "--account", "8", "reply", "7", "-m", "sounds good"); err != nil { + t.Fatalf("reply: %v", err) + } + if sent.ActingSenderID != 42 { + t.Errorf("acting_sender_id = %d, want the account default 42", sent.ActingSenderID) + } +} + // An empty answer from replies/new means everyone HEY excludes is everyone there is — // a thread with yourself — and the local computation is what keeps that reply // addressable. The 404 case rides through every other test in this file, which runs diff --git a/internal/mcpserver/model/PROVENANCE.json b/internal/mcpserver/model/PROVENANCE.json index d62bd2d8..844f2a7f 100644 --- a/internal/mcpserver/model/PROVENANCE.json +++ b/internal/mcpserver/model/PROVENANCE.json @@ -1,7 +1,7 @@ { "source": "github.com/basecamp/hey-sdk", - "commit": "ed5cf6fadf55becfd142ba4138c28b5c240c12cf", - "ref": "go/v0.28.2-0.20260831223614-ed5cf6fadf55", + "commit": "da9d3ca760d55de5339a4ab321f2b0c9b8262b9e", + "ref": "go/v0.28.2-0.20260831231248-da9d3ca760d5", "files": ["behavior-model.json", "openapi.json"], "synced_by": "scripts/sync-mcp-model.sh" } diff --git a/internal/tui/compose.go b/internal/tui/compose.go index 4951e124..1f975cc6 100644 --- a/internal/tui/compose.go +++ b/internal/tui/compose.go @@ -19,18 +19,20 @@ import ( // --- Messages --- // replyContextLoadedMsg carries what a reply needs from the thread: the entry to -// reply to, the "Re: …" subject it goes out under, and who the thread is addressed to. +// reply to, the "Re: …" subject it goes out under, the sender it goes out as, and who +// the thread is addressed to. type replyContextLoadedMsg struct { - requestID uint64 - boxID int64 - topicID int64 - topicName string - entryID int64 - sdk *hey.Client - subject string - to, cc []string - bcc []string - err error + requestID uint64 + boxID int64 + topicID int64 + topicName string + entryID int64 + sdk *hey.Client + actingSenderID int64 + subject string + to, cc []string + bcc []string + err error } // forwardContextLoadedMsg carries HEY's prefilled subject and quoted message @@ -77,12 +79,13 @@ const ( // its inputs, validation and status; sending is done by mailView so the form // stays free of SDK calls. type composeForm struct { - mode composeMode - topicName string - entryID int64 // reply target (composeReply only) - replySubject string // the "Re: …" subject a reply goes out under (composeReply only) - sendSDK *hey.Client - forwardedContent string + mode composeMode + topicName string + entryID int64 // reply target (composeReply only) + replySubject string // the "Re: …" subject a reply goes out under (composeReply only) + replyActingSenderID int64 // the sender a reply goes out as; 0 = account default (composeReply only) + sendSDK *hey.Client + forwardedContent string inputs []textinput.Model // to, cc, bcc, subject (subject omitted for replies) body textarea.Model @@ -138,6 +141,7 @@ func newReplyForm(ctxMsg replyContextLoadedMsg, s styles) *composeForm { f.topicName = ctxMsg.topicName f.entryID = ctxMsg.entryID f.replySubject = ctxMsg.subject + f.replyActingSenderID = ctxMsg.actingSenderID f.sendSDK = ctxMsg.sdk f.inputs[fieldTo].SetValue(strings.Join(ctxMsg.to, ", ")) f.inputs[fieldCc].SetValue(strings.Join(ctxMsg.cc, ", ")) @@ -419,30 +423,35 @@ func (v *mailView) loadReplyContext(topicID int64, topicName string) tea.Cmd { entryID := topic.Entries[len(topic.Entries)-1].Id // HEY's reply prefill (GET /entries/{id}/replies/new) is the authority on how - // a reply starts out: the "Re: …" subject it goes out under, and recipients - // with the acting user's own addresses, aliases and catch-alls excluded — an - // exclusion this client cannot compute locally. A failed read falls back to - // the local computation, and so does an empty answer: on a thread with - // yourself, everyone HEY excludes is everyone there is. The prefill's subject - // survives that recipient fallback — only the recipients needed it. + // a reply starts out: the "Re: …" subject it goes out under, the sender it + // goes out as — on a shared or alternate address, not the account default — + // and recipients with the acting user's own addresses, aliases and catch-alls + // excluded — an exclusion this client cannot compute locally. A failed read + // falls back to the local computation, and so does an empty answer: on a + // thread with yourself, everyone HEY excludes is everyone there is. The + // prefill's subject and sender survive that recipient fallback — only the + // recipients needed it. var prefillSubject string + var prefillSenderID int64 if prefilled, prefillErr := accountSDK.Entries().NewReply(ctx, entryID); prefillErr == nil && prefilled != nil { prefillSubject = prefilled.Subject + prefillSenderID = prefilled.Sender.Id to := addressesOf(prefilled.Addressed.Directly, "") cc := addressesOf(prefilled.Addressed.Copied, "") bcc := addressesOf(prefilled.Addressed.Blindcopied, "") if len(to)+len(cc)+len(bcc) > 0 { return replyContextLoadedMsg{ - requestID: requestID, - boxID: boxID, - topicID: topicID, - topicName: topicName, - entryID: entryID, - sdk: accountSDK, - subject: prefillSubject, - to: to, - cc: cc, - bcc: bcc, + requestID: requestID, + boxID: boxID, + topicID: topicID, + topicName: topicName, + entryID: entryID, + sdk: accountSDK, + actingSenderID: prefillSenderID, + subject: prefillSubject, + to: to, + cc: cc, + bcc: bcc, } } } @@ -464,16 +473,17 @@ func (v *mailView) loadReplyContext(topicID int64, topicName string) tea.Cmd { subject = replySubjectFor(*message) } return replyContextLoadedMsg{ - requestID: requestID, - boxID: boxID, - topicID: topicID, - topicName: topicName, - entryID: entryID, - sdk: accountSDK, - subject: subject, - to: to, - cc: cc, - bcc: bcc, + requestID: requestID, + boxID: boxID, + topicID: topicID, + topicName: topicName, + entryID: entryID, + sdk: accountSDK, + actingSenderID: prefillSenderID, + subject: subject, + to: to, + cc: cc, + bcc: bcc, } } } @@ -608,8 +618,9 @@ func (v *mailView) send(f *composeForm) tea.Cmd { switch f.mode { case composeReply: entryID := f.entryID + actingSenderID := f.replyActingSenderID return func() tea.Msg { - err := sdk.Entries().CreateReply(ctx, entryID, subject, body, to, cc, bcc) + err := sdk.Entries().CreateReply(ctx, entryID, actingSenderID, subject, body, to, cc, bcc) return composeSentMsg{label: "Reply sent", err: err} } case composeForward: diff --git a/internal/tui/compose_test.go b/internal/tui/compose_test.go index eac8328e..4866e802 100644 --- a/internal/tui/compose_test.go +++ b/internal/tui/compose_test.go @@ -59,6 +59,7 @@ func composeTestServer(t *testing.T) (*mailView, *struct { "addressed":{"directly":[{"id":1,"name":"Jane Doe","email_address":"jane@example.com"}]}}`)) case "/entries/501/replies/new.json": _, _ = w.Write([]byte(`{"subject":"Re: Quarterly planning", + "sender":{"id":215,"name":"Support","email_address":"support@example.com"}, "addressed":{"directly":[{"id":3,"name":"Rick Sanchez","email_address":"rick@example.com"}]}}`)) case "/entries/501/forwards/new.json": _, _ = w.Write([]byte(`{"subject":"Fwd: Quarterly planning","content":"
Quoted message
"}`)) @@ -221,7 +222,8 @@ func TestReplyFormPrefillsAndSends(t *testing.T) { v.Resize(80, 30) v.Update(replyContextLoadedMsg{ boxID: 1, topicID: 7, topicName: "Kitchen", entryID: 99, subject: "Re: Kitchen", - to: []string{"jane@x.com"}, cc: []string{"bob@x.com"}, + actingSenderID: 7, + to: []string{"jane@x.com"}, cc: []string{"bob@x.com"}, }) f := composeModal(v) if f == nil || f.mode != composeReply { @@ -252,6 +254,10 @@ func TestReplyFormPrefillsAndSends(t *testing.T) { if rec.body["message"].(map[string]any)["subject"] != "Re: Kitchen" { t.Errorf("subject = %v, want Re: Kitchen", rec.body["message"].(map[string]any)["subject"]) } + // The reply goes out as the sender the context resolved, not the account default. + if got, _ := rec.body["acting_sender_id"].(float64); got != 7 { + t.Errorf("acting_sender_id = %v, want the reply context's sender 7", rec.body["acting_sender_id"]) + } addressed := rec.body["entry"].(map[string]any)["addressed"].(map[string]any) if got := addressed["directly"].([]any); len(got) != 1 || got[0] != "jane@x.com" { t.Errorf("directly = %v", got) @@ -280,6 +286,11 @@ func TestReplyLoadsAndSendsThroughThreadAccount(t *testing.T) { if ctxMsg.subject != "Re: Quarterly planning" { t.Errorf("subject = %q, want %q", ctxMsg.subject, "Re: Quarterly planning") } + // And the sender: on a thread sent to a shared or alternate address, the + // prefill names the identity the reply goes out as. + if ctxMsg.actingSenderID != 215 { + t.Errorf("actingSenderID = %d, want the prefill's sender 215", ctxMsg.actingSenderID) + } v.Update(ctxMsg) typeText(v, "Thanks!") msg := runCmd(v.HandleContentKey(ctrlS())) @@ -289,6 +300,9 @@ func TestReplyLoadsAndSendsThroughThreadAccount(t *testing.T) { if rec.path != "/entries/501/replies.json" || rec.account != "9" { t.Fatalf("reply path/account = %s/%q, want /entries/501/replies.json/9", rec.path, rec.account) } + if got, _ := rec.body["acting_sender_id"].(float64); got != 215 { + t.Errorf("acting_sender_id = %v, want the prefill's sender 215, not the account default", rec.body["acting_sender_id"]) + } } func TestReplyContextFallsBackWhenPrefillIsEmpty(t *testing.T) { @@ -296,16 +310,18 @@ func TestReplyContextFallsBackWhenPrefillIsEmpty(t *testing.T) { // everyone there is; the local computation keeps that reply addressable. The // subject only falls back with it when the prefill answered none. for name, testCase := range map[string]struct { - prefillJSON string - wantSubject string + prefillJSON string + wantSubject string + wantSenderID int64 }{ "an empty prefill derives the subject locally": { prefillJSON: `{}`, wantSubject: "Re: Quarterly planning", }, - "a recipientless prefill keeps its subject": { - prefillJSON: `{"subject":"Re: Quarterly planning per HEY"}`, - wantSubject: "Re: Quarterly planning per HEY", + "a recipientless prefill keeps its subject and sender": { + prefillJSON: `{"subject":"Re: Quarterly planning per HEY","sender":{"id":215,"email_address":"support@example.com"}}`, + wantSubject: "Re: Quarterly planning per HEY", + wantSenderID: 215, }, } { t.Run(name, func(t *testing.T) { @@ -346,6 +362,9 @@ func TestReplyContextFallsBackWhenPrefillIsEmpty(t *testing.T) { if ctxMsg.subject != testCase.wantSubject { t.Errorf("subject = %q, want %q", ctxMsg.subject, testCase.wantSubject) } + if ctxMsg.actingSenderID != testCase.wantSenderID { + t.Errorf("actingSenderID = %d, want %d", ctxMsg.actingSenderID, testCase.wantSenderID) + } }) } } diff --git a/nix/package.nix b/nix/package.nix index d2e18104..bb541f53 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -18,7 +18,7 @@ buildGoModule.override { inherit go; } (finalAttrs: { # To update: run `make update-nix-hash` (Docker). It rewrites this quoted # value in place, so keep it a string literal rather than lib.fakeHash. - vendorHash = "sha256-i6v/DtZ7QDwnSObld0I/P7QO/Y+OkO3bVXczxrc65So="; + vendorHash = "sha256-hd5NMMIn8ovGy/xZSxv9lUR2B9dujsddEcsuYUvNkOM="; subPackages = [ "cmd/hey" ]; From e9db5f47522a9a355918a4b2431dbee5a2ff3b08 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 31 Aug 2026 16:34:48 -0700 Subject: [PATCH 2/5] Refresh the SDK pin for the draft-lifecycle sender carriage The acting-sender branch grew DraftContent.ActingSenderID and the untouched pass-through; pseudo-version, model provenance and vendorHash follow. --- go.mod | 2 +- go.sum | 4 ++-- internal/mcpserver/model/PROVENANCE.json | 4 ++-- nix/package.nix | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 8d5212b1..f1218440 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( charm.land/glamour/v2 v2.0.1 charm.land/lipgloss/v2 v2.0.6 github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655 - github.com/basecamp/hey-sdk/go v0.28.2-0.20260831231248-da9d3ca760d5 + github.com/basecamp/hey-sdk/go v0.28.2-0.20260831233143-d15a4b1319f3 github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d github.com/charmbracelet/x/ansi v0.11.8 github.com/fsnotify/fsnotify v1.10.1 diff --git a/go.sum b/go.sum index 2d9a78be..949cb554 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655 h1:zz0WUSEmjURj0T+soXuTtgX291nYouqa+UoyYY3Xxk8= github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655/go.mod h1:ezaV5z1GXQAsqyejqTs6wCFl2D8Wj+COLQkHc/kwoRs= -github.com/basecamp/hey-sdk/go v0.28.2-0.20260831231248-da9d3ca760d5 h1:GGGWJe4Vuhk2IrpEhMIEUcYIoLYr51qh4q7AZGvPYBI= -github.com/basecamp/hey-sdk/go v0.28.2-0.20260831231248-da9d3ca760d5/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk= +github.com/basecamp/hey-sdk/go v0.28.2-0.20260831233143-d15a4b1319f3 h1:3LQ0beWdJWMff042tiDUYf0pIzkXT1ulTh6TGeorhGI= +github.com/basecamp/hey-sdk/go v0.28.2-0.20260831233143-d15a4b1319f3/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk= github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d h1:zEQVGq1x1nhKMZ2TudFAcSJ32CHT8richI1vQakIKz4= github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d/go.mod h1:Ee2c/q1/pg+5T5741PIuA3s6VJMQC7I0XBNXIHIujzA= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= diff --git a/internal/mcpserver/model/PROVENANCE.json b/internal/mcpserver/model/PROVENANCE.json index 844f2a7f..4dadf30b 100644 --- a/internal/mcpserver/model/PROVENANCE.json +++ b/internal/mcpserver/model/PROVENANCE.json @@ -1,7 +1,7 @@ { "source": "github.com/basecamp/hey-sdk", - "commit": "da9d3ca760d55de5339a4ab321f2b0c9b8262b9e", - "ref": "go/v0.28.2-0.20260831231248-da9d3ca760d5", + "commit": "d15a4b1319f33affed370ae1a36fe82cea55ec21", + "ref": "go/v0.28.2-0.20260831233143-d15a4b1319f3", "files": ["behavior-model.json", "openapi.json"], "synced_by": "scripts/sync-mcp-model.sh" } diff --git a/nix/package.nix b/nix/package.nix index bb541f53..7e2b34a5 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -18,7 +18,7 @@ buildGoModule.override { inherit go; } (finalAttrs: { # To update: run `make update-nix-hash` (Docker). It rewrites this quoted # value in place, so keep it a string literal rather than lib.fakeHash. - vendorHash = "sha256-hd5NMMIn8ovGy/xZSxv9lUR2B9dujsddEcsuYUvNkOM="; + vendorHash = "sha256-k5QNp1qwW5ByiDHit7vwWt33axE6Snucmg+Fv/xpBGA="; subPackages = [ "cmd/hey" ]; From 1cc37a5d825ec05561966529193e3365011f8373 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 31 Aug 2026 16:46:59 -0700 Subject: [PATCH 3/5] Preserve the draft's own sender through edit and send MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit draftContentFrom now says the edit state's sender back on every rewrite — HEY revises a draft from the whole request, so omitting it handed a draft composed from a shared or alternate address back to the account default on its first edit or the final send. Rides the refreshed SDK pin, whose DraftContent carries ActingSenderID (zero still meaning the account default). --- go.mod | 2 +- go.sum | 4 +-- internal/cmd/draft.go | 5 ++++ internal/cmd/draft_test.go | 32 ++++++++++++++++++++++++ internal/mcpserver/model/PROVENANCE.json | 4 +-- nix/package.nix | 2 +- 6 files changed, 43 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index f1218440..f829f7f2 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( charm.land/glamour/v2 v2.0.1 charm.land/lipgloss/v2 v2.0.6 github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655 - github.com/basecamp/hey-sdk/go v0.28.2-0.20260831233143-d15a4b1319f3 + github.com/basecamp/hey-sdk/go v0.28.2-0.20260831234310-e9afaab1730c github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d github.com/charmbracelet/x/ansi v0.11.8 github.com/fsnotify/fsnotify v1.10.1 diff --git a/go.sum b/go.sum index 949cb554..4160fa64 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655 h1:zz0WUSEmjURj0T+soXuTtgX291nYouqa+UoyYY3Xxk8= github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655/go.mod h1:ezaV5z1GXQAsqyejqTs6wCFl2D8Wj+COLQkHc/kwoRs= -github.com/basecamp/hey-sdk/go v0.28.2-0.20260831233143-d15a4b1319f3 h1:3LQ0beWdJWMff042tiDUYf0pIzkXT1ulTh6TGeorhGI= -github.com/basecamp/hey-sdk/go v0.28.2-0.20260831233143-d15a4b1319f3/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk= +github.com/basecamp/hey-sdk/go v0.28.2-0.20260831234310-e9afaab1730c h1:ORPzj47yxHmLI5dutqjZBI9rdG++RH0iXSF3NWo57Tg= +github.com/basecamp/hey-sdk/go v0.28.2-0.20260831234310-e9afaab1730c/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk= github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d h1:zEQVGq1x1nhKMZ2TudFAcSJ32CHT8richI1vQakIKz4= github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d/go.mod h1:Ee2c/q1/pg+5T5741PIuA3s6VJMQC7I0XBNXIHIujzA= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= diff --git a/internal/cmd/draft.go b/internal/cmd/draft.go index 49813271..1879ea95 100644 --- a/internal/cmd/draft.go +++ b/internal/cmd/draft.go @@ -81,6 +81,11 @@ func draftContentFrom(edit *generated.MessageEditState) hey.DraftContent { To: addressEmails(edit.Addressed.Directly), CC: addressEmails(edit.Addressed.Copied), BCC: addressEmails(edit.Addressed.Blindcopied), + + // The identity the draft was saved as rides every rewrite, or a draft + // composed from a shared or alternate address would be handed back to + // the account default by its first edit or the final send. + ActingSenderID: edit.Sender.Id, } if !edit.ScheduledDeliveryAt.IsZero() { // HEY's API reads a schedule's date and hour in UTC (ApiRequest sets diff --git a/internal/cmd/draft_test.go b/internal/cmd/draft_test.go index 3c8ff5a8..a6dd06b4 100644 --- a/internal/cmd/draft_test.go +++ b/internal/cmd/draft_test.go @@ -230,6 +230,38 @@ func TestDraftSendDeliversWithTheDraftsOwnState(t *testing.T) { } } +func TestDraftLifecyclePreservesTheDraftsOwnSender(t *testing.T) { + // A draft composed from a shared or alternate address carries that identity in + // its edit state; every rewrite must say it back, or the first `draft update` + // or the final `draft send` would hand the draft to the account default. + altSenderEditJSON := `{"id":12345,"subject":"Quarterly planning","content":"
Agenda to follow.
", + "updated_at":"2026-08-24T10:00:00Z", + "sender":{"id":4242,"name":"Support","email_address":"support@example.com"}, + "addressed":{"directly":[{"id":7,"name":"Maria Delgado","email_address":"maria@example.com"}]}}` + + var writes []draftWrite + if _, err := runJSONCommand(t, draftLifecycleServer(t, altSenderEditJSON, &writes), + "draft", "send", "12345"); err != nil { + t.Fatalf("draft send: %v", err) + } + if len(writes) != 1 { + t.Fatalf("writes = %+v", writes) + } + if got, _ := writes[0].Body["acting_sender_id"].(float64); got != 4242 { + t.Errorf("acting_sender_id = %v, want the draft's own sender 4242", writes[0].Body["acting_sender_id"]) + } + + // Without a sender in the edit state, the account default still applies. + writes = nil + if _, err := runJSONCommand(t, draftLifecycleServer(t, draftEditJSON, &writes), + "draft", "send", "12345"); err != nil { + t.Fatalf("draft send: %v", err) + } + if got, _ := writes[0].Body["acting_sender_id"].(float64); got != 42 { + t.Errorf("acting_sender_id = %v, want the account default 42", writes[0].Body["acting_sender_id"]) + } +} + func TestDraftSendRefusesADraftWithNoRecipients(t *testing.T) { var writes []draftWrite bare := `{"id":12345,"subject":"Quarterly planning","content":"
Agenda.
","addressed":{}}` diff --git a/internal/mcpserver/model/PROVENANCE.json b/internal/mcpserver/model/PROVENANCE.json index 4dadf30b..5ddfea3b 100644 --- a/internal/mcpserver/model/PROVENANCE.json +++ b/internal/mcpserver/model/PROVENANCE.json @@ -1,7 +1,7 @@ { "source": "github.com/basecamp/hey-sdk", - "commit": "d15a4b1319f33affed370ae1a36fe82cea55ec21", - "ref": "go/v0.28.2-0.20260831233143-d15a4b1319f3", + "commit": "e9afaab1730c20097530e616bc8421ee48d27080", + "ref": "go/v0.28.2-0.20260831234310-e9afaab1730c", "files": ["behavior-model.json", "openapi.json"], "synced_by": "scripts/sync-mcp-model.sh" } diff --git a/nix/package.nix b/nix/package.nix index 7e2b34a5..a64a25bc 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -18,7 +18,7 @@ buildGoModule.override { inherit go; } (finalAttrs: { # To update: run `make update-nix-hash` (Docker). It rewrites this quoted # value in place, so keep it a string literal rather than lib.fakeHash. - vendorHash = "sha256-k5QNp1qwW5ByiDHit7vwWt33axE6Snucmg+Fv/xpBGA="; + vendorHash = "sha256-lNXX4tgjl+TcK7Y7IIGJxl69gNAY3jyZg912sbG2E58="; subPackages = [ "cmd/hey" ]; From d0f716389df9d051a8b932082b4c5b5901a33f1c Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 31 Aug 2026 17:07:57 -0700 Subject: [PATCH 4/5] Refresh the SDK pin for the draft-lifecycle conformance round --- go.mod | 2 +- go.sum | 4 ++-- internal/mcpserver/model/PROVENANCE.json | 4 ++-- nix/package.nix | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index f829f7f2..a144af87 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( charm.land/glamour/v2 v2.0.1 charm.land/lipgloss/v2 v2.0.6 github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655 - github.com/basecamp/hey-sdk/go v0.28.2-0.20260831234310-e9afaab1730c + github.com/basecamp/hey-sdk/go v0.28.2-0.20260901000528-b6be2b8fe0fa github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d github.com/charmbracelet/x/ansi v0.11.8 github.com/fsnotify/fsnotify v1.10.1 diff --git a/go.sum b/go.sum index 4160fa64..8ad303cc 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655 h1:zz0WUSEmjURj0T+soXuTtgX291nYouqa+UoyYY3Xxk8= github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655/go.mod h1:ezaV5z1GXQAsqyejqTs6wCFl2D8Wj+COLQkHc/kwoRs= -github.com/basecamp/hey-sdk/go v0.28.2-0.20260831234310-e9afaab1730c h1:ORPzj47yxHmLI5dutqjZBI9rdG++RH0iXSF3NWo57Tg= -github.com/basecamp/hey-sdk/go v0.28.2-0.20260831234310-e9afaab1730c/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk= +github.com/basecamp/hey-sdk/go v0.28.2-0.20260901000528-b6be2b8fe0fa h1:JgKcApBkvOtcAKCQ1LGS6c1qZziCdJKNq0yvcvc64vA= +github.com/basecamp/hey-sdk/go v0.28.2-0.20260901000528-b6be2b8fe0fa/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk= github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d h1:zEQVGq1x1nhKMZ2TudFAcSJ32CHT8richI1vQakIKz4= github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d/go.mod h1:Ee2c/q1/pg+5T5741PIuA3s6VJMQC7I0XBNXIHIujzA= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= diff --git a/internal/mcpserver/model/PROVENANCE.json b/internal/mcpserver/model/PROVENANCE.json index 5ddfea3b..9278689b 100644 --- a/internal/mcpserver/model/PROVENANCE.json +++ b/internal/mcpserver/model/PROVENANCE.json @@ -1,7 +1,7 @@ { "source": "github.com/basecamp/hey-sdk", - "commit": "e9afaab1730c20097530e616bc8421ee48d27080", - "ref": "go/v0.28.2-0.20260831234310-e9afaab1730c", + "commit": "b6be2b8fe0fab2e78ca196721d052c2af963b297", + "ref": "go/v0.28.2-0.20260901000528-b6be2b8fe0fa", "files": ["behavior-model.json", "openapi.json"], "synced_by": "scripts/sync-mcp-model.sh" } diff --git a/nix/package.nix b/nix/package.nix index a64a25bc..59a47bc1 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -18,7 +18,7 @@ buildGoModule.override { inherit go; } (finalAttrs: { # To update: run `make update-nix-hash` (Docker). It rewrites this quoted # value in place, so keep it a string literal rather than lib.fakeHash. - vendorHash = "sha256-lNXX4tgjl+TcK7Y7IIGJxl69gNAY3jyZg912sbG2E58="; + vendorHash = "sha256-q2NzpGrRnp7PjE9bxdJGfE3QPYCS26cTxyLXMqnjqR0="; subPackages = [ "cmd/hey" ]; From 66ae265f111c29c9a2ae3dd36e256544177123ab Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 31 Aug 2026 17:18:08 -0700 Subject: [PATCH 5/5] Refresh the SDK pin for the conformance recipient carriage --- go.mod | 2 +- go.sum | 4 ++-- internal/mcpserver/model/PROVENANCE.json | 4 ++-- nix/package.nix | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index a144af87..238a39aa 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( charm.land/glamour/v2 v2.0.1 charm.land/lipgloss/v2 v2.0.6 github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655 - github.com/basecamp/hey-sdk/go v0.28.2-0.20260901000528-b6be2b8fe0fa + github.com/basecamp/hey-sdk/go v0.28.2-0.20260901001535-51b9228e1844 github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d github.com/charmbracelet/x/ansi v0.11.8 github.com/fsnotify/fsnotify v1.10.1 diff --git a/go.sum b/go.sum index 8ad303cc..04fa528d 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655 h1:zz0WUSEmjURj0T+soXuTtgX291nYouqa+UoyYY3Xxk8= github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655/go.mod h1:ezaV5z1GXQAsqyejqTs6wCFl2D8Wj+COLQkHc/kwoRs= -github.com/basecamp/hey-sdk/go v0.28.2-0.20260901000528-b6be2b8fe0fa h1:JgKcApBkvOtcAKCQ1LGS6c1qZziCdJKNq0yvcvc64vA= -github.com/basecamp/hey-sdk/go v0.28.2-0.20260901000528-b6be2b8fe0fa/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk= +github.com/basecamp/hey-sdk/go v0.28.2-0.20260901001535-51b9228e1844 h1:EU5eR2Awg3HNFlwfIczwF2ZqwpxqNeXDg7/xbtJLAiY= +github.com/basecamp/hey-sdk/go v0.28.2-0.20260901001535-51b9228e1844/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk= github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d h1:zEQVGq1x1nhKMZ2TudFAcSJ32CHT8richI1vQakIKz4= github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d/go.mod h1:Ee2c/q1/pg+5T5741PIuA3s6VJMQC7I0XBNXIHIujzA= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= diff --git a/internal/mcpserver/model/PROVENANCE.json b/internal/mcpserver/model/PROVENANCE.json index 9278689b..20b8140b 100644 --- a/internal/mcpserver/model/PROVENANCE.json +++ b/internal/mcpserver/model/PROVENANCE.json @@ -1,7 +1,7 @@ { "source": "github.com/basecamp/hey-sdk", - "commit": "b6be2b8fe0fab2e78ca196721d052c2af963b297", - "ref": "go/v0.28.2-0.20260901000528-b6be2b8fe0fa", + "commit": "51b9228e184444d714d3a140d3d1c661217e227c", + "ref": "go/v0.28.2-0.20260901001535-51b9228e1844", "files": ["behavior-model.json", "openapi.json"], "synced_by": "scripts/sync-mcp-model.sh" } diff --git a/nix/package.nix b/nix/package.nix index 59a47bc1..c0aa1e02 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -18,7 +18,7 @@ buildGoModule.override { inherit go; } (finalAttrs: { # To update: run `make update-nix-hash` (Docker). It rewrites this quoted # value in place, so keep it a string literal rather than lib.fakeHash. - vendorHash = "sha256-q2NzpGrRnp7PjE9bxdJGfE3QPYCS26cTxyLXMqnjqR0="; + vendorHash = "sha256-hs2gFLO2xk8x1iFVZhBhkAkVQ/AreTvGdhf7nj5N/MU="; subPackages = [ "cmd/hey" ];