@cotal-ai/delivery 0.48.2 → 0.50.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.
@@ -0,0 +1,374 @@
1
+ /**
2
+ * The delivery daemon's two self-termination decisions, as PURE functions over evidence.
3
+ *
4
+ * Both decisions used to be taken inline from a wall clock, and a wall clock cannot tell "the
5
+ * broker is gone" from "this process did not get scheduled". On 2026-09-05 that cost Plane-3 nine
6
+ * minutes across two exits while `nats-server` had been up continuously for 7.3 days and was
7
+ * listening throughout; the real condition was load 311 on 12 cores (#1318). The detector failed in
8
+ * the direction of its own failure, and it did so exactly when delivery was most needed.
9
+ *
10
+ * Pulled out here for two reasons. The obvious one is that a decision expressed as a function of
11
+ * named evidence is readable. The load-bearing one is that it becomes GRADEABLE without a broker,
12
+ * a host under load, or a spawned process: every branch below is reachable from a literal, so each
13
+ * ACCEPTING branch can be paired with a REFUSING case that differs only in the evidence, and a
14
+ * mutation to any branch reddens a named cell rather than a timing-dependent end-to-end assertion.
15
+ */
16
+ /** How often the broker watch is scheduled. The measured gap between two firings minus this is
17
+ * LOCAL SCHEDULER LAG: a fact about this process, not about the server. */
18
+ export const PROBE_INTERVAL_MS = 2000;
19
+ /** The deadline `isReachable` gives a non-websocket probe. Named here because the probe's own
20
+ * budget is the yardstick that makes a late answer legible as lateness. */
21
+ export const PROBE_BUDGET_MS = 1000;
22
+ /** How far past its own budget an answer may arrive and still be believed as a statement about the
23
+ * server. A probe that answers at its deadline is a normal timeout; one that answers at twice its
24
+ * deadline did not have that deadline enforced against the SERVER at all. It had it enforced
25
+ * against a process that was not running when it expired. Two rather than something larger because
26
+ * the measured case is 2554ms on a 1000ms budget (#1318's triage), and rather than something
27
+ * smaller because it must never reclassify an honest timeout on a merely busy host as starvation,
28
+ * which would blunt the true-positive path this repair is required to preserve. */
29
+ export const PROBE_LATE_FACTOR = 2;
30
+ /** How often the deschedule sampler wakes during a probe. Short enough to resolve the stalls that
31
+ * matter (tens of ms), long enough that the measurement is not itself a meaningful load. */
32
+ export const PROBE_SAMPLE_MS = 25;
33
+ /** How much of its promised budget the server must have been DENIED before a refusal stops counting
34
+ * as evidence about the server. Ordinary Node timer jitter measures tens of milliseconds on a
35
+ * perfectly healthy host, so any non-zero threshold would disqualify every honest budget-ended
36
+ * refusal and leave the daemon exiting only on its backstop. A server handed more than half the
37
+ * time it was promised and still refusing is refusing on its own account. */
38
+ export const STARVED_SHARE = 0.5;
39
+ /**
40
+ * Classify one probe result.
41
+ *
42
+ * `ok === undefined` is a probe that REJECTED rather than resolving: an unanswered question. It was
43
+ * previously swallowed by `.catch(() => {})`, so it neither refreshed the window nor evaluated
44
+ * anything, and silently aged the daemon toward an exit it had gathered no evidence for.
45
+ */
46
+ export function classifyProbe(ok, elapsedMs, budgetMs = PROBE_BUDGET_MS, lateFactor = PROBE_LATE_FACTOR, descheduledDuringMs = 0) {
47
+ if (ok === undefined)
48
+ return { counts: "incomplete" };
49
+ // A POSITIVE IS BELIEVED HOWEVER LATE IT IS. A slow yes still required a server to say it, so
50
+ // lateness cannot turn it into anything weaker; and refusing late positives would make a starved
51
+ // daemon unable to ever clear its own window, which is the defect again with the sign flipped.
52
+ if (ok)
53
+ return { counts: "positive" };
54
+ const ceiling = budgetMs * lateFactor;
55
+ // (1) THE ANSWER ARRIVED FAR PAST ITS OWN DEADLINE. The deadline was enforced against this
56
+ // process, not against the server: a 1000ms budget that reports at 2554ms did not measure a
57
+ // server for 2554ms, it measured a process that could not get back on the CPU to stop waiting.
58
+ // This is the form the #1318 triage captured directly, and it needs no other instrumentation.
59
+ if (elapsedMs > ceiling)
60
+ return { counts: "starved", lateBy: elapsedMs - budgetMs };
61
+ // (2) THE DEADLINE EXPIRED WITHOUT THE SERVER EVER GETTING THE BUDGET IT WAS PROMISED. A refusal
62
+ // is only evidence if the server had the time to answer in. Subtracting the stretch this process
63
+ // spent OFF the runqueue leaves what the server actually had, and the two cases separate cleanly:
64
+ //
65
+ // - a genuinely dead port answers ECONNREFUSED in about a millisecond, so `elapsed` never
66
+ // reaches the budget at all and this clause does not apply. That is a prompt, honest negative
67
+ // and it must stay one, or the repair would buy availability by making a dead broker
68
+ // survivable, which is the failure mode worse than the defect.
69
+ // - a process getting short slices of CPU issues a connect, is descheduled, and its deadline
70
+ // timer fires the instant it is scheduled again. Wall-clock elapsed looks like a normal,
71
+ // prompt timeout; almost none of it was time the server was given. Judged by the clock alone
72
+ // this is indistinguishable from a dead server, which is exactly the confusion #1318 is about.
73
+ //
74
+ // THE MATERIAL-SHARE TEST IS WHAT KEEPS THIS FROM SWALLOWING EVERY REFUSAL, and it was a review
75
+ // finding: requiring merely `attributable < budget` meant ANY non-zero measurement disqualified an
76
+ // honest budget-ended refusal, and ordinary Node timer jitter on a healthy host measures 10-30ms.
77
+ // Every blackholed probe everywhere then read as starvation, the completed-negative count could
78
+ // never rise, and a genuinely dead broker would be exited only by the backstop, four times slower
79
+ // than the shipped window, with the wrong reason in the log. So the server must have been denied a
80
+ // MATERIAL share of its budget, not merely an instant of it. Half is the line: a server given more
81
+ // than half the time it was promised and still refusing is refusing on its own account.
82
+ //
83
+ // Clamped to the probe's own span so a bad measurement cannot manufacture credit.
84
+ const attributableMs = elapsedMs - Math.max(0, Math.min(descheduledDuringMs, elapsedMs));
85
+ if (elapsedMs >= budgetMs && attributableMs < budgetMs * STARVED_SHARE) {
86
+ return { counts: "starved", lateBy: elapsedMs - attributableMs };
87
+ }
88
+ return { counts: "negative" };
89
+ }
90
+ /**
91
+ * Measure how long this process spends OFF the runqueue across a span, by watching a short timer's
92
+ * own lateness.
93
+ *
94
+ * A timer asked to fire every `sampleMs` that instead fires `sampleMs + d` later reports `d` of
95
+ * delay this process could not avoid: the event loop was ready and the process was not running.
96
+ * Summed across a probe. That is the part of the probe's wall-clock that the server was never
97
+ * actually given, which is the difference between "the server did not answer in a second" and "a
98
+ * second passed, and the server had 40ms of it".
99
+ *
100
+ * This measures the same underlying condition as {@link LoopLagMeter} at a finer grain and over a
101
+ * bounded span, which is what lets a per-probe verdict use it. It deliberately reads only the
102
+ * clock: `/proc` sampling, `getrusage`, or cgroup pressure would all be sharper, and all of them are
103
+ * platform-specific in a way that would make this daemon behave differently on the hosts that most
104
+ * need it. A late timer is available everywhere the daemon runs.
105
+ */
106
+ export class DescheduleSampler {
107
+ sampleMs;
108
+ accumulated = 0;
109
+ last = 0;
110
+ timer;
111
+ constructor(sampleMs = PROBE_SAMPLE_MS) {
112
+ this.sampleMs = sampleMs;
113
+ }
114
+ start(now = Date.now()) {
115
+ this.accumulated = 0;
116
+ this.last = now;
117
+ this.timer = setInterval(() => {
118
+ const t = Date.now();
119
+ this.accumulated += Math.max(0, t - this.last - this.sampleMs);
120
+ this.last = t;
121
+ }, this.sampleMs);
122
+ // Never hold the process open for a measurement: the daemon's lifetime is decided elsewhere.
123
+ this.timer.unref?.();
124
+ }
125
+ /** Stop sampling and return the total off-runqueue time observed. Charging the final partial gap
126
+ * matters: under heavy starvation the single longest stall is often the one still in progress
127
+ * when the probe resolves, and dropping it would undercount exactly the worst case. */
128
+ stop(now = Date.now()) {
129
+ if (this.timer !== undefined) {
130
+ clearInterval(this.timer);
131
+ this.timer = undefined;
132
+ }
133
+ this.accumulated += Math.max(0, now - this.last - this.sampleMs);
134
+ return this.accumulated;
135
+ }
136
+ }
137
+ /**
138
+ * Decide whether the broker is GONE, from evidence alone.
139
+ *
140
+ * EXIT REQUIRES BOTH CONJUNCTS, and each exists because the other cannot cover its case:
141
+ *
142
+ * 1. `completedNegatives >= requiredNegatives`, probes that actually RAN and actually said no.
143
+ * Without this, a window that expired while the process was descheduled is read as a server
144
+ * failure, which is the reported defect in its purest form.
145
+ * 2. `msSinceLastReachable - starvedMs > windowMs`, the UNSTARVED part of the window. Without
146
+ * this, a host that schedules the daemon just often enough to fire two probes into a
147
+ * momentarily-saturated loopback exits on two negatives that a healthy host would never have
148
+ * produced.
149
+ *
150
+ * THIS IS NOT A WIDER TIMEOUT. `windowMs` is unchanged from the shipped default; what changed is
151
+ * that the quantity compared against it is now evidence rather than the passage of time. A daemon
152
+ * whose broker is genuinely dead produces completed negatives as fast as it is scheduled, a dead
153
+ * port answers ECONNREFUSED immediately and a blackholed one answers inside the probe deadline ,
154
+ * AND loses its standing connection, so the true-positive path is not delayed by this at all, which
155
+ * is the property that separates a repair from a band-aid. A process so starved that it produces NO
156
+ * completed probe also cannot deliver anything, and ending it would not make Plane-3 more
157
+ * available; it would make the outage permanent for a stock install with no watchdog.
158
+ */
159
+ export function brokerGoneVerdict(e) {
160
+ // Positive evidence inside the window outranks everything: nothing below can be true of a broker
161
+ // that answered us this recently.
162
+ if (e.msSinceLastReachable <= e.windowMs)
163
+ return { exit: false, reason: "reachable" };
164
+ // THE BACKSTOP IS ABSOLUTE, AND IT IS DECIDED FIRST because every clause below it is a reason to
165
+ // keep waiting. This ordering was a review finding, and it is the difference between a bound and
166
+ // a wish: two reviewers independently observed that an open transport ranked ABOVE the backstop,
167
+ // which made the backstop conditional on the very signal most likely to be stale.
168
+ //
169
+ // `transportConnected` is not an observation of the broker. It is this client's cached socket
170
+ // state, and it is only refreshed when nats.js decides the peer is gone. Under a SILENT death ,
171
+ // an OOM kill, a hypervisor pause, a firewall that starts dropping rather than refusing, anything
172
+ // that produces no FIN and no RST, the kernel keeps the connection ESTABLISHED and nats.js does
173
+ // not notice until its own ping cycle expires. With the shipped defaults that is roughly six
174
+ // minutes. Ranked above the backstop, a stale `true` suspended the exit for that entire window no
175
+ // matter how much elapsed time and how many completed refusals had piled up behind it, which is
176
+ // worse than the pre-fix behaviour in exactly the shape an operator would least expect.
177
+ //
178
+ // So the daemon is bounded unconditionally: past the backstop it exits, whatever it believes
179
+ // about its socket. That is the guarantee the coupling exists for, and a guarantee that any
180
+ // single stale flag can defer is not one.
181
+ if (e.msSinceLastReachable > e.backstopMs)
182
+ return { exit: true, reason: "backstop" };
183
+ // AN OPEN TRANSPORT IS ONGOING POSITIVE EVIDENCE, NOT AN EXCUSE FOR ITS ABSENCE, so inside the
184
+ // backstop it still outranks the starvation and evidence clauses below. This daemon's own
185
+ // connection to that same broker is up; it is serving on it; its lease renews across it. A fresh
186
+ // side-probe that cannot complete a handshake to an address we are CURRENTLY CONNECTED TO is a
187
+ // statement about this process's ability to open a new socket, not about the server.
188
+ //
189
+ // Inside the backstop this is still the strongest thing the daemon knows: when the broker really
190
+ // dies the flag goes false, the client detects the loss, the endpoint's status watcher turns it
191
+ // into `transport: connected=false`, and every clause below is live again from that instant. What
192
+ // the flag CANNOT do any more is defer the exit indefinitely while it is stale, because the bound
193
+ // above it has already been decided.
194
+ if (e.transportConnected)
195
+ return { exit: false, reason: "transport-live" };
196
+ // "I could not ask." Credit the lag this process MEASURED on itself before reading the clock as a
197
+ // statement about the server.
198
+ const unstarvedMs = e.msSinceLastReachable - e.starvedMs;
199
+ if (unstarvedMs <= e.windowMs)
200
+ return { exit: false, reason: "starved" };
201
+ // "I asked and was told no", but not yet often enough to be a verdict.
202
+ if (e.completedNegatives < e.requiredNegatives)
203
+ return { exit: false, reason: "insufficient-evidence" };
204
+ return { exit: true, reason: "broker-gone" };
205
+ }
206
+ /**
207
+ * Turn a lease reading into an action.
208
+ *
209
+ * A FAILED RENEW IS A QUESTION, NOT A VERDICT, the same split #1301 made for `down` and the
210
+ * manager's liveness lease already makes. The shipped code exited on ANY renew error, so the
211
+ * measured incident's `wrong last sequence: 0` (the key had EXPIRED during the stall, with nobody
212
+ * else holding it) read identically to a genuine takeover and ended a daemon that was still the
213
+ * only holder there was.
214
+ *
215
+ * The single-holder guarantee is preserved exactly, and by construction rather than by timing:
216
+ * `gone` recovers through an ATOMIC create, so if a replacement did take the slot first, the create
217
+ * fails and that is the genuine loss which exits. `taken` exits immediately. Only "the key is still
218
+ * ours" and "the broker could not be asked" keep serving, and neither of those is a second holder.
219
+ */
220
+ export function leaseAction(reading) {
221
+ switch (reading.kind) {
222
+ // Our own key, at whatever revision the broker says: adopt it and carry on. Covers the renew
223
+ // whose write landed with only its acknowledgement lost.
224
+ case "held": return "keep-serving";
225
+ // Expired or released while we were still here. Put it back; the create arbitrates.
226
+ case "gone": return "reacquire";
227
+ // A different process holds this shard. Exit so the holder stays single.
228
+ case "taken": return "exit";
229
+ // Could not ask. An unanswerable question is not a negative answer; the broker watch above owns
230
+ // the "the server is gone" decision, and it decides on evidence.
231
+ case "unknown": return "keep-serving";
232
+ }
233
+ }
234
+ /**
235
+ * May this process hold Plane-3 bindings on the strength of this reading alone?
236
+ *
237
+ * SEPARATE FROM {@link leaseAction} BECAUSE THEY ANSWER DIFFERENT QUESTIONS, and conflating them was
238
+ * a review finding. `leaseAction` decides whether the PROCESS lives; this decides whether it may
239
+ * SERVE. They agree on `taken` and disagree everywhere else that matters:
240
+ *
241
+ * • `gone` is `reacquire`, the process lives, but it must NOT serve on it. The create has not
242
+ * been attempted yet, and a replacement may already hold the shard; serving through the
243
+ * arbitration is how two daemons end up on one durable.
244
+ * • `unknown` is `keep-serving` for the PROCESS, and a refusal here. Surviving an unanswerable
245
+ * broker is the entire point of #1318, but not being able to ask who owns the shard is not
246
+ * permission to keep acting on it. The daemon stays alive and stays quiet.
247
+ *
248
+ * So this is deliberately the STRICTER of the two: it says yes to exactly one reading, the one that
249
+ * carries positive proof of ownership from the broker. Winning the atomic create is the other way
250
+ * to earn it, and that is not a reading, which is why the daemon re-arms there separately.
251
+ */
252
+ export function mayServeOn(reading) {
253
+ // The only reading that is itself proof: the broker was asked, it answered, and the holder it
254
+ // named is this process.
255
+ return reading.kind === "held";
256
+ }
257
+ /**
258
+ * Measures how late this process's own timers are running.
259
+ *
260
+ * A `setInterval(f, 2000)` that fires 30 seconds after its predecessor did not observe a slow
261
+ * server; it observed a runqueue it was not on. Node hands us no such signal, but the gap between
262
+ * consecutive firings of a timer we own is a direct measurement of it, and it costs one
263
+ * subtraction. Excess over the nominal period is clamped at zero: a timer can fire late, never
264
+ * early, so a negative reading is clock adjustment rather than scheduling and must not CREDIT time
265
+ * the process actually had.
266
+ */
267
+ export class LoopLagMeter {
268
+ intervalMs;
269
+ last;
270
+ accumulated = 0;
271
+ /** Disjoint, sorted [start, end] intervals of measured non-running time in the current window.
272
+ * Bounded: overlapping charges merge, and `reset` clears them on every positive. */
273
+ spans = [];
274
+ constructor(intervalMs = PROBE_INTERVAL_MS) {
275
+ this.intervalMs = intervalMs;
276
+ }
277
+ /** Record a firing at `now`; returns the lag this particular gap contributed.
278
+ *
279
+ * WHAT THE GAP IS CHARGED AS, since this is the half that is easy to misread: the excess of the
280
+ * observed gap over the nominal interval is charged as time THIS PROCESS WAS NOT RUNNING. It is a
281
+ * measurement of the local scheduler, never of the broker - a timer we own fired late, which says
282
+ * the runqueue did not reach us and says nothing at all about the server.
283
+ *
284
+ * It is also NOT the only such charge. {@link credit} adds lateness observed on a probe answer,
285
+ * and the two can describe the same stall or two adjacent ones; nothing in either number reveals
286
+ * which. Neither method deduplicates, deliberately, because suppressing a charge on the guess
287
+ * that it overlaps discards real stalls. The sum is bounded instead, at the point of comparison,
288
+ * by {@link starvedMsWithin}. */
289
+ tick(now) {
290
+ const previous = this.last;
291
+ this.last = now;
292
+ if (previous === undefined)
293
+ return 0; // first firing has no gap to measure
294
+ const lag = Math.max(0, now - previous - this.intervalMs);
295
+ // Charged as a DATED INTERVAL, not a scalar. The stall ended when this firing ran, so it
296
+ // occupied [now - lag, now]. Recording WHEN lets an overlapping probe charge be unioned with it
297
+ // instead of added to it, which is the whole of the double-count repair.
298
+ this.chargeSpan(now - lag, now);
299
+ return lag;
300
+ }
301
+ /** Add lag measured somewhere OTHER than the interval gap, in practice, a probe answer that
302
+ * arrived past its own deadline. Interval gaps alone miss the case where the process IS being
303
+ * scheduled often enough to fire the timer but not often enough to finish a handshake, which is
304
+ * the third of the issue's three mechanisms and the one that produces a completed `false` from a
305
+ * perfectly live server. Clamped at zero for the same reason `tick` is: a measurement that ran
306
+ * backwards must never credit time the process actually had. */
307
+ credit(lagMs, endedAt = Date.now()) {
308
+ // BOTH MEASUREMENTS ARE KEPT. An earlier repair here netted this against the most recent interval
309
+ // gap, on the theory that the two always observe ONE stall. They do not, and a reviewer produced
310
+ // the separating case: over a 4000ms span a timer can fire 2000ms late (a stall happening NOW)
311
+ // while a probe issued at the PREVIOUS tick answers 2500ms late (the stall before it). Netting
312
+ // credited 2500ms there and threw away a 2000ms stall the process had actually measured - which
313
+ // is the very mechanism this method exists for, a process scheduled often enough to fire a timer
314
+ // but not to finish a handshake. I measured my own netting against that case and it produced the
315
+ // lossy answer, so it is gone.
316
+ this.chargeSpan(endedAt - Math.max(0, lagMs), endedAt);
317
+ }
318
+ /** Charge [from, to] as time this process was not running, UNIONED with what is already charged.
319
+ *
320
+ * This is the double-count repair, and it is why both call sites can stay. A stall observed by
321
+ * the interval timer and by a probe answering across it is ONE interval of wall-clock time seen
322
+ * by two instruments. Summing scalars charges it twice (measured: 17s for a 10s stall). Summing
323
+ * INTERVALS cannot, because the union of overlapping intervals is their extent.
324
+ *
325
+ * It also keeps what netting threw away. Two ADJACENT stalls (a timer late by one, a probe from
326
+ * the previous tick late by another) do not overlap, so the union is their sum and both are
327
+ * charged in full. Overlap is decided by the timestamps rather than guessed from the magnitudes,
328
+ * which is the thing neither scalar arithmetic nor a span clamp can do. */
329
+ chargeSpan(from, to) {
330
+ if (!(to > from))
331
+ return; // zero or backwards: never credits time we had
332
+ const merged = [];
333
+ let lo = from, hi = to;
334
+ for (const [s, e] of this.spans) {
335
+ if (e < lo || s > hi) {
336
+ merged.push([s, e]);
337
+ continue;
338
+ } // disjoint: keep as-is
339
+ lo = Math.min(lo, s);
340
+ hi = Math.max(hi, e); // touching: absorb
341
+ }
342
+ merged.push([lo, hi]);
343
+ merged.sort((a, b) => a[0] - b[0]);
344
+ this.spans = merged;
345
+ this.accumulated = merged.reduce((n, [s, e]) => n + (e - s), 0);
346
+ }
347
+ /** Accumulated lag CLAMPED to the span it is about to be compared against.
348
+ *
349
+ * This is the honest way to stop double counting, and the reason the meter cannot do it by
350
+ * arithmetic alone: whether a tick charge and a probe charge describe the same stall or two
351
+ * adjacent ones is not knowable from the two numbers. What IS knowable is that time the process
352
+ * did not have can never exceed time that passed. Overlap is removed by that bound and nothing
353
+ * else is discarded, so the sum stays as informative as its parts allow.
354
+ *
355
+ * Load-bearing, not hygiene: `starvedMs` is SUBTRACTED from elapsed in {@link brokerGoneVerdict}.
356
+ * Unclamped, a stall charged twice drives unstarved time negative, and NO amount of real outage
357
+ * can then clear the evidence clause - a genuinely dead broker stops being detectable by evidence
358
+ * and survives to the backstop instead. Measured: a 30s stall moved an exit from 44s to 62s. */
359
+ starvedMsWithin(spanMs) {
360
+ return Math.min(this.accumulated, Math.max(0, spanMs));
361
+ }
362
+ /** Lag accumulated since the last {@link reset}. */
363
+ get starvedMs() {
364
+ return this.accumulated;
365
+ }
366
+ /** Called when positive evidence arrives: the window restarts, so its lag budget does too. The
367
+ * firing baseline is deliberately KEPT, so a stall spanning a reset is still measured. */
368
+ reset() {
369
+ this.accumulated = 0;
370
+ this.spans = []; // the intervals ARE the accumulator now; clearing one without the other
371
+ // would let a pre-reset stall be re-charged by a later overlapping probe.
372
+ }
373
+ }
374
+ //# sourceMappingURL=watchdog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cotal-ai/delivery",
3
3
  "description": "Cotal delivery daemon: the server-side Plane-3 durable backstop (fan-out writer + trusted reader + membership/ACL authority), a scoped least-privilege NATS client co-located with the broker.",
4
- "version": "0.48.2",
4
+ "version": "0.50.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -18,8 +18,8 @@
18
18
  }
19
19
  },
20
20
  "dependencies": {
21
- "@cotal-ai/core": "0.48.2",
22
- "@cotal-ai/workspace": "0.48.2"
21
+ "@cotal-ai/core": "0.50.0",
22
+ "@cotal-ai/workspace": "0.50.0"
23
23
  },
24
24
  "files": [
25
25
  "dist"