@brightbeamai/chap-coordinator 0.3.1 → 0.3.2

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,17 +2265,179 @@ 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);
2269
+ }
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() });
2058
2281
  }
2059
- /** Persist the current snapshot of one workspace to the store. */
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() };
2440
+ record = { id: ws.id, data: this.snapshotOf(ws), version: next, updated_at: this.now() };
2069
2441
  } catch {
2070
2442
  return;
2071
2443
  }
@@ -2081,8 +2453,8 @@ var Coordinator = class {
2081
2453
  this.reportStoreError(err, record);
2082
2454
  return;
2083
2455
  }
2084
- if (result instanceof Promise) {
2085
- this.trackSave(result, record);
2456
+ if (isThenable(result)) {
2457
+ this.trackSave(Promise.resolve(result), record);
2086
2458
  }
2087
2459
  }
2088
2460
  trackSave(save, record) {
@@ -2100,7 +2472,10 @@ var Coordinator = class {
2100
2472
  this.options.onStoreError(err, { id: record.id, version: record.version });
2101
2473
  } catch {
2102
2474
  }
2475
+ return;
2103
2476
  }
2477
+ const message = err instanceof Error ? err.message : String(err);
2478
+ console.error(`chap coordinator: writing workspace ${record.id} to the store failed: ${message}`);
2104
2479
  }
2105
2480
  async drainSaves() {
2106
2481
  while (this.saveChains.size) {
@@ -2174,122 +2549,150 @@ var Coordinator = class {
2174
2549
  /** Serialise all workspaces to a JSON-safe structure for persistence. */
2175
2550
  snapshot() {
2176
2551
  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
- }
2552
+ for (const ws of this.workspaces.values()) out.push(this.snapshotOf(ws));
2202
2553
  return out;
2203
2554
  }
2555
+ /** The workspace's own fields: everything except its collections and log. */
2556
+ metaOf(ws) {
2557
+ return {
2558
+ id: ws.id,
2559
+ created: ws.created,
2560
+ state: ws.state,
2561
+ profiles: ws.profiles,
2562
+ mode: ws.mode,
2563
+ mode_ceiling: ws.mode_ceiling,
2564
+ routing_policy_uri: ws.routing_policy_uri,
2565
+ step_up_window_sec: ws.step_up_window_sec,
2566
+ min_acr: ws.min_acr,
2567
+ chain_head: ws.chain_head,
2568
+ chain_enabled: ws.chain_enabled
2569
+ };
2570
+ }
2571
+ /** One workspace as `snapshot()` holds it. Reads nothing as a change. */
2572
+ snapshotOf(ws) {
2573
+ const { chain_head, chain_enabled, ...own } = this.metaOf(ws);
2574
+ return {
2575
+ ...own,
2576
+ members: Array.from(rawValues(ws.members)),
2577
+ tasks: Array.from(rawValues(ws.tasks)),
2578
+ overrides: Array.from(rawValues(ws.overrides)),
2579
+ whispers: Array.from(rawValues(ws.whispers)),
2580
+ deliberations: Array.from(rawValues(ws.deliberations)),
2581
+ handoffs: Array.from(rawValues(ws.handoffs)),
2582
+ snapshots: Array.from(rawValues(ws.snapshots)),
2583
+ route_decisions: Array.from(rawValues(ws.route_decisions)),
2584
+ idempotency_keys: { ...ws.idempotency_keys },
2585
+ audit: ws.audit,
2586
+ chain_head,
2587
+ chain_enabled
2588
+ };
2589
+ }
2204
2590
  /** Restore from a snapshot produced by ``snapshot()``. Replaces
2205
2591
  * any existing in-memory workspaces. */
2206
2592
  restore(data) {
2207
2593
  this.workspaces.clear();
2594
+ this.unflushed.clear();
2208
2595
  if (!Array.isArray(data)) return;
2209
2596
  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
- }
2597
+ let ws;
2598
+ try {
2599
+ ws = this.rebuild(w);
2600
+ } catch (err) {
2601
+ const message = err instanceof Error ? err.message : String(err);
2602
+ console.warn(`chap coordinator: workspace ${String(w?.id)} could not be restored: ${message}`);
2603
+ continue;
2604
+ }
2605
+ this.workspaces.set(ws.id, ws);
2606
+ for (const name of COLLECTIONS) ws[name].dirty.clear();
2607
+ this.persisted.set(ws, { seq: 0, replace: true, keys: /* @__PURE__ */ new Set() });
2608
+ }
2609
+ }
2610
+ /** One workspace from a snapshot entry, its maps tracked. */
2611
+ rebuild(w) {
2612
+ const ws = {
2613
+ id: w.id,
2614
+ created: w.created,
2615
+ state: w.state ?? "active",
2616
+ profiles: w.profiles ?? [],
2617
+ mode: w.mode ?? "trial",
2618
+ mode_ceiling: w.mode_ceiling ?? "production",
2619
+ routing_policy_uri: w.routing_policy_uri,
2620
+ step_up_window_sec: w.step_up_window_sec ?? 300,
2621
+ min_acr: w.min_acr,
2622
+ members: this.newMap(),
2623
+ tasks: this.newMap(),
2624
+ overrides: this.newMap(),
2625
+ whispers: this.newMap(),
2626
+ deliberations: this.newMap(),
2627
+ handoffs: this.newMap(),
2628
+ snapshots: this.newMap(),
2629
+ route_decisions: this.newMap(),
2630
+ idempotency_keys: w.idempotency_keys ?? {},
2631
+ audit: w.audit ?? [],
2632
+ chain_head: w.chain_head,
2633
+ chain_enabled: !!w.chain_enabled
2634
+ };
2635
+ for (const m of w.members ?? []) {
2636
+ if (!Array.isArray(m.keys)) m.keys = [];
2637
+ if (m.paused === void 0) m.paused = false;
2638
+ ws.members.set(m.uri, m);
2639
+ }
2640
+ for (const t of w.tasks ?? []) ws.tasks.set(t.id, t);
2641
+ for (const o of w.overrides ?? []) ws.overrides.set(o.id, o);
2642
+ for (const x of w.whispers ?? [])
2643
+ ws.whispers.set(x.id, x);
2644
+ for (const x of w.deliberations ?? [])
2645
+ ws.deliberations.set(x.id, x);
2646
+ for (const x of w.handoffs ?? [])
2647
+ ws.handoffs.set(x.id, x);
2648
+ for (const x of w.snapshots ?? []) {
2649
+ const content = structuredClone(x.content ?? {
2650
+ workspace: x.workspace,
2651
+ audit_seq: x.audit_seq,
2652
+ include: x.include,
2653
+ state: x.state,
2654
+ ...x.label ? { label: x.label } : {}
2655
+ });
2656
+ if (!x.content) {
2657
+ const oldState = content.state;
2658
+ const state = {};
2659
+ for (const key of ["mode_ceiling", "routing_policy_uri", "audit_seq"]) {
2660
+ if (oldState[key] != null && oldState[key] !== "") state[key] = oldState[key];
2661
+ }
2662
+ for (const [key, fields] of [
2663
+ ["members", ["uri", "type", "role", "scopes"]],
2664
+ ["open_tasks", ["id", "kind", "state", "assignee"]]
2665
+ ]) {
2666
+ const items = oldState[key];
2667
+ if (Array.isArray(items) && items.length) {
2668
+ 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
2669
  }
2269
- content.state = state;
2270
2670
  }
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);
2671
+ content.state = state;
2672
+ }
2673
+ ws.snapshots.set(x.id, {
2674
+ id: x.id,
2675
+ kind: "snapshot",
2676
+ produced_by: x.produced_by ?? x.by,
2677
+ produced_at: x.produced_at ?? x.ts,
2678
+ content_hash: x.content_hash ?? contentHash(content),
2679
+ content
2680
+ });
2283
2681
  }
2682
+ for (const x of w.route_decisions ?? [])
2683
+ ws.route_decisions.set(x.id, x);
2684
+ return ws;
2284
2685
  }
2285
2686
  // -- dispatch ------------------------------------------------------
2286
2687
  dispatch(envelope) {
2287
2688
  this.frozenNow = this.advanceClock();
2689
+ this.tracker.depth++;
2288
2690
  try {
2289
2691
  const response = this._dispatch(envelope);
2290
2692
  if (response.error) this.recordRefusal(envelope, response.error);
2291
2693
  return response;
2292
2694
  } finally {
2695
+ if (--this.tracker.depth === 0) this.flushUnflushed();
2293
2696
  this.frozenNow = void 0;
2294
2697
  }
2295
2698
  }
@@ -2401,7 +2804,7 @@ var Coordinator = class {
2401
2804
  }
2402
2805
  let out;
2403
2806
  try {
2404
- out = handler(params);
2807
+ out = handler(ownCopy(params));
2405
2808
  } catch (e) {
2406
2809
  const msg = e instanceof Error ? e.message : String(e);
2407
2810
  return reply(envelope, { error: rpcError(E.INTERNAL, "Internal error", { detail: msg }) });
@@ -2412,7 +2815,7 @@ var Coordinator = class {
2412
2815
  const ws = this.workspaces.get(wsId);
2413
2816
  if (ws) this.recordAudit(ws, envelope);
2414
2817
  }
2415
- return reply(envelope, { result: out.result });
2818
+ return reply(envelope, { result: ownCopy(out.result) });
2416
2819
  }
2417
2820
  // -- audit ---------------------------------------------------------
2418
2821
  recordAudit(ws, envelope) {
@@ -2656,20 +3059,21 @@ var Coordinator = class {
2656
3059
  routing_policy_uri: p.routing_policy_uri,
2657
3060
  step_up_window_sec: typeof p.step_up_window_sec === "number" ? p.step_up_window_sec : 300,
2658
3061
  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(),
3062
+ members: this.newMap(),
3063
+ tasks: this.newMap(),
3064
+ overrides: this.newMap(),
3065
+ whispers: this.newMap(),
3066
+ deliberations: this.newMap(),
3067
+ handoffs: this.newMap(),
3068
+ snapshots: this.newMap(),
3069
+ route_decisions: this.newMap(),
2667
3070
  idempotency_keys: {},
2668
3071
  audit: [],
2669
3072
  chain_enabled: chainEnabled,
2670
3073
  chain_head: chainEnabled ? ZERO_HASH : void 0
2671
3074
  };
2672
3075
  this.workspaces.set(id, ws);
3076
+ this.persisted.set(ws, { seq: 0, replace: false, keys: /* @__PURE__ */ new Set() });
2673
3077
  return { result: { workspace: id, created: ws.created } };
2674
3078
  }
2675
3079
  opWorkspaceDescribe(p) {
@@ -2690,7 +3094,7 @@ var Coordinator = class {
2690
3094
  max_envelope_bytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES,
2691
3095
  step_up_window_sec: ws.step_up_window_sec,
2692
3096
  profiles: ws.profiles,
2693
- members: Array.from(ws.members.values()).map(memberToDict),
3097
+ members: Array.from(rawValues(ws.members)).map(memberToDict),
2694
3098
  audit_count: ws.audit.length,
2695
3099
  task_count: ws.tasks.size,
2696
3100
  override_count: ws.overrides.size,
@@ -2865,8 +3269,12 @@ var Coordinator = class {
2865
3269
  ws.tasks.set(taskId, task);
2866
3270
  if (typeof p.idempotency_key === "string") {
2867
3271
  ws.idempotency_keys[p.idempotency_key] = taskId;
3272
+ this.markKey(ws, p.idempotency_key);
2868
3273
  const keys = Object.keys(ws.idempotency_keys);
2869
- if (keys.length > MAX_IDEMPOTENCY_KEYS) delete ws.idempotency_keys[keys[0]];
3274
+ if (keys.length > MAX_IDEMPOTENCY_KEYS) {
3275
+ delete ws.idempotency_keys[keys[0]];
3276
+ this.markKey(ws, keys[0]);
3277
+ }
2870
3278
  }
2871
3279
  return { result: { task_id: taskId, state: "created" } };
2872
3280
  }
@@ -2917,7 +3325,7 @@ var Coordinator = class {
2917
3325
  const now = this.now();
2918
3326
  if (!task.review) {
2919
3327
  const producer = p.from;
2920
- const eligible = [...ws.members.entries()].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
3328
+ const eligible = [...rawEntries(ws.members)].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
2921
3329
  if (eligible.length === 0) {
2922
3330
  return { error: rpcError(
2923
3331
  E.NOT_AUTHORISED,
@@ -3481,6 +3889,7 @@ export {
3481
3889
  MemoryStore,
3482
3890
  PRIVILEGED_METHODS,
3483
3891
  PatchError,
3892
+ StoreConflictError,
3484
3893
  ZERO_HASH,
3485
3894
  applyJsonPatch,
3486
3895
  call,