Skip to content

fix(sdk)!: apply the review and live-test fixes from #433 - #439

Merged
dodeja merged 27 commits into
mainfrom
sdk-fixes
Oct 17, 2026
Merged

dodeja merged 27 commits into
mainfrom
sdk-fixes

Conversation

@dodeja

@dodeja dodeja commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Why

#433 merged before its review and live-test fixes landed. Live runs against the latest staging found SDK methods that sent the wrong request, called the wrong route, or returned unmapped data. This PR fixes them on top of #438.

What changed

  • feat(sdk): export the party role, container update, party, and custom field value types
  • fix(sdk)!: update documents with documentType and classificationNotes
  • fix(sdk)!: map a tracking request's shipment from tracked_object
  • fix(sdk)!: limit tracking request includes to tracked_object, customer, user
  • fix(sdk): keep the server's detail on 401 errors
  • fix(sdk)!: map containers.get and containers.update in mapped and both formats
  • fix(sdk): keep shipment container ids that are not included
  • fix(sdk)!: map vessels and vessel future positions
  • fix(sdk): send stop_tracking and resume_tracking without a body
  • fix(sdk): filter shipping lines by search on the client
  • fix(sdk): search shipping lines whose name columns are null
  • refactor(sdk): list entity custom fields through the typed client
  • fix(sdk)!: list custom field options through the typed client
  • fix(sdk)!: use the account routes for custom field definitions
  • feat(sdk): cancel tracking requests
  • fix(sdk)!: stop sending page params to unpaginated custom field lists
  • fix(sdk,cli)!: require the event for webhook notification examples
  • test(sdk): cover a container with no POD terminal
  • fix(sdk)!: type tracking request writes from the spec
  • fix(sdk)!: infer Transport.execute's result from the openapi-fetch call
  • fix(sdk)!: type shipment updates from the spec
  • docs: update the SDK method list for mapped results and corrected routes
  • docs: describe the 401 AuthenticationError the SDK now throws
  • fix(sdk): send the caller's numberType from createTrackingRequestFromInfer

Scope

sdks/typescript-sdk, docs/sdk, and the SDK nav. It also includes the minimal CLI edits needed to compile, such as webhook-notifications examples --event.

Tradeoffs

Writes are typed from the spec instead of Record<string, any>. That catches bad payloads at compile time, but callers who passed undocumented attributes will now get a type error.

Blast Radius

Commits marked ! break existing call sites. They change mapped results for containers.get/update, vessels, and tracking requests, tracking request includes, the custom field definition routes, document update params, and the webhook notification examples' required event. Each commit has a BREAKING CHANGE footer.

Verification

  • vp run check and vp run test pass at every commit, and lint and generators are clean at the tip.
  • Live, against a local backend built from staging 51711be:
    • containers: 84/84 pass.
    • webhooks and misc: 77/77 pass.
    • custom fields and documents: 63/64 pass. The failure is DEV-13515, a backend 500.
    • shipments and tracking requests: 85/105 pass. The failures are gaps in the test fixtures or the backend issue DEV-13514.

Base: #438.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

RetriggerView in GreptileConfidence Score: 4/5

The PR appears safe to merge, with non-blocking fixes needed for bodyless result types and the fixture capture script.

Fix All in CodexFindings

  1. P2 Empty successes get wrong types ▶
  2. P2 Fixture capture stops early ▶
Fix with agent prompt
### Issue 1
sdks/typescript-sdk/src/client/transport.ts:73-80
`Transport.execute` removes `undefined` even when it is the successful result. A call with `data: undefined` now returns `Promise<never>`, although it resolves normally. The SDK has bodyless DELETE operations, so this affects supported calls. Preserve `undefined` or `void` for bodyless successes and add a type test for that case.

### Issue 2
sdks/typescript-sdk-cli/src/commands/custom-field-definitions.ts:57-60
Removing `--page-size` leaves `capture-live-fixtures.sh` calling `custom-field-definitions list --page-size 2` at line 73. With `set -euo pipefail`, the unknown option stops the script before it finishes. Update the script alongside this change. It also passes removed pagination flags to `custom-fields` and calls webhook examples without the newly required `--event`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR corrects SDK requests, routes, mapped results, and write types.

  • Tracking requests show their linked shipment from tracked_object.
  • Mapped container and vessel calls return their simplified results.
  • Shipment results keep container IDs when containers are not included.
  • Custom field definitions use account routes and template lists use the shared route.
  • Document updates use documentType and can set or clear classification notes.
  • Shipping-line search checks names and codes on the returned list.
  • Tracking requests can be canceled, and inferred requests use the caller's number type.
  • Shipment tracking controls send no body, and 401 errors keep the API's detail.
  • SDK write inputs and transport results follow the API's generated types.
  • Webhook example calls require a documented event name.

Reviews (1) · Last reviewed commit: "fix(sdk): send the caller's numberType f..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
api Ready Ready Preview Oct 17, 2026 6:23am UTC

Request Review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T06:25:26.924994Z f0bba87 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f448de33b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +57 to +64
cmd
.command('list')
.description('List custom field definitions')
.action(
listAction(
'custom-field-definitions.list',
async ({ client, globals }) => {
const result = await client.customFieldDefinitions.list({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve template listing in the CLI

The SDK now defaults customFieldDefinitions.list() to the account-scoped route and requires scope: 'templates' to access the top-level template route, but this CLI command offers no scope option and always invokes the default. As a result, users who previously ran t49 custom-field-definitions list to obtain template IDs can no longer list templates through the CLI, including when they need a template_id for creation; expose a templates/scope flag and pass it through here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. t49 custom-field-definitions list --templates passes scope: 'templates', and without the flag the command still lists the account definitions. A regression test checks both request URLs.

Comment on lines +73 to +80
public async execute<R extends { data?: unknown }>(
fn: () => Promise<R>,
): Promise<NonNullable<R['data']>> {
const { data } = await fn();
return data as T;
// SAFETY: ErrorMappingInterceptor throws on every non-2xx response, so a
// call that returns here carries its operation's success body. Only an
// operation the spec gives no response body resolves to undefined.
return data as NonNullable<R['data']>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Empty successes get wrong types

Transport.execute removes undefined even when it is the successful result. A call with data: undefined now returns Promise<never>, although it resolves normally. The SDK has bodyless DELETE operations, so this affects supported calls. Preserve undefined or void for bodyless successes and add a type test for that case.

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdks/typescript-sdk/src/client/transport.ts
Line: 73-80

Comment:
**Empty successes get wrong types**

`Transport.execute` removes `undefined` even when it is the successful result. A call with `data: undefined` now returns `Promise<never>`, although it resolves normally. The SDK has bodyless DELETE operations, so this affects supported calls. Preserve `undefined` or `void` for bodyless successes and add a type test for that case.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. execute now returns the data type of openapi-fetch's success branch, so a 204 DELETE resolves to undefined and GET /me keeps its body type. A type test covers both.

Comment on lines +57 to +60
cmd
.command('list')
.description('List custom field definitions')
.action(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Fixture capture stops early

Removing --page-size leaves capture-live-fixtures.sh calling custom-field-definitions list --page-size 2 at line 73. With set -euo pipefail, the unknown option stops the script before it finishes. Update the script alongside this change. It also passes removed pagination flags to custom-fields and calls webhook examples without the newly required --event.

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdks/typescript-sdk-cli/src/commands/custom-field-definitions.ts
Line: 57-60

Comment:
**Fixture capture stops early**

Removing `--page-size` leaves `capture-live-fixtures.sh` calling `custom-field-definitions list --page-size 2` at line 73. With `set -euo pipefail`, the unknown option stops the script before it finishes. Update the script alongside this change. It also passes removed pagination flags to `custom-fields` and calls webhook examples without the newly required `--event`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the commits that removed the flags. Against local staging, the old commands exit 2 with a usage error. The new ones exit 0, or exit 6 with the expected 400 for the unknown event.

@dodeja
dodeja added this pull request to stack #442 October 17, 2026 05:08
Base automatically changed from openapi-fixes to main October 17, 2026 06:22
@mintlify

mintlify Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
terminal49 🟢 Ready View Preview Oct 17, 2026, 6:22 AM

dodeja and others added 17 commits October 16, 2026 23:22
… field value types

addPartyRole on containers and shipments, containers.update,
parties.create and parties.update, and the setCustomField methods take
types derived from the spec, but none of those types were exported, so
callers could not name what they pass. Export
ContainerAddPartyRoleParams, ShipmentAddPartyRoleParams,
ContainerUpdateAttributes, PartyAttributes, and CustomFieldValue. The
three managers share CustomFieldValue from the models module instead of
each declaring its own copy.

The containers.update test now sends every attribute the spec
documents for PATCH /containers/{id}, and a type test pins the new
exports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PATCH /documents/{id} accepts document_type, classification_notes, and
extracted_data_manual. documents.update took documentTypeManual, which
named a field the API never read. It now takes documentType, which
sends document_type, and classificationNotes, which sends
classification_notes and accepts null to clear the notes.

BREAKING CHANGE: documents.update no longer accepts documentTypeManual.
Pass documentType instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API serializes a tracking request's shipment as the polymorphic
`tracked_object` relationship. The mapper read `shipment` and
`container` relationships that the serializer never sends, so
`mapped.shipment` was always null. It now resolves `tracked_object`:
the included shipment gives the id, bill of lading, and SCAC, and a
shipment that is not included still yields its id.

BREAKING CHANGE: mapped tracking requests no longer have a `container`
key, and `TrackingRequest` no longer declares it. The API never links a
container to a tracking request, so the value was always null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r, user

The tracking request serializer exposes only the tracked_object,
customer, and user relationships. `TrackingRequestInclude` offered
`shipment` and `container`, and the API rejects both with 400.

BREAKING CHANGE: `TrackingRequestInclude` is now
'tracked_object' | 'customer' | 'user'. Replace `include: ['shipment']`
with `include: ['tracked_object']`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API answers 401 both for a bad token and for a valid token that the
action's policy denies, such as resume_tracking on a shipment that is
not stopped. The SDK replaced every 401 message with "Invalid or
missing API token", which hid the real reason. AuthenticationError now
carries the server's detail and falls back to the generic message only
when the response has none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h formats

containers.get and containers.update passed no mapper, so `mapped`
returned the raw JSON:API document and `both` returned it twice. With
`defaultFormat: 'mapped'`, `id`, `number`, and `events` were undefined.
Both now map through mapContainer, like containers.list. getDemurrage
and getRailMilestones read raw attributes, so they now request the raw
format and keep working under a mapped default.

BREAKING CHANGE: containers.get, containers.update, and getContainer
return a mapped `Container` for format `mapped`, and `{ raw, mapped }`
with a mapped `Container` for format `both`. Request format `raw` to
keep the JSON:API document. The CLI defaults to format `mapped`, so
`t49 containers get` now prints the mapped `Container` instead of the
JSON:API document. Pass `--format raw` to keep the old output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mapShipment built `containers` from the included container resources
only. A shipment fetched without `include=containers` still links its
containers by id, but the mapped shipment showed none. The mapper now
reads `relationships.containers.data`, emits `{ id }` for each linked
container, and adds `number` when the container is included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vessels.get, getByImo, futurePositions, and futurePositionsWithCoords
passed no mapper, so format `mapped` returned the raw JSON:API document
and `both` returned it twice. They now map to a `Vessel` (name, IMO,
MMSI, last position, speed, and heading). The future-position methods
map to a `VesselWithFuturePositions`, which adds the estimated
`positions` (latitude, longitude, heading, timestamp, estimated).

BREAKING CHANGE: the vessel methods return `Vessel` or
`VesselWithFuturePositions` for format `mapped`, and `{ raw, mapped }`
with the mapped model for format `both`. Request format `raw` to keep
the JSON:API document. The CLI defaults to format `mapped`, so
`t49 vessels get`, `get-by-imo`, `future-positions`, and
`future-positions-coords` now print the mapped model instead of the
JSON:API document. Pass `--format raw` to keep the old output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spec defines no request body for PATCH /shipments/{id}/stop_tracking
or resume_tracking, and the backend reads only the path id. The SDK sent
a JSON:API document through an `as any` cast. Both calls now send the
path id only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /shipping_lines has no search parameter. The SDK sent `search`
through an `as any` cast, the server ignored it, and
listShippingLines('hapag') returned every line. The SDK now requests
the full list and keeps the lines whose SCAC, alternative SCACs, name,
or short name contain the search text, ignoring case. The filter
applies to the raw document too, so every format agrees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spec marks a shipping line's name, short_name, and alternative_scacs
as required strings, but the backing columns are nullable. The search
filter called toLowerCase on each and spread alternative_scacs, so one
line with a null column made listShippingLines(search) throw. The
filter now skips null values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spec now documents `include` on GET /containers/{id}/custom_fields,
/shipments/{id}/custom_fields, and /tracking_requests/{id}/custom_fields.
The three customFields() methods drop their hand-built executeManual
URLs and call the typed client with `include: 'definition'`. The request
is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spec now documents that GET /custom_field_definitions/{id}/options
is not paginated: the API ignores page[number] and page[size] and
returns every option. The SDK sent those params through a hand-built
executeManual URL. It now calls the typed client without them.

customFieldOptions.list and listCustomFieldOptions take CallOptions, so
passing `page` or `pageSize` is a type error instead of a silently
ignored option. `t49 custom-field-options list` drops --page,
--page-size, and --all for the same reason.

BREAKING CHANGE: customFieldOptions.list and listCustomFieldOptions take
CallOptions; page and pageSize no longer compile. t49
custom-field-options list drops --page, --page-size, and --all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET and POST /custom_field_definitions are the Terminal49 template
routes. For a customer account, list returned the templates (often an
empty list) instead of the account's definitions, and create answered
401. customFieldDefinitions.list and create now call
/accounts/{account_id}/custom_field_definitions. The API scopes that
route to the authenticated account, so the SDK fills account_id from
the client's `accountId` or sends the placeholder `current`.
`list({ scope: 'templates' })` still lists the templates.

BREAKING CHANGE: customFieldDefinitions.list() returns the account's
definitions instead of the Terminal49 templates. Pass
`scope: 'templates'` for the templates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spec now documents PATCH /tracking_requests/{id}/cancel, the one
operation the SDK did not cover. trackingRequests.cancel(id) cancels a
request that is still pending or awaiting a manifest. The API returns
an empty object, and 422 for any other status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spec now documents that GET /custom_fields and the custom field
definition lists are not paginated: the API ignores page[number] and
page[size] and returns every row. customFields.list and
customFieldDefinitions.list no longer send them, matching
customFieldOptions.list. The PaginatedQuery intersections that let the
SDK send undeclared params are gone.

Both methods now take CallOptions instead of ListOptions, so passing
`page` or `pageSize` is a type error instead of a silently ignored
option. CustomFieldDefinitionListOptions extends CallOptions. `t49
custom-fields list` and `t49 custom-field-definitions list` drop
--page, --page-size, and --all, and the live fixture capture script
stops passing --page-size to them.

BREAKING CHANGE: customFields.list, listCustomFields,
customFieldDefinitions.list, and listCustomFieldDefinitions take
CallOptions; page and pageSize no longer compile. t49 custom-fields
list and t49 custom-field-definitions list drop --page, --page-size,
and --all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /webhook_notifications/examples answers 400 without an event, and
the spec marks the parameter required. The SDK typed it optional and
cast any string past the generated enum, so examples() with no event
always threw ValidationError: Bad Request.

webhookNotifications.examples and getWebhookNotificationExamples now
take a required WebhookEvent, exported from the generated paths, and
pass it through without a cast. The CLI calls examples(), so
webhook-notifications examples makes --event a required option typed
as WebhookEvent. The CLI README documents it, and the live fixture
capture script gets its 400 envelope from an unknown event instead of
a missing one, which the CLI now rejects before calling the API.

BREAKING CHANGE: webhookNotifications.examples(event, options?) and
getWebhookNotificationExamples(event, options?) require event, typed as
the exported WebhookEvent union. Calls without an event, or with a
string outside the documented events, no longer compile. The CLI
command webhook-notifications examples now requires --event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dodeja and others added 9 commits October 16, 2026 23:22
The generated container type accepts relationships.pod_terminal with
data: null, and containers.get maps such a container with null
podTerminal and destinationTerminal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
trackingRequests.update and updateTrackingRequest took any attributes
and cast the body to any, so `{ ref_number: 'X' }` compiled and the API
answered 200 without changing anything. Both now take
TrackingRequestUpdateAttributes, derived from PATCH /tracking_requests/{id}
(`ref_numbers` only). create and inferNumber also send bodies typed from
their operations instead of casting to any. The wire payloads are
unchanged.

BREAKING CHANGE: trackingRequests.update and updateTrackingRequest accept
only `{ ref_numbers?: string[] }`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
execute returned `any` by default, so the typed mappers
(mapContainerRoute, and mapVessel and mapVesselFuturePositions from
earlier in this series) were never checked against their endpoint:
handing containers.route() the vessel mapper still compiled. execute
now returns the data type of the call's success branch, so each mapper
is checked against its route's 200 response, and a bodyless success
such as a 204 DELETE resolves to undefined. The redundant
`ShippingLineResource` annotation in shippingLines.list() goes away
for the same reason.

BREAKING CHANGE: Transport.execute no longer takes a result type
argument; it infers the result from the request it runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shipments.update and updateShipment took any attributes and cast the
body to any, so `{ tags: 'a,b' }` or `{ ref_number: 'X' }` compiled and
went out unchecked. Both now take ShipmentUpdateAttributes, derived
from PATCH /shipments/{id} (`ref_numbers` and `tags`, each a string
list), the same way tracking request updates are typed. The wire
payload is unchanged.

BREAKING CHANGE: shipments.update and updateShipment accept only
`{ ref_numbers?: string[]; tags?: string[] }`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
methods.mdx now says what `mapped` returns for containers.get and the
vessel methods, lists the includes a tracking request accepts and where
its mapped shipment comes from, and adds trackingRequests.cancel. It
names the account routes that customFieldDefinitions.list and create
call, notes that the custom field lists are not paginated, describes
the client-side shippingLines search, marks the webhook notification
examples event as required, and documents documents.update with
documentType and classificationNotes. Methods without mapping return
the JSON:API response unchanged in `mapped` format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AuthenticationError covers every HTTP 401, including an action the API
refuses for the key, and its message carries the API's detail.
AuthorizationError is the HTTP 403 case. The example logs the message
instead of assuming the key is invalid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Infer

createFromInfer resolved the type as the inferred number_type first and
the caller's numberType second. infer_number always returns a
number_type, so the option was never used. A booking number that infer
classified as bill_of_lading was submitted as bill_of_lading, and a
retry with numberType after an unrecognized inferred type failed the
same way. The caller's numberType now wins over the inferred value.
A numberType that is not container, bill_of_lading, booking_number, or
booking throws a ValidationError that names it, before any request is
sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
customFieldDefinitions.list() now reads the account definitions route
by default and takes scope: 'templates' for the top-level template
route, but custom-field-definitions list had no way to pass the scope.
That left the CLI unable to list the templates whose ids
custom-field-definitions create needs as template_id. list --templates
reads the templates, and list without it still reads the account
definitions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The method reference says it lists every SDK method, but it left out
client.me(), which describes the credential, its account, and the
gated features it can use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0bba87ab8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}),
);
return this.formatResult(raw, options?.format);
return this.formatResult(raw, options?.format, mapContainer);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve linked relationship IDs in mapped container updates

When containers.update() is used with format: 'mapped' (or a mapped default), the PATCH response contains container relationship linkage but no included resources, and mapContainer() resolves relationships only through JsonApiDocument.getRelationship(). Consequently, a container that still belongs to a shipment is returned with shipment: null, and terminal relationships are similarly erased. Preserve the linkage IDs when the related resources are not included, as the updated shipment mapper already does.

Useful? React with 👍 / 👎.

@dodeja
dodeja merged commit 375d216 into main Oct 17, 2026
7 checks passed
dodeja added a commit that referenced this pull request Oct 17, 2026
Transport.execute now infers the response body type from the typed
openapi-fetch call (#439), so the explicit body type arguments no longer
fit its signature. The inferred types are the same TradeIntel* schemas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dodeja added a commit that referenced this pull request Oct 17, 2026
Regenerated on top of main after #438 and #439.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dodeja added a commit that referenced this pull request Oct 17, 2026
Regenerated on top of main after #438 and #439.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot mentioned this pull request Oct 17, 2026
dodeja added a commit that referenced this pull request Oct 17, 2026
Transport.execute now infers the response body type from the typed
openapi-fetch call (#439), so the explicit body type arguments no longer
fit its signature. The inferred types are the same TradeIntel* schemas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dodeja added a commit that referenced this pull request Oct 17, 2026
Regenerated on top of main after #438 and #439.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dodeja pushed a commit that referenced this pull request Oct 17, 2026
Lets an agent assign a party (such as the drayage carrier) or set an account
custom field on up to 50 containers or shipments, reporting per-record
updated, unchanged, failed and not_attempted results.

The tools call the SDK methods #433 and #439 added (partyRoles,
addPartyRole, removePartyRole, setCustomField, deleteCustomField). The
party_role list is parsed in the tool, since the SDK returns it unmapped.

set_custom_field accepts an array of option values for multi-select fields
and treats the same options in another order as unchanged. A call stops
starting new records after 20s, under the 30s /mcp maxDuration, and lists
the records it did not reach in not_attempted.

The server now lists 16 tools; the protocol smoke scripts and tests expect
that count and the two new names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dodeja added a commit that referenced this pull request Oct 18, 2026
…415)

* docs: add Trade Intelligence endpoints to the API reference

Documents the nine /v2/trade_intel endpoints (US import bill-of-lading
records from US Customs manifests, January 2022 onward) under a public
Trade Intelligence tag: request and response schemas as TradeIntel*
components, the 400/401/403/422/429/502/504 error bodies, and endpoint
descriptions that carry the data caveats (physical containers,
estimated values, partial current month, names split by state or
withheld, substring company filter, carrier spelling variants).

Adds the reference pages to the API Reference navigation and
regenerates the Postman collection.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(sdk): add tradeIntel namespace for trade intelligence

client.tradeIntel exposes meta, searchCompanies, companyProfile,
searchCommodities, topImporters, trends, breakdown, lookupContainer, and
lookupBillOfLading. They return the endpoints' plain JSON bodies with
types generated from docs/openapi.json, and surface errors through the
existing error types: the 403 "not enabled" body maps to
FeatureNotEnabledError, 400 and 422 (including FastAPI detail arrays)
to ValidationError.

The trade intelligence POSTs are read-only queries, so the retry policy
now treats POSTs under /trade_intel/ like GETs: 429, 5xx, and network
failures are retried with backoff. Other writes still need an
Idempotency-Key. Types are generated with --default-non-nullable=false
so request fields with server defaults (limit, months, top) stay
optional; no existing type changes.

Fixture-based tests cover every method (path, method, body, response
passthrough) and the 400/422/403/429/502 paths. Regenerates the SDK
reference docs and documents the namespace.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: state the real commodity ordering in trade intelligence lookups

Lookups sort commodity lines by bill, then by estimated value within each
bill, and the 200-line cap applies to that order. The schemas promised a
global highest-value-first order, which a multi-bill response does not
give. Found in Codex review.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: mark always-returned trade intelligence response fields required

meta, the companies/search results, commodities/search results, trend
points, and top importers always carry these fields in production, so
the generated SDK types no longer make them optional. Fields that are
conditional stay optional: a trend point's partial flag, a ranking's
estimated_value without hs4, and a profile's details when found is
false. Raised in Codex review on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(sdk): regenerate types and reference for required response fields

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: restrict trade intelligence month fields to real calendar months

since and until in trends and breakdown accepted any two-digit month,
so schema-validating clients and generated Postman examples allowed
values like 2026-60. Raised in Codex review on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(mcp): trade intelligence tools (#417)

* feat(mcp): add trade intelligence tools

Seven read-only tools for US ocean import market data, built on the
SDK's tradeIntel namespace: search_importers, get_importer_profile,
search_commodities, rank_importers, get_trade_trends,
get_trade_breakdown, and get_trade_data_coverage. Record-level container
and bill of lading lookups stay in the SDK only.

The tool descriptions and server instructions carry the caveats agents
got wrong without guidance: search ranks by match not size and splits
companies by state, the profile with months: 24 is the way to get one
company's trend (the trends company filter is a substring match),
trends and breakdowns include the incomplete current month, history
starts in January 2022, missing volume can mean withheld names, values
are estimates, and volumes don't add across products or companies.

Inputs are strict (enums for measure, interval, dimensions, and coast;
real YYYY-MM months; bounded limits). A 403 returns a plain
"not enabled, contact support@terminal49.com" result instead of an
error; 400 shows the API's reason, and 429 and 5xx get plain messages
without internals. Responses drop internal fields, round numbers, cap
long lists, and flag truncation.

Adds unit and server tests, eval cases that pass with or without the
feature, the new tools in both store listings, and an Unreleased
changelog entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(mcp): document the trade intelligence tools

Adds a trade intelligence section to the MCP tools reference with each
tool's parameters, example questions, the per-account enablement note,
and the caveats for reading the numbers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(mcp): address Codex review of the trade intelligence tools

- A high notify-party share is a clue, not proof of a forwarder: large
  cargo owners can be mostly notify party, so the descriptions and
  instructions now say to weigh the name and product mix too.
- Name and filter inputs accept up to 200 characters, matching the API,
  so a long company name from search can be reused in later calls.
- search_importers reports more_not_shown when it cuts a row's ports,
  origins, carriers, or products to five, and search_commodities keeps
  up to 15 common goods and counts the rest in common_goods_not_shown.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(mcp): qualify the notify-party signal and the common goods cap

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(mcp): skip trade intelligence evals where the tools aren't deployed

Push-time CI runs the live eval against production, which doesn't list
tools a PR adds until it deploys. The new cases now skip when the server
under test doesn't list the tool, and still run against the PR preview.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(mcp): describe the search window honestly and score gated evals fairly

- search_importers results cover the search index's 12-month window,
  which can end a month or two before the last full month. The period
  note, min_containers, and the tool description now say so and point to
  get_importer_profile for the latest months.
- Eval cases for account-gated tools score a "not enabled" reply as the
  allowed outcome it is, so the scorecard and report no longer count it
  as a contract failure. Adds unit tests for that scorer.

Raised in the Codex and Greptile reviews on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(mcp): spell out TEU and HS, and describe the search window

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(mcp): cap trend series by whole periods and breakdowns by whole levels

Trend points arrive ordered by period then group, so cutting the flat
series at 500 dropped the newest periods and could split one, while the
reported range still claimed them. The series now keeps the most recent
whole periods, reports the range actually returned, and counts the
earlier periods left out. Breakdowns drop whole deeper levels instead of
cutting a parent's children. Raised in Codex review on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(mcp): describe default windows and min_containers precisely

The trends and breakdown defaults run from about two years (or one
year) back through the current, incomplete month, inclusive, so they are
not a clean 24 or 12 months. Descriptions now say so and tell agents to
set since and until for annual totals. min_containers applies to the
search window in period, not the last 12 full months. Raised in Codex
review on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(mcp): describe the trends and breakdown default windows precisely

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(mcp): keep a capped trend's range in one period format

A capped quarterly or yearly series reported a range mixing a point
label (2026-Q1) with a month boundary (2026-06). period now always holds
the requested month range, and periods_shown gives the first and last
periods actually returned, in the series' own labels. Raised in Codex
review on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: regenerate SDK types, SDK reference docs, and Postman collection

Regenerated once on top of main after the rebase (main's SDK resync in

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: state which trade intelligence dimensions can't be combined

Lane dimensions (pol, pol_country, container_type) can't be combined with
company or product dimensions in one request, and estimated_value isn't
available by them; the API answers 400. The MCP tool descriptions said
so but the API reference didn't. Raised in Codex review on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: merge trade intelligence responses into the shared responses block

Main's spec now declares components.responses (#438), and the rebase's
text merge added the trade intelligence block as a second "responses"
key in the same object. JSON parsers keep the last key, but YAML-strict
tooling such as openapi-typescript rejects the duplicate. Both sets now
live in one block.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sdk): let tradeIntel calls infer their types from the spec

Transport.execute now infers the response body type from the typed
openapi-fetch call (#439), so the explicit body type arguments no longer
fit its signature. The inferred types are the same TradeIntel* schemas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: regenerate SDK types, SDK reference docs, and Postman collection

Regenerated on top of main after #438 and #439.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: require exact-length trade intelligence state, SCAC and HS4 codes

State filters (company_state, dest_state, state) now require two letters,
the scac filter four letters, and top importers hs4 four digits, matching
the MCP tool schemas. maxLength alone let values like "F" or "123" through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sdk): keep the RetryInterceptor doc comment on the class

The retry explanation sat above the new ReplayableRequest and
RetryRequestParams declarations, so the generated reference lost it.
Move it back above RetryInterceptor and say that read-only trade
intelligence query POSTs are retried like GETs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: regenerate SDK reference docs and Postman collection

Regenerated after the trade intelligence code patterns and the
RetryInterceptor doc comment move. SDK types and MCP filter schemas
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: Auto-generate Postman collection from openapi.json [skip ci]

* docs(mcp): say estimated_value is not available by lane

The API rejects estimated_value with pol, pol_country or container_type
as a dimension or filter (docs/openapi.json TradeIntelDimension and
TradeIntelFilters). The MCP guide presented estimated_value as generally
available, so it now states the restriction for get_trade_trends and
get_trade_breakdown and lists the lane combination rules with the other
trade intelligence caveats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mcp): warn that estimated_value is not available by lane

The measure and dimension descriptions for get_trade_trends and
get_trade_breakdown now say that estimated_value cannot be used with
pol, pol_country or container_type, which the API answers with a 400.
A protocol test checks the advertised measure description and the MCP
docs section for both tools.

Addresses the Codex review on packages/mcp/src/tools/trade-intel.ts:82.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mcp): count all 21 tools in the README, overview and setup guide

The PR registers seven trade intelligence tools, but the MCP README
header and table, the TOOLS_OVERVIEW summary and the setup guide's
"list the tools" check still said 14. The README table now lists the
trade intelligence tools too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mcp): keep documented tool counts and the README table on tools/list

Fails when the README, TOOLS_OVERVIEW or the setup guide state a tool
count other than the tools/list length, or when the README tool table
misses or adds a tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mcp): require the lane-filter case in the estimated_value warning

The API rejects estimated_value when pol, pol_country or container_type
is used at all, as a dimension or as a filter. The test now fails if the
tool description or the docs drop the filter case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mcp): pin the trade trends and breakdown default windows

Without since, the trade-intel service returns the N months ending with
until (24 for trends, 12 for breakdown), so since is N - 1 months before
until. The test fails while tools/list, docs/mcp/home.mdx or the OpenAPI
spec describe the default as "N months ago" or "about a year back".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(trade-intel): state the trends and breakdown default windows exactly

Without since, the API returns the N months ending with until (24 for
trends, 12 for breakdown), so since defaults to N - 1 months before
until. The since fields said "Defaults to 24/12 months ago", which reads
as N + 1 calendar months. The breakdown and trends descriptions and the
MCP guide now give the same window. Source: analytics._range in the
trade-intel service.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: Auto-generate Postman collection from openapi.json [skip ci]

* fix(mcp,sdk): describe the trade default windows as the API computes them

get_trade_trends and get_trade_breakdown said "about two years back" and
"about a year back, not a clean 12 months". They now say the window is
the 24 or 12 months ending with the current, incomplete month, with
since 23 or 11 months before until. The SDK JSDoc, regenerated types
and SDK reference match the OpenAPI source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(openapi): match the trade intelligence contract to what the service returns

Checked against Terminal49/t49 staging (apps/trade-intel build.py,
analytics.py, api.py, index.py):

- meta dates (since_month, until_month_exclusive, facts_since_month,
  partial_month, refreshed_since) and company first_month/last_month are
  first-of-month dates, YYYY-MM-DD; facts_until_exclusive is the day after
  the build; mode is full or incremental.
- importers/top: states can be null, estimated_value is always sent and is
  null without hs4, and filters echo port_of_discharge as pod.
- companies/profile: until is absent when found is false, and totals.states
  can be null.
- trends periods are always YYYY-MM (the first month of the quarter or
  year), not YYYY-Qn or YYYY.
- a 422 can also be the query cost-cap error, { error }.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sdk): type trade intelligence nulls and keep retries within maxRetries

Regenerated types from the corrected spec, so TradeIntelTopImporter
states and estimated_value, profile totals.states and profile until are
typed as the service sends them, ranking filters expose pod, and the 422
body includes the cost-cap { error } variant. Type tests in
spec-types.test.ts fail against the old types.

Fixtures now use the service's real formats (YYYY-MM-DD meta dates,
YYYY-MM quarterly periods), and the live smoke test expects
facts_since_month as a first-of-month date.

RetryInterceptor kept separate retry counters in onError and onResponse,
so network failure, network failure, 503, 503, success made five requests
with maxRetries 2. Both paths now draw on one per-request budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: Auto-generate Postman collection from openapi.json [skip ci]

* fix(mcp): read the trade index dates the service actually sends

The service sends meta and search-window dates as YYYY-MM-DD, but
previousMonth() only accepted YYYY-MM, so get_trade_data_coverage lost
last_full_month and search_window.to, and search_importers lost
period.to. Index dates are now accepted in either form and shown as
YYYY-MM, including first_month and last_month. rank_importers also leaves
out estimated_value_usd when the service sends null instead of emitting
an undefined key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
dodeja added a commit that referenced this pull request Oct 19, 2026
* feat(sdk): party roles and clearing custom fields on containers and shipments

Adds partyRoles, addPartyRole, removePartyRole and clearCustomField to the
container and shipment managers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(mcp): set_party_role and set_custom_field tools

Lets an agent assign a party (such as the drayage carrier) or set an account
custom field on up to 50 containers or shipments, reporting per-record
updated, unchanged and failed results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mcp): document set_party_role and set_custom_field

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(mcp): set_party_role and set_custom_field tools

Lets an agent assign a party (such as the drayage carrier) or set an account
custom field on up to 50 containers or shipments, reporting per-record
updated, unchanged, failed and not_attempted results.

The tools call the SDK methods #433 and #439 added (partyRoles,
addPartyRole, removePartyRole, setCustomField, deleteCustomField). The
party_role list is parsed in the tool, since the SDK returns it unmapped.

set_custom_field accepts an array of option values for multi-select fields
and treats the same options in another order as unchanged. A call stops
starting new records after 20s, under the 30s /mcp maxDuration, and lists
the records it did not reach in not_attempted.

The server now lists 16 tools; the protocol smoke scripts and tests expect
that count and the two new names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mcp): document set_party_role and set_custom_field

Adds both tools to the MCP tools reference and TOOLS_OVERVIEW, including
multi-select values and not_attempted, and updates the tool count in the
setup guide to 16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: Auto-generate Postman collection from openapi.json [skip ci]

* chore: keep main's Postman collection

The bot regenerated it because the rebase changed docs/openapi.json relative
to the old branch tip, but the spec is identical to main's, and
openapi2postmanv2 output differs from run to run. This PR does not change the
API, so it carries no collection change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mcp): make set_party_role swap single-party roles explicitly and mark record edits destructive

A container accepts a second pickup_dray_carrier beside the first, so
"add" with a different carrier left two carriers on the record and
reported success. Adding one to a shipment deletes the previous carrier
server-side, and a second shipment customer hits a unique index.

- add now fails a record where a different party already holds
  pickup_dray_carrier or customer, and names the replace action.
- New replace action makes the party the only one in the role: it
  attaches first, then detaches the others, treats an already-deleted
  assignment as done, and reports a partial swap. It refuses to replace
  a shipment customer rather than leave the shipment without one.
- set_party_role and set_custom_field now set destructiveHint: true.
  Per the MCP spec, false means additive-only; detaching a party or
  overwriting a field value is not. The ChatGPT listing follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mcp): document set_party_role replace and the destructive record edits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mcp): report set_party_role swap failures accurately

From cross-vendor review of the replace action:
- A second shipment customer on add now points to remove-then-add
  instead of replace, which refuses customers.
- A failed detach no longer says the party was attached when the
  record already had it and no POST was made.
- The action description names the customer exception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: Auto-generate Postman collection from openapi.json [skip ci]

* chore: keep main's Postman collection

The generator bot regenerated the collection on this branch after the
main merge; the output differs from main's only in regenerated IDs. The
PR changes no API surface, so it carries main's file unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mcp): cover record writes still running at the call deadline

The 20s budget only stopped workers from starting new records. A read or
write already in flight kept the call open, and an SDK request can take
30s plus retries, past the hosted function's 30s limit. These cases fail
until applyEach stops waiting at the deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mcp): stop record writes at the call deadline, not just new starts

applyEach now races each record against a 20s deadline. A record whose
read was cut off is reported in not_attempted (nothing changed). A
record with a write in flight is reported in failed, saying it may or may
not have been applied. Every write goes through a save guard, so no write
starts after the call has given up on that record. The SDK cannot cancel
a request, so the call stops waiting rather than aborting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mcp): say what set_party_role and set_custom_field return at the deadline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mcp): count sign-in time against the record-write deadline

The 20s budget for set_party_role and set_custom_field started inside the
executor, after the gateway had resolved the WorkOS token. A slow resolve plus
a full batch could still run past the 30s function limit, and the client got
nothing back. The gateway now fixes the deadline when the request arrives and
passes it to the server, so auth time and record work share one budget. Auth
policy, token validation and access are unchanged. A deadline already spent
before the call starts touches no record and lists every ID as not_attempted.

The text content of these two tools was only the summary line, so text-only
clients never saw which records failed, which were not reached, or that a
write may have applied. Their text content is now the full result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mcp): say the hosted write deadline counts from the request

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mcp): hold set_party_role to the request-wide write deadline

The gateway tests only called set_custom_field, so dropping the deadline or the full-result text from set_party_role went unnoticed. This case fails if either is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mcp): name the credential check in the write deadline and the may-have-applied case

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mcp): isolate the write gateway tests from ambient gateway env

A shell with T49_API_TOKEN or T49_MCP_ALLOWED_HOSTS exported sent these tests down the server-token or host-rejection branch. They now clear and restore the same variables api-handler.test.ts clears.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: Auto-generate Postman collection from openapi.json [skip ci]

* fix(mcp): warn that a record write may have applied when it fails after dispatch

A write that failed after it was sent, for example on a dropped
connection, a request timeout or a 5xx, was reported as "The change could
not be saved." The write may have been applied, so the record now says
so and asks the caller to check it, in the structured result and in the
full-result text. Only a 4xx answer counts as a refused write. Which
requests the SDK retries is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(mcp): bound the OAuth resolver call by the request deadline

The gateway resolves a WorkOS token with a fetch to the Terminal49
backend before the tool call starts. Neither that fetch nor reading its
response body had a deadline, so a slow resolver could hold the request
past the 30s function limit, even though the write tools already count
sign-in time against CALL_BUDGET_MS.

The fetch and the body read now share an AbortSignal that fires at the
request's deadline. Running past it is an upstream failure, so the client
gets the same generic 502 as any other resolver outage. The auth policy
and the client-facing errors are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(mcp): warm the gateway import before the record-write gateway tests

The first test in record-writes-gateway.test.ts timed out at 5s once in
a full `vp run test`, and passed on the rerun and in isolation. That test
paid for the cold import of api/mcp.ts and the whole server graph: run
alone, the first test takes about 870ms cold and 130ms warm, and with
every package testing at once the cold import ran past 5s. A beforeAll
now imports the gateway once with a 60s hook timeout, so each test
re-imports it after vi.resetModules() from the warm transform cache.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(mcp): say a write that fails without a clear answer may have applied

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

This branch was successfully deployed

2 active (1 outdated) deployments
Preview — 2276aa49 Deployed Oct 17, 2026 by vercel[bot]
staging - docs — d1b6b3fd Deployed Oct 17, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant