| D-M2-30-0 |
2026-10-23 |
Built on the old integration line (base int/m1-wave55) with M2-29 stacked, and ported to main after M2-26 and M2-29 landed: only the ticket's own commits were brought over. The commit that ported the row webhook.receiver_tolerance of chapter 11 section 16 is dropped, because main already carries it; the additions to the German chapter and ADR files of the old base go to their English files on main, and the references in the SDK's comments name the English files; changelog entries and rulings are fragments (PL/24); the whsec_ test secrets are assembled at runtime, because the secret-format hook of main refuses literals of that format. |
The rules of the landing replace the integration branches; main is the reference. |
| D-M2-30-1 |
2026-10-21 |
Parity with M2-29: module names of 11 11.2, class names equal to the Python class names, function names the camelCase form of the Python names; where the brief names differ, the Python names win (Adapter for AgentLoop, RunPatcher for RunUpdater, Gateway, GatewaySource and PollingSource for GatewayConsumer and PollingConsumer, questionCard, reconcileCommands, guardedAutocomplete, the fault kinds stored_then_502, stored_late, rate_limited, disconnect, the verdicts of InvocationVerifier.check). The five scenario functions and their doubles are ported step for step to src/testing/scenarios.ts and src/testing/doubles.ts; sdk/testdata/scenarios/README.md lists the scenarios and both SDKs test their names against it; sdk/testdata/context/ holds golden files that both renderers match byte for byte. Place: sdk/typescript/src/, sdk/testdata/, sdk/python/tests/test_scenario_list.py, sdk/python/tests/test_context_golden.py. |
One behaviour in two languages (ADR 0026 "Confirmation"); the brief lets the merged names of M2-29 win (D-W-100). |
| D-M2-30-2 |
2026-10-21 |
Entry points . (Node.js, everything except testing), ./core (browser-safe: generated types, tolerance layer, errors, settings, components, renderContext, uuidv7, pages; a test proves its module graph has no node: import and none of auth, invocation, gateway, webhooks, outbox) and ./testing; runtime dependencies openapi-fetch, jose 6.2.13 and ws 8.22.0 (only testing imports ws); webhooks and DPoP use node:crypto; npm run build writes dist/, which git ignores. Place: sdk/typescript/package.json, tsconfig.build.json, test/packaging.test.ts. |
11 11.3 allows the browser only for parts without secrets; the fewest third-party packages in the security path (SEC-SC-1). |
| D-M2-30-3 |
2026-10-21 |
The client builds a fresh DPoP proof per attempt, keeps the newest DPoP-Nonce and repeats once on 401 use_dpop_nonce outside the retry count; it retries only GET, HEAD, PUT, DELETE, requests with Idempotency-Key and the operations of IDEMPOTENT_EXEMPT_OPERATIONS (reasons agent_runtime, client_id, read_only, held equal to openapi.yaml); attempt n waits uniformly in [sdk.retry_backoff_base, min(sdk.retry_backoff_cap, base times two to the power n-1)], at least Retry-After; bindRun holds the run's newest token in an AsyncLocalStorage, so two concurrent runs never see each other's Invocation-Token. Place: sdk/typescript/src/client.ts, src/retry/. |
06 3.1 point 2, 06 7 point 3, D-M2-26-7, 07 SR9. |
| D-M2-30-4 |
2026-10-21 |
Gateway close codes for agents: stop without reconnecting on 1000, 4004, 4007, 4011, 4012, 4015, 4016, 4017, 4018; 4008 waits the seconds of the close reason (retry_after=<seconds>, the form the server writes), else backoff; 4013 waits uniformly up to sync.reconnect_backoff_base on the first attempt; every other code reconnects with backoff and tries resume first, 4006 after resume falls back to identify. Every close the SDK starts sends 1000, because the WebSocket client of Node.js only sends 1000 or 3000 to 4999; the reconnect decision uses the remembered local reason. Place: sdk/typescript/src/gateway/gateway.ts. |
04 6.4 and 6.5; an API token cannot be refreshed by the SDK. |
| D-M2-30-5 |
2026-10-21 |
InvocationVerifier checks in the order of 09 4.5 with jose compactVerify and algorithms: ["EdDSA"] (header alg and typ before any key lookup, times compared by the SDK), so a token that is expired and has another billing mode gives billing_mode_mismatch as in Python; the JWKS cache lives inside the verifier as in Python; instanceId and agentId are required options; a failed allow list gives invalid; the tool class order is a Map, so inherited object names never count as classes. Place: sdk/typescript/src/invocation/verifier.ts. |
09 4.5 allows three refusal reasons; jwtVerify would check expiry first. |
| D-M2-30-6 |
2026-10-21 |
The adapter acknowledges an event with a token only after the check and the inbox write; an unknown key leaves the event unacknowledged; a replayed jti under a new event ID is ignored and not acknowledged; a refused token finishes the run (failed with the reason) only for an event that would start or resume a run, every other refused event is logged and acknowledged (D-M2-29-8); 403 invocation_token_invalid follows its reason; 409 already_started, 409 billing_mode_mismatch, 409 tool_class_mismatch and 403 agent_invocation_denied drop the entry; FileInboxStore writes one JSON file per entry with mode 0600 in a 0700 directory through write and rename instead of SQLite. Place: sdk/typescript/src/runs/adapter.ts, src/invocation/. |
Parity with M2-29; a forged event must not end a running run; Node.js has no SQLite without a native dependency. |
| D-M2-30-7 |
2026-10-21 |
Patches go over REST patchRun: the local check of the path table of 09 5.3 throws PatchPathError without a request, 409 base_version_conflict rebases on current.version like RunPatcher of M2-29, and only run_not_active or run_inactive stop the patcher. Restored changes of a failed patch go back to the front of the queue, and finish still sends finishRun when its last patch got no answer. Place: sdk/typescript/src/runs/patcher.ts, handle.ts. |
The conflict answer carries the document; the two order rules fix defects the review found in the port (the Python SDK has the same two gaps, see the report). |
| D-M2-30-8 |
2026-10-21 |
reconcileCommands lists the commands per declared scope, keeps those the agent owns, registers, updates (with the full definition, so dropped options and control are noticed) and removes, also commands recorded in its replaceable CommandStore; it checks command.description_max_chars and command.max_per_principal first. Place: sdk/typescript/src/commands/. |
09 10.2 has no operation that lists an agent's own commands in one request. |
| D-M2-30-9 |
2026-10-21 |
Webhook verifier: verifyWebhook and verifyWebhookRequest over the raw body with the error codes missing_header, invalid_timestamp, timestamp_out_of_tolerance, no_trusted_key, signature_mismatch; WebhookKeyCache trusts every listed key and refreshes only after the max-age of the response (without it agent.jwks_max_age), never because of a request; no webhook-id store in the SDK and no Standard Webhooks library. Place: sdk/typescript/src/webhooks/, docs/api/webhooks.md 3. |
A forged request must not make the app fetch; duplicate detection needs the app's own storage; the vectors of M2-26 were checked against the reference library. |
| D-M2-30-10 |
2026-10-21 |
The FakeServer models agents and API tokens with DPoP and nonces, consumer cursors with acknowledgement, inflight window and redelivery, a fresh EdDSA token per delivery, the run, patch, message, command, component, heartbeat and autocomplete operations and the webhook keys; faults per operation and count; restart() keeps the state and redelivers unacknowledged events with new tokens; requests() records method, path and status only. Place: sdk/typescript/src/testing/fakeserver.ts, src/testing/fake/. |
The rules the SDK depends on, the faults of M2-29. |
| D-M2-30-11 |
2026-10-21 |
Live runs reuse the unchanged sdkserver of M2-29 over its stdin protocol; the Vitest project live carries the globalSetup that builds the binary once, so a run of unit files alone needs no Go; both projects run in npm test; the live files fail and never skip without Go or Docker; no Go test is added. Place: sdk/typescript/vitest.config.ts, test/live/. |
One live mechanism for both SDKs (Design note 12); Vitest starts a project's globalSetup only for a project with files in the run. |
| D-M2-30-12 |
2026-10-21 |
typedoc 0.28.17 with notDocumented, notExported and invalidLink as errors (treatValidationWarningsAsErrors, treatWarningsAsErrors), the generated core marked external and excluded; docs:check is typedoc --emit none and runs at the end of npm run lint. Place: sdk/typescript/typedoc.json, package.json, tools/ci/tests/test_sdk_typescript_doc_gate.py. |
23 "Documentation in code"; the generated core is documented by openapi.yaml. |
| D-M2-30-13 |
2026-10-21 |
The reference is built by npm --prefix sdk/typescript run docs in make docs and mounted under api/typescript-reference/ by the MkDocs hook tools/docs/mkdocs_sdk_reference.py (fail closed with the package tracked and the reference missing); make linkcheck excludes exactly ^file://.*/docs/api/typescript-reference/; the docs workflow installs Node.js and the SDK before make docs. Place: Makefile, mkdocs.yml, .github/workflows/docs.yml. |
23 1 point 3; the mount exists only in the site build. |
| D-M2-30-14 |
2026-10-21 |
DEFAULTS has one member per configuration key the SDK uses, each named by its key; test/config-defaults.test.ts holds every value equal to the "Default" column of docs/operations/configuration.md. Place: sdk/typescript/src/config.ts. |
Limits only through named keys (fixed rule 9). |
| D-M2-30-15 |
2026-10-23 |
Tests of the SDK wait for something that must happen with src/testing/patience.ts (eventually, withinPatience), whose only bound is PATIENCE_MS, the value of quality.test.patience; the unit project of Vitest takes the same value as its test and hook limit, and the scenario wait fails only after its server-time limit and the patience both passed. test/no-fixed-waits.test.ts fails on a deadline from Date.now(), a polling loop bounded by turns, a numeric test timeout and a real timer of a second or more (a literal or a parameter default, like the 3 s close of the gateway collector) in test/ and src/testing/; the discovery fixture uses the neutral product name of the Python fixtures, and the samples of non-ASCII text are no German words. |
The rules of deterministic tests (2026-10-21): "runs over REST polling" gave up after 400 turns on a loaded machine; the Python SDK fixed the same class with _patience and test_no_fixed_waits.py, so both SDKs hold one rule. |
| D-M2-30-16 |
2026-10-23 |
Each REST attempt has its own AbortController, aborted by the caller's signal or by a timer of sdk.http_timeout, and the client races the fetch against that signal: a response that arrives after the abort is cancelled and the attempt ends with TimeoutError. Place: sdk/typescript/src/client.ts (attemptSignal, untilAborted). |
Under load a custom fetch that waits on request.signal hung past the timeout (12 of 300 sequential requests with allocation pressure on Node.js 26): the signal of a Request follows the given signal only through a weak reference, so a garbage collection broke the chain; the race makes the bound independent of how a fetch honours the signal, and a unit test with a fetch that ignores the signal proves it. |
| D-M2-30-17 |
2026-10-23 |
Every REST attempt is sent with redirect: "error": a redirect fails as TransportError and is never followed. Place: sdk/typescript/src/client.ts (prepare). |
A followed redirect would carry Invocation-Token and DPoP to its target, also to another origin; no REST operation of the API redirects, and the Python SDK does not follow redirects either. |