@schoolai/shipyard 3.9.1 → 3.10.0-rc.20260609.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 (61) hide show
  1. package/dist/{auth-UF3MLB77.js → auth-GGM253LQ.js} +3 -3
  2. package/dist/capability-detector-worker.js +9 -9
  3. package/dist/{chunk-LBTLMT5Z.js → chunk-2EQOL57Z.js} +2 -2
  4. package/dist/{chunk-CVMNGYPR.js → chunk-3WEEGJJN.js} +2 -2
  5. package/dist/{chunk-DGX2QR6G.js → chunk-4PBXNWJV.js} +26 -3
  6. package/dist/chunk-4PBXNWJV.js.map +1 -0
  7. package/dist/{chunk-K3QG7S6V.js → chunk-4THTCNVI.js} +4 -4
  8. package/dist/{chunk-K3QG7S6V.js.map → chunk-4THTCNVI.js.map} +1 -1
  9. package/dist/{chunk-7OD3UJUP.js → chunk-6LINHACK.js} +29 -9
  10. package/dist/chunk-6LINHACK.js.map +1 -0
  11. package/dist/{chunk-APVDHUPT.js → chunk-AEUTFH76.js} +28 -4
  12. package/dist/chunk-AEUTFH76.js.map +1 -0
  13. package/dist/{chunk-WYP4NTFE.js → chunk-GM6MH4CD.js} +2 -2
  14. package/dist/{chunk-G7W4GFUC.js → chunk-IJHF4OM4.js} +2 -2
  15. package/dist/{chunk-2I5XDMUD.js → chunk-KRX7OJER.js} +5 -5
  16. package/dist/{chunk-7AB4NH6T.js → chunk-Q2HUVPOL.js} +38 -10
  17. package/dist/chunk-Q2HUVPOL.js.map +1 -0
  18. package/dist/{chunk-3SXIJEPM.js → chunk-QJP7JCIS.js} +31 -13
  19. package/dist/chunk-QJP7JCIS.js.map +1 -0
  20. package/dist/{chunk-MCDZOOAI.js → chunk-RW2OTTUA.js} +50 -15
  21. package/dist/chunk-RW2OTTUA.js.map +1 -0
  22. package/dist/{chunk-AVF7LE7Q.js → chunk-WMOR5Q6C.js} +181 -23
  23. package/dist/chunk-WMOR5Q6C.js.map +1 -0
  24. package/dist/{chunk-MLEGFDFW.js → chunk-Z37T5W6S.js} +13 -2
  25. package/dist/chunk-Z37T5W6S.js.map +1 -0
  26. package/dist/cursor-runner.js +4 -4
  27. package/dist/electron-utility.js +5 -5
  28. package/dist/{git-repo-723BKZIH.js → git-repo-QNGPCJLI.js} +6 -4
  29. package/dist/index.js +8 -8
  30. package/dist/{logger-KICM6IPJ.js → logger-2F3CBS3V.js} +7 -5
  31. package/dist/{login-OKFUEGZW.js → login-NZKH63H7.js} +7 -7
  32. package/dist/{logout-5RGCOHCI.js → logout-HY3MPOY5.js} +5 -5
  33. package/dist/{mcp-servers-CAUI2K5W.js → mcp-servers-ICHOWXZB.js} +4 -4
  34. package/dist/{roi-Q3BQLIO7.js → roi-YM5OOWHG.js} +3 -3
  35. package/dist/{serve-76X367VD.js → serve-FJT6POBH.js} +64212 -62725
  36. package/dist/{serve-76X367VD.js.map → serve-FJT6POBH.js.map} +1 -1
  37. package/dist/{skills-YEZJMAPT.js → skills-W2Y6TWHA.js} +2 -2
  38. package/dist/{start-QOGAKRUP.js → start-MD62XHS6.js} +11 -10
  39. package/dist/{start-QOGAKRUP.js.map → start-MD62XHS6.js.map} +1 -1
  40. package/package.json +1 -1
  41. package/dist/chunk-3SXIJEPM.js.map +0 -1
  42. package/dist/chunk-7AB4NH6T.js.map +0 -1
  43. package/dist/chunk-7OD3UJUP.js.map +0 -1
  44. package/dist/chunk-APVDHUPT.js.map +0 -1
  45. package/dist/chunk-AVF7LE7Q.js.map +0 -1
  46. package/dist/chunk-DGX2QR6G.js.map +0 -1
  47. package/dist/chunk-MCDZOOAI.js.map +0 -1
  48. package/dist/chunk-MLEGFDFW.js.map +0 -1
  49. /package/dist/{auth-UF3MLB77.js.map → auth-GGM253LQ.js.map} +0 -0
  50. /package/dist/{chunk-LBTLMT5Z.js.map → chunk-2EQOL57Z.js.map} +0 -0
  51. /package/dist/{chunk-CVMNGYPR.js.map → chunk-3WEEGJJN.js.map} +0 -0
  52. /package/dist/{chunk-WYP4NTFE.js.map → chunk-GM6MH4CD.js.map} +0 -0
  53. /package/dist/{chunk-G7W4GFUC.js.map → chunk-IJHF4OM4.js.map} +0 -0
  54. /package/dist/{chunk-2I5XDMUD.js.map → chunk-KRX7OJER.js.map} +0 -0
  55. /package/dist/{git-repo-723BKZIH.js.map → git-repo-QNGPCJLI.js.map} +0 -0
  56. /package/dist/{logger-KICM6IPJ.js.map → logger-2F3CBS3V.js.map} +0 -0
  57. /package/dist/{login-OKFUEGZW.js.map → login-NZKH63H7.js.map} +0 -0
  58. /package/dist/{logout-5RGCOHCI.js.map → logout-HY3MPOY5.js.map} +0 -0
  59. /package/dist/{mcp-servers-CAUI2K5W.js.map → mcp-servers-ICHOWXZB.js.map} +0 -0
  60. /package/dist/{roi-Q3BQLIO7.js.map → roi-YM5OOWHG.js.map} +0 -0
  61. /package/dist/{skills-YEZJMAPT.js.map → skills-W2Y6TWHA.js.map} +0 -0
@@ -2,13 +2,13 @@
2
2
  import {
3
3
  PUBLISHED_PREVIEW_KINDS,
4
4
  PUBLISH_TTL_CHOICES
5
- } from "./chunk-DGX2QR6G.js";
5
+ } from "./chunk-4PBXNWJV.js";
6
6
  import {
7
7
  assertNever
8
8
  } from "./chunk-X3MULCV5.js";
9
9
  import {
10
10
  logger
11
- } from "./chunk-3SXIJEPM.js";
11
+ } from "./chunk-QJP7JCIS.js";
12
12
  import {
13
13
  external_exports
14
14
  } from "./chunk-CNR7O5YH.js";
@@ -6417,6 +6417,14 @@ var CanvasRepository = class {
6417
6417
  #epoch;
6418
6418
  #storageAdapter;
6419
6419
  #canvasDocs = /* @__PURE__ */ new Map();
6420
+ /**
6421
+ * Live Loro-subscription refcount per task. `subscribe()` increments; the
6422
+ * returned unsubscribe decrements (idempotently). `invalidate()` is a no-op
6423
+ * while a task's count is > 0 because evicting a doc out from under a live
6424
+ * subscription orphans the subscriber's callback against a dropped LoroDoc —
6425
+ * a silent data-loss bug. The idle sweep only evicts zero-refcount docs.
6426
+ */
6427
+ #subscriberCounts = /* @__PURE__ */ new Map();
6420
6428
  constructor(repo, epoch, storageAdapter) {
6421
6429
  this.#repo = repo;
6422
6430
  this.#epoch = epoch;
@@ -6496,18 +6504,70 @@ var CanvasRepository = class {
6496
6504
  /** Subscribe to CRDT changes on a canvas document. Returns unsubscribe function. */
6497
6505
  subscribe(taskId, callback) {
6498
6506
  const doc2 = this.getOrCreateCanvasDoc(taskId);
6499
- return subscribe(doc2, callback);
6507
+ const unsubscribe = subscribe(doc2, callback);
6508
+ this.#subscriberCounts.set(taskId, (this.#subscriberCounts.get(taskId) ?? 0) + 1);
6509
+ let released = false;
6510
+ return () => {
6511
+ if (released) return;
6512
+ released = true;
6513
+ unsubscribe();
6514
+ this.#releaseSubscriber(taskId);
6515
+ };
6516
+ }
6517
+ #releaseSubscriber(taskId) {
6518
+ const next = (this.#subscriberCounts.get(taskId) ?? 0) - 1;
6519
+ if (next <= 0) this.#subscriberCounts.delete(taskId);
6520
+ else this.#subscriberCounts.set(taskId, next);
6521
+ }
6522
+ /** Number of live Loro subscriptions held on this task's canvas doc. */
6523
+ subscriberCount(taskId) {
6524
+ return this.#subscriberCounts.get(taskId) ?? 0;
6500
6525
  }
6501
6526
  /**
6502
6527
  * Drop the cached `Doc` reference for this task so the next
6503
- * `getOrCreateCanvasDoc(taskId)` call returns a fresh doc from the
6504
- * underlying repo. Called by the corruption recovery service after
6505
- * `repo.delete(docId)` evicts the poisoned doc from the synchronizer
6506
- * model without this, callers would keep getting back the dangling
6507
- * reference to the now-evicted (and still-poisoned) LoroDoc instance.
6528
+ * `getOrCreateCanvasDoc(taskId)` returns a fresh doc from the repo.
6529
+ *
6530
+ * Refcount-gated: a no-op (returns false) while a live Loro subscription
6531
+ * is held on the doc, because evicting it would orphan the subscriber's
6532
+ * callback against a dropped LoroDoc. The idle-sweep leak fix and
6533
+ * `removeTask` both route through here so neither can drop a doc a
6534
+ * resolver/viz-watcher is still reading. Returns true if the doc was
6535
+ * evicted (or was already absent).
6536
+ *
6537
+ * For the corruption-recovery path — where the doc is poisoned and its
6538
+ * subscription is already dead — use `forceInvalidate` instead.
6508
6539
  */
6509
6540
  invalidate(taskId) {
6541
+ if (this.subscriberCount(taskId) > 0) return false;
6542
+ this.#evict(taskId);
6543
+ return true;
6544
+ }
6545
+ /**
6546
+ * Unconditionally drop the cached `Doc` reference, ignoring the
6547
+ * subscription refcount. Used only by the corruption-recovery service:
6548
+ * a poisoned doc's wasm methods throw, so its subscription is already
6549
+ * dead and pinning the reference would re-hand the poisoned doc on the
6550
+ * next `repo.get`. The refcount gate on `invalidate` must NOT block that
6551
+ * recovery — hence this escape hatch.
6552
+ */
6553
+ forceInvalidate(taskId) {
6554
+ this.#evict(taskId);
6555
+ }
6556
+ /**
6557
+ * Eviction's remote-safety depends on every personal/canvas repo using
6558
+ * `deletion: () => false`: `repo.delete` fans out a `channel/delete-request`
6559
+ * that peers currently ignore, so dropping the local cache never propagates
6560
+ * as a remote delete. If any repo's `deletion` ever becomes permissive,
6561
+ * eviction here would tombstone the doc on peers — re-audit this call site.
6562
+ */
6563
+ #evict(taskId) {
6510
6564
  this.#canvasDocs.delete(taskId);
6565
+ this.#subscriberCounts.delete(taskId);
6566
+ const repoDelete = this.#repo.delete;
6567
+ if (repoDelete) {
6568
+ const docId = buildCanvasDocId(taskId, this.#epoch);
6569
+ void repoDelete.call(this.#repo, docId);
6570
+ }
6511
6571
  }
6512
6572
  /**
6513
6573
  * Number of cached canvas docs. Exposed for health metrics — see
@@ -6517,8 +6577,23 @@ var CanvasRepository = class {
6517
6577
  get cachedDocCount() {
6518
6578
  return this.#canvasDocs.size;
6519
6579
  }
6580
+ /** Task ids with a currently-cached canvas doc. Drives the idle sweep's candidate set. */
6581
+ cachedTaskIds() {
6582
+ return [...this.#canvasDocs.keys()];
6583
+ }
6584
+ /**
6585
+ * The repo-level docId this task's canvas maps to. Lets the daemon-side idle
6586
+ * sweep ask the personal repo's synchronizer "is any peer syncing this doc?"
6587
+ * (a browser viewer holds no daemon-side `subscribe()` refcount — it syncs
6588
+ * over WebRTC — so a doc a viewer is editing must be excluded from eviction
6589
+ * by its live peer subscription, not by the refcount).
6590
+ */
6591
+ docIdFor(taskId) {
6592
+ return buildCanvasDocId(taskId, this.#epoch);
6593
+ }
6520
6594
  dispose() {
6521
6595
  this.#canvasDocs.clear();
6596
+ this.#subscriberCounts.clear();
6522
6597
  }
6523
6598
  };
6524
6599
 
@@ -17195,6 +17270,14 @@ var PlanRepository = class {
17195
17270
  #epoch;
17196
17271
  #planDocs = /* @__PURE__ */ new Map();
17197
17272
  #contentBridge;
17273
+ /**
17274
+ * Live Loro-subscription refcount per task. `subscribe()` increments; the
17275
+ * returned unsubscribe decrements (idempotently). `invalidate()` is a no-op
17276
+ * while a task's count is > 0 because evicting a doc out from under a live
17277
+ * subscription orphans the subscriber's callback against a dropped LoroDoc —
17278
+ * a silent data-loss bug. The idle sweep only evicts zero-refcount docs.
17279
+ */
17280
+ #subscriberCounts = /* @__PURE__ */ new Map();
17198
17281
  constructor(repo, epoch, contentBridge) {
17199
17282
  this.#repo = repo;
17200
17283
  this.#epoch = epoch;
@@ -17223,7 +17306,7 @@ var PlanRepository = class {
17223
17306
  try {
17224
17307
  this.#contentBridge.writeMarkdown(doc2, markdown);
17225
17308
  } catch (err) {
17226
- this.invalidate(taskId);
17309
+ this.forceInvalidate(taskId);
17227
17310
  throw err;
17228
17311
  }
17229
17312
  }
@@ -17243,26 +17326,76 @@ var PlanRepository = class {
17243
17326
  /** Subscribe to CRDT changes on a plan document. Returns unsubscribe function. */
17244
17327
  subscribe(taskId, callback) {
17245
17328
  const doc2 = this.getOrCreatePlanDoc(taskId);
17246
- return subscribe(doc2, callback);
17329
+ const unsubscribe = subscribe(doc2, callback);
17330
+ this.#subscriberCounts.set(taskId, (this.#subscriberCounts.get(taskId) ?? 0) + 1);
17331
+ let released = false;
17332
+ return () => {
17333
+ if (released) return;
17334
+ released = true;
17335
+ unsubscribe();
17336
+ this.#releaseSubscriber(taskId);
17337
+ };
17338
+ }
17339
+ #releaseSubscriber(taskId) {
17340
+ const next = (this.#subscriberCounts.get(taskId) ?? 0) - 1;
17341
+ if (next <= 0) this.#subscriberCounts.delete(taskId);
17342
+ else this.#subscriberCounts.set(taskId, next);
17343
+ }
17344
+ /** Number of live Loro subscriptions held on this task's plan doc. */
17345
+ subscriberCount(taskId) {
17346
+ return this.#subscriberCounts.get(taskId) ?? 0;
17247
17347
  }
17248
17348
  /**
17249
17349
  * Drop the cached `Doc` reference for this task so the next
17250
- * `getOrCreatePlanDoc(taskId)` call returns a fresh doc.
17350
+ * `getOrCreatePlanDoc(taskId)` call returns a fresh doc from disk.
17251
17351
  *
17252
- * Local map clear is necessary but not sufficient: loro-repo maintains
17253
- * its own per-docId cache and `repo.get(docId, schema)` returns the
17254
- * same `RepoDoc` until it is also evicted there. When the repo
17255
- * implementation provides `delete`, fire it (no await kept sync to
17256
- * preserve the existing call contract; microtask ordering guarantees
17257
- * eviction completes before the next `get` reaches the synchronizer).
17352
+ * Refcount-gated: a no-op (returns false) while a live Loro subscription
17353
+ * is held on the doc, because evicting it would orphan the subscriber's
17354
+ * callback against a dropped LoroDoc. The idle-sweep leak fix and
17355
+ * `removeTask` both route through here so neither can drop a doc a
17356
+ * resolver/file-bridge is still reading. Returns true if the doc was
17357
+ * evicted (or was already absent).
17258
17358
  *
17259
- * Without the repo-side eviction, the wasm-panic recovery path in
17260
- * `updateContent` re-hands the poisoned doc on the next write,
17261
- * triggering the same panic again — the 47s detection wait reappears
17262
- * on every subsequent attempt.
17359
+ * For the corruption-recovery path — where the doc is poisoned and its
17360
+ * subscription is already dead use `forceInvalidate` instead.
17263
17361
  */
17264
17362
  invalidate(taskId) {
17363
+ if (this.subscriberCount(taskId) > 0) return false;
17364
+ this.#evict(taskId);
17365
+ return true;
17366
+ }
17367
+ /**
17368
+ * Unconditionally evict, ignoring the subscription refcount. Used by the
17369
+ * corruption-recovery service and the `updateContent` bridge-throw path:
17370
+ * a poisoned doc's wasm methods throw, so its subscription is already dead
17371
+ * and pinning the reference would re-hand the poisoned doc on the next
17372
+ * `repo.get` — re-triggering the panic. The refcount gate must NOT block
17373
+ * that recovery.
17374
+ */
17375
+ forceInvalidate(taskId) {
17376
+ this.#evict(taskId);
17377
+ }
17378
+ /**
17379
+ * Drop the local Map ref AND loro-repo's `#docCache` + synchronizer entry.
17380
+ *
17381
+ * Local map clear is necessary but not sufficient: loro-repo maintains its
17382
+ * own per-docId cache and `repo.get(docId, schema)` returns the same
17383
+ * `RepoDoc` until it is also evicted there — so without `repo.delete` the
17384
+ * `LoroDoc` is never released and its wasm linear-memory leaks. When the
17385
+ * repo implementation provides `delete`, fire it (no await — kept sync to
17386
+ * preserve the call contract; microtask ordering guarantees eviction
17387
+ * completes before the next `get` reaches the synchronizer). The persisted
17388
+ * on-disk chunks are NOT removed, so the next `get` rehydrates from disk.
17389
+ *
17390
+ * Remote-safety depends on every personal/plan repo using
17391
+ * `deletion: () => false`: `repo.delete` fans out a `channel/delete-request`
17392
+ * that peers currently ignore, so dropping the local cache never propagates
17393
+ * as a remote delete. If any repo's `deletion` ever becomes permissive,
17394
+ * eviction here would tombstone the doc on peers — re-audit this call site.
17395
+ */
17396
+ #evict(taskId) {
17265
17397
  this.#planDocs.delete(taskId);
17398
+ this.#subscriberCounts.delete(taskId);
17266
17399
  const repoDelete = this.#repo.delete;
17267
17400
  if (repoDelete) {
17268
17401
  const docId = buildPlanDocId(taskId, this.#epoch);
@@ -17278,8 +17411,23 @@ var PlanRepository = class {
17278
17411
  get cachedDocCount() {
17279
17412
  return this.#planDocs.size;
17280
17413
  }
17414
+ /** Task ids with a currently-cached plan doc. Drives the idle sweep's candidate set. */
17415
+ cachedTaskIds() {
17416
+ return [...this.#planDocs.keys()];
17417
+ }
17418
+ /**
17419
+ * The repo-level docId this task's plan maps to. Lets the daemon-side idle
17420
+ * sweep ask the personal repo's synchronizer "is any peer syncing this doc?"
17421
+ * (a browser viewer holds no daemon-side `subscribe()` refcount — it syncs
17422
+ * over WebRTC — so a doc a viewer is editing must be excluded from eviction
17423
+ * by its live peer subscription, not by the refcount).
17424
+ */
17425
+ docIdFor(taskId) {
17426
+ return buildPlanDocId(taskId, this.#epoch);
17427
+ }
17281
17428
  dispose() {
17282
17429
  this.#planDocs.clear();
17430
+ this.#subscriberCounts.clear();
17283
17431
  }
17284
17432
  };
17285
17433
 
@@ -21920,7 +22068,17 @@ var DaemonToBrowserControlMessageSchema = external_exports.discriminatedUnion("t
21920
22068
  connectionStatus: external_exports.enum(["connected", "failed", "needs-auth", "connecting", "disabled"]).nullable(),
21921
22069
  terminalReason: external_exports.literal("unsupported").optional(),
21922
22070
  error: external_exports.string().optional(),
21923
- toolCount: external_exports.number().optional()
22071
+ toolCount: external_exports.number().optional(),
22072
+ /**
22073
+ * PROTOCOL_VERSION 130 (additive): data-driven error surface. `errorType`
22074
+ * is the classified backend/gateway code (raw `error` still preserved,
22075
+ * unmasked per #4475); `remediation` is an optional generic CTA the picker
22076
+ * renders without per-connector branching; `sources` is the dedup
22077
+ * provenance set. All optional — pre-v130 browsers drop them, MIN stays 129.
22078
+ */
22079
+ errorType: external_exports.string().optional(),
22080
+ remediation: external_exports.object({ message: external_exports.string(), url: external_exports.string().optional() }).optional(),
22081
+ sources: external_exports.array(external_exports.string()).optional()
21924
22082
  })
21925
22083
  )
21926
22084
  }),
@@ -27761,4 +27919,4 @@ export {
27761
27919
  toRecord,
27762
27920
  buildCursorUserPrompt
27763
27921
  };
27764
- //# sourceMappingURL=chunk-AVF7LE7Q.js.map
27922
+ //# sourceMappingURL=chunk-WMOR5Q6C.js.map