@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.cjs CHANGED
@@ -27,6 +27,7 @@ __export(src_exports, {
27
27
  MemoryStore: () => MemoryStore,
28
28
  PRIVILEGED_METHODS: () => PRIVILEGED_METHODS,
29
29
  PatchError: () => PatchError,
30
+ StoreConflictError: () => StoreConflictError,
30
31
  ZERO_HASH: () => ZERO_HASH,
31
32
  applyJsonPatch: () => applyJsonPatch,
32
33
  call: () => call,
@@ -706,21 +707,211 @@ function diffJsonPatch(from, to, basePath = "") {
706
707
  }
707
708
 
708
709
  // src/storage/store.ts
710
+ var COLLECTIONS = [
711
+ "members",
712
+ "tasks",
713
+ "overrides",
714
+ "whispers",
715
+ "deliberations",
716
+ "handoffs",
717
+ "snapshots",
718
+ "route_decisions"
719
+ ];
720
+ var IDEMPOTENCY_KEYS = "idempotency_keys";
721
+ var StoreConflictError = class extends Error {
722
+ workspace;
723
+ /** Audit entries the store holds. */
724
+ held;
725
+ /** Audit entries the change expected. */
726
+ base;
727
+ constructor(workspace, held, base) {
728
+ super(
729
+ `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.`
730
+ );
731
+ this.name = "StoreConflictError";
732
+ this.workspace = workspace;
733
+ this.held = held;
734
+ this.base = base;
735
+ }
736
+ /**
737
+ * Matched by name, so `instanceof` holds across the package's entry points,
738
+ * each of which is bundled with its own copy of this class.
739
+ */
740
+ static [Symbol.hasInstance](value) {
741
+ return value instanceof Error && value.name === "StoreConflictError";
742
+ }
743
+ };
744
+ function isCollection(name) {
745
+ return COLLECTIONS.includes(name);
746
+ }
747
+ function objectKey(collection, item) {
748
+ if (item === null || typeof item !== "object") return void 0;
749
+ const o = item;
750
+ const key = collection === "members" ? o.uri ?? o.id : o.id ?? o.uri;
751
+ return typeof key === "string" ? key : void 0;
752
+ }
753
+ function keyed(collection, value) {
754
+ const out = {};
755
+ if (Array.isArray(value)) {
756
+ for (const item of value) {
757
+ const key = objectKey(collection, item);
758
+ if (key !== void 0) out[key] = item;
759
+ }
760
+ } else if (value !== null && typeof value === "object") {
761
+ Object.assign(out, value);
762
+ }
763
+ return out;
764
+ }
765
+ function changeFromRecord(record) {
766
+ const data = record.data !== null && typeof record.data === "object" ? record.data : {};
767
+ const meta = {};
768
+ const objects = {};
769
+ let entries = [];
770
+ for (const [name, value] of Object.entries(data)) {
771
+ if (name === "audit") entries = Array.isArray(value) ? value : [];
772
+ else if (name === IDEMPOTENCY_KEYS || isCollection(name)) objects[name] = keyed(name, value);
773
+ else meta[name] = value;
774
+ }
775
+ if (meta.id === void 0) meta.id = record.id;
776
+ return {
777
+ id: record.id,
778
+ base: 0,
779
+ version: record.version,
780
+ updated_at: record.updated_at,
781
+ meta,
782
+ entries,
783
+ objects,
784
+ replace: true
785
+ };
786
+ }
787
+ function assembleSnapshot(meta, entries, objects) {
788
+ const data = { ...meta };
789
+ for (const name of COLLECTIONS) data[name] = [];
790
+ const keys = {};
791
+ data[IDEMPOTENCY_KEYS] = keys;
792
+ for (const [collection, key, value] of objects) {
793
+ if (collection === IDEMPOTENCY_KEYS) {
794
+ keys[key] = value;
795
+ } else {
796
+ const list = Array.isArray(data[collection]) ? data[collection] : data[collection] = [];
797
+ list.push(value);
798
+ }
799
+ }
800
+ data.audit = entries;
801
+ return data;
802
+ }
709
803
  var MemoryStore = class {
710
- records = /* @__PURE__ */ new Map();
804
+ held = /* @__PURE__ */ new Map();
711
805
  load() {
712
- return Array.from(this.records.values());
806
+ const out = [];
807
+ for (const [id, h] of this.held) {
808
+ const objects = [];
809
+ for (const [collection, items] of h.objects) {
810
+ for (const [key, value] of items) objects.push([collection, key, JSON.parse(value)]);
811
+ }
812
+ out.push({
813
+ id,
814
+ data: assembleSnapshot(JSON.parse(h.meta), h.entries.map((e) => JSON.parse(e)), objects),
815
+ version: h.version,
816
+ updated_at: h.updated_at
817
+ });
818
+ }
819
+ return out;
713
820
  }
714
821
  save(record) {
715
- this.records.set(record.id, record);
822
+ this.apply(changeFromRecord(record));
823
+ }
824
+ apply(change) {
825
+ const meta = JSON.stringify(change.meta);
826
+ const entries = change.entries.map((e) => JSON.stringify(e));
827
+ 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)])]);
828
+ let h = this.held.get(change.id);
829
+ if (!change.replace) {
830
+ const holds = h ? h.entries.length : 0;
831
+ if (holds !== change.base) throw new StoreConflictError(change.id, holds, change.base);
832
+ }
833
+ if (!h || change.replace) {
834
+ h = { meta: "{}", version: 0, updated_at: "", entries: [], objects: /* @__PURE__ */ new Map() };
835
+ }
836
+ for (const entry of entries) h.entries.push(entry);
837
+ for (const [collection, items] of objects) {
838
+ let held = h.objects.get(collection);
839
+ if (!held) h.objects.set(collection, held = /* @__PURE__ */ new Map());
840
+ for (const [key, value] of items) {
841
+ if (value === null) held.delete(key);
842
+ else held.set(key, value);
843
+ }
844
+ }
845
+ h.meta = meta;
846
+ h.version = change.version;
847
+ h.updated_at = change.updated_at;
848
+ this.held.set(change.id, h);
716
849
  }
717
850
  delete(id) {
718
- this.records.delete(id);
851
+ this.held.delete(id);
719
852
  }
720
- close() {
721
- this.records.clear();
853
+ };
854
+
855
+ // src/tracked.ts
856
+ var TrackedMap = class extends Map {
857
+ /** Keys whose objects may have changed since the last write. */
858
+ dirty = /* @__PURE__ */ new Set();
859
+ tracker;
860
+ constructor(tracker) {
861
+ super();
862
+ this.tracker = tracker;
863
+ }
864
+ reading() {
865
+ return this.tracker.enabled && this.tracker.depth > 0;
866
+ }
867
+ touchAll() {
868
+ if (this.reading()) for (const key of super.keys()) this.dirty.add(key);
869
+ }
870
+ get(key) {
871
+ const value = super.get(key);
872
+ if (value !== void 0 && this.reading()) this.dirty.add(key);
873
+ return value;
874
+ }
875
+ set(key, value) {
876
+ if (this.tracker.enabled) this.dirty.add(key);
877
+ return super.set(key, value);
878
+ }
879
+ delete(key) {
880
+ const had = super.delete(key);
881
+ if (had && this.tracker.enabled) this.dirty.add(key);
882
+ return had;
883
+ }
884
+ clear() {
885
+ if (this.tracker.enabled) for (const key of super.keys()) this.dirty.add(key);
886
+ super.clear();
887
+ }
888
+ values() {
889
+ this.touchAll();
890
+ return super.values();
891
+ }
892
+ entries() {
893
+ this.touchAll();
894
+ return super.entries();
895
+ }
896
+ [Symbol.iterator]() {
897
+ this.touchAll();
898
+ return super[Symbol.iterator]();
899
+ }
900
+ forEach(callback, thisArg) {
901
+ this.touchAll();
902
+ super.forEach(callback, thisArg);
903
+ }
904
+ /** Read one value without recording it. */
905
+ peek(key) {
906
+ return super.get(key);
722
907
  }
723
908
  };
909
+ function rawValues(map) {
910
+ return Map.prototype.values.call(map);
911
+ }
912
+ function rawEntries(map) {
913
+ return Map.prototype.entries.call(map);
914
+ }
724
915
 
725
916
  // src/api.ts
726
917
  var CoordinatorError = class extends Error {
@@ -1046,7 +1237,7 @@ function registerControl(coord) {
1046
1237
  const include = Array.isArray(p.include) ? [...p.include] : ["members", "open_tasks", "mode_ceiling"];
1047
1238
  const state = {};
1048
1239
  if (include.includes("members")) {
1049
- const members = Array.from(ws.members.values()).map((m) => ({
1240
+ const members = Array.from(rawValues(ws.members)).map((m) => ({
1050
1241
  uri: m.uri,
1051
1242
  type: m.type,
1052
1243
  ...m.role ? { role: m.role } : {},
@@ -1055,7 +1246,7 @@ function registerControl(coord) {
1055
1246
  if (members.length) state.members = members;
1056
1247
  }
1057
1248
  if (include.includes("open_tasks")) {
1058
- const tasks = Array.from(ws.tasks.values()).filter((t) => t.state !== "completed" && t.state !== "declined" && t.state !== "cancelled" && t.state !== "superseded").map((t) => ({
1249
+ const tasks = Array.from(rawValues(ws.tasks)).filter((t) => t.state !== "completed" && t.state !== "declined" && t.state !== "cancelled" && t.state !== "superseded").map((t) => ({
1059
1250
  id: t.id,
1060
1251
  kind: t.kind,
1061
1252
  state: t.state,
@@ -1924,6 +2115,7 @@ function registerWhisper(coord) {
1924
2115
  if (cutoff < deadline) continue;
1925
2116
  prompt.state = "lapsed";
1926
2117
  prompt.default_applied = prompt.default_if_lapsed;
2118
+ ws.whispers.set(prompt.id, prompt);
1927
2119
  const notify = {
1928
2120
  jsonrpc: "2.0",
1929
2121
  method: "notify.message",
@@ -2046,6 +2238,16 @@ function indexSigned(index, entry) {
2046
2238
  if (entry.outcome != null) index.refused.set(digest, { seq: entry.seq, code: entry.outcome.code });
2047
2239
  else index.accepted.add(digest);
2048
2240
  }
2241
+ function isThenable(value) {
2242
+ return typeof value?.then === "function";
2243
+ }
2244
+ function ownCopy(value) {
2245
+ try {
2246
+ return structuredClone(value);
2247
+ } catch {
2248
+ return value;
2249
+ }
2250
+ }
2049
2251
  var REVIEW_REQUEST_STOPPED = ["cancelled", "superseded", "paused"];
2050
2252
  var Coordinator = class {
2051
2253
  workspaces = /* @__PURE__ */ new Map();
@@ -2054,6 +2256,14 @@ var Coordinator = class {
2054
2256
  store;
2055
2257
  wsVersions = /* @__PURE__ */ new Map();
2056
2258
  saveChains = /* @__PURE__ */ new Map();
2259
+ /** Records which objects change, for stores that implement `apply`. */
2260
+ tracker;
2261
+ /** What the store holds of each workspace, as far as this instance knows. */
2262
+ persisted = /* @__PURE__ */ new WeakMap();
2263
+ /** Workspaces with entries not yet written in the call being handled. */
2264
+ unflushed = /* @__PURE__ */ new Set();
2265
+ /** Workspaces with a write queued behind an async write in flight. */
2266
+ flushQueued = /* @__PURE__ */ new Set();
2057
2267
  clockMs;
2058
2268
  frozenNow;
2059
2269
  auditListeners = [];
@@ -2074,6 +2284,7 @@ var Coordinator = class {
2074
2284
  constructor(options = {}) {
2075
2285
  this.options = options;
2076
2286
  this.store = options.store ?? new MemoryStore();
2287
+ this.tracker = { enabled: typeof this.store.apply === "function", depth: 0 };
2077
2288
  this.ids = new IdFactory(!!options.deterministicIds, 0n, 17e11);
2078
2289
  if (options.deterministicClock) this.clockMs = 17e11;
2079
2290
  if (options.onAudit) this.auditListeners.push(options.onAudit);
@@ -2089,7 +2300,7 @@ var Coordinator = class {
2089
2300
  });
2090
2301
  try {
2091
2302
  const loaded = this.store.load();
2092
- if (!(loaded instanceof Promise)) {
2303
+ if (!isThenable(loaded)) {
2093
2304
  this.applyRecords(loaded);
2094
2305
  }
2095
2306
  } catch {
@@ -2107,18 +2318,181 @@ var Coordinator = class {
2107
2318
  this.wsVersions.clear();
2108
2319
  this.restore(records.map((r) => r.data));
2109
2320
  for (const r of records) this.wsVersions.set(r.id, r.version);
2321
+ for (const ws of this.workspaces.values()) this.markWritten(ws);
2110
2322
  }
2111
- /** Persist the current snapshot of one workspace to the store. */
2323
+ /** A map for a workspace collection, tracked for stores that implement `apply`. */
2324
+ newMap() {
2325
+ return new TrackedMap(this.tracker);
2326
+ }
2327
+ /** Record that the store holds this workspace exactly as it is in memory. */
2328
+ markWritten(ws) {
2329
+ for (const name of COLLECTIONS) {
2330
+ const map = ws[name];
2331
+ if (map instanceof TrackedMap) map.dirty.clear();
2332
+ }
2333
+ this.persisted.set(ws, { seq: ws.audit.length, replace: false, keys: /* @__PURE__ */ new Set() });
2334
+ }
2335
+ /**
2336
+ * What the store holds of a workspace. A workspace this instance did not
2337
+ * create or load, such as one an application put in `workspaces` itself,
2338
+ * or one whose maps an application replaced, is tracked from here on and
2339
+ * written whole once.
2340
+ */
2341
+ persistedOf(ws) {
2342
+ let p = this.persisted.get(ws);
2343
+ let untracked = false;
2344
+ for (const name of COLLECTIONS) {
2345
+ const map = ws[name];
2346
+ if (map instanceof TrackedMap && map.tracker === this.tracker) continue;
2347
+ untracked = true;
2348
+ const tracked = new TrackedMap(this.tracker);
2349
+ for (const [key, value] of Map.prototype.entries.call(map)) {
2350
+ tracked.set(key, value);
2351
+ }
2352
+ ws[name] = tracked;
2353
+ }
2354
+ if (!p || untracked) {
2355
+ p = { seq: p?.seq ?? 0, replace: true, keys: /* @__PURE__ */ new Set(), updatedAt: p?.updatedAt };
2356
+ this.persisted.set(ws, p);
2357
+ }
2358
+ return p;
2359
+ }
2360
+ /** Record that a task.create idempotency key was written or removed. */
2361
+ markKey(ws, key) {
2362
+ if (this.tracker.enabled) this.persistedOf(ws).keys.add(key);
2363
+ }
2364
+ /** After an entry is appended: write now, or when the call being handled ends. */
2112
2365
  persist(ws) {
2366
+ if (!this.tracker.enabled) {
2367
+ this.persistSnapshot(ws);
2368
+ return;
2369
+ }
2370
+ this.persistedOf(ws).updatedAt = this.now();
2371
+ if (this.tracker.depth > 0) this.unflushed.add(ws);
2372
+ else this.flush(ws);
2373
+ }
2374
+ flushUnflushed() {
2375
+ if (!this.unflushed.size) return;
2376
+ const list = [...this.unflushed];
2377
+ this.unflushed.clear();
2378
+ for (const ws of list) this.flush(ws);
2379
+ }
2380
+ /** Write what changed in one workspace since its last write. */
2381
+ flush(ws) {
2382
+ const pending = this.saveChains.get(ws.id);
2383
+ if (pending) {
2384
+ if (this.flushQueued.has(ws)) return;
2385
+ this.flushQueued.add(ws);
2386
+ this.chainWrite(ws.id, pending.then(() => {
2387
+ this.flushQueued.delete(ws);
2388
+ return this.writeChange(ws);
2389
+ }));
2390
+ return;
2391
+ }
2392
+ const result = this.writeChange(ws);
2393
+ if (result) this.chainWrite(ws.id, result);
2394
+ }
2395
+ chainWrite(id, work) {
2396
+ const guarded = work.catch(() => void 0);
2397
+ this.saveChains.set(id, guarded);
2398
+ void guarded.then(() => {
2399
+ if (this.saveChains.get(id) === guarded) this.saveChains.delete(id);
2400
+ });
2401
+ }
2402
+ writeChange(ws) {
2403
+ const p = this.persistedOf(ws);
2404
+ let change;
2405
+ try {
2406
+ change = this.takeChange(ws, p);
2407
+ } catch (err) {
2408
+ p.replace = true;
2409
+ this.reportStoreError(err, { id: ws.id, version: ws.audit.length });
2410
+ return;
2411
+ }
2412
+ if (!change) return;
2413
+ const taken = change;
2414
+ const done = () => {
2415
+ p.seq = taken.version;
2416
+ if (taken.replace) p.replace = false;
2417
+ };
2418
+ const failed = (err) => {
2419
+ this.giveBack(ws, p, taken);
2420
+ this.reportStoreError(err, { id: taken.id, version: taken.version });
2421
+ };
2422
+ let result;
2423
+ try {
2424
+ result = this.store.apply(taken);
2425
+ } catch (err) {
2426
+ failed(err);
2427
+ return;
2428
+ }
2429
+ if (isThenable(result)) return Promise.resolve(result).then(done, failed);
2430
+ done();
2431
+ }
2432
+ /** The change since the last write, taking the recorded keys with it. */
2433
+ takeChange(ws, p) {
2434
+ const replace = p.replace;
2435
+ const objects = {};
2436
+ for (const name of COLLECTIONS) {
2437
+ const map = ws[name];
2438
+ if (replace) {
2439
+ const all = {};
2440
+ for (const [key, value] of Map.prototype.entries.call(map)) {
2441
+ all[key] = value;
2442
+ }
2443
+ objects[name] = all;
2444
+ } else if (map.dirty.size) {
2445
+ const changed = {};
2446
+ for (const key of map.dirty) changed[key] = map.peek(key) ?? null;
2447
+ objects[name] = changed;
2448
+ }
2449
+ map.dirty.clear();
2450
+ }
2451
+ if (replace) {
2452
+ objects[IDEMPOTENCY_KEYS] = { ...ws.idempotency_keys };
2453
+ } else if (p.keys.size) {
2454
+ const changed = {};
2455
+ for (const key of p.keys) changed[key] = ws.idempotency_keys[key] ?? null;
2456
+ objects[IDEMPOTENCY_KEYS] = changed;
2457
+ }
2458
+ p.keys.clear();
2459
+ const entries = ws.audit.slice(replace ? 0 : p.seq);
2460
+ if (!replace && !entries.length && !Object.keys(objects).length) return void 0;
2461
+ return {
2462
+ id: ws.id,
2463
+ base: replace ? 0 : p.seq,
2464
+ version: ws.audit.length,
2465
+ updated_at: p.updatedAt ?? ws.audit[ws.audit.length - 1]?.arrived ?? ws.created,
2466
+ meta: this.metaOf(ws),
2467
+ entries,
2468
+ objects,
2469
+ ...replace ? { replace: true } : {}
2470
+ };
2471
+ }
2472
+ /** A failed write's keys go back, so the next write carries them again. */
2473
+ giveBack(ws, p, change) {
2474
+ if (change.replace) {
2475
+ p.replace = true;
2476
+ return;
2477
+ }
2478
+ for (const [name, items] of Object.entries(change.objects)) {
2479
+ if (name === IDEMPOTENCY_KEYS) {
2480
+ for (const key of Object.keys(items)) p.keys.add(key);
2481
+ continue;
2482
+ }
2483
+ const map = ws[name];
2484
+ if (map instanceof TrackedMap) for (const key of Object.keys(items)) map.dirty.add(key);
2485
+ }
2486
+ }
2487
+ /** Persist the whole workspace, for a store that implements only `save`. */
2488
+ persistSnapshot(ws) {
2113
2489
  const next = (this.wsVersions.get(ws.id) ?? 0) + 1;
2114
2490
  this.wsVersions.set(ws.id, next);
2115
2491
  let record;
2116
2492
  try {
2117
- const all = this.snapshot();
2118
- const data = all.find((w) => w.id === ws.id);
2119
- if (!data) return;
2120
- record = { id: ws.id, data, version: next, updated_at: this.now() };
2121
- } catch {
2493
+ record = { id: ws.id, data: this.snapshotOf(ws), version: next, updated_at: this.now() };
2494
+ } catch (err) {
2495
+ this.reportStoreError(err, { id: ws.id, version: next });
2122
2496
  return;
2123
2497
  }
2124
2498
  const pending = this.saveChains.get(ws.id);
@@ -2133,8 +2507,8 @@ var Coordinator = class {
2133
2507
  this.reportStoreError(err, record);
2134
2508
  return;
2135
2509
  }
2136
- if (result instanceof Promise) {
2137
- this.trackSave(result, record);
2510
+ if (isThenable(result)) {
2511
+ this.trackSave(Promise.resolve(result), record);
2138
2512
  }
2139
2513
  }
2140
2514
  trackSave(save, record) {
@@ -2152,7 +2526,10 @@ var Coordinator = class {
2152
2526
  this.options.onStoreError(err, { id: record.id, version: record.version });
2153
2527
  } catch {
2154
2528
  }
2529
+ return;
2155
2530
  }
2531
+ const message = err instanceof Error ? err.message : String(err);
2532
+ console.error(`chap coordinator: writing workspace ${record.id} to the store failed: ${message}`);
2156
2533
  }
2157
2534
  async drainSaves() {
2158
2535
  while (this.saveChains.size) {
@@ -2226,122 +2603,150 @@ var Coordinator = class {
2226
2603
  /** Serialise all workspaces to a JSON-safe structure for persistence. */
2227
2604
  snapshot() {
2228
2605
  const out = [];
2229
- for (const ws of this.workspaces.values()) {
2230
- out.push({
2231
- id: ws.id,
2232
- created: ws.created,
2233
- state: ws.state,
2234
- profiles: ws.profiles,
2235
- mode: ws.mode,
2236
- mode_ceiling: ws.mode_ceiling,
2237
- routing_policy_uri: ws.routing_policy_uri,
2238
- step_up_window_sec: ws.step_up_window_sec,
2239
- min_acr: ws.min_acr,
2240
- members: Array.from(ws.members.values()),
2241
- tasks: Array.from(ws.tasks.values()),
2242
- overrides: Array.from(ws.overrides.values()),
2243
- whispers: Array.from(ws.whispers.values()),
2244
- deliberations: Array.from(ws.deliberations.values()),
2245
- handoffs: Array.from(ws.handoffs.values()),
2246
- snapshots: Array.from(ws.snapshots.values()),
2247
- route_decisions: Array.from(ws.route_decisions.values()),
2248
- idempotency_keys: ws.idempotency_keys,
2249
- audit: ws.audit,
2250
- chain_head: ws.chain_head,
2251
- chain_enabled: ws.chain_enabled
2252
- });
2253
- }
2606
+ for (const ws of this.workspaces.values()) out.push(this.snapshotOf(ws));
2254
2607
  return out;
2255
2608
  }
2609
+ /** The workspace's own fields: everything except its collections and log. */
2610
+ metaOf(ws) {
2611
+ return {
2612
+ id: ws.id,
2613
+ created: ws.created,
2614
+ state: ws.state,
2615
+ profiles: ws.profiles,
2616
+ mode: ws.mode,
2617
+ mode_ceiling: ws.mode_ceiling,
2618
+ routing_policy_uri: ws.routing_policy_uri,
2619
+ step_up_window_sec: ws.step_up_window_sec,
2620
+ min_acr: ws.min_acr,
2621
+ chain_head: ws.chain_head,
2622
+ chain_enabled: ws.chain_enabled
2623
+ };
2624
+ }
2625
+ /** One workspace as `snapshot()` holds it. Reads nothing as a change. */
2626
+ snapshotOf(ws) {
2627
+ const { chain_head, chain_enabled, ...own } = this.metaOf(ws);
2628
+ return {
2629
+ ...own,
2630
+ members: Array.from(rawValues(ws.members)),
2631
+ tasks: Array.from(rawValues(ws.tasks)),
2632
+ overrides: Array.from(rawValues(ws.overrides)),
2633
+ whispers: Array.from(rawValues(ws.whispers)),
2634
+ deliberations: Array.from(rawValues(ws.deliberations)),
2635
+ handoffs: Array.from(rawValues(ws.handoffs)),
2636
+ snapshots: Array.from(rawValues(ws.snapshots)),
2637
+ route_decisions: Array.from(rawValues(ws.route_decisions)),
2638
+ idempotency_keys: { ...ws.idempotency_keys },
2639
+ audit: ws.audit,
2640
+ chain_head,
2641
+ chain_enabled
2642
+ };
2643
+ }
2256
2644
  /** Restore from a snapshot produced by ``snapshot()``. Replaces
2257
2645
  * any existing in-memory workspaces. */
2258
2646
  restore(data) {
2259
2647
  this.workspaces.clear();
2648
+ this.unflushed.clear();
2260
2649
  if (!Array.isArray(data)) return;
2261
2650
  for (const w of data) {
2262
- const ws = {
2263
- id: w.id,
2264
- created: w.created,
2265
- state: w.state ?? "active",
2266
- profiles: w.profiles ?? [],
2267
- mode: w.mode ?? "trial",
2268
- mode_ceiling: w.mode_ceiling ?? "production",
2269
- routing_policy_uri: w.routing_policy_uri,
2270
- step_up_window_sec: w.step_up_window_sec ?? 300,
2271
- min_acr: w.min_acr,
2272
- members: /* @__PURE__ */ new Map(),
2273
- tasks: /* @__PURE__ */ new Map(),
2274
- overrides: /* @__PURE__ */ new Map(),
2275
- whispers: /* @__PURE__ */ new Map(),
2276
- deliberations: /* @__PURE__ */ new Map(),
2277
- handoffs: /* @__PURE__ */ new Map(),
2278
- snapshots: /* @__PURE__ */ new Map(),
2279
- route_decisions: /* @__PURE__ */ new Map(),
2280
- idempotency_keys: w.idempotency_keys ?? {},
2281
- audit: w.audit ?? [],
2282
- chain_head: w.chain_head,
2283
- chain_enabled: !!w.chain_enabled
2284
- };
2285
- for (const m of w.members ?? []) {
2286
- if (!Array.isArray(m.keys)) m.keys = [];
2287
- if (m.paused === void 0) m.paused = false;
2288
- ws.members.set(m.uri, m);
2289
- }
2290
- for (const t of w.tasks ?? []) ws.tasks.set(t.id, t);
2291
- for (const o of w.overrides ?? []) ws.overrides.set(o.id, o);
2292
- for (const x of w.whispers ?? [])
2293
- ws.whispers.set(x.id, x);
2294
- for (const x of w.deliberations ?? [])
2295
- ws.deliberations.set(x.id, x);
2296
- for (const x of w.handoffs ?? [])
2297
- ws.handoffs.set(x.id, x);
2298
- for (const x of w.snapshots ?? []) {
2299
- const content = structuredClone(x.content ?? {
2300
- workspace: x.workspace,
2301
- audit_seq: x.audit_seq,
2302
- include: x.include,
2303
- state: x.state,
2304
- ...x.label ? { label: x.label } : {}
2305
- });
2306
- if (!x.content) {
2307
- const oldState = content.state;
2308
- const state = {};
2309
- for (const key of ["mode_ceiling", "routing_policy_uri", "audit_seq"]) {
2310
- if (oldState[key] != null && oldState[key] !== "") state[key] = oldState[key];
2311
- }
2312
- for (const [key, fields] of [
2313
- ["members", ["uri", "type", "role", "scopes"]],
2314
- ["open_tasks", ["id", "kind", "state", "assignee"]]
2315
- ]) {
2316
- const items = oldState[key];
2317
- if (Array.isArray(items) && items.length) {
2318
- 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]])));
2319
- }
2651
+ let ws;
2652
+ try {
2653
+ ws = this.rebuild(w);
2654
+ } catch (err) {
2655
+ const message = err instanceof Error ? err.message : String(err);
2656
+ console.warn(`chap coordinator: workspace ${String(w?.id)} could not be restored: ${message}`);
2657
+ continue;
2658
+ }
2659
+ this.workspaces.set(ws.id, ws);
2660
+ for (const name of COLLECTIONS) ws[name].dirty.clear();
2661
+ this.persisted.set(ws, { seq: 0, replace: true, keys: /* @__PURE__ */ new Set() });
2662
+ }
2663
+ }
2664
+ /** One workspace from a snapshot entry, its maps tracked. */
2665
+ rebuild(w) {
2666
+ const ws = {
2667
+ id: w.id,
2668
+ created: w.created,
2669
+ state: w.state ?? "active",
2670
+ profiles: w.profiles ?? [],
2671
+ mode: w.mode ?? "trial",
2672
+ mode_ceiling: w.mode_ceiling ?? "production",
2673
+ routing_policy_uri: w.routing_policy_uri,
2674
+ step_up_window_sec: w.step_up_window_sec ?? 300,
2675
+ min_acr: w.min_acr,
2676
+ members: this.newMap(),
2677
+ tasks: this.newMap(),
2678
+ overrides: this.newMap(),
2679
+ whispers: this.newMap(),
2680
+ deliberations: this.newMap(),
2681
+ handoffs: this.newMap(),
2682
+ snapshots: this.newMap(),
2683
+ route_decisions: this.newMap(),
2684
+ idempotency_keys: w.idempotency_keys ?? {},
2685
+ audit: w.audit ?? [],
2686
+ chain_head: w.chain_head,
2687
+ chain_enabled: !!w.chain_enabled
2688
+ };
2689
+ for (const m of w.members ?? []) {
2690
+ if (!Array.isArray(m.keys)) m.keys = [];
2691
+ if (m.paused === void 0) m.paused = false;
2692
+ ws.members.set(m.uri, m);
2693
+ }
2694
+ for (const t of w.tasks ?? []) ws.tasks.set(t.id, t);
2695
+ for (const o of w.overrides ?? []) ws.overrides.set(o.id, o);
2696
+ for (const x of w.whispers ?? [])
2697
+ ws.whispers.set(x.id, x);
2698
+ for (const x of w.deliberations ?? [])
2699
+ ws.deliberations.set(x.id, x);
2700
+ for (const x of w.handoffs ?? [])
2701
+ ws.handoffs.set(x.id, x);
2702
+ for (const x of w.snapshots ?? []) {
2703
+ const content = structuredClone(x.content ?? {
2704
+ workspace: x.workspace,
2705
+ audit_seq: x.audit_seq,
2706
+ include: x.include,
2707
+ state: x.state,
2708
+ ...x.label ? { label: x.label } : {}
2709
+ });
2710
+ if (!x.content) {
2711
+ const oldState = content.state;
2712
+ const state = {};
2713
+ for (const key of ["mode_ceiling", "routing_policy_uri", "audit_seq"]) {
2714
+ if (oldState[key] != null && oldState[key] !== "") state[key] = oldState[key];
2715
+ }
2716
+ for (const [key, fields] of [
2717
+ ["members", ["uri", "type", "role", "scopes"]],
2718
+ ["open_tasks", ["id", "kind", "state", "assignee"]]
2719
+ ]) {
2720
+ const items = oldState[key];
2721
+ if (Array.isArray(items) && items.length) {
2722
+ 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]])));
2320
2723
  }
2321
- content.state = state;
2322
2724
  }
2323
- ws.snapshots.set(x.id, {
2324
- id: x.id,
2325
- kind: "snapshot",
2326
- produced_by: x.produced_by ?? x.by,
2327
- produced_at: x.produced_at ?? x.ts,
2328
- content_hash: x.content_hash ?? contentHash(content),
2329
- content
2330
- });
2331
- }
2332
- for (const x of w.route_decisions ?? [])
2333
- ws.route_decisions.set(x.id, x);
2334
- this.workspaces.set(ws.id, ws);
2725
+ content.state = state;
2726
+ }
2727
+ ws.snapshots.set(x.id, {
2728
+ id: x.id,
2729
+ kind: "snapshot",
2730
+ produced_by: x.produced_by ?? x.by,
2731
+ produced_at: x.produced_at ?? x.ts,
2732
+ content_hash: x.content_hash ?? contentHash(content),
2733
+ content
2734
+ });
2335
2735
  }
2736
+ for (const x of w.route_decisions ?? [])
2737
+ ws.route_decisions.set(x.id, x);
2738
+ return ws;
2336
2739
  }
2337
2740
  // -- dispatch ------------------------------------------------------
2338
2741
  dispatch(envelope) {
2339
2742
  this.frozenNow = this.advanceClock();
2743
+ this.tracker.depth++;
2340
2744
  try {
2341
2745
  const response = this._dispatch(envelope);
2342
2746
  if (response.error) this.recordRefusal(envelope, response.error);
2343
2747
  return response;
2344
2748
  } finally {
2749
+ if (--this.tracker.depth === 0) this.flushUnflushed();
2345
2750
  this.frozenNow = void 0;
2346
2751
  }
2347
2752
  }
@@ -2453,7 +2858,7 @@ var Coordinator = class {
2453
2858
  }
2454
2859
  let out;
2455
2860
  try {
2456
- out = handler(params);
2861
+ out = handler(ownCopy(params));
2457
2862
  } catch (e) {
2458
2863
  const msg = e instanceof Error ? e.message : String(e);
2459
2864
  return reply(envelope, { error: rpcError(E.INTERNAL, "Internal error", { detail: msg }) });
@@ -2464,7 +2869,7 @@ var Coordinator = class {
2464
2869
  const ws = this.workspaces.get(wsId);
2465
2870
  if (ws) this.recordAudit(ws, envelope);
2466
2871
  }
2467
- return reply(envelope, { result: out.result });
2872
+ return reply(envelope, { result: ownCopy(out.result) });
2468
2873
  }
2469
2874
  // -- audit ---------------------------------------------------------
2470
2875
  recordAudit(ws, envelope) {
@@ -2708,20 +3113,21 @@ var Coordinator = class {
2708
3113
  routing_policy_uri: p.routing_policy_uri,
2709
3114
  step_up_window_sec: typeof p.step_up_window_sec === "number" ? p.step_up_window_sec : 300,
2710
3115
  min_acr: p.min_acr,
2711
- members: /* @__PURE__ */ new Map(),
2712
- tasks: /* @__PURE__ */ new Map(),
2713
- overrides: /* @__PURE__ */ new Map(),
2714
- whispers: /* @__PURE__ */ new Map(),
2715
- deliberations: /* @__PURE__ */ new Map(),
2716
- handoffs: /* @__PURE__ */ new Map(),
2717
- snapshots: /* @__PURE__ */ new Map(),
2718
- route_decisions: /* @__PURE__ */ new Map(),
3116
+ members: this.newMap(),
3117
+ tasks: this.newMap(),
3118
+ overrides: this.newMap(),
3119
+ whispers: this.newMap(),
3120
+ deliberations: this.newMap(),
3121
+ handoffs: this.newMap(),
3122
+ snapshots: this.newMap(),
3123
+ route_decisions: this.newMap(),
2719
3124
  idempotency_keys: {},
2720
3125
  audit: [],
2721
3126
  chain_enabled: chainEnabled,
2722
3127
  chain_head: chainEnabled ? ZERO_HASH : void 0
2723
3128
  };
2724
3129
  this.workspaces.set(id, ws);
3130
+ this.persisted.set(ws, { seq: 0, replace: false, keys: /* @__PURE__ */ new Set() });
2725
3131
  return { result: { workspace: id, created: ws.created } };
2726
3132
  }
2727
3133
  opWorkspaceDescribe(p) {
@@ -2742,7 +3148,7 @@ var Coordinator = class {
2742
3148
  max_envelope_bytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES,
2743
3149
  step_up_window_sec: ws.step_up_window_sec,
2744
3150
  profiles: ws.profiles,
2745
- members: Array.from(ws.members.values()).map(memberToDict),
3151
+ members: Array.from(rawValues(ws.members)).map(memberToDict),
2746
3152
  audit_count: ws.audit.length,
2747
3153
  task_count: ws.tasks.size,
2748
3154
  override_count: ws.overrides.size,
@@ -2917,8 +3323,12 @@ var Coordinator = class {
2917
3323
  ws.tasks.set(taskId, task);
2918
3324
  if (typeof p.idempotency_key === "string") {
2919
3325
  ws.idempotency_keys[p.idempotency_key] = taskId;
3326
+ this.markKey(ws, p.idempotency_key);
2920
3327
  const keys = Object.keys(ws.idempotency_keys);
2921
- if (keys.length > MAX_IDEMPOTENCY_KEYS) delete ws.idempotency_keys[keys[0]];
3328
+ if (keys.length > MAX_IDEMPOTENCY_KEYS) {
3329
+ delete ws.idempotency_keys[keys[0]];
3330
+ this.markKey(ws, keys[0]);
3331
+ }
2922
3332
  }
2923
3333
  return { result: { task_id: taskId, state: "created" } };
2924
3334
  }
@@ -2969,7 +3379,7 @@ var Coordinator = class {
2969
3379
  const now = this.now();
2970
3380
  if (!task.review) {
2971
3381
  const producer = p.from;
2972
- const eligible = [...ws.members.entries()].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
3382
+ const eligible = [...rawEntries(ws.members)].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
2973
3383
  if (eligible.length === 0) {
2974
3384
  return { error: rpcError(
2975
3385
  E.NOT_AUTHORISED,
@@ -3534,6 +3944,7 @@ function wrapA2aMessageExchange(coord, workspace, options) {
3534
3944
  MemoryStore,
3535
3945
  PRIVILEGED_METHODS,
3536
3946
  PatchError,
3947
+ StoreConflictError,
3537
3948
  ZERO_HASH,
3538
3949
  applyJsonPatch,
3539
3950
  call,