@cotal-ai/core 0.66.1 → 0.67.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 (67) hide show
  1. package/dist/auth-provider.d.ts +16 -9
  2. package/dist/auth-provider.d.ts.map +1 -1
  3. package/dist/backup-config.d.ts +1 -1
  4. package/dist/backup-config.d.ts.map +1 -1
  5. package/dist/backup-config.js +6 -2
  6. package/dist/backup-config.js.map +1 -1
  7. package/dist/connector.d.ts +24 -0
  8. package/dist/connector.d.ts.map +1 -1
  9. package/dist/connector.js.map +1 -1
  10. package/dist/endpoint-action.d.ts.map +1 -1
  11. package/dist/endpoint-action.js +9 -10
  12. package/dist/endpoint-action.js.map +1 -1
  13. package/dist/endpoint-envelope.d.ts.map +1 -1
  14. package/dist/endpoint-envelope.js +1 -3
  15. package/dist/endpoint-envelope.js.map +1 -1
  16. package/dist/endpoint-grants.d.ts +1 -1
  17. package/dist/endpoint-grants.d.ts.map +1 -1
  18. package/dist/endpoint-grants.js +1 -0
  19. package/dist/endpoint-grants.js.map +1 -1
  20. package/dist/endpoint-invoke.d.ts +6 -0
  21. package/dist/endpoint-invoke.d.ts.map +1 -1
  22. package/dist/endpoint-invoke.js +6 -0
  23. package/dist/endpoint-invoke.js.map +1 -1
  24. package/dist/endpoint-reconcile.js +2 -2
  25. package/dist/endpoint-reconcile.js.map +1 -1
  26. package/dist/endpoint-serve-kv.d.ts.map +1 -1
  27. package/dist/endpoint-serve-kv.js +29 -44
  28. package/dist/endpoint-serve-kv.js.map +1 -1
  29. package/dist/endpoint-service.d.ts +3 -10
  30. package/dist/endpoint-service.d.ts.map +1 -1
  31. package/dist/endpoint-service.js +3 -3
  32. package/dist/endpoint-service.js.map +1 -1
  33. package/dist/endpoint.d.ts +8 -8
  34. package/dist/endpoint.d.ts.map +1 -1
  35. package/dist/endpoint.js +19 -20
  36. package/dist/endpoint.js.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/lifecycle-saga.d.ts.map +1 -1
  42. package/dist/lifecycle-saga.js +24 -24
  43. package/dist/lifecycle-saga.js.map +1 -1
  44. package/dist/lifecycle-state.d.ts +44 -36
  45. package/dist/lifecycle-state.d.ts.map +1 -1
  46. package/dist/lifecycle-state.js.map +1 -1
  47. package/dist/provision.d.ts +14 -1
  48. package/dist/provision.d.ts.map +1 -1
  49. package/dist/provision.js +27 -4
  50. package/dist/provision.js.map +1 -1
  51. package/dist/remote-manager-authority.d.ts +13 -1
  52. package/dist/remote-manager-authority.d.ts.map +1 -1
  53. package/dist/remote-manager-authority.js +24 -17
  54. package/dist/remote-manager-authority.js.map +1 -1
  55. package/dist/spaces.d.ts +5 -1
  56. package/dist/spaces.d.ts.map +1 -1
  57. package/dist/spaces.js +50 -15
  58. package/dist/spaces.js.map +1 -1
  59. package/dist/subjects.d.ts +9 -0
  60. package/dist/subjects.d.ts.map +1 -1
  61. package/dist/subjects.js +14 -0
  62. package/dist/subjects.js.map +1 -1
  63. package/dist/transfer.d.ts +110 -0
  64. package/dist/transfer.d.ts.map +1 -0
  65. package/dist/transfer.js +325 -0
  66. package/dist/transfer.js.map +1 -0
  67. package/package.json +1 -1
package/dist/endpoint.js CHANGED
@@ -8,7 +8,7 @@ import { credsClaims, credsFingerprint, credsRenewalDelayMs, idFromCreds } from
8
8
  import { requireBrokerFloor } from "./broker-floor.js";
9
9
  import { inspectCredHealth } from "./provision.js";
10
10
  import { parseSecretStoreIdentity, } from "./secret-store.js";
11
- import { resolveService, invokeCommand, submitAndFollowGoal } from "./endpoint-invoke.js";
11
+ import { BIND_SPLIT_REISSUES, resolveService, invokeCommand, submitAndFollowGoal } from "./endpoint-invoke.js";
12
12
  import { EpEnvelopeError, respondedButUnbound, replyRefusedBeforeEffect, EP_BIND_REFUSED } from "./endpoint-envelope.js";
13
13
  import { isRepeatSafeCommand } from "./endpoint-grants.js";
14
14
  import { assertIdToken, assertGeneration } from "./endpoint-subjects.js";
@@ -98,15 +98,19 @@ const READ_HISTORY_DEFAULT_LIMIT = 100;
98
98
  * request pull a channel's whole retained set through it. Clamped, not refused — a UI asking for
99
99
  * more than a page should get a page, and `complete: false` already tells it more remains. */
100
100
  const READ_HISTORY_MAX_LIMIT = 200;
101
- /** The NEWEST prefix-from-the-end of `items` whose serialized size fits `budget` bytes, order
102
- * preserved. Returns `[]` when not even the newest single message fits — the caller must refuse
103
- * loudly there rather than serve an empty page, which would read as "no history".
101
+ /** The NEWEST prefix-from-the-end of `items` whose serialized `readHistory` reply fits `budget`
102
+ * bytes, order preserved. Returns `[]` when not even the newest single message fits — the caller
103
+ * must refuse loudly there rather than serve an empty page, which would read as "no history".
104
+ *
105
+ * The `ControlReply` around the items rides the same NATS message, so it is charged too: items
106
+ * that fit on their own can still make a reply the broker refuses.
104
107
  *
105
108
  * Measured in ENCODED bytes, not `string.length`: a page of multi-byte text would otherwise be
106
109
  * undercounted and still overflow the broker. Same discipline as `assertFactFits`. */
107
110
  export function fitHistoryPage(items, budget) {
108
111
  const enc = new TextEncoder();
109
- let used = 2; // the enclosing `[]`
112
+ // The reply with no items, at its longer `complete` value.
113
+ let used = enc.encode(JSON.stringify({ ok: true, data: { items: [], complete: false } })).length;
110
114
  let first = items.length; // index of the oldest kept item
111
115
  for (let i = items.length - 1; i >= 0; i--) {
112
116
  const size = enc.encode(JSON.stringify(items[i])).length + 1; // + the `,` separator
@@ -2017,7 +2021,8 @@ export class CotalEndpoint extends EventEmitter {
2017
2021
  * and only knowing someone answered:
2018
2022
  * - the RESPONDER fenced it on the request's `bind` (§13.2, an `ok:false` reply marked
2019
2023
  * {@link replyRefusedBeforeEffect}): the command did not run, so the bind is dropped and the
2020
- * call re-issued ONCE for any command. If that re-issue cannot be resolved, the refusal
2024
+ * call re-issued for any command, up to {@link BIND_SPLIT_REISSUES} times while each re-issue
2025
+ * is refused the same way. If a re-issue cannot be resolved, the refusal
2021
2026
  * surfaces — still saying the command did not run — naming the resolve failure as why the
2022
2027
  * repair could not be attempted.
2023
2028
  * - this CLIENT caught it on the reply ({@link respondedButUnbound}: a different instance,
@@ -2066,7 +2071,7 @@ export class CotalEndpoint extends EventEmitter {
2066
2071
  const doInvoke = async (signal = opts.signal) => {
2067
2072
  signal?.throwIfAborted();
2068
2073
  try {
2069
- const r = await invokeResolved(await resolve(signal), signal);
2074
+ let r = await invokeResolved(await resolve(signal), signal);
2070
2075
  // THE RESPONDER FENCED IT (§13.2 `ai.cotal.ep.bind-refused`): a class member saw the call
2071
2076
  // was bound to a different incarnation and refused BEFORE running the command.
2072
2077
  //
@@ -2075,9 +2080,10 @@ export class CotalEndpoint extends EventEmitter {
2075
2080
  // otherwise keep its stale bind and meet the same refusal forever.
2076
2081
  //
2077
2082
  // The re-issue is NOT gated on {@link isRepeatSafeCommand}: the responder states the command
2078
- // did not run, so this is a FIRST attempt, not a second. Exactly once; a second refusal
2079
- // surfaces.
2080
- if (r.reply.ok === false && replyRefusedBeforeEffect(r.reply.error)) {
2083
+ // did not run, so each re-issue is a FIRST attempt, not a second. It repeats up to the bound
2084
+ // the CLI uses, because the re-issue rides the same class queue and splits again at the same
2085
+ // rate; repairing once left a quarter of all calls in a two-manager space failing (#443).
2086
+ for (let reissues = 0; reissues < BIND_SPLIT_REISSUES && r.reply.ok === false && replyRefusedBeforeEffect(r.reply.error); reissues++) {
2081
2087
  // Counted before it is repaired: a recovery that leaves no trace takes the split rate with it.
2082
2088
  this.splitsRecovered++;
2083
2089
  // `boundTo` is the other half of `servedBy`: who the handle THOUGHT it was talking to,
@@ -2127,7 +2133,7 @@ export class CotalEndpoint extends EventEmitter {
2127
2133
  // re-issue never went out. Prose is for the reader; this is for the machine.
2128
2134
  "not-executed");
2129
2135
  }
2130
- return await invokeResolved(reissueTarget, signal);
2136
+ r = await invokeResolved(reissueTarget, signal);
2131
2137
  }
2132
2138
  return r;
2133
2139
  }
@@ -4346,23 +4352,16 @@ export class CotalEndpoint extends EventEmitter {
4346
4352
  // So bound by BYTES too, keeping the NEWEST that fit — which needs no new vocabulary, because
4347
4353
  // `complete: false` already means "older history remains behind this page". Trimming here is the
4348
4354
  // documented truncation signal doing its job, not a silent degradation.
4349
- const fitted = fitHistoryPage(wanted, this.payloadBudget());
4355
+ const fitted = fitHistoryPage(wanted, this.maxPayload);
4350
4356
  // `wanted.length > 0` is load-bearing, not defensive: an EMPTY channel also fits nothing, and
4351
4357
  // without this guard a channel nobody has posted to was refused with "the newest message exceeds
4352
4358
  // the payload budget" — a confident, entirely wrong explanation for a legitimately empty result.
4353
4359
  // Genuine emptiness is `{ items: [], complete: true }`; only a message too large to ever send is
4354
4360
  // the error.
4355
4361
  if (wanted.length > 0 && fitted.length === 0)
4356
- return { ok: false, error: `readHistory: the newest message on "${channel}" alone exceeds the broker payload budget (${this.payloadBudget()} bytes) - refused loudly rather than served as an empty page` };
4362
+ return { ok: false, error: `readHistory: the newest message on "${channel}" alone exceeds the broker payload budget (${this.maxPayload} bytes) - refused loudly rather than served as an empty page` };
4357
4363
  return { ok: true, data: { items: fitted, complete: reachedStart && fitted.length === wanted.length } };
4358
4364
  }
4359
- /** Bytes a control reply may occupy: the broker's `max_payload` less headroom for the `ControlReply`
4360
- * envelope wrapped around the items. Read from the live server info rather than assumed, since an
4361
- * operator can raise or lower it. */
4362
- payloadBudget() {
4363
- const max = this.nc?.info?.max_payload ?? 1_048_576;
4364
- return Math.max(1, Math.floor(max * 0.9));
4365
- }
4366
4365
  /** Stop serving Plane-3 WITHOUT tearing down the connection, so a daemon that has just learned its
4367
4366
  * lease may no longer be its own can stop acting on the shard while it finds out for certain.
4368
4367
  *