node-autotask 0.39.4 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +6 -2
- package/CHANGELOG.md +33 -0
- package/MCP_TOOL_CATALOG.json +10 -32
- package/MCP_TOOL_MANIFEST.md +12 -13
- package/N8N_MCP_MANIFEST.json +5 -44
- package/N8N_MCP_MANIFEST.md +7 -8
- package/README.md +29 -37
- package/capabilities.json +1346 -1535
- package/dist/base-CVx2VMSj.d.cts +103 -0
- package/dist/base-CVx2VMSj.d.ts +103 -0
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-SGKGNJEI.cjs → chunk-DKAD7MOV.cjs} +771 -1835
- package/dist/{chunk-DJCRQLQE.cjs → chunk-DXEBPK55.cjs} +287 -292
- package/dist/{chunk-GHRB46YN.js → chunk-LR6FDJI3.js} +614 -1678
- package/dist/{chunk-VR6RYXTD.cjs → chunk-MJ6J74F3.cjs} +1345 -1534
- package/dist/{chunk-JBQ55HYG.js → chunk-P5655PGY.js} +2 -2
- package/dist/{chunk-DBLXPY4B.js → chunk-TCND5PFF.js} +4 -9
- package/dist/{chunk-H4S4KSXX.cjs → chunk-YAHFPOQF.cjs} +143 -143
- package/dist/{chunk-GDSCYY2G.js → chunk-ZVSSAULC.js} +1345 -1534
- package/dist/{index-DkZVRMeW.d.ts → index-CqO1HNHl.d.cts} +125 -851
- package/dist/{index-DkZVRMeW.d.cts → index-CqO1HNHl.d.ts} +125 -851
- package/dist/index.cjs +23 -51
- package/dist/index.d.cts +103 -102
- package/dist/index.d.ts +103 -102
- package/dist/index.js +19 -47
- package/dist/operations/index.cjs +178 -178
- package/dist/operations/index.d.cts +5 -4
- package/dist/operations/index.d.ts +5 -4
- package/dist/operations/index.js +3 -3
- package/dist/pool.cjs +17 -60
- package/dist/pool.d.cts +5 -13
- package/dist/pool.d.ts +5 -13
- package/dist/pool.js +13 -56
- package/dist/resources/index.cjs +4 -4
- package/dist/resources/index.d.cts +1 -1
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.js +3 -3
- package/dist/types/index.d.cts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/docs/API.md +138 -340
- package/docs/N8N-ADAPTER.md +7 -7
- package/examples/mcp-server-http.ts +18 -18
- package/package.json +1 -1
package/docs/API.md
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> Generated from: `src/` (core modules), `src/errors/`, `src/config/`,
|
|
4
4
|
> `src/pagination.ts`, `src/query.ts`, `src/operations/`, and the capability
|
|
5
|
-
> registry (`capabilities.json` + `capabilities.plan.json`).
|
|
6
|
-
>
|
|
7
|
-
> surface of v0.1.0 (the 121-entity MVP set). The per-entity table in
|
|
5
|
+
> registry (`capabilities.json` + `capabilities.plan.json`). `ARCHITECTURE.md` is a historical v0.1 design record; this reference documents the public
|
|
6
|
+
> surface (see `capabilities.json` for the current resource set). The per-entity table in
|
|
8
7
|
> section 8 is generated by `npm run docs:render` — do not hand-edit it.
|
|
9
8
|
|
|
10
9
|
## 1. Core modules
|
|
@@ -14,7 +13,7 @@
|
|
|
14
13
|
The top-level facade. The constructor validates fail-closed (section 6 of
|
|
15
14
|
`ARCHITECTURE.md`) — invalid configuration throws `AutotaskConfigError` and
|
|
16
15
|
construction issues no request. The client exposes one read-only accessor per
|
|
17
|
-
resource (
|
|
16
|
+
resource (incl. the integration endpoints) plus `config` and the
|
|
18
17
|
shared `http` transport:
|
|
19
18
|
|
|
20
19
|
```ts
|
|
@@ -94,7 +93,7 @@ allows <= 10 concurrent), one correlation id per request (surfaced on every
|
|
|
94
93
|
error and to `onAudit`), and the opt-in client-side hourly budget tracker
|
|
95
94
|
(`rateLimit.perHour`, default off — a sliding one-hour window that waits for a slot within the call deadline, else throws `RateLimitError` with `retryAfter`; the API allows 10,000 req/hour). The tracker is PER PROCESS: it counts only this client's attempts and cannot see other instances or integrations on the tenant (`thresholdInformation.get()` is the only cross-integration signal; the local count is `client.hourlyBudget()`, section 3.2.1). Under the `agent` config profile the exhaustion `suggestedAction` says stop or narrow the query — never raise the limit.
|
|
96
95
|
Redirects are never followed (a 3xx throws `REDIRECT_BLOCKED`, so auth headers cannot leave the configured origin). Retry waits are capped at 60 s and never outlast the call deadline: a larger `Retry-After` (or one that exceeds the remaining budget) throws the `RateLimitError` immediately. `RequestOptions` adds `signal` (caller `AbortSignal`, combined with the deadline; aborting throws `AutotaskAbortError`, code `ABORTED`, and cancels retry sleeps) and `idempotent` (retry a non-GET method on 5xx/network errors; set on the `/query` and `/query/count` POST reads; on a mutation it is the issue #105 opt-back into the pre-#105 auto-retry — never set on real writes by the SDK). `client.withSignal(signal, () => ...)` applies a caller `AbortSignal` to every request (and retry wait) made inside the callback — the way to propagate MCP request cancellation through `invoke` without a signal parameter on each method. A real write clears the read cache. A non-JSON 2xx body is typed by the response `Content-Type` (section 2.1): a non-JSON-ish header throws the retryable `UNEXPECTED_CONTENT_TYPE` (the edge/maintenance page — a maintenance notice is waited out up to `maintenanceMaxWaitMs`, default 1 h, for reads and writes; the same wait recognises the page at any status for calls safe to re-send (a GET or an explicitly idempotent-flagged call — an unflagged mutation makes ONE send with `details.maintenanceDetected`): section 2.1; otherwise a GET or idempotent-flagged call retries and a write makes one call); a JSON-ish or ABSENT header keeps `INVALID_RESPONSE` (the #13/W-A7 shape-drift key); JSON under any other header still decodes (the decode attempt is unconditional). A blank 2xx on a call that expects a result (every method but `DELETE` by default — `RequestOptions.expectBody`) throws the typed `EMPTY_RESPONSE` (`DELETE` and 204 keep returning `undefined`). A response above `maxResponseBytes` throws `RESPONSE_TOO_LARGE`; HTTP 412 maps to `ConflictError` (category `conflict`).
|
|
97
|
-
Per-tenant 429 cooldown gate (`cooldown`, ON by default): once a 429 surfaces from the transport (retries spent, a `Retry-After` too long to wait out, or a non-retriable method), the client records a cooldown deadline for that tenant (lowercased `UserName` + lowercased host — impersonation does not change the key), and every further call for that tenant fails LOCALLY — before the semaphore, the hourly limiter and fetch, with zero upstream calls — with a `RateLimitError` (`code: 'RATE_LIMITED_COOLDOWN'`, `notSent: true`, `retryAfter` = remaining seconds; no credentials appear in the message) until the deadline passes. Duration is `Retry-After` clamped to `cooldown.maxMs` (default 15 minutes), or `cooldown.defaultMs` (default 60 s) when the header is missing or unparseable; the thread-threshold 429 (vendor code `Itgenatr005`, which clears in seconds) arms only a short window: min(`Retry-After`, 10 s), default 5 s. A 2xx from a call that got through clears the gate early; expiry is lazy (the first call after the deadline deletes the entry and proceeds); a hostile `Retry-After` (huge, zero, negative, non-numeric) is clamped to the cap or falls back to the default, so a lock-out is always bounded. `thresholdInformation.get()` bypasses the gate so a throttled tenant can still read its own headroom (a 2xx from it clears the gate; a 429 re-arms it). The
|
|
96
|
+
Per-tenant 429 cooldown gate (`cooldown`, ON by default): once a 429 surfaces from the transport (retries spent, a `Retry-After` too long to wait out, or a non-retriable method), the client records a cooldown deadline for that tenant (lowercased `UserName` + lowercased host — impersonation does not change the key), and every further call for that tenant fails LOCALLY — before the semaphore, the hourly limiter and fetch, with zero upstream calls — with a `RateLimitError` (`code: 'RATE_LIMITED_COOLDOWN'`, `notSent: true`, `retryAfter` = remaining seconds; no credentials appear in the message) until the deadline passes. Duration is `Retry-After` clamped to `cooldown.maxMs` (default 15 minutes), or `cooldown.defaultMs` (default 60 s) when the header is missing or unparseable; the thread-threshold 429 (vendor code `Itgenatr005`, which clears in seconds) arms only a short window: min(`Retry-After`, 10 s), default 5 s. A 2xx from a call that got through clears the gate early; expiry is lazy (the first call after the deadline deletes the entry and proceeds); a hostile `Retry-After` (huge, zero, negative, non-numeric) is clamped to the cap or falls back to the default, so a lock-out is always bounded. `thresholdInformation.get()` bypasses the gate so a throttled tenant can still read its own headroom (a 2xx from it clears the gate; a 429 re-arms it). The state is one in-process map shared by every client instance (expired entries are swept when a new cooldown is armed); `cooldown: { enabled: false }` disables the gate. Gated calls emit an audit event marked `notSent` and do not count toward `rateLimit.perHour`. Limits worth knowing: the gate is per process — other processes and integrations on the same tenant are invisible to it (`thresholdInformation` is the only cross-integration signal); it can mask early recovery if the vendor window clears before `Retry-After`; a process that loads both the ESM and the CJS build gets two maps (deliberately not anchored on `globalThis`).
|
|
98
97
|
|
|
99
98
|
Credential-shaped fields are redacted in audit payloads by default.
|
|
100
99
|
Every 2xx JSON body also passes through the response redaction transform
|
|
@@ -145,17 +144,11 @@ live-verified on `company_notes.create`): it drives the transport's no-retry dec
|
|
|
145
144
|
and the `errorFromStatus` override that marks a matching 500 permanent
|
|
146
145
|
(`retryable: false`, `details.deterministic: true`, a field-catalog
|
|
147
146
|
`suggestedAction`) while keeping the `ServerError` class + `SERVER_ERROR`
|
|
148
|
-
code + status 500 (issue #23
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
`
|
|
152
|
-
`
|
|
153
|
-
for the half-open probe-wait refusal) — a DELIBERATE divergence from #6's
|
|
154
|
-
`RATE_LIMITED_COOLDOWN` refusal (`retryable: false`): the open period is
|
|
155
|
-
bounded and advertised via `retryAfter`, so retrying the SAME call after
|
|
156
|
-
the wait is the correct caller behaviour; `details = { state: 'open' |
|
|
157
|
-
'half-open', retryAfterMs, failureCount }` rides the error and the MCP
|
|
158
|
-
served payload verbatim).
|
|
147
|
+
code + status 500 (issue #23). A third house class is thrown by the transport's pre-send gate
|
|
148
|
+
(issue #285): `MaintenanceError` (`MAINTENANCE`, server, `notSent: true` — the zone's
|
|
149
|
+
maintenance gate is open and the call was refused locally and never sent;
|
|
150
|
+
`retryable: true` with `retryAfter` = seconds to the next probe slot;
|
|
151
|
+
`details = { since, retryAfterMs }` — section 3.2.2).
|
|
159
152
|
|
|
160
153
|
### 1.6 `pagination.ts` — page walk
|
|
161
154
|
|
|
@@ -261,7 +254,7 @@ status → what the vendor does that triggers it.
|
|
|
261
254
|
| `ServerError` | `SERVER_ERROR` | server | 5xx | vendor 5xx — retryable — EXCEPT a deterministic field-validation 500 (a known phrase, `isDeterministicVendor500`): same class/code/status, `retryable: false` + `details.deterministic: true` (issue #23) |
|
|
262
255
|
| `UnexpectedContentTypeError` | `UNEXPECTED_CONTENT_TYPE` | server | 2xx (non-JSON body + a non-JSON-ish `Content-Type`) | the edge serves a non-JSON page (maintenance) — the vendor never processed the request; `details = { httpStatus, contentType, bodySnippet (256 B) }` — retryable (advisory). A non-JSON `2xx` maintenance page is waited out up to `maintenanceMaxWaitMs` (default 1 h) for reads AND writes; at `4xx`/`5xx` the page is waited out only for calls safe to re-send (GET / explicitly idempotent-flagged) — an unflagged mutation makes one send and carries `details.maintenanceDetected` (issue #114); a wait that ran is reported on the surfaced error as `details.maintenance` (section 2.1) |
|
|
263
256
|
| `EmptyResponseError` | `EMPTY_RESPONSE` | server | 2xx (empty body, `expectBody`) | a blank 2xx on a call that expects a result (non-DELETE by default — section 2.1); `details = { httpStatus, method }` — retryable for non-DELETE (advisory: the loop never retries 2xx) |
|
|
264
|
-
| `
|
|
257
|
+
| `MaintenanceError` | `MAINTENANCE` | server | — (refused, not sent) or the page's status | the zone is in its maintenance window (issue #285): either the gate refused the call LOCALLY (zero upstream calls, no hourly stamp, `notSent: true`) or the call itself received the maintenance page and did not wait it out (`httpStatus` set, `details.maintenanceDetected`); `retryAfter`, `details.since` — **retryable: true**, except an unflagged write answered at a non-2xx status (outcome unknown; section 3.2.2) |
|
|
265
258
|
| `ResolutionError` | `RESOLUTION_AMBIGUOUS` / `RESOLUTION_TRUNCATED` / `NOT_FOUND` | resolution | — | `resolve`: multiple exact matches (candidates in `resourceIds`, labeled in `details.idRefs`); a cap-tripped bounded client scan (`resolution.scanOnMiss` only; never a silent `null`); a numeric-id miss |
|
|
266
259
|
| `PaginationLimitError` | `PAGINATION_LIMIT` | policy | — | a page walk that would exceed the 1000-page guard (`suggestedAction` names the tighter filter) |
|
|
267
260
|
| `QueryTooLongError` | `QUERY_TOO_LONG` | policy | — | caller-requested `via: 'get'` whose serialized URL would exceed the 2048-char GET cap — the SDK never silently degrades to POST |
|
|
@@ -281,12 +274,9 @@ body fails identically on retry — issue #23). `UNEXPECTED_CONTENT_TYPE` and
|
|
|
281
274
|
`EMPTY_RESPONSE` are retryable (advisory — the transport retries a non-JSON 2xx
|
|
282
275
|
per policy on idempotent methods only, and never retries a blank 2xx).
|
|
283
276
|
`RATE_LIMITED_COOLDOWN` is not retryable before its deadline — it is a local
|
|
284
|
-
gate and the call was never sent. `
|
|
285
|
-
`notSent: true` + `retryAfter`
|
|
286
|
-
|
|
287
|
-
`RATE_LIMITED_COOLDOWN`'s not-retryable — the open period is bounded and
|
|
288
|
-
advertised, so retrying the SAME call after the wait is the correct caller
|
|
289
|
-
behaviour (service health, not request shape — no narrowing needed).
|
|
277
|
+
gate and the call was never sent. `MAINTENANCE` is retryable with
|
|
278
|
+
`notSent: true` + `retryAfter` — the zone's maintenance gate refused the call
|
|
279
|
+
locally (issue #285); retry the SAME call after the window.
|
|
290
280
|
`suggestedAction` is deterministic next-step text only (never free prose
|
|
291
281
|
from the vendor).
|
|
292
282
|
|
|
@@ -324,7 +314,7 @@ transport reads the header:
|
|
|
324
314
|
|
|
325
315
|
| `Content-Type` on the response | outcome |
|
|
326
316
|
|---|---|
|
|
327
|
-
| non-JSON-ish (`text/*` — incl. `text/json` — or anything not `application/json*` / a plus-json suffix) | `UNEXPECTED_CONTENT_TYPE` (server, retryable — advisory; `details = { httpStatus, contentType, bodySnippet }`) |
|
|
317
|
+
| non-JSON-ish (`text/*` — incl. `text/json` — or anything not `application/json*` / a plus-json suffix) | `UNEXPECTED_CONTENT_TYPE` (server, retryable — advisory; `details = { httpStatus, contentType, bodySnippet }`); a body that reads as the maintenance page is `MAINTENANCE` instead (section 3.2.2) |
|
|
328
318
|
| JSON-ish (`application/json*`, a plus-json suffix) | `INVALID_RESPONSE` (status quo — the #13/W-A7 shape-drift key; a wrong-shape 2xx is a shape-level failure, not a transport one) |
|
|
329
319
|
| ABSENT | `INVALID_RESPONSE` (status quo — conservative; the maintenance-page premise carries a header) |
|
|
330
320
|
|
|
@@ -357,8 +347,8 @@ notice:
|
|
|
357
347
|
- a non-JSON **`4xx`/`5xx`** page is waited out and retried only for calls that
|
|
358
348
|
are **safe to re-send** (`retriable5xx`: a `GET` or an explicitly
|
|
359
349
|
`idempotent`-flagged call). An UNFLAGGED mutation answered with such a page
|
|
360
|
-
makes **exactly one send** and surfaces
|
|
361
|
-
`details.maintenanceDetected = true` and a verify-before-reissuing action — a
|
|
350
|
+
makes **exactly one send** and surfaces `MaintenanceError` (`retryable:
|
|
351
|
+
false`) with `details.maintenanceDetected = true` and a verify-before-reissuing action — a
|
|
362
352
|
proxy can serve that page AFTER forwarding the write, so the outcome is
|
|
363
353
|
unknown (the §2.2 write rule is intact).
|
|
364
354
|
|
|
@@ -368,7 +358,9 @@ observed ceiling is 4 h (`MAINTENANCE_WINDOW_MAX_MS`; 19 live windows:
|
|
|
368
358
|
30–240 min, median ≈ 2 h) — an unattended worker should pass
|
|
369
359
|
`maintenanceMaxWaitMs: MAINTENANCE_WINDOW_MAX_MS`. The wait does not consume
|
|
370
360
|
`maxRetries`, each post-wait attempt gets a fresh `timeoutMs`, and a caller
|
|
371
|
-
`AbortSignal` interrupts the wait.
|
|
361
|
+
`AbortSignal` interrupts the wait. Only one call per zone waits at a time: the
|
|
362
|
+
first page opens the zone's maintenance gate and every other call fails fast
|
|
363
|
+
with `MAINTENANCE` (section 3.2.2). A non-JSON HTML-ish page that does not look
|
|
372
364
|
like a maintenance notice (for example a wrong `baseUrl` serving a web site, or
|
|
373
365
|
a stock proxy page) is surfaced at once with
|
|
374
366
|
`details.maintenancePatternMiss = true` and the 256-byte snippet logged at
|
|
@@ -448,7 +440,7 @@ carries `notSent: true` is row 1, not row 3):
|
|
|
448
440
|
|
|
449
441
|
| # | condition | state | retryable | why |
|
|
450
442
|
|---|-----------|-------|-----------|-----|
|
|
451
|
-
| 1 | `notSent === true` (the cooldown gate's /
|
|
443
|
+
| 1 | `notSent === true` (the cooldown gate's / maintenance gate's local refusal) | `failed` | the error's own flag | the request never reached the vendor; the maintenance refusal retries the same call after the window, the cooldown's is `retryable: false` (wait, then narrow the query first) |
|
|
452
444
|
| 2 | `httpStatus` in 400..499 except 429 | `failed` | `false` | the vendor answered; the write is definitely not applied |
|
|
453
445
|
| 3 | `httpStatus === 429` | `failed` | `true` | a pre-processing refusal — retry once the cooldown / Retry-After allows |
|
|
454
446
|
| 4 | `httpStatus === 500` and `parseVendorErrors(vendorError)` returns a non-empty array | `failed` | `false` | a deterministic business-rule rejection — the same body fails identically on retry |
|
|
@@ -513,21 +505,18 @@ Host form: `https://<name>.autotask.net/atservicesrest` (lowercase
|
|
|
513
505
|
| `timeoutMs` | 30 000 | — | whole-call budget, incl. retries |
|
|
514
506
|
| `maxRetries` | 3 | — | the client's retry budget, overridable per call (`InvokeOptions.maxRetries` / `MutationOpts.maxRetries`; `0` = exactly one send per request the call performs). The budget caps three legs (issues #105/#109): the 429 leg (retried on GET/PUT/DELETE + flagged calls — unchanged) and the 5xx/network + non-JSON 2xx legs (GET + flagged only) |
|
|
515
507
|
| `maintenanceMaxWaitMs` | 3 600 000 (1 h) | — | **maintenance-window auto-retry** (weekly Autotask window): when the first 256 bytes of a non-JSON body read as a maintenance notice, the transport waits `maintenancePollMs` and retries for up to this long from the FIRST such page. A `2xx` page is waited out for reads AND writes (edge-served before execution); a `4xx`/`5xx` page only for calls safe to re-send (GET / explicitly idempotent-flagged) — an unflagged mutation makes ONE send and carries `details.maintenanceDetected` (a proxy can serve the page after forwarding). JSON bodies never enter. Does not consume `maxRetries`; each post-wait attempt gets a fresh `timeoutMs`. `0` disables the wait. A non-matching HTML-ish page stamps `details.maintenancePatternMiss`. Observed ceiling 4 h: `MAINTENANCE_WINDOW_MAX_MS` (19 windows: 30–240 min, median ≈ 2 h) |
|
|
516
|
-
| `maintenancePollMs` | 30 000 | — | poll interval while waiting out a maintenance page |
|
|
508
|
+
| `maintenancePollMs` | 30 000 | — | poll interval while waiting out a maintenance page, and the maintenance gate's probe interval (section 3.2.2) |
|
|
517
509
|
| `maxConcurrent` | 10 | 10 (vendor) | concurrency guard |
|
|
518
510
|
| `profile` | `library` | — | `library` = today's defaults (changes nothing); `agent` = the conservative `AGENT_PROFILE` limits for LLM-driven workloads (below). Resolution: explicit config > profile > library default |
|
|
519
511
|
| `maxResponseBytes` | 67 108 864 (64 MiB) | — | largest response body read; above it the call throws `RESPONSE_TOO_LARGE` (non-retryable) |
|
|
520
512
|
| `rateLimit.perHour` | off | 10 000 (vendor) | opt-in client-side hourly limiter (sliding window; waits within the call deadline, else `RateLimitError`). `null` is the explicit "disabled" value (beats the profile). The limiter is **per process** — it cannot see other instances or integrations on the tenant (`thresholdInformation.get()` is the tenant-wide readout; `client.hourlyBudget()` is the local one) |
|
|
521
|
-
| `cooldown` | on | 15 min (`cooldown.maxMs`) | per-tenant 429 cooldown gate (issue #6) — after a surfaced 429, further calls for that tenant (lowercased username + host) fail locally (`RATE_LIMITED_COOLDOWN`, `notSent: true`) until the deadline; `enabled: false` disables; `defaultMs` is the missing-`Retry-After` window (60 s)
|
|
522
|
-
| `circuitBreaker` | off | — | opt-in per-host circuit breaker (issue #26): `failureThreshold` (5) counted failures inside `failureWindowMs` (10 s) open the circuit for `openTimeoutMs` (30 s); `successesToClose` (2) probe successes close it; `halfOpenProbes` (default `= successesToClose`; validated `>= successesToClose`) bounds the half-open probes. Key = the host of the base URL (host-only — on a shared host, one tenant's outage opens the circuit for that host's other tenants; section 3.2.2). `thresholdInformation.get()` bypasses the refusal (the outcome is still recorded); `store` / `isFailure` inject the store / the failure predicate |
|
|
513
|
+
| `cooldown` | on | 15 min (`cooldown.maxMs`) | per-tenant 429 cooldown gate (issue #6) — after a surfaced 429, further calls for that tenant (lowercased username + host) fail locally (`RATE_LIMITED_COOLDOWN`, `notSent: true`) until the deadline; `enabled: false` disables; `defaultMs` is the missing-`Retry-After` window (60 s) |
|
|
523
514
|
| `resolution.maxScanRecords` | 500 | — | bounded client-scan cap (section 7, `resolve`) |
|
|
524
515
|
| `resolution.maxScanPages` | 4 | — | bounded client-scan cap |
|
|
525
516
|
| `resolution.scanOnMiss` | `false` | — | re-check a bare-value `resolve` miss with the bounded client scan (exact match on the search fields). Off: the server filter's zero items is a complete non-match (`null`, one call) |
|
|
526
517
|
| `logger` | `NoopLogger` | — | request log sink |
|
|
527
518
|
| `redactPersonalData` | `true` | — | response redaction for PII + confidential fields (section 10); `false` = raw personal data, secrets still redacted |
|
|
528
|
-
| `cache` | on |
|
|
529
|
-
| `fieldCache` | on | `maxEntities` 256 | `{ ttlMs?, maxEntities? }` live field-metadata channel for the entityInformation trio (`getEntityInfo` / `getFieldInfo` / `getUdfInfo`) (issue #80: default-on — absent = the default instance, 5 min; `false` = the uncached wire path, bit-for-bit) — see section 11.14. With it `false` the trio is the uncached wire path, bit-for-bit. Live reads are snapshots at fetch time; a vendor drift (a deterministic validation 500, or an INVALID_RESPONSE on a metadata read) invalidates the named entity's entries; a record write never invalidates (it says nothing about the field catalog). Empty results are never cached; concurrent callers share one in-flight request |
|
|
530
|
-
| `resourceNameCache` | on | `maxEntries` 1024 | `{ ttlMs?, maxEntries? }` resource-name channel for `operations.expandResourceNames` (issue #65; issue #80: default-on — absent = the default instance, 1 h; `false` = the uncached per-id path) — see section 11.21. With it `false` the op is the uncached per-id path (per-call de-duplication still applies). Live names are snapshots at fetch time; a `resources` write through the same client clears the channel, writes elsewhere show up once `ttlMs` expires. Failures are never cached; a vendor true not-found IS cached as a blind marker (a repeat miss costs no wire) |
|
|
519
|
+
| `cache` | on | read 4096 / field 256 entries | `{ ttlMs?, fieldTtlMs?, store? }` the SDK's in-memory caches (issue #286; one block): single-record reads (get-by-id, the id branch of `resolve`, composite FK reads, `operations.expandResourceNames` names) live `ttlMs` (default 5 min; the reference tables `REFERENCE_ENTITIES` live 1 h), entity field metadata lives `fieldTtlMs` (default 5 min). `false` = every channel off. A real write through the client clears the record cache; a vendor drift signal drops one entity's field metadata (section 11.14). Lists, queries, counts, mutations and errors are never cached. `fieldCache`, `resourceNameCache` and `cache.perResourceTtlMs` were removed in 0.40.0 (the first two are refused) |
|
|
531
520
|
| `update` | off | — | `{ putFallback?: boolean }` opt-in PUT item-path fallback for a 404'd non-collectionUpdate update route (issue #34) — see section 11.16. A TYPED 404 retries ONCE as `PUT /{Entity}/{id}` with the caller's fields merged onto a fresh unredacted read (never a null-clearing partial); collectionUpdate routes, `raw: true` updates and every non-404 fault are never retried — a declined gate throws the self-explaining error naming the route, the status and the opt-in |
|
|
532
521
|
| `reResolveZoneOn401` | off | — | opt-in zone re-resolution on 401 (issue #43) — see section 11.19. A 401 on a relative path against a CATALOG base makes ONE unauthenticated vendor zone-discovery call; if the tenant's zone base has moved, the client re-points its live base URL (process-lifetime, observable via `http.baseUrl`) and retries the SAME request ONCE. An explicit `baseUrl` and an absolute path are never re-pointed; a resolved base identical to the live one, or a discovery fault, propagates today's typed 401 untouched (zero retry); a retry that 401s again carries `details.zoneRecovery = { from, to, retried: true }` |
|
|
533
522
|
| `hostPolicy` | off | — | opt-in config-time HOST BOUNDS (skill 3.5.0 / L2) — see section 11.20. `{ blockPrivateHosts?: boolean; allowedOrigins?: readonly string[] }`: the RESOLVED base URL (explicit `baseUrl` or zone-derived) must pass every ACTIVE bound at construction time, pre-wire; a violation throws the typed `HostPolicyDeniedError` (`HOST_POLICY_DENIED`, category `policy`, `retryable: false`) naming the offending origin. `blockPrivateHosts: true` refuses a LITERAL private / loopback / link-local host (IPv4 10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, 0/8; IPv6 `::1`, `fc00::/7`, `fe80::/10`, IPv4-mapped `::ffff:a.b.c.d`); `allowedOrigins` is an EXACT normalised-origin allow-list. Checks run on the canonical `new URL(origin).host` — HOSTNAMES are never blocked (no DNS lookups); the private bound is checked FIRST and WINS; a present block with no active bound is refused fail-closed; with `reResolveZoneOn401: true` the same bounds gate the discovery-provided re-point target (a refused host is a discovery fault) |
|
|
@@ -590,88 +579,41 @@ shell-quoting/line-break artifacts — check your env load (…)`) or was clean
|
|
|
590
579
|
likely cause`). The message and `suggestedAction` are unchanged — the
|
|
591
580
|
diagnostic is appended as a separate field, and a 401 is still never retried.
|
|
592
581
|
|
|
593
|
-
#### 3.2.2 The per-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
**States (timer-free — derived from `Date.now()` lazily at each request,
|
|
630
|
-
exactly like the #6 cooldown):** `closed` (no entry / nothing armed — a
|
|
631
|
-
healthy host leaves no store entry) → at `failureThreshold` counted
|
|
632
|
-
failures strictly inside the sliding `failureWindowMs` window, the circuit
|
|
633
|
-
OPENS (`openedAt` recorded) → while open, calls are refused LOCALLY in the
|
|
634
|
-
same pre-send slot as the #6 cooldown gate (OUTSIDE the retry loop, before
|
|
635
|
-
the hourly limiter, the semaphore and fetch: zero fetches, no hourly
|
|
636
|
-
stamp, the audit event carries `notSent: true` + `errorCode:
|
|
637
|
-
'CIRCUIT_BREAKER_OPEN'`) → after `openTimeoutMs` the state is half-open
|
|
638
|
-
(the lazy transition): up to `halfOpenProbes` probes go upstream (further
|
|
639
|
-
calls while the probe budget is in flight are refused with
|
|
640
|
-
`details.state: 'half-open'` and no defined `retryAfter` — there is no
|
|
641
|
-
defined wait; a probe verdict clears the refusal); a probe SUCCESS counts
|
|
642
|
-
toward `successesToClose` (reached → closed, counters reset, the entry
|
|
643
|
-
deleted); a probe FAILURE re-arms the open period from now. A counted
|
|
644
|
-
failure is the SURFACED outcome (after the transport's own retries) —
|
|
645
|
-
retries are the transport's business; the breaker counts the call's end.
|
|
646
|
-
|
|
647
|
-
**The default failure predicate** (injectable via
|
|
648
|
-
`circuitBreaker.isFailure`; the default `isBreakerFailure` counts exactly):
|
|
649
|
-
`NETWORK_ERROR`, `TIMEOUT`, a vendor 429 (keyed on the STATUS — a surfaced
|
|
650
|
-
429 may carry an adopted vendor machine code, e.g. the thread-threshold
|
|
651
|
-
`Itgenatr005`), a 5xx EXCEPT the deterministic field-validation 500 (keyed
|
|
652
|
-
on #23's `details.deterministic` signal — by that signal only), and
|
|
653
|
-
`UNEXPECTED_CONTENT_TYPE` (the #24 maintenance page, by code — NEVER
|
|
654
|
-
`INVALID_RESPONSE`: the W-A7 shape-drift and the #13 marker-recovery key
|
|
655
|
-
are untouched). Never counted: `ABORTED` (a caller's cancellation says
|
|
656
|
-
nothing about service health), any 4xx, and local/policy refusals
|
|
657
|
-
(`notSent` — the cooldown gate's or the breaker's own; a call that never
|
|
658
|
-
reached the vendor is never a service failure).
|
|
659
|
-
|
|
660
|
-
**Validation (fail-closed at construction, the #6 pattern):** every number
|
|
661
|
-
must be a positive integer; `halfOpenProbes` MUST be `>= successesToClose`
|
|
662
|
-
(a breaker that can never close is a misconfiguration — refused); the
|
|
663
|
-
store must implement `get`/`set`/`delete`; `isFailure` must be a function.
|
|
664
|
-
Validation runs even with `enabled: false`.
|
|
665
|
-
|
|
666
|
-
**The threshold bypass (the #6 precedent):** `thresholdInformation.get()`
|
|
667
|
-
bypasses the REFUSAL so a downed host can still be probed for headroom —
|
|
668
|
-
its outcome is still RECORDED: a 2xx counts toward `successesToClose`
|
|
669
|
-
(closes toward recovery); a counted failure re-arms the open period. An
|
|
670
|
-
already-aborted caller signal still wins over an open-circuit refusal (the
|
|
671
|
-
abort error, not the breaker error).
|
|
672
|
-
|
|
673
|
-
Surfacing breaker state in the doctor is a FOLLOW-UP (not requested by the
|
|
674
|
-
issue); the state is inspectable through the injected/default store.
|
|
582
|
+
#### 3.2.2 The per-zone maintenance gate (issue #285)
|
|
583
|
+
|
|
584
|
+
Autotask takes each zone down for a weekly maintenance window; the edge then
|
|
585
|
+
serves an HTML maintenance page (recognised at any status when the 256-byte
|
|
586
|
+
head of a non-JSON body contains `maintenance`, `outage`, `downtime` or
|
|
587
|
+
`down for`; a JSON body never is). Generic 503 wording (`service
|
|
588
|
+
unavailable`, `temporarily unavailable`) is NOT the signature: such a page
|
|
589
|
+
keeps the ordinary 5xx retry leg and never opens the gate. The gate is process-wide, keyed by the
|
|
590
|
+
lowercased host of the live base URL, and always on:
|
|
591
|
+
|
|
592
|
+
- The first call that sees the page OPENS the zone's gate (`since` = now).
|
|
593
|
+
- While open, every other call on the zone is refused LOCALLY in the pre-send
|
|
594
|
+
slot (after the #6 cooldown check, before the hourly limiter, the semaphore
|
|
595
|
+
and fetch: zero fetches, no hourly stamp, the audit event carries
|
|
596
|
+
`notSent: true` + `errorCode: 'MAINTENANCE'`) with `MaintenanceError`.
|
|
597
|
+
- One call per `maintenancePollMs` is admitted as the PROBE (single-flight: a
|
|
598
|
+
call that is waiting the window out owns the probe slot for its whole wait).
|
|
599
|
+
The first non-maintenance response from the zone, from any call, CLOSES it.
|
|
600
|
+
- The call that saw the page (or the admitted probe) waits the window out up
|
|
601
|
+
to `maintenanceMaxWaitMs`, polling every `maintenancePollMs`, when it is
|
|
602
|
+
safe to re-send (section 2.1). Every call that receives the page and does
|
|
603
|
+
not wait it out (wait off, budget spent, another call owns the probe slot,
|
|
604
|
+
or an unflagged write at a non-2xx status) throws the same
|
|
605
|
+
`MaintenanceError`, with `httpStatus` set, no `notSent`, and
|
|
606
|
+
`details.maintenanceDetected` + the 256-byte `bodySnippet`; the unflagged
|
|
607
|
+
non-2xx write is `retryable: false` with a verify-first action (its outcome
|
|
608
|
+
is unknown). So a consumer maps one code: `MAINTENANCE`.
|
|
609
|
+
`maintenanceMaxWaitMs: 0` disables the wait (fail fast); the gate still opens. A maintenance page never takes
|
|
610
|
+
the generic retry leg.
|
|
611
|
+
- A non-matching HTML page (a wrong `baseUrl` serving a web site) never opens
|
|
612
|
+
the gate; it surfaces with `details.maintenancePatternMiss`.
|
|
613
|
+
- Only the page signature is detected: a window that answers with refused or
|
|
614
|
+
reset connections surfaces as `NETWORK_ERROR` and does not open the gate.
|
|
615
|
+
- The state is per process (like the #6 gate and the hourly tracker); an
|
|
616
|
+
N-process gateway sends up to N probes per interval.
|
|
675
617
|
|
|
676
618
|
### 3.3 `onAudit(event)`
|
|
677
619
|
|
|
@@ -680,8 +622,8 @@ One event per completed request (never called before the request settles):
|
|
|
680
622
|
errorCode?, notSent? }` — `body` is the request body with credential-shaped
|
|
681
623
|
fields redacted by default; `status` is absent when the request failed before
|
|
682
624
|
a response; `notSent: true` marks a call that was refused locally by the
|
|
683
|
-
tenant cooldown gate or the
|
|
684
|
-
`errorCode`: `RATE_LIMITED_COOLDOWN` vs `
|
|
625
|
+
tenant cooldown gate or the zone maintenance gate (section 3.2.2 — disambiguate on
|
|
626
|
+
`errorCode`: `RATE_LIMITED_COOLDOWN` vs `MAINTENANCE`); in either
|
|
685
627
|
case nothing reached the wire.
|
|
686
628
|
|
|
687
629
|
## 4. Pagination contract
|
|
@@ -777,33 +719,9 @@ carry paging parameters.
|
|
|
777
719
|
`limit >= latest` (above that the returned count is governed by
|
|
778
720
|
`latest`; `limit` is the blind-branch walk window). `latest` is
|
|
779
721
|
query-verb-ONLY (`queryAll` / `count` bags are unchanged).
|
|
780
|
-
**
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
guard, keyed by (resource plan key, canonical filter serialisation).
|
|
784
|
-
A re-asked "top N" within 30 s makes ZERO extra vendor count calls for
|
|
785
|
-
every CACHED probe (full-set AND threshold mode); a probe that answered
|
|
786
|
-
0 is deliberately re-wired (a 0 is never cached — see below), so a
|
|
787
|
-
threshold search that hit a 0 pays those probes again. A real write
|
|
788
|
-
through the SAME client
|
|
789
|
-
clears the cache — and a probe still in flight across the write is
|
|
790
|
-
DISCARDED by the cache's invalidation epoch (issue #139: its pre-write
|
|
791
|
-
answer must not re-anchor a stale top-N within the TTL — the next
|
|
792
|
-
`latest` re-probes). Never cached: the blind-surface / pinned-empty
|
|
793
|
-
sentinels (the plan-pinned stamps stay fresh and re-derive on every
|
|
794
|
-
call), errors, and anything outside the probe path — the plain `count`
|
|
795
|
-
op and `operations.aggregate` call the vendor count directly and
|
|
796
|
-
promise fresh totals. A write made ELSEWHERE (another client, the
|
|
797
|
-
Autotask UI) shows up once the 30 s TTL expires — that is the
|
|
798
|
-
documented snapshot trade. The truncation guard covers the
|
|
799
|
-
growth-of-total direction; a stale-LOW total within the TTL is NOT
|
|
800
|
-
left to it: when a full-set walk truncates against a cached total,
|
|
801
|
-
the SDK re-probes the total ONCE fresh (invalidating the entry) and
|
|
802
|
-
continues the walk to a larger fresh total — the
|
|
803
|
-
`LATEST_WINDOW_INCOMPLETE` refusal stands only when the fresh probe
|
|
804
|
-
confirms the cached value (bounded: at most one extra count probe per
|
|
805
|
-
walk). A total of `0` is NEVER cached, so a repeated call after an
|
|
806
|
-
out-of-band insert re-probes instead of returning a false empty.
|
|
722
|
+
**Count probes are never cached** (issue #286 removed the issue #127
|
|
723
|
+
count cache): every `latest` call pays its own count probes, so a
|
|
724
|
+
repeated "top N" sees the vendor's current totals.
|
|
807
725
|
- **`noteChars: N` (issue #61) — the opt-in per-field free-text cap.**
|
|
808
726
|
`N` (integer >= 1, pre-request validated, zero wire) caps EVERY
|
|
809
727
|
string value in the resource's untrusted-class free-text fields
|
|
@@ -975,16 +893,15 @@ types, type-only) alongside `./capabilities` and `./operations`.
|
|
|
975
893
|
|
|
976
894
|
### Pooled clients and the shared read cache (`./pool` subpath, issue #219 N6)
|
|
977
895
|
|
|
978
|
-
`createClientPool({ maxSize?,
|
|
896
|
+
`createClientPool({ maxSize?, sharedReadCache? })` keeps
|
|
979
897
|
one `AutotaskClient` per credential tuple (`{ get, delete, clear, size }`) so a
|
|
980
898
|
multi-tenant gateway reuses each tenant's limiter state. Each pooled client has
|
|
981
899
|
its own read cache, so a write through one client never cleared what another
|
|
982
900
|
client of the SAME tenant would serve — a post-write read-back routed to a
|
|
983
|
-
re-created client (a `delete(config)` on a 401
|
|
984
|
-
expiry) could return the pre-write snapshot for up to the resource TTL
|
|
901
|
+
re-created client (a `delete(config)` on a 401 or an LRU eviction) could return the pre-write snapshot for up to the resource TTL
|
|
985
902
|
(contacts: 5 min).
|
|
986
903
|
|
|
987
|
-
**Default (shared):** the pool OWNS one `
|
|
904
|
+
**Default (shared):** the pool OWNS one record `TtlCache` per credential tuple and
|
|
988
905
|
hands it to every client it builds for that tuple (via the `cache.store`
|
|
989
906
|
config seam), so any write through any pooled client of that tenant voids the
|
|
990
907
|
read cache for all of them and a read-back is never served the pre-write
|
|
@@ -993,8 +910,8 @@ entries still expire on their own TTL), is pruned at `maxSize` and dropped by
|
|
|
993
910
|
`clear()`. `sharedReadCache: false` restores per-client caches (then route a
|
|
994
911
|
post-write read-back through the writing client). `cache: false` opts the tuple
|
|
995
912
|
out entirely. `cache.store` is otherwise a plain advanced seam: a supplied
|
|
996
|
-
`
|
|
997
|
-
|
|
913
|
+
`TtlCache` instance is used as-is, and `ttlMs` beside it refuses (the instance
|
|
914
|
+
owns its TTL).
|
|
998
915
|
|
|
999
916
|
### Fresh single-record reads (`fresh: true`, issue #250)
|
|
1000
917
|
|
|
@@ -1155,7 +1072,7 @@ be cross-mode-refused.
|
|
|
1155
1072
|
whose MCP annotation is `readOnlyHint !== true` — today exactly **267
|
|
1156
1073
|
tools**: the 265 dedicated write/delete tools (209 `effect=write` + 56
|
|
1157
1074
|
`effect=destructive`, every one of them has a dedicated tool) +
|
|
1158
|
-
`autotask_write` + `autotask_delete`. The remaining **
|
|
1075
|
+
`autotask_write` + `autotask_delete`. The remaining **634 tools** (the 621 dedicated read tools + the 10 core read tools + `autotask_read` +
|
|
1159
1076
|
`autotask_catalog` + `autotask_describe`) all carry `readOnlyHint: true`
|
|
1160
1077
|
and are safe ungated: every op they can route is `effect=read` (zero
|
|
1161
1078
|
mutating requests — pinned by the test inventory, §6 of the split design).
|
|
@@ -1299,7 +1216,7 @@ the authority read (section 11.2).
|
|
|
1299
1216
|
one wire verb is `GET /V1.0/Resources/{id}`, an id of any other vendor
|
|
1300
1217
|
entity (a ticket / company / contact / billing-code id) resolves to an
|
|
1301
1218
|
unrelated person's name when it collides with a live staff id (and the
|
|
1302
|
-
wrong name then sticks in the default-on
|
|
1219
|
+
wrong name then sticks in the default-on read cache under that id
|
|
1303
1220
|
until TTL — a one-off mis-feed keeps serving it; section 11.21), or to a
|
|
1304
1221
|
misleading `blind: true` marker when it does not. An `AutotaskError`'s
|
|
1305
1222
|
legacy `resourceIds` may carry exactly those (the field is unlabeled —
|
|
@@ -1335,17 +1252,14 @@ the unlabeled field.
|
|
|
1335
1252
|
resolved, blind, failed }`.
|
|
1336
1253
|
- **Wire cost**: `N` unique ids cold = `N` GETs; warm (inside the TTL of
|
|
1337
1254
|
the channel below) = 0.
|
|
1338
|
-
- **Cache**: the
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
per-id `resources.get` fetches live and refreshes the ReadCache entry as
|
|
1347
|
-
well) — a rename made elsewhere is served on the very next fresh call,
|
|
1348
|
-
never a stale snapshot (issue #77).
|
|
1255
|
+
- **Cache**: the record cache (section 11.21) — each distinct id is
|
|
1256
|
+
fetched at most once per cache lifetime (the not-found marker is cached
|
|
1257
|
+
too: a repeat miss costs no wire; a failure is never cached). With
|
|
1258
|
+
`cache: false` the op is the uncached per-id path (per-call
|
|
1259
|
+
de-duplication still applies). Any write through the same client clears
|
|
1260
|
+
it; a rename made elsewhere shows up once `ttlMs` expires. A
|
|
1261
|
+
`fresh: true` call bypasses and refreshes the cache — a rename made
|
|
1262
|
+
elsewhere is served on the very next fresh call (issue #77).
|
|
1349
1263
|
|
|
1350
1264
|
```ts
|
|
1351
1265
|
import { expandResourceNames } from 'node-autotask/operations';
|
|
@@ -1488,9 +1402,10 @@ notes cannot be deleted, so they never use create-then-delete.
|
|
|
1488
1402
|
optionally scoped to one company (a `companyID eq` leaf and-joined server-side with the title match) and
|
|
1489
1403
|
projected to the listed columns (`id` is always returned). `searchWithDetail` takes the same bag. Malformed
|
|
1490
1404
|
`companyID` / `includeFields` are refused before any request. Other resources' `search` is unchanged.
|
|
1491
|
-
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1405
|
+
- `search` refuses an unknown option key (a misspelt `companyId` can't silently widen the search), and drops an
|
|
1406
|
+
empty `userDefinedFields` the vendor appends to projected rows.
|
|
1407
|
+
- **Hours per ticket for a resource** (`time_entries.recentTickets` was removed in 0.41.0, issue #290): aggregate
|
|
1408
|
+
the time entries grouped by ticket, using relation filters (§5) and default labels (§11.27):
|
|
1494
1409
|
|
|
1495
1410
|
```ts
|
|
1496
1411
|
const res = await aggregate(client, 'time_entries.query', [
|
|
@@ -1505,22 +1420,9 @@ notes cannot be deleted, so they never use create-then-delete.
|
|
|
1505
1420
|
|
|
1506
1421
|
For per-ticket status / assignee / company use `operations.fetchByIds` on the group ticket ids (labels
|
|
1507
1422
|
included: `statusLabel`, `assignedResourceIDName`, `companyIDName`).
|
|
1508
|
-
- `
|
|
1509
|
-
resource logged time against in an inclusive `dateWorked` window (`YYYY-MM-DD`; UTC days, or the local days in
|
|
1510
|
-
`tz`). One bounded time-entry read (`dateWorked` + `resourceID`; `limit` 1-500, default 500, bounds the
|
|
1511
|
-
**entries** scanned) plus `id in [...]` ticket reads (100 ids each). Result `{ items, entriesRead,
|
|
1512
|
-
entriesWithoutTicket, unresolvedTicketIDs, window, truncated }`; each item carries `ticketID`, `ticketNumber`,
|
|
1513
|
-
`title`, `status`, `assignedResourceID`, `companyID`, `entryCount`, `totalHours`, `lastWorked`, newest first.
|
|
1514
|
-
TimeEntry has no `companyID`, so `companyID` is applied **after the join**, client-side on the ticket rows'
|
|
1515
|
-
own `companyID` (the ticket read is not company-filtered on the wire). Entries on other companies' tickets
|
|
1516
|
-
still consume `limit`; those tickets are only counted in `excludedTickets` (ids are not returned), and
|
|
1517
|
-
`unresolvedTicketIDs` holds only tickets the read did not return at all (deleted or invisible, dropped from
|
|
1518
|
-
`items` when `companyID` is set). `truncated: true` (same meaning as `postedWithin`) means the rollup covers
|
|
1519
|
-
the scanned entries only; narrow the window. `search` refuses an unknown option key (a misspelt `companyId`
|
|
1520
|
-
can't silently widen the search), and drops an empty `userDefinedFields` the vendor appends to projected rows.
|
|
1521
|
-
- **`tz` on the `dateWorked` ops** (`recentTickets`, `postedWithin`, `dateWorkedWindow`, `customRange` and the relative
|
|
1423
|
+
- **`tz` on the `dateWorked` ops** (`postedWithin`, `dateWorkedWindow`, `customRange` and the relative
|
|
1522
1424
|
windows): the vendor stores `dateWorked` as a date-only value at 00:00 UTC, so the wire bounds are always whole UTC days.
|
|
1523
|
-
For `
|
|
1425
|
+
For `postedWithin` / `dateWorkedWindow` / `customRange` the `from`/`to` days ARE the days, so `tz` is
|
|
1524
1426
|
validated but never shifts them. For the relative windows (`today`, `last7`, `lastFullMonth`, ...) `tz` picks which
|
|
1525
1427
|
calendar dates the window covers in that zone and the bounds are whole UTC days of those dates. (Before 0.24.0 a `tz`
|
|
1526
1428
|
behind UTC, e.g. `America/Los_Angeles`, selected the NEXT UTC day; live-verified 2026-10-23.)
|
|
@@ -1585,8 +1487,7 @@ resolver holds a status literal: the move engine's closed-target refusal,
|
|
|
1585
1487
|
`tickets.forResource(resourceID, opts?)` lists the tickets assigned to one
|
|
1586
1488
|
resource, as primary assignee, secondary resource, or both (the `scope` option,
|
|
1587
1489
|
default `both`; the seam is `Tickets.assignedResourceID` for primary and the
|
|
1588
|
-
`TicketSecondaryResources` link table for secondary).
|
|
1589
|
-
`time_entries.recentTickets`: two bounded reads (a `Tickets/query` on
|
|
1490
|
+
`TicketSecondaryResources` link table for secondary). Two bounded reads (a `Tickets/query` on
|
|
1590
1491
|
`assignedResourceID` and a `TicketSecondaryResources/query` on `resourceID`,
|
|
1591
1492
|
both `id in [...]`-chunked when the link set is large) unioned client-side —
|
|
1592
1493
|
deliberately not the cross-field grouped `or`, whose live semantics are
|
|
@@ -1814,7 +1715,7 @@ const lob = await revenueRollup(client, { tenantWide: true, dateFrom: '2025-07-0
|
|
|
1814
1715
|
// lob.rows[i].keys.lineOfBusiness → { id: 7, name: '<level2>', division: '<level1>' } | { id: null, label: 'no LOB assigned' }
|
|
1815
1716
|
```
|
|
1816
1717
|
|
|
1817
|
-
## 8. Resource surface
|
|
1718
|
+
## 8. Resource surface
|
|
1818
1719
|
|
|
1819
1720
|
<!-- BEGIN GENERATED:api-per-entity -->
|
|
1820
1721
|
Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page guarded), `queryBounded` (server-side pagination — one call walks the cursor until `limit` TOTAL records, default 500 / max 2500, with a `hasMore` flag; the MCP `query` surface, routed by `invoke`), `queryAll` (collect → `T[]` — the unbounded raw walk), `count`, `get` (id → record; `item: null` → `NotFoundError`), `create` / `update` / `delete` (where the vendor defines them — mutations take a `MutationOpts` bag: `{ dryRun, lastModifiedDate?, maxRetries? }` (issue #105: `maxRetries` overrides the configured retry budget for every request the mutation issues; `0` = exactly one send per request)), plus the helper floor where the plan declares it: `resolve` (all 121), `search` (40 — bounded contains on the resource text field), `getContext` (5 workflow resources), `findBy<Field>` (3), `createIfNotExists` (14). Entity metadata on every resource: `getEntityInfo`, `getFieldInfo`, `getUdfInfo` (UDF-capable 16). Parent-scoped children take `(parentId, id)` on the CRUD methods and require `parentId` on `createIfNotExists` (the scoped create POSTs the scoped collection; the dedup query stays global); `query`/`count`/`resolve`/`search` stay global (narrow with a `parentField` filter). Scoped `update` PATCHes the collection (the vendor keys the record by the id in the body).
|
|
@@ -1829,7 +1730,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1829
1730
|
| `checklist_libraries` | ChecklistLibrary | entity | extended | no | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_checklistLibrary_get |
|
|
1830
1731
|
| `checklist_library_checklist_items` | ChecklistLibraryChecklistItem | entity | extended | yes (checklist_libraries) | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_checklistLibraryChecklistItem_get |
|
|
1831
1732
|
| `classification_icons` | ClassificationIcon | entity | advanced | no | query, count, get, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_classificationIcon_get |
|
|
1832
|
-
| `companies` | Company | entity | core | no | query, count, get, create, update, resolve, search, getContext | fullProfile, getEntityInfo, getFieldInfo, getUdfInfo, searchByDomain, searchByIdentity, unbilledWork, upsertByName | autotask_company_get |
|
|
1733
|
+
| `companies` | Company | entity | core | no | query, count, get, create, update, resolve, search, getContext | fullProfile, getEntityInfo, getFieldInfo, getUdfInfo, prepareCrmWrite, searchByDomain, searchByIdentity, unbilledWork, upsertByName | autotask_company_get |
|
|
1833
1734
|
| `company_alerts` | CompanyAlert | entity | extended | yes (companies) | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_companyAlert_get |
|
|
1834
1735
|
| `company_locations` | CompanyLocation | entity | extended | yes (companies) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_companyLocation_get |
|
|
1835
1736
|
| `company_notes` | CompanyNote | entity | extended | yes (companies) | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_companyNote_get |
|
|
@@ -1870,8 +1771,9 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1870
1771
|
| `contract_service_units` | ContractServiceUnit | entity | extended | yes (contracts) | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractServiceUnit_get |
|
|
1871
1772
|
| `contract_services` | ContractService | entity | extended | yes (contracts) | query, count, get, create, update, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractService_get |
|
|
1872
1773
|
| `contract_ticket_purchases` | ContractTicketPurchases | entity | extended | yes (contracts) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractTicketPurchase_get |
|
|
1873
|
-
| `contracts` | Contract | entity | extended | no | query, count, get, create, update, resolve, search | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo, profitability | autotask_contract_get |
|
|
1774
|
+
| `contracts` | Contract | entity | extended | no | query, count, get, create, update, resolve, search | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo, lineOfBusiness, profitability | autotask_contract_get |
|
|
1874
1775
|
| `countries` | Country | entity | advanced | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_country_get |
|
|
1776
|
+
| `currencies` | Currency | entity | advanced | no | query, count, get, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_currency_get |
|
|
1875
1777
|
| `departments` | Department | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_department_get |
|
|
1876
1778
|
| `domain_registrars` | DomainRegistrar | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_DomainRegistrar_get |
|
|
1877
1779
|
| `expense_item_attachments` | ExpenseItemAttachment | entity | extended | yes (expense_items) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_expenseItemAttachment_get |
|
|
@@ -1935,7 +1837,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1935
1837
|
| `ticket_categories` | TicketCategory | entity | extended | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketCategories_get |
|
|
1936
1838
|
| `ticket_category_field_defaults` | TicketCategoryFieldDefault | entity | extended | yes (ticket_categories) | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketCategoryFieldDefaults_get |
|
|
1937
1839
|
| `ticket_change_request_approvals` | TicketChangeRequestApproval | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketChangeRequestApprovals_get |
|
|
1938
|
-
| `ticket_charges` | TicketCharge | entity | extended | yes (tickets) | query, count, get, create, update, delete, resolve, search | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketCharge_get |
|
|
1840
|
+
| `ticket_charges` | TicketCharge | entity | extended | yes (tickets) | query, count, get, create, update, delete, resolve, search | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo, moveToTicket | autotask_ticketCharge_get |
|
|
1939
1841
|
| `ticket_checklist_items` | TicketChecklistItem | entity | extended | yes (tickets) | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketChecklistItem_get |
|
|
1940
1842
|
| `ticket_checklist_libraries` | TicketChecklistLibrary | entity | extended | yes (tickets) | create, resolve | attachToDocument, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketChecklistLibrary |
|
|
1941
1843
|
| `ticket_history` | TicketHistory | entity | extended | no | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketHistory_get |
|
|
@@ -1944,8 +1846,8 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1944
1846
|
| `ticket_notes` | TicketNote | entity | extended | yes (tickets) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketNote_get |
|
|
1945
1847
|
| `ticket_secondary_resources` | TicketSecondaryResource | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketSecondaryResources_get |
|
|
1946
1848
|
| `ticket_webhooks` | TicketWebhook | entity | extended | yes (tickets) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketWebhook_get |
|
|
1947
|
-
| `tickets` | Ticket | entity | core | no | query, count, get, create, update, resolve, search, getContext | changeStatus, findByTicketNumber, forResource, fullContext, getEntityInfo, getFieldInfo, getUdfInfo, openWithNoteAndAssign, slaHealthCheck, statusSemantics, summary, timeline | autotask_ticket_get |
|
|
1948
|
-
| `time_entries` | TimeEntry | entity | extended | no | query, count, get, create, update, delete, resolve | approve, createForResource, createIfNotExists, customRange, dateWorkedWindow, getEntityInfo, getFieldInfo, getPosted, getUdfInfo, getUnposted, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, postedWithin, today, yesterday | autotask_timeEntry_get |
|
|
1849
|
+
| `tickets` | Ticket | entity | core | no | query, count, get, create, update, resolve, search, getContext | changeStatus, findByTicketNumber, forResource, fullContext, getEntityInfo, getFieldInfo, getUdfInfo, lineOfBusiness, openWithNoteAndAssign, slaHealthCheck, statusSemantics, summary, timeline | autotask_ticket_get |
|
|
1850
|
+
| `time_entries` | TimeEntry | entity | extended | no | query, count, get, create, update, delete, resolve | approve, createForResource, createIfNotExists, customRange, dateWorkedWindow, getEntityInfo, getFieldInfo, getPosted, getUdfInfo, getUnposted, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, moveToTicket, postedWithin, today, yesterday | autotask_timeEntry_get |
|
|
1949
1851
|
| `time_entry_attachments` | TimeEntryAttachment | entity | extended | yes (time_entries) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_timeEntryAttachment_get |
|
|
1950
1852
|
| `time_off_requests` | TimeOffRequest | entity | extended | no | query, count, get, resolve | approve, getEntityInfo, getFieldInfo, getUdfInfo, reject | autotask_timeOffRequest_get |
|
|
1951
1853
|
| `inventory_products` | InventoryProduct | entity | extended | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo, stockedItemsAdd, stockedItemsRemove, stockedItemsTransfer | autotask_inventoryProduct_get |
|
|
@@ -1957,7 +1859,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1957
1859
|
| `threshold_information` | — | integration | core | no | get | — | autotask_threshold_information |
|
|
1958
1860
|
| `zone_information` | — | integration | advanced | no | get | — | autotask_zone_information |
|
|
1959
1861
|
|
|
1960
|
-
|
|
1862
|
+
128 resources (121 entity + 7 integration). Rows are generated from the registry — do not hand-type.
|
|
1961
1863
|
<!-- END GENERATED:api-per-entity -->
|
|
1962
1864
|
|
|
1963
1865
|
**`companies.search` include_family (issue #59).** `companies.search` is
|
|
@@ -2890,10 +2792,10 @@ reality*), so the point-in-time pins (`operations.createGuidance`, the
|
|
|
2890
2792
|
`zoneFacts` lists) are only as fresh as their `verifiedAt`. The live
|
|
2891
2793
|
channel is the refresh side of that pair:
|
|
2892
2794
|
|
|
2893
|
-
**Config (on by default; `false` opts out):** `
|
|
2894
|
-
separate
|
|
2895
|
-
different lifetime, different invalidation signal: drift vs
|
|
2896
|
-
|
|
2795
|
+
**Config (on by default; `cache: false` opts out):** `cache: { fieldTtlMs? }` — a
|
|
2796
|
+
separate `TtlCache` instance from the record cache (different key — entity vs
|
|
2797
|
+
resource+id —, different lifetime, different invalidation signal: drift vs
|
|
2798
|
+
write). With `cache: false` there is no field cache at all and the
|
|
2897
2799
|
`getEntityInfo` / `getFieldInfo` / `getUdfInfo` trio is the uncached wire
|
|
2898
2800
|
path, bit-for-bit; absent, the default instance is built and the trio
|
|
2899
2801
|
goes through one private helper
|
|
@@ -3039,52 +2941,18 @@ closed bag, the W-A4 sentence in the served purpose, and the
|
|
|
3039
2941
|
`udfCapable` / `getUdfInfo` / `getFieldInfo` row invariants);
|
|
3040
2942
|
`test/scripts/g16-field-catalog.test.ts` is the tripwire.
|
|
3041
2943
|
|
|
3042
|
-
### 11.15
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
at construction). Issue #80: the plan-derived table
|
|
3055
|
-
`DEFAULT_PER_RESOURCE_TTL_MS` IS the default per-resource map — with the
|
|
3056
|
-
operator map absent, every plan resource rides its table row (the 1h
|
|
3057
|
-
reference class / the 5min default class) and a plan-unnamed path expires
|
|
3058
|
-
on `ttlMs`; operator entries OVERRIDE table entries per resource (the
|
|
3059
|
-
effective map is operator entry > table entry > `ttlMs`).
|
|
3060
|
-
|
|
3061
|
-
**The key contract (pinned):** the entry's TTL is the override for the
|
|
3062
|
-
**plan key of the entity the read path points to** — the cache key is
|
|
3063
|
-
still the wire path (`/V1.0/<Entity>/<id>`, unchanged), and the base read
|
|
3064
|
-
path keys the TTL by the entity of that path. A cross-entity read
|
|
3065
|
-
therefore expires on the READ entity's override: a contact read through
|
|
3066
|
-
`tickets.getContext` is stored under the contact's key (the plan-derived
|
|
3067
|
-
path index `RESOURCE_KEY_BY_PATH`), so a `contacts`/`resources` override
|
|
3068
|
-
bounds it, not the ticket's. A path the plan does not name falls back to
|
|
3069
|
-
`ttlMs` (the entry, not the call, decides the expiry).
|
|
3070
|
-
|
|
3071
|
-
**The plan-derived default table (not hand-written):** the plan carries
|
|
3072
|
-
no staleness marker (the closed `kind` set is entity/integration), so the
|
|
3073
|
-
reference SET is a pinned design decision in `scripts/lib/cache-ttl.mjs`
|
|
3074
|
-
— the issue's own motivation names the human-timescale lookup tables
|
|
3075
|
-
(countries, departments, roles, billing_codes, services) and the prior
|
|
3076
|
-
art (the independent WYRE-AI `calculateResponseTTL`) adds phases. The
|
|
3077
|
-
table STRUCTURE is plan-derived: `src/cache-ttl-tables.ts` (the 7th
|
|
3078
|
-
generated artifact, planHash-stamped, regenerated by
|
|
3079
|
-
`generate-capabilities`) carries one row per plan resource — reference
|
|
3080
|
-
class 1h (`CACHE_TTL_REFERENCE_MS`) vs default class 5min
|
|
3081
|
-
(`CACHE_TTL_DEFAULT_MS`) — so a plan change regenerates the split, and a
|
|
3082
|
-
hand-written table would be the one drift hazard. It is a REFERENCE for
|
|
3083
|
-
building the operator's map (spread + per-operator pins, e.g.
|
|
3084
|
-
`{ ...DEFAULT_PER_RESOURCE_TTL_MS, tickets: 60_000 }`), never applied
|
|
3085
|
-
implicitly. MCP: nothing new served; the config is operator-side.
|
|
3086
|
-
Gate: `check-capabilities` clause 8a + `test/scripts/cache-ttl-artifact.test.ts`
|
|
3087
|
-
(byte-equal re-derivation — the table regenerates with the plan).
|
|
2944
|
+
### 11.15 Read-cache TTLs (issue #32, simplified in #286)
|
|
2945
|
+
|
|
2946
|
+
Reference tables change on a human timescale; records change minute to
|
|
2947
|
+
minute. The record cache therefore keys its TTL on the entity segment of the
|
|
2948
|
+
cached read path (`readTtlFor` in `src/cache.ts`): the tables in
|
|
2949
|
+
`REFERENCE_ENTITIES` (BillingCodes, Countries, Departments, Phases, Roles,
|
|
2950
|
+
Services) live `REFERENCE_TTL_MS` (1 h, or `ttlMs` when that is longer);
|
|
2951
|
+
every other record lives `cache.ttlMs` (default 5 min). The key is the wire
|
|
2952
|
+
path, so a cross-entity read (a contact read through `tickets.getContext`)
|
|
2953
|
+
expires on the READ entity's TTL, and a not-found marker on its path's TTL.
|
|
2954
|
+
The per-resource override map (`cache.perResourceTtlMs`) and the generated
|
|
2955
|
+
per-resource table were removed in 0.40.0.
|
|
3088
2956
|
|
|
3089
2957
|
### 11.16 The opt-in PUT item-path fallback for a 404'd update route (issue #34)
|
|
3090
2958
|
|
|
@@ -3208,9 +3076,7 @@ config surface, no new ops) and pins the two hazards the issue names:
|
|
|
3208
3076
|
- **The FK parts are cached; the ticket root stays fresh.** The
|
|
3209
3077
|
reference-table FK parts ride the normal cached get-by-id path (the #32
|
|
3210
3078
|
keying: each part is stored under the read entity's path and expires on
|
|
3211
|
-
that entity's
|
|
3212
|
-
class via `DEFAULT_PER_RESOURCE_TTL_MS`, or the operator's
|
|
3213
|
-
`perResourceTtlMs` pin). The ticket read itself is FRESH (`fetchRecordRaw`
|
|
3079
|
+
that entity's TTL (section 11.15). The ticket read itself is FRESH (`fetchRecordRaw`
|
|
3214
3080
|
with `fresh: true`): the composite root is always the live record —
|
|
3215
3081
|
caching is scoped to the reference-table FK parts only (a repeat
|
|
3216
3082
|
`getContext` wires exactly the ticket; a repeat `fullContext` wires the
|
|
@@ -3219,7 +3085,7 @@ config surface, no new ops) and pins the two hazards the issue names:
|
|
|
3219
3085
|
the current (cacheable) root behaviour.
|
|
3220
3086
|
- **Negative cache, no poisoning.** A vendor NOT-FOUND on an FK part (the
|
|
3221
3087
|
404 or the 200 `item: null` signal) is stored as an 'absent' marker in
|
|
3222
|
-
the SAME
|
|
3088
|
+
the SAME record cache under a namespaced key the positive path never
|
|
3223
3089
|
constructs (the `.get` / `resolve` single-record read paths are bit-
|
|
3224
3090
|
for-bit untouched): a repeat composite serves 'absent' with zero wire
|
|
3225
3091
|
for that part. The marker expires on the read entity's TTL and dies
|
|
@@ -3250,7 +3116,7 @@ config surface, no new ops) and pins the two hazards the issue names:
|
|
|
3250
3116
|
|
|
3251
3117
|
- **Opt-in names/labels ride these same channels (issue #77).** The
|
|
3252
3118
|
composites' opt-in `names` siblings resolve over the existing
|
|
3253
|
-
default-on channels (the
|
|
3119
|
+
default-on channels (the record cache / the field cache —
|
|
3254
3120
|
no new cache) with a separate name-read budget (`maxNameReads`,
|
|
3255
3121
|
default 20, lowerable never raised) — see section 11.21 for the
|
|
3256
3122
|
per-field-class wiring.
|
|
@@ -3305,21 +3171,12 @@ what makes it usable as a recovery signal from inside the client.
|
|
|
3305
3171
|
transport's size cap): today's typed 401 propagates
|
|
3306
3172
|
byte-identically — zero retry (the recovery must not turn a credential
|
|
3307
3173
|
fault into a discovery fault).
|
|
3308
|
-
- **State hygiene.** The transport's per-host
|
|
3309
|
-
and the
|
|
3310
|
-
on the re-point, so the re-pointed host starts FRESH — no
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
breaker PROBE admitted on the stale host is pinned to that host's key:
|
|
3315
|
-
if the call re-points, the probe is voided (its outcome was measured on
|
|
3316
|
-
a different host) — the re-pointed host's probe budget / close count is
|
|
3317
|
-
never touched by a stale-host probe, and the stale host's admitted
|
|
3318
|
-
budget dangles for the rest of the open period (bounded, conservative,
|
|
3319
|
-
nothing crosses hosts). The probe is equally pinned to the open WINDOW
|
|
3320
|
-
it was admitted in (issue #271): a probe that settles after that window
|
|
3321
|
-
closed and re-opened is voided the same way — the newer window's probe
|
|
3322
|
-
budget / close count is never touched by a stale-window probe. The
|
|
3174
|
+
- **State hygiene.** The transport's per-host gate keys (the tenant
|
|
3175
|
+
cooldown key and the maintenance-gate key, both keyed on the base URL) are
|
|
3176
|
+
RE-DERIVED on the re-point, so the re-pointed host starts FRESH — no
|
|
3177
|
+
cooldown / maintenance-gate inheritance from the stale host (its armed
|
|
3178
|
+
entries stay for the other clients that still talk to it; nothing is copied
|
|
3179
|
+
across). The
|
|
3323
3180
|
re-point is
|
|
3324
3181
|
recorded on the client: the live base URL is observable via
|
|
3325
3182
|
`http.baseUrl` (the configured `config.baseUrl` stays the
|
|
@@ -3341,9 +3198,8 @@ what makes it usable as a recovery signal from inside the client.
|
|
|
3341
3198
|
lifetime, like a cache); no change to the served MCP/n8n text (transport
|
|
3342
3199
|
behaviour, like #34).
|
|
3343
3200
|
|
|
3344
|
-
Pins: `test/transport-zone-reresolve/*.test.ts` (
|
|
3345
|
-
|
|
3346
|
-
host-pinning pin).
|
|
3201
|
+
Pins: `test/transport-zone-reresolve/*.test.ts` (mock wire only — no vendor
|
|
3202
|
+
wire in tests).
|
|
3347
3203
|
|
|
3348
3204
|
### 11.20 Config-time host bounds — `hostPolicy` (skill 3.5.0 / L2, audit-fix A1)
|
|
3349
3205
|
|
|
@@ -3416,74 +3272,19 @@ Pins: `test/config-host-policy.test.ts` (10 pins — the exported helper is
|
|
|
3416
3272
|
CONSUMED by the tests, never re-implemented; mock wire only, no vendor
|
|
3417
3273
|
wire).
|
|
3418
3274
|
|
|
3419
|
-
### 11.21 Resource name expansion
|
|
3275
|
+
### 11.21 Resource name expansion (issue #65; names in the read cache since #286)
|
|
3420
3276
|
|
|
3421
3277
|
`operations.expandResourceNames` (section 7.7) resolves vendor numeric
|
|
3422
|
-
resource ids to display names with PER-ID `resources.get` reads ONLY.
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
- **Key space**: `resources:<id>` (the canonical id). Disjoint from the
|
|
3433
|
-
ReadCache (wire-PATH keys) and the FieldCache (`resource:kind` keys —
|
|
3434
|
-
the closed three-kind metadata set). The namespace leaves a documented
|
|
3435
|
-
path to future FK tables — v1 serves `resources` only.
|
|
3436
|
-
- **What is stored**: exactly the two deterministic vendor answers — the
|
|
3437
|
-
resolved name `{ name, blind: false }` and the vendor's TRUE not-found
|
|
3438
|
-
blind marker `{ name: null, blind: true }` (the issue-#36
|
|
3439
|
-
absent-marker precedent: a repeat miss costs no wire). A 200 WITH a
|
|
3440
|
-
record but NO name part is NOT stored (a transient partial record
|
|
3441
|
-
must not stick for the whole TTL). A FAILURE is never cached
|
|
3442
|
-
(a fault rejects before any `set` can run).
|
|
3443
|
-
- **In-flight de-duplication**: concurrent callers for the same id share
|
|
3444
|
-
ONE wire request (the promise is dropped on settle, success OR
|
|
3445
|
-
failure — the fieldCache two-armed pattern).
|
|
3446
|
-
- **Cap / TTL**: `ttlMs` an optional positive integer, default 1 h (fail-closed
|
|
3447
|
-
`AutotaskConfigError`); `maxEntries` default 1024
|
|
3448
|
-
(`RESOURCE_NAME_CACHE_MAX_ENTRIES`); past the cap → purge expired
|
|
3449
|
-
first, then evict the oldest-inserted (lazy purge on read).
|
|
3450
|
-
- **Scoped invalidation**: a real write to the `resources` resource
|
|
3451
|
-
through the same client (a rename via this client) clears the channel;
|
|
3452
|
-
a write to ANY OTHER resource never invalidates (it says nothing about
|
|
3453
|
-
names — the fieldCache "an ordinary record write never invalidates"
|
|
3454
|
-
rule, scoped variant). The existing ReadCache all-write clear is
|
|
3455
|
-
UNCHANGED.
|
|
3456
|
-
- **Coexistence with the ReadCache** (when BOTH are configured): the
|
|
3457
|
-
op's per-id `get` rides the ReadCache transparently (a ReadCache hit
|
|
3458
|
-
inside its TTL costs zero wire and the name is then stored in the name
|
|
3459
|
-
cache — double layering, disjoint key spaces). A `resources` write
|
|
3460
|
-
clears BOTH; a write to another resource clears only the ReadCache. A
|
|
3461
|
-
`fresh: true` call bypasses BOTH channels and refreshes both (the
|
|
3462
|
-
per-id GET fetches live — issue #77: a rename made elsewhere is served
|
|
3463
|
-
on the very next fresh call, never a stale snapshot).
|
|
3464
|
-
- **Zero interaction with the #64 field-cache surface**: `FIELD_CACHE_KINDS`
|
|
3465
|
-
stays the closed `['info','fields','udf']`; the fieldCatalog bag, its
|
|
3466
|
-
honesty wording and the `changeStatus` live guard and its
|
|
3467
|
-
`getFieldInfo` path are bit-for-bit untouched. The only shared code is
|
|
3468
|
-
the `http.request` completion hook, which gains ONE
|
|
3469
|
-
scoped-invalidation line (the fieldCache drift block and the readCache
|
|
3470
|
-
all-write clear are unmodified).
|
|
3471
|
-
- **NO new row in `src/cache-ttl-tables.ts`** (stated so the absence is
|
|
3472
|
-
not hunted): the artifact's row set is pinned at exactly one row per
|
|
3473
|
-
PLAN RESOURCE, and issue #65 adds a plan OPERATION, not a plan
|
|
3474
|
-
resource. The rows — including `resources: 300000`, the ReadCache
|
|
3475
|
-
per-resource TTL REFERENCE for `resources.get` record reads — are
|
|
3476
|
-
unchanged; the name channel's TTL is the operator's
|
|
3477
|
-
`resourceNameCache.ttlMs`, not the table's row. Only the artifact's
|
|
3478
|
-
`planHash` header line re-stamps (the plan bytes move).
|
|
3479
|
-
|
|
3480
|
-
**Staleness contract**: a resource renamed OUTSIDE this client (the UI,
|
|
3481
|
-
another integration) serves its old name until `ttlMs` expires (or a
|
|
3482
|
-
local `resources` write clears the channel) — the exact staleness trade
|
|
3483
|
-
every house cache documents ("a cached read is a snapshot AT FETCH
|
|
3484
|
-
TIME"). A `fresh: true` call closes the gap immediately: it bypasses
|
|
3485
|
-
BOTH the name cache and the ReadCache and refreshes both, so the rename
|
|
3486
|
-
is served on the very next fresh call (issue #77).
|
|
3278
|
+
resource ids to display names with PER-ID `resources.get` reads ONLY. The
|
|
3279
|
+
names live in the record cache (section 11.15), not a channel of their own:
|
|
3280
|
+
a resolved id is the cached `/V1.0/Resources/{id}` record, and the vendor's
|
|
3281
|
+
true not-found is the same not-found marker the composite FK reads store
|
|
3282
|
+
(`contextAbsentKey`), so a repeat id (resolved OR blind) inside the TTL costs
|
|
3283
|
+
zero wire. A failure is never cached. Any real write through the client
|
|
3284
|
+
clears names with the rest of the record cache; a rename made elsewhere
|
|
3285
|
+
shows up once `cache.ttlMs` expires, or at once with `fresh: true` (the
|
|
3286
|
+
per-id GET fetches live and refreshes the entry, issue #77). `cache: false`
|
|
3287
|
+
is the uncached per-id path; per-call de-duplication is always on.
|
|
3487
3288
|
|
|
3488
3289
|
**Per-part projection (issue #200)**: `companies.fullProfile` accepts
|
|
3489
3290
|
`fields?: { locations?, contacts?, notes?, projects?, openTickets? }` — each a
|
|
@@ -3505,10 +3306,9 @@ output is byte-identical) + `maxNameReads?: number` (default 20,
|
|
|
3505
3306
|
lowerable, never raised). With `names: true`, the siblings ride the
|
|
3506
3307
|
EXISTING default-on channels — no new cache: resource ids (creator /
|
|
3507
3308
|
owner / lead / the time-entry resource) resolve through
|
|
3508
|
-
`operations.expandResourceNames` (
|
|
3309
|
+
`operations.expandResourceNames` (read cache → one wire read
|
|
3509
3310
|
per distinct uncached id); record-FK names (role / billing code /
|
|
3510
|
-
contract) ride the per-resource single-record gets (
|
|
3511
|
-
per-resource TTLs, the #36 absent-marker contract untouched); the CLOSED
|
|
3311
|
+
contract) ride the per-resource single-record gets (the record cache, the #36 absent-marker contract untouched); the CLOSED
|
|
3512
3312
|
picklist scope (owner-approved list: the ticket fields
|
|
3513
3313
|
`status` / `priority` / `ticketType` / `issueType` / `subIssueType` /
|
|
3514
3314
|
`source` / `queue` ONLY — project / company picklists stay label-less)
|
|
@@ -3528,7 +3328,7 @@ rows and the reason rows.
|
|
|
3528
3328
|
|
|
3529
3329
|
**AC mapping**: AC1 (opt-in; the explicit-`false` output unchanged) —
|
|
3530
3330
|
the op is invoked only on request and the channel is default-on (issue
|
|
3531
|
-
#80; `
|
|
3331
|
+
#80; `cache: false` keeps the uncached per-id path
|
|
3532
3332
|
bit-for-bit — the explicit-`false` pins are that proof). AC2 (each
|
|
3533
3333
|
distinct id fetched at most once per cache lifetime) —
|
|
3534
3334
|
per-call de-duplication is ALWAYS on (zero-config); with the channel
|
|
@@ -3541,10 +3341,8 @@ only wire verb is the per-id GET; the module imports no query builder
|
|
|
3541
3341
|
(static guarantee) and the tests assert METHOD + PATH on every wire
|
|
3542
3342
|
call.
|
|
3543
3343
|
|
|
3544
|
-
Pins: `test/operations/expandResourceNames.test.ts` (
|
|
3545
|
-
|
|
3546
|
-
class unit + the client-option wiring + the scoped invalidation; mock
|
|
3547
|
-
wire only, synthetic data, no vendor wire).
|
|
3344
|
+
Pins: `test/operations/expandResourceNames.test.ts` (the plan row's `tests[]`)
|
|
3345
|
+
and `test/resources/cache-write-invalidation.test.ts` (mock wire only).
|
|
3548
3346
|
|
|
3549
3347
|
### 11.22 Timezone-anchored date windows for billing items and invoices (issue #66)
|
|
3550
3348
|
|
|
@@ -4154,8 +3952,8 @@ keys of `operations.aggregate`, and the found records of `operations.fetchMany`
|
|
|
4154
3952
|
invoke `<resource>.query` op when the summary matters.
|
|
4155
3953
|
- Opt out per call: `names: false` (query bag key, aggregate opts, fetchMany /
|
|
4156
3954
|
fetchByIds opts). A non-boolean `names` is refused pre-request.
|
|
4157
|
-
- Caches: picklists ride the
|
|
4158
|
-
|
|
3955
|
+
- Caches: picklists ride the field cache, staff and record names the record
|
|
3956
|
+
cache (all default-on; invalidated as documented in §3 —
|
|
4159
3957
|
a write through the client clears the affected resource's entries, TTL
|
|
4160
3958
|
otherwise). A warm call costs zero extra vendor calls; a cold one costs one
|
|
4161
3959
|
field-metadata GET for the resource plus one GET per distinct uncached id.
|