node-autotask 0.19.0 → 0.21.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/MCP_TOOL_CATALOG.json +1397 -10
  3. package/MCP_TOOL_MANIFEST.md +130 -130
  4. package/MCP_TOOL_OVERRIDES.json +1 -1
  5. package/N8N_MCP_MANIFEST.json +44 -9
  6. package/N8N_MCP_MANIFEST.md +8 -8
  7. package/capabilities.json +11135 -2033
  8. package/capabilities.schema.json +39 -0
  9. package/dist/capabilities.cjs +2 -2
  10. package/dist/capabilities.d.cts +9 -0
  11. package/dist/capabilities.d.ts +9 -0
  12. package/dist/capabilities.js +1 -1
  13. package/dist/{chunk-JM52ZU7T.cjs → chunk-CBUY3MKB.cjs} +11134 -2032
  14. package/dist/{chunk-NDK7WYWA.js → chunk-DM67SXCO.js} +11134 -2032
  15. package/dist/{chunk-D5RAQ4DE.js → chunk-GBVUXAUA.js} +3 -3
  16. package/dist/{chunk-7RDR5DSI.cjs → chunk-HEW4WJLC.cjs} +394 -394
  17. package/dist/{chunk-4YKV7ORF.js → chunk-JRSU6PKP.js} +2 -2
  18. package/dist/{chunk-LB2X4X4B.js → chunk-KD6LOSAK.js} +215 -54
  19. package/dist/{chunk-TIM75K3G.cjs → chunk-PMPFQDVM.cjs} +135 -135
  20. package/dist/{chunk-QDYSHAU6.cjs → chunk-QWAWNQEZ.cjs} +223 -62
  21. package/dist/{index-aZ3tCh1h.d.cts → index-ByWYfNog.d.cts} +105 -9
  22. package/dist/{index-DLugNSr7.d.ts → index-DSnuyiTr.d.ts} +105 -9
  23. package/dist/index.cjs +22 -22
  24. package/dist/index.d.cts +2 -2
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +4 -4
  27. package/dist/operations/index.cjs +184 -136
  28. package/dist/operations/index.d.cts +2 -2
  29. package/dist/operations/index.d.ts +2 -2
  30. package/dist/operations/index.js +79 -31
  31. package/dist/pool.cjs +8 -8
  32. package/dist/pool.d.cts +1 -1
  33. package/dist/pool.d.ts +1 -1
  34. package/dist/pool.js +4 -4
  35. package/dist/resources/index.cjs +4 -4
  36. package/dist/resources/index.d.cts +1 -1
  37. package/dist/resources/index.d.ts +1 -1
  38. package/dist/resources/index.js +3 -3
  39. package/docs/API.md +83 -24
  40. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7;
2
2
 
3
- var _chunkJM52ZU7Tcjs = require('./chunk-JM52ZU7T.cjs');
3
+ var _chunkCBUY3MKBcjs = require('./chunk-CBUY3MKB.cjs');
4
4
 
5
5
  // src/auth.ts
6
6
  var AUTH_HEADERS = {
@@ -2074,6 +2074,88 @@ function refusedFilterShape(filter) {
2074
2074
  // src/http.ts
2075
2075
  var _async_hooks = require('async_hooks');
2076
2076
 
2077
+ // src/latest-count-cache.ts
2078
+ var LATEST_COUNT_CACHE_TTL_MS = 3e4;
2079
+ var LATEST_COUNT_CACHE_MAX_ENTRIES = 4096;
2080
+ var LatestCountCache = (_class6 = class {
2081
+ /** The TTL in ms. */
2082
+
2083
+ /** The entry bound (applies to STORED entries). */
2084
+
2085
+ __init10() {this.store = /* @__PURE__ */ new Map()}
2086
+ /** The invalidation epoch (the `ReadCache` pattern — issue #139): bumped
2087
+ * by `clear()`; capture it before a wire read and pass it to `set` so a
2088
+ * probe in flight across a write cannot re-store its stale answer. */
2089
+ __init11() {this.epochCounter = 0}
2090
+ constructor(ttlMs, maxEntries = LATEST_COUNT_CACHE_MAX_ENTRIES) {;_class6.prototype.__init10.call(this);_class6.prototype.__init11.call(this);
2091
+ if (!Number.isInteger(ttlMs) || ttlMs <= 0) {
2092
+ throw new AutotaskConfigError(`latestCountCache.ttlMs must be a positive integer (got ${String(ttlMs)})`);
2093
+ }
2094
+ if (!Number.isInteger(maxEntries) || maxEntries <= 0) {
2095
+ throw new AutotaskConfigError(`latestCountCache.maxEntries must be a positive integer (got ${String(maxEntries)})`);
2096
+ }
2097
+ this.ttlMs = ttlMs;
2098
+ this.maxEntries = maxEntries;
2099
+ }
2100
+ /** The composite key: (resource plan key, canonical filter serialisation)
2101
+ * — the `\u0000` separator is unencodable in a filter value, so no two
2102
+ * (resource, conditions) pairs collide. */
2103
+ static key(resourceKey, conditions) {
2104
+ return `${resourceKey}\0${JSON.stringify(_nullishCoalesce(conditions, () => ( [])))}`;
2105
+ }
2106
+ /** The live entry's count, or `undefined` (absent OR expired — an expired
2107
+ * entry is purged lazily on this read). */
2108
+ get(key) {
2109
+ const entry = this.store.get(key);
2110
+ if (entry === void 0) return void 0;
2111
+ if (entry.expiresAt <= Date.now()) {
2112
+ this.store.delete(key);
2113
+ return void 0;
2114
+ }
2115
+ return entry.value;
2116
+ }
2117
+ /** The invalidation epoch: bumped by `clear()`; capture it before a wire
2118
+ * read and pass it to `set` so a probe in flight across a write cannot
2119
+ * re-store its stale answer (issue #139, the `ReadCache` pattern). */
2120
+ get epoch() {
2121
+ return this.epochCounter;
2122
+ }
2123
+ /** Store a SUCCESSFUL plain-number count under `key`, live for `ttlMs`.
2124
+ * `epoch` is the invalidation epoch the caller captured BEFORE its wire
2125
+ * read: a probe that straddled a `clear()` (a write) is dropped, never
2126
+ * re-stored with its pre-write body (issue #139). Past the cap: purge
2127
+ * expired entries first, then evict the oldest-inserted (Map iteration
2128
+ * order = insertion order). */
2129
+ set(key, value, epoch = this.epochCounter) {
2130
+ if (epoch !== this.epochCounter) return;
2131
+ if (!Number.isFinite(value)) return;
2132
+ const entry = { value, expiresAt: Date.now() + this.ttlMs };
2133
+ if (!this.store.has(key) && this.store.size >= this.maxEntries) {
2134
+ for (const [k, e] of this.store) {
2135
+ if (e.expiresAt <= Date.now()) this.store.delete(k);
2136
+ }
2137
+ if (this.store.size >= this.maxEntries) {
2138
+ const oldest = this.store.keys().next();
2139
+ if (!oldest.done) this.store.delete(oldest.value);
2140
+ }
2141
+ }
2142
+ this.store.set(key, entry);
2143
+ }
2144
+ /** Drop one entry (absent = no-op). Issue #157: the stale-LOW recovery
2145
+ * deletes the cached total before its one fresh wire re-probe, so the
2146
+ * re-probe cannot be served by the entry it is meant to replace. */
2147
+ delete(key) {
2148
+ this.store.delete(key);
2149
+ }
2150
+ /** Clear every entry (a real write through the owning client — the
2151
+ * write-then-read re-fetch rule) and bump the invalidation epoch: a
2152
+ * `set` captured before this `clear` is discarded (issue #139). */
2153
+ clear() {
2154
+ this.epochCounter += 1;
2155
+ this.store.clear();
2156
+ }
2157
+ }, _class6);
2158
+
2077
2159
  // src/relationships.ts
2078
2160
  var RELATIONSHIP_ZONE = {
2079
2161
  "zone": "ww29",
@@ -8306,7 +8388,7 @@ var RELATIONSHIP_MAP = {
8306
8388
  ],
8307
8389
  "naturalKeys": {
8308
8390
  "hasSearch": true,
8309
- "searchCovers": "title only (contains); ticketNumber lookups use resolve / findByTicketNumber",
8391
+ "searchCovers": "title only (contains \u2014 the vendor's contains match is looser than a literal substring (case/token matching): a search can return MORE hits than a local title-substring check (measured 2026-10-19: 25 vs 24); a per-field contains filter in tickets.query is the precise tool); ticketNumber lookups use resolve / findByTicketNumber",
8310
8392
  "findBy": [
8311
8393
  "findByTicketNumber"
8312
8394
  ]
@@ -10253,8 +10335,8 @@ var EMPTY_REASON_STAMPS = {
10253
10335
  var POINTER_NOTES = {
10254
10336
  "ticket_history.query": `W1 (ww29 (australia-nz) live 2026-10-04; vendor contract change live-verified 2026-10-06): the vendor requires ticket_history.query to be supplied with exactly ONE ticketID-equals filter \u2014 an unfiltered call 500s by contract (10-06 text: "When querying the TicketHistory entity you are required to supply one and only one ticketID equals filter"). WITH the filter the query WORKS (live 2026-10-06: rows returned, hasMore true \u2014 a single {op:'eq', field:'ticketID', value:<id>} condition; the filter vocabulary is eq/noteq/gt/gte/lt/lte/beginsWith/endsWith/contains/exist/notExist/in/notIn/and/or). tickets.timeline / tickets.fullContext remain the convenient composites; raw history rows come from the filtered query.`,
10255
10337
  "time_entries.create": `W-A3 (ww29 (australia-nz) live 2026-10-04; drift re-verified 2026-10-06): the vendor now refuses ALL time-entry writes for this API-only user with "API-only user cannot be selected for this field" (10-04 text: "The specified AssignedResourceID and AssignedRoleID combination is not currently defined"). UNSATISFIABLE for an API-only integration user: the resources table is blind (0 rows) and the only selectable resource is the user's own API-only record, which the vendor refuses. createForResource \u2014 stage-11 PROVEN (8 live creates, 10-04) \u2014 is walled by the SAME drift (confirm-100 receipt 2026-10-06). Tenant evidence that valid pairs exist: postedWithin rows (resourceID 31131373 / 29869529 with roleID 29682834) \u2014 human resources the API user cannot select.`,
10256
- "time_entries.getPosted": `W-D3 (ww29 (australia-nz) live 2026-10-04, re-verified live 2026-10-15): this zone's TimeEntry entity exposes NO posted-state filter field \u2014 the vendor rejects postingStatus, postedDate and status with "Unable to find X in the TimeEntry Entity" (live field-existence oracle 2026-10-04: only dateWorked / resourceID / ticketID / taskID resolve as filter fields). Re-probed 2026-10-15 (ww29 re-probe campaign): time_entries.getPosted live 500, vendor body verbatim 'Unable to find postedDate in the TimeEntry Entity.' \u2014 a vendor endpoint defect on AU03, not a transient fault: posted cannot be filtered server-side on this zone \u2014 use time_entries.postedWithin (the dateWorked window, live-verified working) or read time_entries.query and derive posted-state from the fields your zone's entity exposes`,
10257
- "time_entries.getUnposted": `W-D3 (ww29 (australia-nz) live 2026-10-04, re-verified live 2026-10-15): this zone's TimeEntry entity exposes NO posted-state filter field \u2014 the vendor rejects postingStatus, postedDate and status with "Unable to find X in the TimeEntry Entity" (live field-existence oracle 2026-10-04: only dateWorked / resourceID / ticketID / taskID resolve as filter fields). Re-probed 2026-10-15 (ww29 re-probe campaign): time_entries.getUnposted live 500, vendor body verbatim 'Unable to find postedDate in the TimeEntry Entity.' \u2014 a vendor endpoint defect on AU03, not a transient fault: unposted cannot be filtered server-side on this zone \u2014 use time_entries.postedWithin (the dateWorked window, live-verified working) or read time_entries.query and derive posted-state from the fields your zone's entity exposes`,
10338
+ "time_entries.getPosted": "W-D3 (ww29 (australia-nz) live 2026-10-04, re-verified live 2026-10-15): this zone's TimeEntry entity exposes NO posted-state filter field \u2014 the vendor rejects postingStatus, postedDate and status with \"Unable to find X in the TimeEntry Entity\" (live field-existence oracle 2026-10-04: only dateWorked / resourceID / ticketID / taskID resolve as filter fields). Re-probed 2026-10-15 (ww29 re-probe campaign): time_entries.getPosted live 500, vendor body verbatim 'Unable to find postedDate in the TimeEntry Entity.' \u2014 a vendor endpoint defect on AU03, not a transient fault: posted cannot be filtered server-side on this zone \u2014 use time_entries.postedWithin (the dateWorked window, live-verified working \u2014 omit the `posted` narrowing on this zone: it sends postedDate, which the zone 500s) or read time_entries.query and derive posted-state from the fields your zone's entity exposes",
10339
+ "time_entries.getUnposted": "W-D3 (ww29 (australia-nz) live 2026-10-04, re-verified live 2026-10-15): this zone's TimeEntry entity exposes NO posted-state filter field \u2014 the vendor rejects postingStatus, postedDate and status with \"Unable to find X in the TimeEntry Entity\" (live field-existence oracle 2026-10-04: only dateWorked / resourceID / ticketID / taskID resolve as filter fields). Re-probed 2026-10-15 (ww29 re-probe campaign): time_entries.getUnposted live 500, vendor body verbatim 'Unable to find postedDate in the TimeEntry Entity.' \u2014 a vendor endpoint defect on AU03, not a transient fault: unposted cannot be filtered server-side on this zone \u2014 use time_entries.postedWithin (the dateWorked window, live-verified working \u2014 omit the `posted` narrowing on this zone: it sends postedDate, which the zone 500s) or read time_entries.query and derive posted-state from the fields your zone's entity exposes",
10258
10340
  "authenticate.get": "W2: authenticate.get is DESIGNATOR-ONLY \u2014 it returns the designator identity, not a tenant data surface; a miss/empty here is not a data-empty signal (no emptyReason is attached by design \u2014 a false reason would be worse than none)"
10259
10341
  };
10260
10342
  var BLIND_SURFACE_INVENTORY = {
@@ -14859,7 +14941,7 @@ function credentialShapeExpectation(headers, auth) {
14859
14941
  ...problem !== void 0 ? { problem } : {}
14860
14942
  };
14861
14943
  }
14862
- var HttpClient = (_class6 = class {
14944
+ var HttpClient = (_class7 = class {
14863
14945
  /**
14864
14946
  * The LIVE base URL (issue #43): the construction-time value until a zone
14865
14947
  * re-point happens, then the tenant's newly resolved zone base. Reads are
@@ -14888,6 +14970,11 @@ var HttpClient = (_class6 = class {
14888
14970
 
14889
14971
  /** Resource-name cache for `operations.expandResourceNames` (issue #80: the default instance when `resourceNameCache` is absent; undefined only when the user set `false` explicitly, issue #65). */
14890
14972
 
14973
+ /** The `latest` walk's count-probe cache (issue #127: the short-TTL
14974
+ * per-client count cache — ALWAYS on, process-internal: no config
14975
+ * surface, no served keys; the plain `count` op and `operations.aggregate`
14976
+ * bypass it by construction and promise fresh totals). */
14977
+
14891
14978
  /** Resolved update fallback gate (issue #34; OFF by default). */
14892
14979
 
14893
14980
 
@@ -14901,7 +14988,7 @@ var HttpClient = (_class6 = class {
14901
14988
  * exhaustion `suggestedAction` (stop/narrow, never "raise the limit"). */
14902
14989
 
14903
14990
  /** Request timestamps inside the trailing hour (only used when `rateLimit.perHour` is set). */
14904
- __init10() {this.hourStamps = []}
14991
+ __init12() {this.hourStamps = []}
14905
14992
  /** Tenant 429 cooldown gate store (undefined when the gate is disabled). */
14906
14993
 
14907
14994
 
@@ -14945,14 +15032,15 @@ var HttpClient = (_class6 = class {
14945
15032
  * already travels in every auth header; nothing new crosses the boundary). */
14946
15033
 
14947
15034
 
14948
- __init11() {this.inFlight = 0}
14949
- __init12() {this.waitQueue = []}
14950
- constructor(config) {;_class6.prototype.__init10.call(this);_class6.prototype.__init11.call(this);_class6.prototype.__init12.call(this);
15035
+ __init13() {this.inFlight = 0}
15036
+ __init14() {this.waitQueue = []}
15037
+ constructor(config) {;_class7.prototype.__init12.call(this);_class7.prototype.__init13.call(this);_class7.prototype.__init14.call(this);
14951
15038
  this.liveBaseUrl = config.baseUrl;
14952
15039
  this.resolution = config.resolution;
14953
15040
  this.readCache = config.readCache;
14954
15041
  this.fieldCache = config.fieldCache;
14955
15042
  this.resourceNameCache = config.resourceNameCache;
15043
+ this.latestCountCache = new LatestCountCache(LATEST_COUNT_CACHE_TTL_MS);
14956
15044
  this.update = config.update;
14957
15045
  this.zone = config.zone;
14958
15046
  this.reResolveZoneOn401 = config.reResolveZoneOn401;
@@ -15686,6 +15774,7 @@ var HttpClient = (_class6 = class {
15686
15774
  clearTimeout(deadlineTimer);
15687
15775
  if (!(method === "GET" || opts.idempotent === true)) {
15688
15776
  _optionalChain([this, 'access', _122 => _122.readCache, 'optionalAccess', _123 => _123.clear, 'call', _124 => _124()]);
15777
+ this.latestCountCache.clear();
15689
15778
  const nameOp = typeof opts.operation === "string" ? opts.operation : void 0;
15690
15779
  const nameDot = nameOp !== void 0 ? nameOp.indexOf(".") : -1;
15691
15780
  if (this.resourceNameCache !== void 0 && nameOp !== void 0 && nameDot > 0 && nameOp.slice(0, nameDot) === "resources") {
@@ -15695,7 +15784,7 @@ var HttpClient = (_class6 = class {
15695
15784
  }
15696
15785
  throw new AutotaskError("unreachable: the request loop ended without an outcome");
15697
15786
  }
15698
- }, _class6);
15787
+ }, _class7);
15699
15788
  var INVALID_SHAPE_ACTION = "Report the unexpected response shape; verify baseUrl points at the REST API.";
15700
15789
  var META_TRIO_VERBS = /* @__PURE__ */ new Set(["getEntityInfo", "getFieldInfo", "getUdfInfo"]);
15701
15790
  function attachEmptyReasonDetails(err) {
@@ -15946,7 +16035,7 @@ async function expandResourceNames(client, ids, opts = {}) {
15946
16035
  canonical[i] = parseIdentifier(raw, at);
15947
16036
  } catch (e16) {
15948
16037
  if (typeof raw === "string") {
15949
- problems.push(`${at}: "${raw}" is not a vendor numeric resource id \u2014 non-numeric string identifiers are autotask_resolve_any territory (resolveAny / the resource's findBy* helpers)`);
16038
+ problems.push(`${at}: "${raw}" is not a vendor numeric resource id \u2014 non-numeric string identifiers are operations.resolveAny territory (the resource's findBy* helpers)`);
15950
16039
  } else if (typeof raw === "number") {
15951
16040
  problems.push(`${at}: ${raw} is not a non-negative safe integer (a JS number this large is already corrupted \u2014 pass the canonical integer string form)`);
15952
16041
  } else {
@@ -15957,7 +16046,7 @@ async function expandResourceNames(client, ids, opts = {}) {
15957
16046
  if (problems.length > 0) {
15958
16047
  throw new AutotaskConfigError(`expandResourceNames: ${problems.length} problem(s) found pre-request (zero wire was issued)
15959
16048
  - ` + problems.join("\n- "), {
15960
- suggestedAction: "Fix every id above and call again \u2014 the ids are vendor NUMERIC resource ids only (name-to-id resolution is autotask_resolve_any territory)."
16049
+ suggestedAction: "Fix every id above and call again \u2014 the ids are vendor NUMERIC resource ids only (name-to-id resolution is operations.resolveAny territory)."
15961
16050
  });
15962
16051
  }
15963
16052
  const unique = [];
@@ -16082,7 +16171,7 @@ function capabilitySets() {
16082
16171
  const fieldOps = /* @__PURE__ */ new Map();
16083
16172
  const udfOps = /* @__PURE__ */ new Map();
16084
16173
  const resources = /* @__PURE__ */ new Set();
16085
- for (const rec of _chunkJM52ZU7Tcjs.CAPABILITIES) {
16174
+ for (const rec of _chunkCBUY3MKBcjs.CAPABILITIES) {
16086
16175
  if (rec.resource === "operations") continue;
16087
16176
  resources.add(rec.resource);
16088
16177
  if (rec.operation === "getFieldInfo") fieldOps.set(rec.resource, rec);
@@ -16705,7 +16794,7 @@ function validateFetchManyItems(items, cap, opName = "fetchMany", capRef = `FETC
16705
16794
  } catch (e17) {
16706
16795
  if (typeof item.id === "string") {
16707
16796
  problems.push(
16708
- `${at}.id: "${item.id}" is not a vendor numeric id \u2014 non-numeric string identifiers are autotask_resolve_any territory (resolveAny / the resource's findBy* helpers)`
16797
+ `${at}.id: "${item.id}" is not a vendor numeric id \u2014 non-numeric string identifiers are operations.resolveAny territory (the resource's findBy* helpers)`
16709
16798
  );
16710
16799
  } else if (typeof item.id === "number") {
16711
16800
  problems.push(
@@ -17345,17 +17434,57 @@ var BaseResource = class {
17345
17434
  const bigB = typeof b === "number" ? BigInt(String(b)) : BigInt(b);
17346
17435
  return bigA < bigB ? -1 : bigA > bigB ? 1 : 0;
17347
17436
  }
17437
+ /** Issue #127: the `latest` walk's count probes through the per-client
17438
+ * short-TTL count cache (`LatestCountCache` — fixed 30 s TTL, the
17439
+ * memory guard; cleared on every real write through this client, the
17440
+ * write-then-read re-fetch rule). Only a SUCCESSFUL plain-number vendor
17441
+ * count answer is stored: the blind-surface / pinned-empty sentinels are
17442
+ * RECORDS and pass through un-stored (the plan-pinned stamps stay fresh
17443
+ * and re-derive on every call), and an error rejects before any store.
17444
+ * The key is (resource plan key, canonical filter serialisation — the
17445
+ * normalised conditions' JSON): the initial `count(F)` probe and every
17446
+ * `id gt T` threshold probe are separate entries, and a re-asked
17447
+ * "top N" within the TTL makes ZERO extra count calls for every CACHED
17448
+ * probe (the bracket/search sequence is deterministic given the counts);
17449
+ * a probe that answered 0 is never cached, so it re-wires (issue #157).
17450
+ * Issue #157: two corrections to the snapshot trade. (1) A total of 0 is
17451
+ * NEVER stored — a 0 is rare and cheap to re-probe, and a cached 0 fed
17452
+ * the false-empty branch (a repeat after an out-of-band insert returned
17453
+ * no rows without walking). (2) `fresh: true` DELETES the entry before
17454
+ * the wire read and re-stores the fresh answer — the ONE re-probe the
17455
+ * stale-LOW recovery in `latestWalk` is allowed. Counts are snapshots,
17456
+ * not contracts: the truncation guard covers the growth-of-total
17457
+ * direction; a stale-LOW total within the TTL is resolved by that one
17458
+ * fresh re-probe (never by the guard alone). The plain `count` op and
17459
+ * `operations.aggregate` call `countGlobal` directly and promise fresh
17460
+ * totals — this wrapper is used ONLY on the latest-walk probe path.
17461
+ */
17462
+ async latestProbeCount(filter, opts = {}) {
17463
+ const cache = this.http.latestCountCache;
17464
+ const key = LatestCountCache.key(this.config.key, normalizedConditions(filter));
17465
+ if (opts.fresh === true) {
17466
+ cache.delete(key);
17467
+ } else {
17468
+ const hit = cache.get(key);
17469
+ if (hit !== void 0) return { value: hit, cached: true };
17470
+ }
17471
+ const epoch = cache.epoch;
17472
+ const res = await this.countGlobal({ filter });
17473
+ const value = res;
17474
+ if (typeof value === "number" && value !== 0) cache.set(key, value, epoch);
17475
+ return { value, cached: false };
17476
+ }
17348
17477
  /** Issue #59: one `latest` threshold count probe — a ROWLESS
17349
17478
  * `count(F ∧ id gt T)` call (the vendor caps — ≤500 OR-conditions,
17350
17479
  * 8000 chars serialised — leave a single `gt` condition room on any
17351
- * filter). The `BlindCountResult` sentinel (a record, not a number —
17480
+ * filter). Issue #127: it rides the per-client short-TTL count cache — the same (resource, conditions) probe within the 30 s window is reused (zero vendor calls); a sentinel answer is never cached. The `BlindCountResult` sentinel (a record, not a number —
17352
17481
  * the declared `number` return is the documented under-type) unwraps to
17353
17482
  * its `count` value (always 0): at or above the max matching id the
17354
17483
  * count is 0, which is exactly the bracket-termination signal. */
17355
17484
  async countAboveId(baseConds, t) {
17356
17485
  const value = t <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(t) : t.toString();
17357
- const res = await this.countGlobal({ filter: [...baseConds, { op: "gt", field: "id", value }] });
17358
- return typeof res === "object" && res !== null && "count" in res ? res.count : res;
17486
+ const probed = await this.latestProbeCount([...baseConds, { op: "gt", field: "id", value }]);
17487
+ return typeof probed.value === "object" && probed.value !== null && "count" in probed.value ? probed.value.count : probed.value;
17359
17488
  }
17360
17489
  /**
17361
17490
  * Issue #59: the `latest: N` mechanism — the N highest-`id` rows,
@@ -17373,8 +17502,11 @@ var BaseResource = class {
17373
17502
  * 1 + ceil(C/500) <= 6, independent of table size. A truncated walk
17374
17503
  * (the vendor's rows disagree with its own count) is
17375
17504
  * LATEST_WINDOW_INCOMPLETE — the top-N of a truncated set is
17376
- * unprovable. Sort id-descending; return the top N (a set of
17377
- * C < N returns itself).
17505
+ * unprovable — EXCEPT issue #157: when `C` came from the CACHE, the
17506
+ * total is re-probed ONCE fresh (the stale-LOW recovery: a larger
17507
+ * fresh total continues the walk against it; the refusal stands only
17508
+ * when the fresh probe confirms the cached value). Sort
17509
+ * id-descending; return the top N (a set of C < N returns itself).
17378
17510
  * - `C > 2500` → THRESHOLD mode: find the smallest id threshold `T`
17379
17511
  * with `count(F ∧ id gt T) <= N` by exponential bracketing
17380
17512
  * (T = 1, 2, 4, …) + binary search — each probe ONE rowless count
@@ -17401,13 +17533,29 @@ var BaseResource = class {
17401
17533
  * answer.
17402
17534
  * `includeFields` always carries `id` (the P1 id-append,
17403
17535
  * `buildQueryRequest`), which `latest` sorts on. Pure read; no mutation
17404
- * paths involved.
17536
+ * paths involved. Issue #127: every count probe of this walk (the
17537
+ * initial `count(F)` + the `id gt T` threshold probes) rides the
17538
+ * per-client short-TTL count cache (30 s; a real write through this
17539
+ * client clears it) — a re-asked "top N" within the window reuses every
17540
+ * CACHED probe (zero extra vendor count calls); sentinel answers are
17541
+ * never cached (the plan-pinned stamps stay fresh), and a 0 total is
17542
+ * never cached either (issue #157: a 0 is cheap to re-probe and a cached
17543
+ * 0 would shadow an out-of-band insert), so a 0-answering probe re-wires.
17544
+ * The stale-LOW direction is closed by the ONE fresh re-probe above. The
17545
+ * stale-HIGH direction is the documented snapshot trade, NOT "safe by
17546
+ * construction": the threshold probes are cache-served like every other
17547
+ * probe (no fresh re-probe exists on that path), so a cached threshold
17548
+ * count higher than reality can under-report the ROW COUNT within the
17549
+ * TTL — the returned top-N stays correct, and full-set mode ends when the
17550
+ * cursor exhausts. The plain `count` op and
17551
+ * `operations.aggregate` bypass the cache by construction and promise
17552
+ * fresh totals.
17405
17553
  */
17406
17554
  async latestWalk(params, limit, latest) {
17407
17555
  const baseConds = _nullishCoalesce(normalizedConditions(params.filter), () => ( []));
17408
- const probe = await this.countGlobal(params);
17409
- if (typeof probe === "object" && probe !== null) {
17410
- const rec = probe;
17556
+ const probe = await this.latestProbeCount(params.filter);
17557
+ if (typeof probe.value === "object" && probe.value !== null) {
17558
+ const rec = probe.value;
17411
17559
  if (rec.blindSurface !== void 0) {
17412
17560
  return this.latestBlindWalk(params, limit, latest);
17413
17561
  }
@@ -17415,47 +17563,60 @@ var BaseResource = class {
17415
17563
  this.stampEmptyQueryResult(result, params);
17416
17564
  return result;
17417
17565
  }
17418
- const total = probe;
17419
- if (total === 0) {
17420
- const result = { items: [], fetched: 0, hasMore: false, pagesWalked: 0 };
17421
- this.stampEmptyQueryResult(result, params);
17422
- return result;
17423
- }
17424
- if (total <= BOUNDED_QUERY_MAX_LIMIT) {
17425
- const { items: items2, pages: pages2, guard: guard2, sliced: sliced2, cursor: cursor2 } = await this.walkBounded(params, total);
17426
- if (this.walkTruncated(total, { itemsLength: items2.length, guard: guard2, sliced: sliced2, cursor: cursor2 })) {
17427
- throw this.latestWindowIncompleteError(total);
17566
+ let total = probe.value;
17567
+ let cacheSourced = probe.cached;
17568
+ let reProbed = false;
17569
+ for (; ; ) {
17570
+ if (total === 0) {
17571
+ const result = { items: [], fetched: 0, hasMore: false, pagesWalked: 0 };
17572
+ this.stampEmptyQueryResult(result, params);
17573
+ return result;
17428
17574
  }
17429
- return this.finishLatest(items2, latest, pages2);
17430
- }
17431
- let probes = 1;
17432
- const spend = () => {
17433
- probes += 1;
17434
- if (probes > LATEST_MAX_COUNT_PROBES) throw this.latestThresholdUnresolvedError();
17435
- };
17436
- let lo = 0n;
17437
- let hi = 0n;
17438
- for (let t = 1n; ; t = t << 1n) {
17439
- spend();
17440
- if (await this.countAboveId(baseConds, t) <= latest) {
17441
- hi = t;
17442
- break;
17575
+ if (total <= BOUNDED_QUERY_MAX_LIMIT) {
17576
+ const { items: items2, pages: pages2, guard: guard2, sliced: sliced2, cursor: cursor2 } = await this.walkBounded(params, total);
17577
+ if (this.walkTruncated(total, { itemsLength: items2.length, guard: guard2, sliced: sliced2, cursor: cursor2 })) {
17578
+ if (cacheSourced && !reProbed) {
17579
+ reProbed = true;
17580
+ const fresh = await this.latestProbeCount(params.filter, { fresh: true });
17581
+ if (typeof fresh.value === "number" && fresh.value > total) {
17582
+ total = fresh.value;
17583
+ cacheSourced = false;
17584
+ continue;
17585
+ }
17586
+ }
17587
+ throw this.latestWindowIncompleteError(total);
17588
+ }
17589
+ return this.finishLatest(items2, latest, pages2);
17443
17590
  }
17444
- lo = t;
17445
- }
17446
- while (hi - lo > 1n) {
17447
- const mid = lo + hi >> 1n;
17448
- spend();
17449
- if (await this.countAboveId(baseConds, mid) <= latest) hi = mid;
17450
- else lo = mid;
17451
- }
17452
- const windowValue = hi <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(hi) : hi.toString();
17453
- const window = [...baseConds, { op: "gte", field: "id", value: windowValue }];
17454
- const { items, pages, guard, sliced, cursor } = await this.walkBounded({ ...params, filter: window }, latest + 1);
17455
- if (this.walkTruncated(latest + 1, { itemsLength: items.length, guard, sliced, cursor })) {
17456
- throw this.latestWindowIncompleteError(latest + 1);
17591
+ let probes = reProbed ? 2 : 1;
17592
+ const spend = () => {
17593
+ probes += 1;
17594
+ if (probes > LATEST_MAX_COUNT_PROBES) throw this.latestThresholdUnresolvedError();
17595
+ };
17596
+ let lo = 0n;
17597
+ let hi = 0n;
17598
+ for (let t = 1n; ; t = t << 1n) {
17599
+ spend();
17600
+ if (await this.countAboveId(baseConds, t) <= latest) {
17601
+ hi = t;
17602
+ break;
17603
+ }
17604
+ lo = t;
17605
+ }
17606
+ while (hi - lo > 1n) {
17607
+ const mid = lo + hi >> 1n;
17608
+ spend();
17609
+ if (await this.countAboveId(baseConds, mid) <= latest) hi = mid;
17610
+ else lo = mid;
17611
+ }
17612
+ const windowValue = hi <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(hi) : hi.toString();
17613
+ const window = [...baseConds, { op: "gte", field: "id", value: windowValue }];
17614
+ const { items, pages, guard, sliced, cursor } = await this.walkBounded({ ...params, filter: window }, latest + 1);
17615
+ if (this.walkTruncated(latest + 1, { itemsLength: items.length, guard, sliced, cursor })) {
17616
+ throw this.latestWindowIncompleteError(latest + 1);
17617
+ }
17618
+ return this.finishLatest(items, latest, pages);
17457
17619
  }
17458
- return this.finishLatest(items, latest, pages);
17459
17620
  }
17460
17621
  /** The shared `latest` completion: the (provably complete or
17461
17622
  * stamp-scoped) set sorted id-descending, top N taken. `hasMore: false`