listMessages now requests the most-recent N messages via
orderBy=createTime desc (Chat defaults to ASC, which returned the
OLDEST page and never the recent turns), then reverses the page so the
result is chronological (oldest->newest) while containing the latest
turns. New signature: listMessages(space, opts?: { pageSize?; threadName? }).
When threadName is set, a Chat `filter` query param scopes the listing to
thread.name = "<threadName>" (URL-encoded by searchParams), fixing
cross-thread history leakage. Absent threadName = whole-space/DM scope.
uploadAttachment now accepts opts?: { threadName? } and posts the
attachment message into the thread by delegating step 2 to
createMessage(space, { attachment: [...] }, { threadName, replyToThread: true }),
reusing the existing threading mechanism (thread + messageReplyOption)
instead of duplicating the POST. Previously files posted as top-level
space messages. Still returns { ok, fileId?, error? }.
Both opts are optional with backward-compatible defaults. Existing
old-signature callers compile unchanged and are left for follow-on agents:
- adapter.ts:264 this.chatClient.listMessages(t.space)
- adapter.ts:286 this.chatClient.uploadAttachment(t.space, args.bytes, args.filename)
- conversation-store.ts:41 this.client.listMessages(key.spaceId)
Tests: updated/added in chat-client.test.ts (listMessages with/without
threadName ordering+filter assertions; uploadAttachment threading
assertion). All 62 tests pass; tsc (tsconfig + tsconfig.check) and build
clean.
Google Chat clears any field named in updateMask but absent from the request
body. The edit-in-place streaming path patches a message repeatedly with a
text-only body ({ text }) while passing mask text,cardsV2, which would blank
any cardsV2 payload on the edited message.
Changed (streaming, text-only body → mask text):
- event-renderer.ts ChunkedMessageStream updateAt closure (patchMessage(name, { text }, text))
- adapter.ts stream() ChunkedMessageStream updateAt closure (patchMessage(name, { text }, text))
Left unchanged (correct):
- adapter.ts update() — sends a full renderGoogleChatMessage(ir) body that may
legitimately include cardsV2, so text,cardsV2 is correct.
- event-renderer.ts onToolCallEndEvent tool-status patch — out of scope for this
finding (not a ChunkedMessageStream updateAt closure).
Three Cards v2 validity fixes in render/cards-v2.ts (renderGoogleChatMessage):
- renderNodeWidgets "fields"/"field" cases: emit decoratedText.text instead of
decoratedText.topLabel. Cards v2 REQUIRES text on decoratedText; topLabel is
an optional adornment above it. Budget truncation now applied to text.
- renderGoogleChatMessage: when clamped widgets are empty and there is no
header, return a plain text body instead of an invalid empty-section card.
The section is now only attached when widgets are non-empty, so a header-only
card never emits widgets: [].
- renderGoogleChatMessage: let widgets -> const widgets (never reassigned;
prefer-const).
Tests (render/cards-v2.test.ts): added coverage that a field renders a
decoratedText whose text carries the content, and that non-plain-text IR
producing zero widgets returns a plain text body rather than an empty section.
Existing tests unchanged.
- uploadAttachment now implements the real two-step Google Chat media upload:
(1) POST bytes to {upload base}/{space}/attachments:upload?uploadType=multipart
(the /upload/v1 base, derived from apiUrl by swapping the /v1 suffix),
(2) POST /v1/{space}/messages with { attachment: [{ attachmentDataRef }] }.
Returns { ok: true, fileId } (resourceName/attachmentUploadToken) so the
postFile contract's fileId? is populated; { ok: false, error } on failure.
- Construct upload Blob via new Uint8Array(bytes) so the part is a concrete
ArrayBuffer-backed BlobPart (fixes TS2322 under tsconfig.check.json / lib dom).
- createMessage now throws "google-chat createMessage: response missing message
name" on a 2xx response lacking a name, instead of returning { name: "" }
(which silently corrupted MessageRef.id and downstream patch/delete URLs).
Affected call sites:
- createMessage callers: adapter.ts postMessage/reply paths consuming { name }.
- uploadAttachment callers: adapter.ts postFile (BotReplyTarget) now receives
fileId on success.
verifyIdToken silently dropped the certsUrl option (cast away), so inbound
Chat webhook JWTs were validated against Google's federated OIDC certs
instead of the chat@system.gserviceaccount.com x509 certs that actually
sign them, returning 401 to all legitimate traffic.
Now fetch the Chat system x509 cert map (cached in closure scope) and call
OAuth2Client.verifySignedJwtWithCertsAsync(token, certs, audience,
[CHAT_ISSUER]), which validates signature, audience, and issuer in one call.
Removed the unsupported certsUrl option and the as Parameters<> cast; the
separate iss check is now redundant. resolveCredentials JSON.parse now
rethrows with a clear message on invalid GOOGLE_CHAT_CREDENTIALS.
Call sites checked for the changed auth.ts symbols:
- createInboundVerifier: adapter.ts:67 (GoogleChatAdapter ctor), re-exported
from index.ts:21; produced InboundVerifier consumed in server.ts:10/adapter.ts:43.
- resolveCredentials: internal to auth.ts, called only by createTokenProvider:39.
- UnauthorizedError / InboundVerifier: unchanged public surface, exercised by
auth.test.ts and server.ts.
Test rewritten to mock verifySignedJwtWithCertsAsync and stub the cert fetch.