@brightbeamai/chap-coordinator 0.3.1 → 0.3.3

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/dist/index.js CHANGED
@@ -654,21 +654,211 @@ function diffJsonPatch(from, to, basePath = "") {
654
654
  }
655
655
 
656
656
  // src/storage/store.ts
657
+ var COLLECTIONS = [
658
+ "members",
659
+ "tasks",
660
+ "overrides",
661
+ "whispers",
662
+ "deliberations",
663
+ "handoffs",
664
+ "snapshots",
665
+ "route_decisions"
666
+ ];
667
+ var IDEMPOTENCY_KEYS = "idempotency_keys";
668
+ var StoreConflictError = class extends Error {
669
+ workspace;
670
+ /** Audit entries the store holds. */
671
+ held;
672
+ /** Audit entries the change expected. */
673
+ base;
674
+ constructor(workspace, held, base) {
675
+ super(
676
+ `The store holds ${held} audit entries for workspace ${workspace} and this write expected ${base}, so nothing was written. Another coordinator may be writing to this workspace, the store may have been changed outside this coordinator, or this coordinator could not load the workspace when it started. Restart this coordinator to reload.`
677
+ );
678
+ this.name = "StoreConflictError";
679
+ this.workspace = workspace;
680
+ this.held = held;
681
+ this.base = base;
682
+ }
683
+ /**
684
+ * Matched by name, so `instanceof` holds across the package's entry points,
685
+ * each of which is bundled with its own copy of this class.
686
+ */
687
+ static [Symbol.hasInstance](value) {
688
+ return value instanceof Error && value.name === "StoreConflictError";
689
+ }
690
+ };
691
+ function isCollection(name) {
692
+ return COLLECTIONS.includes(name);
693
+ }
694
+ function objectKey(collection, item) {
695
+ if (item === null || typeof item !== "object") return void 0;
696
+ const o = item;
697
+ const key = collection === "members" ? o.uri ?? o.id : o.id ?? o.uri;
698
+ return typeof key === "string" ? key : void 0;
699
+ }
700
+ function keyed(collection, value) {
701
+ const out = {};
702
+ if (Array.isArray(value)) {
703
+ for (const item of value) {
704
+ const key = objectKey(collection, item);
705
+ if (key !== void 0) out[key] = item;
706
+ }
707
+ } else if (value !== null && typeof value === "object") {
708
+ Object.assign(out, value);
709
+ }
710
+ return out;
711
+ }
712
+ function changeFromRecord(record) {
713
+ const data = record.data !== null && typeof record.data === "object" ? record.data : {};
714
+ const meta = {};
715
+ const objects = {};
716
+ let entries = [];
717
+ for (const [name, value] of Object.entries(data)) {
718
+ if (name === "audit") entries = Array.isArray(value) ? value : [];
719
+ else if (name === IDEMPOTENCY_KEYS || isCollection(name)) objects[name] = keyed(name, value);
720
+ else meta[name] = value;
721
+ }
722
+ if (meta.id === void 0) meta.id = record.id;
723
+ return {
724
+ id: record.id,
725
+ base: 0,
726
+ version: record.version,
727
+ updated_at: record.updated_at,
728
+ meta,
729
+ entries,
730
+ objects,
731
+ replace: true
732
+ };
733
+ }
734
+ function assembleSnapshot(meta, entries, objects) {
735
+ const data = { ...meta };
736
+ for (const name of COLLECTIONS) data[name] = [];
737
+ const keys = {};
738
+ data[IDEMPOTENCY_KEYS] = keys;
739
+ for (const [collection, key, value] of objects) {
740
+ if (collection === IDEMPOTENCY_KEYS) {
741
+ keys[key] = value;
742
+ } else {
743
+ const list = Array.isArray(data[collection]) ? data[collection] : data[collection] = [];
744
+ list.push(value);
745
+ }
746
+ }
747
+ data.audit = entries;
748
+ return data;
749
+ }
657
750
  var MemoryStore = class {
658
- records = /* @__PURE__ */ new Map();
751
+ held = /* @__PURE__ */ new Map();
659
752
  load() {
660
- return Array.from(this.records.values());
753
+ const out = [];
754
+ for (const [id, h] of this.held) {
755
+ const objects = [];
756
+ for (const [collection, items] of h.objects) {
757
+ for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);
758
+ }
759
+ out.push({
760
+ id,
761
+ data: assembleSnapshot(JSON.parse(h.meta), h.entries.map((e) => JSON.parse(e)), objects),
762
+ version: h.version,
763
+ updated_at: h.updated_at
764
+ });
765
+ }
766
+ return out;
661
767
  }
662
768
  save(record) {
663
- this.records.set(record.id, record);
769
+ this.apply(changeFromRecord(record));
770
+ }
771
+ apply(change) {
772
+ const meta = JSON.stringify(change.meta);
773
+ const entries = change.entries.map((e) => JSON.stringify(e));
774
+ const objects = Object.entries(change.objects).map(([collection, items]) => [collection, Object.entries(items).map(([key, value]) => [key, value === null || value === void 0 ? null : JSON.stringify(value)])]);
775
+ let h = this.held.get(change.id);
776
+ if (!change.replace) {
777
+ const holds = h ? h.entries.length : 0;
778
+ if (holds !== change.base) throw new StoreConflictError(change.id, holds, change.base);
779
+ }
780
+ if (!h || change.replace) {
781
+ h = { meta: "{}", version: 0, updated_at: "", entries: [], objects: /* @__PURE__ */ new Map() };
782
+ }
783
+ for (const entry of entries) h.entries.push(entry);
784
+ for (const [collection, items] of objects) {
785
+ let held = h.objects.get(collection);
786
+ if (!held) h.objects.set(collection, held = /* @__PURE__ */ new Map());
787
+ for (const [key, value] of items) {
788
+ if (value === null) held.delete(key);
789
+ else held.set(key, value);
790
+ }
791
+ }
792
+ h.meta = meta;
793
+ h.version = change.version;
794
+ h.updated_at = change.updated_at;
795
+ this.held.set(change.id, h);
664
796
  }
665
797
  delete(id) {
666
- this.records.delete(id);
798
+ this.held.delete(id);
667
799
  }
668
- close() {
669
- this.records.clear();
800
+ };
801
+
802
+ // src/tracked.ts
803
+ var TrackedMap = class extends Map {
804
+ /** Keys whose objects may have changed since the last write. */
805
+ dirty = /* @__PURE__ */ new Set();
806
+ tracker;
807
+ constructor(tracker) {
808
+ super();
809
+ this.tracker = tracker;
810
+ }
811
+ reading() {
812
+ return this.tracker.enabled && this.tracker.depth > 0;
813
+ }
814
+ touchAll() {
815
+ if (this.reading()) for (const key of super.keys()) this.dirty.add(key);
816
+ }
817
+ get(key) {
818
+ const value = super.get(key);
819
+ if (value !== void 0 && this.reading()) this.dirty.add(key);
820
+ return value;
821
+ }
822
+ set(key, value) {
823
+ if (this.tracker.enabled) this.dirty.add(key);
824
+ return super.set(key, value);
825
+ }
826
+ delete(key) {
827
+ const had = super.delete(key);
828
+ if (had && this.tracker.enabled) this.dirty.add(key);
829
+ return had;
830
+ }
831
+ clear() {
832
+ if (this.tracker.enabled) for (const key of super.keys()) this.dirty.add(key);
833
+ super.clear();
834
+ }
835
+ values() {
836
+ this.touchAll();
837
+ return super.values();
838
+ }
839
+ entries() {
840
+ this.touchAll();
841
+ return super.entries();
842
+ }
843
+ [Symbol.iterator]() {
844
+ this.touchAll();
845
+ return super[Symbol.iterator]();
846
+ }
847
+ forEach(callback, thisArg) {
848
+ this.touchAll();
849
+ super.forEach(callback, thisArg);
850
+ }
851
+ /** Read one value without recording it. */
852
+ peek(key) {
853
+ return super.get(key);
670
854
  }
671
855
  };
856
+ function rawValues(map) {
857
+ return Map.prototype.values.call(map);
858
+ }
859
+ function rawEntries(map) {
860
+ return Map.prototype.entries.call(map);
861
+ }
672
862
 
673
863
  // src/api.ts
674
864
  var CoordinatorError = class extends Error {
@@ -994,7 +1184,7 @@ function registerControl(coord) {
994
1184
  const include = Array.isArray(p.include) ? [...p.include] : ["members", "open_tasks", "mode_ceiling"];
995
1185
  const state = {};
996
1186
  if (include.includes("members")) {
997
- const members = Array.from(ws.members.values()).map((m) => ({
1187
+ const members = Array.from(rawValues(ws.members)).map((m) => ({
998
1188
  uri: m.uri,
999
1189
  type: m.type,
1000
1190
  ...m.role ? { role: m.role } : {},
@@ -1003,7 +1193,7 @@ function registerControl(coord) {
1003
1193
  if (members.length) state.members = members;
1004
1194
  }
1005
1195
  if (include.includes("open_tasks")) {
1006
- const tasks = Array.from(ws.tasks.values()).filter((t) => t.state !== "completed" && t.state !== "declined" && t.state !== "cancelled" && t.state !== "superseded").map((t) => ({
1196
+ const tasks = Array.from(rawValues(ws.tasks)).filter((t) => t.state !== "completed" && t.state !== "declined" && t.state !== "cancelled" && t.state !== "superseded").map((t) => ({
1007
1197
  id: t.id,
1008
1198
  kind: t.kind,
1009
1199
  state: t.state,
@@ -1872,6 +2062,7 @@ function registerWhisper(coord) {
1872
2062
  if (cutoff < deadline) continue;
1873
2063
  prompt.state = "lapsed";
1874
2064
  prompt.default_applied = prompt.default_if_lapsed;
2065
+ ws.whispers.set(prompt.id, prompt);
1875
2066
  const notify = {
1876
2067
  jsonrpc: "2.0",
1877
2068
  method: "notify.message",
@@ -1994,6 +2185,16 @@ function indexSigned(index, entry) {
1994
2185
  if (entry.outcome != null) index.refused.set(digest, { seq: entry.seq, code: entry.outcome.code });
1995
2186
  else index.accepted.add(digest);
1996
2187
  }
2188
+ function isThenable(value) {
2189
+ return typeof value?.then === "function";
2190
+ }
2191
+ function ownCopy(value) {
2192
+ try {
2193
+ return structuredClone(value);
2194
+ } catch {
2195
+ return value;
2196
+ }
2197
+ }
1997
2198
  var REVIEW_REQUEST_STOPPED = ["cancelled", "superseded", "paused"];
1998
2199
  var Coordinator = class {
1999
2200
  workspaces = /* @__PURE__ */ new Map();
@@ -2002,6 +2203,14 @@ var Coordinator = class {
2002
2203
  store;
2003
2204
  wsVersions = /* @__PURE__ */ new Map();
2004
2205
  saveChains = /* @__PURE__ */ new Map();
2206
+ /** Records which objects change, for stores that implement `apply`. */
2207
+ tracker;
2208
+ /** What the store holds of each workspace, as far as this instance knows. */
2209
+ persisted = /* @__PURE__ */ new WeakMap();
2210
+ /** Workspaces with entries not yet written in the call being handled. */
2211
+ unflushed = /* @__PURE__ */ new Set();
2212
+ /** Workspaces with a write queued behind an async write in flight. */
2213
+ flushQueued = /* @__PURE__ */ new Set();
2005
2214
  clockMs;
2006
2215
  frozenNow;
2007
2216
  auditListeners = [];
@@ -2022,6 +2231,7 @@ var Coordinator = class {
2022
2231
  constructor(options = {}) {
2023
2232
  this.options = options;
2024
2233
  this.store = options.store ?? new MemoryStore();
2234
+ this.tracker = { enabled: typeof this.store.apply === "function", depth: 0 };
2025
2235
  this.ids = new IdFactory(!!options.deterministicIds, 0n, 17e11);
2026
2236
  if (options.deterministicClock) this.clockMs = 17e11;
2027
2237
  if (options.onAudit) this.auditListeners.push(options.onAudit);
@@ -2037,7 +2247,7 @@ var Coordinator = class {
2037
2247
  });
2038
2248
  try {
2039
2249
  const loaded = this.store.load();
2040
- if (!(loaded instanceof Promise)) {
2250
+ if (!isThenable(loaded)) {
2041
2251
  this.applyRecords(loaded);
2042
2252
  }
2043
2253
  } catch {
@@ -2055,18 +2265,181 @@ var Coordinator = class {
2055
2265
  this.wsVersions.clear();
2056
2266
  this.restore(records.map((r) => r.data));
2057
2267
  for (const r of records) this.wsVersions.set(r.id, r.version);
2268
+ for (const ws of this.workspaces.values()) this.markWritten(ws);
2058
2269
  }
2059
- /** Persist the current snapshot of one workspace to the store. */
2270
+ /** A map for a workspace collection, tracked for stores that implement `apply`. */
2271
+ newMap() {
2272
+ return new TrackedMap(this.tracker);
2273
+ }
2274
+ /** Record that the store holds this workspace exactly as it is in memory. */
2275
+ markWritten(ws) {
2276
+ for (const name of COLLECTIONS) {
2277
+ const map = ws[name];
2278
+ if (map instanceof TrackedMap) map.dirty.clear();
2279
+ }
2280
+ this.persisted.set(ws, { seq: ws.audit.length, replace: false, keys: /* @__PURE__ */ new Set() });
2281
+ }
2282
+ /**
2283
+ * What the store holds of a workspace. A workspace this instance did not
2284
+ * create or load, such as one an application put in `workspaces` itself,
2285
+ * or one whose maps an application replaced, is tracked from here on and
2286
+ * written whole once.
2287
+ */
2288
+ persistedOf(ws) {
2289
+ let p = this.persisted.get(ws);
2290
+ let untracked = false;
2291
+ for (const name of COLLECTIONS) {
2292
+ const map = ws[name];
2293
+ if (map instanceof TrackedMap && map.tracker === this.tracker) continue;
2294
+ untracked = true;
2295
+ const tracked = new TrackedMap(this.tracker);
2296
+ for (const [key, value] of Map.prototype.entries.call(map)) {
2297
+ tracked.set(key, value);
2298
+ }
2299
+ ws[name] = tracked;
2300
+ }
2301
+ if (!p || untracked) {
2302
+ p = { seq: p?.seq ?? 0, replace: true, keys: /* @__PURE__ */ new Set(), updatedAt: p?.updatedAt };
2303
+ this.persisted.set(ws, p);
2304
+ }
2305
+ return p;
2306
+ }
2307
+ /** Record that a task.create idempotency key was written or removed. */
2308
+ markKey(ws, key) {
2309
+ if (this.tracker.enabled) this.persistedOf(ws).keys.add(key);
2310
+ }
2311
+ /** After an entry is appended: write now, or when the call being handled ends. */
2060
2312
  persist(ws) {
2313
+ if (!this.tracker.enabled) {
2314
+ this.persistSnapshot(ws);
2315
+ return;
2316
+ }
2317
+ this.persistedOf(ws).updatedAt = this.now();
2318
+ if (this.tracker.depth > 0) this.unflushed.add(ws);
2319
+ else this.flush(ws);
2320
+ }
2321
+ flushUnflushed() {
2322
+ if (!this.unflushed.size) return;
2323
+ const list = [...this.unflushed];
2324
+ this.unflushed.clear();
2325
+ for (const ws of list) this.flush(ws);
2326
+ }
2327
+ /** Write what changed in one workspace since its last write. */
2328
+ flush(ws) {
2329
+ const pending = this.saveChains.get(ws.id);
2330
+ if (pending) {
2331
+ if (this.flushQueued.has(ws)) return;
2332
+ this.flushQueued.add(ws);
2333
+ this.chainWrite(ws.id, pending.then(() => {
2334
+ this.flushQueued.delete(ws);
2335
+ return this.writeChange(ws);
2336
+ }));
2337
+ return;
2338
+ }
2339
+ const result = this.writeChange(ws);
2340
+ if (result) this.chainWrite(ws.id, result);
2341
+ }
2342
+ chainWrite(id, work) {
2343
+ const guarded = work.catch(() => void 0);
2344
+ this.saveChains.set(id, guarded);
2345
+ void guarded.then(() => {
2346
+ if (this.saveChains.get(id) === guarded) this.saveChains.delete(id);
2347
+ });
2348
+ }
2349
+ writeChange(ws) {
2350
+ const p = this.persistedOf(ws);
2351
+ let change;
2352
+ try {
2353
+ change = this.takeChange(ws, p);
2354
+ } catch (err) {
2355
+ p.replace = true;
2356
+ this.reportStoreError(err, { id: ws.id, version: ws.audit.length });
2357
+ return;
2358
+ }
2359
+ if (!change) return;
2360
+ const taken = change;
2361
+ const done = () => {
2362
+ p.seq = taken.version;
2363
+ if (taken.replace) p.replace = false;
2364
+ };
2365
+ const failed = (err) => {
2366
+ this.giveBack(ws, p, taken);
2367
+ this.reportStoreError(err, { id: taken.id, version: taken.version });
2368
+ };
2369
+ let result;
2370
+ try {
2371
+ result = this.store.apply(taken);
2372
+ } catch (err) {
2373
+ failed(err);
2374
+ return;
2375
+ }
2376
+ if (isThenable(result)) return Promise.resolve(result).then(done, failed);
2377
+ done();
2378
+ }
2379
+ /** The change since the last write, taking the recorded keys with it. */
2380
+ takeChange(ws, p) {
2381
+ const replace = p.replace;
2382
+ const objects = {};
2383
+ for (const name of COLLECTIONS) {
2384
+ const map = ws[name];
2385
+ if (replace) {
2386
+ const all = {};
2387
+ for (const [key, value] of Map.prototype.entries.call(map)) {
2388
+ all[key] = value;
2389
+ }
2390
+ objects[name] = all;
2391
+ } else if (map.dirty.size) {
2392
+ const changed = {};
2393
+ for (const key of map.dirty) changed[key] = map.peek(key) ?? null;
2394
+ objects[name] = changed;
2395
+ }
2396
+ map.dirty.clear();
2397
+ }
2398
+ if (replace) {
2399
+ objects[IDEMPOTENCY_KEYS] = { ...ws.idempotency_keys };
2400
+ } else if (p.keys.size) {
2401
+ const changed = {};
2402
+ for (const key of p.keys) changed[key] = ws.idempotency_keys[key] ?? null;
2403
+ objects[IDEMPOTENCY_KEYS] = changed;
2404
+ }
2405
+ p.keys.clear();
2406
+ const entries = ws.audit.slice(replace ? 0 : p.seq);
2407
+ if (!replace && !entries.length && !Object.keys(objects).length) return void 0;
2408
+ return {
2409
+ id: ws.id,
2410
+ base: replace ? 0 : p.seq,
2411
+ version: ws.audit.length,
2412
+ updated_at: p.updatedAt ?? ws.audit[ws.audit.length - 1]?.arrived ?? ws.created,
2413
+ meta: this.metaOf(ws),
2414
+ entries,
2415
+ objects,
2416
+ ...replace ? { replace: true } : {}
2417
+ };
2418
+ }
2419
+ /** A failed write's keys go back, so the next write carries them again. */
2420
+ giveBack(ws, p, change) {
2421
+ if (change.replace) {
2422
+ p.replace = true;
2423
+ return;
2424
+ }
2425
+ for (const [name, items] of Object.entries(change.objects)) {
2426
+ if (name === IDEMPOTENCY_KEYS) {
2427
+ for (const key of Object.keys(items)) p.keys.add(key);
2428
+ continue;
2429
+ }
2430
+ const map = ws[name];
2431
+ if (map instanceof TrackedMap) for (const key of Object.keys(items)) map.dirty.add(key);
2432
+ }
2433
+ }
2434
+ /** Persist the whole workspace, for a store that implements only `save`. */
2435
+ persistSnapshot(ws) {
2061
2436
  const next = (this.wsVersions.get(ws.id) ?? 0) + 1;
2062
2437
  this.wsVersions.set(ws.id, next);
2063
2438
  let record;
2064
2439
  try {
2065
- const all = this.snapshot();
2066
- const data = all.find((w) => w.id === ws.id);
2067
- if (!data) return;
2068
- record = { id: ws.id, data, version: next, updated_at: this.now() };
2069
- } catch {
2440
+ record = { id: ws.id, data: this.snapshotOf(ws), version: next, updated_at: this.now() };
2441
+ } catch (err) {
2442
+ this.reportStoreError(err, { id: ws.id, version: next });
2070
2443
  return;
2071
2444
  }
2072
2445
  const pending = this.saveChains.get(ws.id);
@@ -2081,8 +2454,8 @@ var Coordinator = class {
2081
2454
  this.reportStoreError(err, record);
2082
2455
  return;
2083
2456
  }
2084
- if (result instanceof Promise) {
2085
- this.trackSave(result, record);
2457
+ if (isThenable(result)) {
2458
+ this.trackSave(Promise.resolve(result), record);
2086
2459
  }
2087
2460
  }
2088
2461
  trackSave(save, record) {
@@ -2100,7 +2473,10 @@ var Coordinator = class {
2100
2473
  this.options.onStoreError(err, { id: record.id, version: record.version });
2101
2474
  } catch {
2102
2475
  }
2476
+ return;
2103
2477
  }
2478
+ const message = err instanceof Error ? err.message : String(err);
2479
+ console.error(`chap coordinator: writing workspace ${record.id} to the store failed: ${message}`);
2104
2480
  }
2105
2481
  async drainSaves() {
2106
2482
  while (this.saveChains.size) {
@@ -2174,122 +2550,150 @@ var Coordinator = class {
2174
2550
  /** Serialise all workspaces to a JSON-safe structure for persistence. */
2175
2551
  snapshot() {
2176
2552
  const out = [];
2177
- for (const ws of this.workspaces.values()) {
2178
- out.push({
2179
- id: ws.id,
2180
- created: ws.created,
2181
- state: ws.state,
2182
- profiles: ws.profiles,
2183
- mode: ws.mode,
2184
- mode_ceiling: ws.mode_ceiling,
2185
- routing_policy_uri: ws.routing_policy_uri,
2186
- step_up_window_sec: ws.step_up_window_sec,
2187
- min_acr: ws.min_acr,
2188
- members: Array.from(ws.members.values()),
2189
- tasks: Array.from(ws.tasks.values()),
2190
- overrides: Array.from(ws.overrides.values()),
2191
- whispers: Array.from(ws.whispers.values()),
2192
- deliberations: Array.from(ws.deliberations.values()),
2193
- handoffs: Array.from(ws.handoffs.values()),
2194
- snapshots: Array.from(ws.snapshots.values()),
2195
- route_decisions: Array.from(ws.route_decisions.values()),
2196
- idempotency_keys: ws.idempotency_keys,
2197
- audit: ws.audit,
2198
- chain_head: ws.chain_head,
2199
- chain_enabled: ws.chain_enabled
2200
- });
2201
- }
2553
+ for (const ws of this.workspaces.values()) out.push(this.snapshotOf(ws));
2202
2554
  return out;
2203
2555
  }
2556
+ /** The workspace's own fields: everything except its collections and log. */
2557
+ metaOf(ws) {
2558
+ return {
2559
+ id: ws.id,
2560
+ created: ws.created,
2561
+ state: ws.state,
2562
+ profiles: ws.profiles,
2563
+ mode: ws.mode,
2564
+ mode_ceiling: ws.mode_ceiling,
2565
+ routing_policy_uri: ws.routing_policy_uri,
2566
+ step_up_window_sec: ws.step_up_window_sec,
2567
+ min_acr: ws.min_acr,
2568
+ chain_head: ws.chain_head,
2569
+ chain_enabled: ws.chain_enabled
2570
+ };
2571
+ }
2572
+ /** One workspace as `snapshot()` holds it. Reads nothing as a change. */
2573
+ snapshotOf(ws) {
2574
+ const { chain_head, chain_enabled, ...own } = this.metaOf(ws);
2575
+ return {
2576
+ ...own,
2577
+ members: Array.from(rawValues(ws.members)),
2578
+ tasks: Array.from(rawValues(ws.tasks)),
2579
+ overrides: Array.from(rawValues(ws.overrides)),
2580
+ whispers: Array.from(rawValues(ws.whispers)),
2581
+ deliberations: Array.from(rawValues(ws.deliberations)),
2582
+ handoffs: Array.from(rawValues(ws.handoffs)),
2583
+ snapshots: Array.from(rawValues(ws.snapshots)),
2584
+ route_decisions: Array.from(rawValues(ws.route_decisions)),
2585
+ idempotency_keys: { ...ws.idempotency_keys },
2586
+ audit: ws.audit,
2587
+ chain_head,
2588
+ chain_enabled
2589
+ };
2590
+ }
2204
2591
  /** Restore from a snapshot produced by ``snapshot()``. Replaces
2205
2592
  * any existing in-memory workspaces. */
2206
2593
  restore(data) {
2207
2594
  this.workspaces.clear();
2595
+ this.unflushed.clear();
2208
2596
  if (!Array.isArray(data)) return;
2209
2597
  for (const w of data) {
2210
- const ws = {
2211
- id: w.id,
2212
- created: w.created,
2213
- state: w.state ?? "active",
2214
- profiles: w.profiles ?? [],
2215
- mode: w.mode ?? "trial",
2216
- mode_ceiling: w.mode_ceiling ?? "production",
2217
- routing_policy_uri: w.routing_policy_uri,
2218
- step_up_window_sec: w.step_up_window_sec ?? 300,
2219
- min_acr: w.min_acr,
2220
- members: /* @__PURE__ */ new Map(),
2221
- tasks: /* @__PURE__ */ new Map(),
2222
- overrides: /* @__PURE__ */ new Map(),
2223
- whispers: /* @__PURE__ */ new Map(),
2224
- deliberations: /* @__PURE__ */ new Map(),
2225
- handoffs: /* @__PURE__ */ new Map(),
2226
- snapshots: /* @__PURE__ */ new Map(),
2227
- route_decisions: /* @__PURE__ */ new Map(),
2228
- idempotency_keys: w.idempotency_keys ?? {},
2229
- audit: w.audit ?? [],
2230
- chain_head: w.chain_head,
2231
- chain_enabled: !!w.chain_enabled
2232
- };
2233
- for (const m of w.members ?? []) {
2234
- if (!Array.isArray(m.keys)) m.keys = [];
2235
- if (m.paused === void 0) m.paused = false;
2236
- ws.members.set(m.uri, m);
2237
- }
2238
- for (const t of w.tasks ?? []) ws.tasks.set(t.id, t);
2239
- for (const o of w.overrides ?? []) ws.overrides.set(o.id, o);
2240
- for (const x of w.whispers ?? [])
2241
- ws.whispers.set(x.id, x);
2242
- for (const x of w.deliberations ?? [])
2243
- ws.deliberations.set(x.id, x);
2244
- for (const x of w.handoffs ?? [])
2245
- ws.handoffs.set(x.id, x);
2246
- for (const x of w.snapshots ?? []) {
2247
- const content = structuredClone(x.content ?? {
2248
- workspace: x.workspace,
2249
- audit_seq: x.audit_seq,
2250
- include: x.include,
2251
- state: x.state,
2252
- ...x.label ? { label: x.label } : {}
2253
- });
2254
- if (!x.content) {
2255
- const oldState = content.state;
2256
- const state = {};
2257
- for (const key of ["mode_ceiling", "routing_policy_uri", "audit_seq"]) {
2258
- if (oldState[key] != null && oldState[key] !== "") state[key] = oldState[key];
2259
- }
2260
- for (const [key, fields] of [
2261
- ["members", ["uri", "type", "role", "scopes"]],
2262
- ["open_tasks", ["id", "kind", "state", "assignee"]]
2263
- ]) {
2264
- const items = oldState[key];
2265
- if (Array.isArray(items) && items.length) {
2266
- state[key] = items.map((item) => Object.fromEntries(fields.filter((field) => item[field] != null && item[field] !== "" && (!Array.isArray(item[field]) || item[field].length > 0)).map((field) => [field, item[field]])));
2267
- }
2598
+ let ws;
2599
+ try {
2600
+ ws = this.rebuild(w);
2601
+ } catch (err) {
2602
+ const message = err instanceof Error ? err.message : String(err);
2603
+ console.warn(`chap coordinator: workspace ${String(w?.id)} could not be restored: ${message}`);
2604
+ continue;
2605
+ }
2606
+ this.workspaces.set(ws.id, ws);
2607
+ for (const name of COLLECTIONS) ws[name].dirty.clear();
2608
+ this.persisted.set(ws, { seq: 0, replace: true, keys: /* @__PURE__ */ new Set() });
2609
+ }
2610
+ }
2611
+ /** One workspace from a snapshot entry, its maps tracked. */
2612
+ rebuild(w) {
2613
+ const ws = {
2614
+ id: w.id,
2615
+ created: w.created,
2616
+ state: w.state ?? "active",
2617
+ profiles: w.profiles ?? [],
2618
+ mode: w.mode ?? "trial",
2619
+ mode_ceiling: w.mode_ceiling ?? "production",
2620
+ routing_policy_uri: w.routing_policy_uri,
2621
+ step_up_window_sec: w.step_up_window_sec ?? 300,
2622
+ min_acr: w.min_acr,
2623
+ members: this.newMap(),
2624
+ tasks: this.newMap(),
2625
+ overrides: this.newMap(),
2626
+ whispers: this.newMap(),
2627
+ deliberations: this.newMap(),
2628
+ handoffs: this.newMap(),
2629
+ snapshots: this.newMap(),
2630
+ route_decisions: this.newMap(),
2631
+ idempotency_keys: w.idempotency_keys ?? {},
2632
+ audit: w.audit ?? [],
2633
+ chain_head: w.chain_head,
2634
+ chain_enabled: !!w.chain_enabled
2635
+ };
2636
+ for (const m of w.members ?? []) {
2637
+ if (!Array.isArray(m.keys)) m.keys = [];
2638
+ if (m.paused === void 0) m.paused = false;
2639
+ ws.members.set(m.uri, m);
2640
+ }
2641
+ for (const t of w.tasks ?? []) ws.tasks.set(t.id, t);
2642
+ for (const o of w.overrides ?? []) ws.overrides.set(o.id, o);
2643
+ for (const x of w.whispers ?? [])
2644
+ ws.whispers.set(x.id, x);
2645
+ for (const x of w.deliberations ?? [])
2646
+ ws.deliberations.set(x.id, x);
2647
+ for (const x of w.handoffs ?? [])
2648
+ ws.handoffs.set(x.id, x);
2649
+ for (const x of w.snapshots ?? []) {
2650
+ const content = structuredClone(x.content ?? {
2651
+ workspace: x.workspace,
2652
+ audit_seq: x.audit_seq,
2653
+ include: x.include,
2654
+ state: x.state,
2655
+ ...x.label ? { label: x.label } : {}
2656
+ });
2657
+ if (!x.content) {
2658
+ const oldState = content.state;
2659
+ const state = {};
2660
+ for (const key of ["mode_ceiling", "routing_policy_uri", "audit_seq"]) {
2661
+ if (oldState[key] != null && oldState[key] !== "") state[key] = oldState[key];
2662
+ }
2663
+ for (const [key, fields] of [
2664
+ ["members", ["uri", "type", "role", "scopes"]],
2665
+ ["open_tasks", ["id", "kind", "state", "assignee"]]
2666
+ ]) {
2667
+ const items = oldState[key];
2668
+ if (Array.isArray(items) && items.length) {
2669
+ state[key] = items.map((item) => Object.fromEntries(fields.filter((field) => item[field] != null && item[field] !== "" && (!Array.isArray(item[field]) || item[field].length > 0)).map((field) => [field, item[field]])));
2268
2670
  }
2269
- content.state = state;
2270
2671
  }
2271
- ws.snapshots.set(x.id, {
2272
- id: x.id,
2273
- kind: "snapshot",
2274
- produced_by: x.produced_by ?? x.by,
2275
- produced_at: x.produced_at ?? x.ts,
2276
- content_hash: x.content_hash ?? contentHash(content),
2277
- content
2278
- });
2279
- }
2280
- for (const x of w.route_decisions ?? [])
2281
- ws.route_decisions.set(x.id, x);
2282
- this.workspaces.set(ws.id, ws);
2672
+ content.state = state;
2673
+ }
2674
+ ws.snapshots.set(x.id, {
2675
+ id: x.id,
2676
+ kind: "snapshot",
2677
+ produced_by: x.produced_by ?? x.by,
2678
+ produced_at: x.produced_at ?? x.ts,
2679
+ content_hash: x.content_hash ?? contentHash(content),
2680
+ content
2681
+ });
2283
2682
  }
2683
+ for (const x of w.route_decisions ?? [])
2684
+ ws.route_decisions.set(x.id, x);
2685
+ return ws;
2284
2686
  }
2285
2687
  // -- dispatch ------------------------------------------------------
2286
2688
  dispatch(envelope) {
2287
2689
  this.frozenNow = this.advanceClock();
2690
+ this.tracker.depth++;
2288
2691
  try {
2289
2692
  const response = this._dispatch(envelope);
2290
2693
  if (response.error) this.recordRefusal(envelope, response.error);
2291
2694
  return response;
2292
2695
  } finally {
2696
+ if (--this.tracker.depth === 0) this.flushUnflushed();
2293
2697
  this.frozenNow = void 0;
2294
2698
  }
2295
2699
  }
@@ -2401,7 +2805,7 @@ var Coordinator = class {
2401
2805
  }
2402
2806
  let out;
2403
2807
  try {
2404
- out = handler(params);
2808
+ out = handler(ownCopy(params));
2405
2809
  } catch (e) {
2406
2810
  const msg = e instanceof Error ? e.message : String(e);
2407
2811
  return reply(envelope, { error: rpcError(E.INTERNAL, "Internal error", { detail: msg }) });
@@ -2412,7 +2816,7 @@ var Coordinator = class {
2412
2816
  const ws = this.workspaces.get(wsId);
2413
2817
  if (ws) this.recordAudit(ws, envelope);
2414
2818
  }
2415
- return reply(envelope, { result: out.result });
2819
+ return reply(envelope, { result: ownCopy(out.result) });
2416
2820
  }
2417
2821
  // -- audit ---------------------------------------------------------
2418
2822
  recordAudit(ws, envelope) {
@@ -2656,20 +3060,21 @@ var Coordinator = class {
2656
3060
  routing_policy_uri: p.routing_policy_uri,
2657
3061
  step_up_window_sec: typeof p.step_up_window_sec === "number" ? p.step_up_window_sec : 300,
2658
3062
  min_acr: p.min_acr,
2659
- members: /* @__PURE__ */ new Map(),
2660
- tasks: /* @__PURE__ */ new Map(),
2661
- overrides: /* @__PURE__ */ new Map(),
2662
- whispers: /* @__PURE__ */ new Map(),
2663
- deliberations: /* @__PURE__ */ new Map(),
2664
- handoffs: /* @__PURE__ */ new Map(),
2665
- snapshots: /* @__PURE__ */ new Map(),
2666
- route_decisions: /* @__PURE__ */ new Map(),
3063
+ members: this.newMap(),
3064
+ tasks: this.newMap(),
3065
+ overrides: this.newMap(),
3066
+ whispers: this.newMap(),
3067
+ deliberations: this.newMap(),
3068
+ handoffs: this.newMap(),
3069
+ snapshots: this.newMap(),
3070
+ route_decisions: this.newMap(),
2667
3071
  idempotency_keys: {},
2668
3072
  audit: [],
2669
3073
  chain_enabled: chainEnabled,
2670
3074
  chain_head: chainEnabled ? ZERO_HASH : void 0
2671
3075
  };
2672
3076
  this.workspaces.set(id, ws);
3077
+ this.persisted.set(ws, { seq: 0, replace: false, keys: /* @__PURE__ */ new Set() });
2673
3078
  return { result: { workspace: id, created: ws.created } };
2674
3079
  }
2675
3080
  opWorkspaceDescribe(p) {
@@ -2690,7 +3095,7 @@ var Coordinator = class {
2690
3095
  max_envelope_bytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES,
2691
3096
  step_up_window_sec: ws.step_up_window_sec,
2692
3097
  profiles: ws.profiles,
2693
- members: Array.from(ws.members.values()).map(memberToDict),
3098
+ members: Array.from(rawValues(ws.members)).map(memberToDict),
2694
3099
  audit_count: ws.audit.length,
2695
3100
  task_count: ws.tasks.size,
2696
3101
  override_count: ws.overrides.size,
@@ -2865,8 +3270,12 @@ var Coordinator = class {
2865
3270
  ws.tasks.set(taskId, task);
2866
3271
  if (typeof p.idempotency_key === "string") {
2867
3272
  ws.idempotency_keys[p.idempotency_key] = taskId;
3273
+ this.markKey(ws, p.idempotency_key);
2868
3274
  const keys = Object.keys(ws.idempotency_keys);
2869
- if (keys.length > MAX_IDEMPOTENCY_KEYS) delete ws.idempotency_keys[keys[0]];
3275
+ if (keys.length > MAX_IDEMPOTENCY_KEYS) {
3276
+ delete ws.idempotency_keys[keys[0]];
3277
+ this.markKey(ws, keys[0]);
3278
+ }
2870
3279
  }
2871
3280
  return { result: { task_id: taskId, state: "created" } };
2872
3281
  }
@@ -2917,7 +3326,7 @@ var Coordinator = class {
2917
3326
  const now = this.now();
2918
3327
  if (!task.review) {
2919
3328
  const producer = p.from;
2920
- const eligible = [...ws.members.entries()].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
3329
+ const eligible = [...rawEntries(ws.members)].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
2921
3330
  if (eligible.length === 0) {
2922
3331
  return { error: rpcError(
2923
3332
  E.NOT_AUTHORISED,
@@ -3481,6 +3890,7 @@ export {
3481
3890
  MemoryStore,
3482
3891
  PRIVILEGED_METHODS,
3483
3892
  PatchError,
3893
+ StoreConflictError,
3484
3894
  ZERO_HASH,
3485
3895
  applyJsonPatch,
3486
3896
  call,