@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.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,17 +2318,179 @@ 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);
2322
+ }
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() });
2110
2334
  }
2111
- /** Persist the current snapshot of one workspace to the store. */
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() };
2493
+ record = { id: ws.id, data: this.snapshotOf(ws), version: next, updated_at: this.now() };
2121
2494
  } catch {
2122
2495
  return;
2123
2496
  }
@@ -2133,8 +2506,8 @@ var Coordinator = class {
2133
2506
  this.reportStoreError(err, record);
2134
2507
  return;
2135
2508
  }
2136
- if (result instanceof Promise) {
2137
- this.trackSave(result, record);
2509
+ if (isThenable(result)) {
2510
+ this.trackSave(Promise.resolve(result), record);
2138
2511
  }
2139
2512
  }
2140
2513
  trackSave(save, record) {
@@ -2152,7 +2525,10 @@ var Coordinator = class {
2152
2525
  this.options.onStoreError(err, { id: record.id, version: record.version });
2153
2526
  } catch {
2154
2527
  }
2528
+ return;
2155
2529
  }
2530
+ const message = err instanceof Error ? err.message : String(err);
2531
+ console.error(`chap coordinator: writing workspace ${record.id} to the store failed: ${message}`);
2156
2532
  }
2157
2533
  async drainSaves() {
2158
2534
  while (this.saveChains.size) {
@@ -2226,122 +2602,150 @@ var Coordinator = class {
2226
2602
  /** Serialise all workspaces to a JSON-safe structure for persistence. */
2227
2603
  snapshot() {
2228
2604
  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
- }
2605
+ for (const ws of this.workspaces.values()) out.push(this.snapshotOf(ws));
2254
2606
  return out;
2255
2607
  }
2608
+ /** The workspace's own fields: everything except its collections and log. */
2609
+ metaOf(ws) {
2610
+ return {
2611
+ id: ws.id,
2612
+ created: ws.created,
2613
+ state: ws.state,
2614
+ profiles: ws.profiles,
2615
+ mode: ws.mode,
2616
+ mode_ceiling: ws.mode_ceiling,
2617
+ routing_policy_uri: ws.routing_policy_uri,
2618
+ step_up_window_sec: ws.step_up_window_sec,
2619
+ min_acr: ws.min_acr,
2620
+ chain_head: ws.chain_head,
2621
+ chain_enabled: ws.chain_enabled
2622
+ };
2623
+ }
2624
+ /** One workspace as `snapshot()` holds it. Reads nothing as a change. */
2625
+ snapshotOf(ws) {
2626
+ const { chain_head, chain_enabled, ...own } = this.metaOf(ws);
2627
+ return {
2628
+ ...own,
2629
+ members: Array.from(rawValues(ws.members)),
2630
+ tasks: Array.from(rawValues(ws.tasks)),
2631
+ overrides: Array.from(rawValues(ws.overrides)),
2632
+ whispers: Array.from(rawValues(ws.whispers)),
2633
+ deliberations: Array.from(rawValues(ws.deliberations)),
2634
+ handoffs: Array.from(rawValues(ws.handoffs)),
2635
+ snapshots: Array.from(rawValues(ws.snapshots)),
2636
+ route_decisions: Array.from(rawValues(ws.route_decisions)),
2637
+ idempotency_keys: { ...ws.idempotency_keys },
2638
+ audit: ws.audit,
2639
+ chain_head,
2640
+ chain_enabled
2641
+ };
2642
+ }
2256
2643
  /** Restore from a snapshot produced by ``snapshot()``. Replaces
2257
2644
  * any existing in-memory workspaces. */
2258
2645
  restore(data) {
2259
2646
  this.workspaces.clear();
2647
+ this.unflushed.clear();
2260
2648
  if (!Array.isArray(data)) return;
2261
2649
  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
- }
2650
+ let ws;
2651
+ try {
2652
+ ws = this.rebuild(w);
2653
+ } catch (err) {
2654
+ const message = err instanceof Error ? err.message : String(err);
2655
+ console.warn(`chap coordinator: workspace ${String(w?.id)} could not be restored: ${message}`);
2656
+ continue;
2657
+ }
2658
+ this.workspaces.set(ws.id, ws);
2659
+ for (const name of COLLECTIONS) ws[name].dirty.clear();
2660
+ this.persisted.set(ws, { seq: 0, replace: true, keys: /* @__PURE__ */ new Set() });
2661
+ }
2662
+ }
2663
+ /** One workspace from a snapshot entry, its maps tracked. */
2664
+ rebuild(w) {
2665
+ const ws = {
2666
+ id: w.id,
2667
+ created: w.created,
2668
+ state: w.state ?? "active",
2669
+ profiles: w.profiles ?? [],
2670
+ mode: w.mode ?? "trial",
2671
+ mode_ceiling: w.mode_ceiling ?? "production",
2672
+ routing_policy_uri: w.routing_policy_uri,
2673
+ step_up_window_sec: w.step_up_window_sec ?? 300,
2674
+ min_acr: w.min_acr,
2675
+ members: this.newMap(),
2676
+ tasks: this.newMap(),
2677
+ overrides: this.newMap(),
2678
+ whispers: this.newMap(),
2679
+ deliberations: this.newMap(),
2680
+ handoffs: this.newMap(),
2681
+ snapshots: this.newMap(),
2682
+ route_decisions: this.newMap(),
2683
+ idempotency_keys: w.idempotency_keys ?? {},
2684
+ audit: w.audit ?? [],
2685
+ chain_head: w.chain_head,
2686
+ chain_enabled: !!w.chain_enabled
2687
+ };
2688
+ for (const m of w.members ?? []) {
2689
+ if (!Array.isArray(m.keys)) m.keys = [];
2690
+ if (m.paused === void 0) m.paused = false;
2691
+ ws.members.set(m.uri, m);
2692
+ }
2693
+ for (const t of w.tasks ?? []) ws.tasks.set(t.id, t);
2694
+ for (const o of w.overrides ?? []) ws.overrides.set(o.id, o);
2695
+ for (const x of w.whispers ?? [])
2696
+ ws.whispers.set(x.id, x);
2697
+ for (const x of w.deliberations ?? [])
2698
+ ws.deliberations.set(x.id, x);
2699
+ for (const x of w.handoffs ?? [])
2700
+ ws.handoffs.set(x.id, x);
2701
+ for (const x of w.snapshots ?? []) {
2702
+ const content = structuredClone(x.content ?? {
2703
+ workspace: x.workspace,
2704
+ audit_seq: x.audit_seq,
2705
+ include: x.include,
2706
+ state: x.state,
2707
+ ...x.label ? { label: x.label } : {}
2708
+ });
2709
+ if (!x.content) {
2710
+ const oldState = content.state;
2711
+ const state = {};
2712
+ for (const key of ["mode_ceiling", "routing_policy_uri", "audit_seq"]) {
2713
+ if (oldState[key] != null && oldState[key] !== "") state[key] = oldState[key];
2714
+ }
2715
+ for (const [key, fields] of [
2716
+ ["members", ["uri", "type", "role", "scopes"]],
2717
+ ["open_tasks", ["id", "kind", "state", "assignee"]]
2718
+ ]) {
2719
+ const items = oldState[key];
2720
+ if (Array.isArray(items) && items.length) {
2721
+ 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
2722
  }
2321
- content.state = state;
2322
2723
  }
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);
2724
+ content.state = state;
2725
+ }
2726
+ ws.snapshots.set(x.id, {
2727
+ id: x.id,
2728
+ kind: "snapshot",
2729
+ produced_by: x.produced_by ?? x.by,
2730
+ produced_at: x.produced_at ?? x.ts,
2731
+ content_hash: x.content_hash ?? contentHash(content),
2732
+ content
2733
+ });
2335
2734
  }
2735
+ for (const x of w.route_decisions ?? [])
2736
+ ws.route_decisions.set(x.id, x);
2737
+ return ws;
2336
2738
  }
2337
2739
  // -- dispatch ------------------------------------------------------
2338
2740
  dispatch(envelope) {
2339
2741
  this.frozenNow = this.advanceClock();
2742
+ this.tracker.depth++;
2340
2743
  try {
2341
2744
  const response = this._dispatch(envelope);
2342
2745
  if (response.error) this.recordRefusal(envelope, response.error);
2343
2746
  return response;
2344
2747
  } finally {
2748
+ if (--this.tracker.depth === 0) this.flushUnflushed();
2345
2749
  this.frozenNow = void 0;
2346
2750
  }
2347
2751
  }
@@ -2453,7 +2857,7 @@ var Coordinator = class {
2453
2857
  }
2454
2858
  let out;
2455
2859
  try {
2456
- out = handler(params);
2860
+ out = handler(ownCopy(params));
2457
2861
  } catch (e) {
2458
2862
  const msg = e instanceof Error ? e.message : String(e);
2459
2863
  return reply(envelope, { error: rpcError(E.INTERNAL, "Internal error", { detail: msg }) });
@@ -2464,7 +2868,7 @@ var Coordinator = class {
2464
2868
  const ws = this.workspaces.get(wsId);
2465
2869
  if (ws) this.recordAudit(ws, envelope);
2466
2870
  }
2467
- return reply(envelope, { result: out.result });
2871
+ return reply(envelope, { result: ownCopy(out.result) });
2468
2872
  }
2469
2873
  // -- audit ---------------------------------------------------------
2470
2874
  recordAudit(ws, envelope) {
@@ -2708,20 +3112,21 @@ var Coordinator = class {
2708
3112
  routing_policy_uri: p.routing_policy_uri,
2709
3113
  step_up_window_sec: typeof p.step_up_window_sec === "number" ? p.step_up_window_sec : 300,
2710
3114
  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(),
3115
+ members: this.newMap(),
3116
+ tasks: this.newMap(),
3117
+ overrides: this.newMap(),
3118
+ whispers: this.newMap(),
3119
+ deliberations: this.newMap(),
3120
+ handoffs: this.newMap(),
3121
+ snapshots: this.newMap(),
3122
+ route_decisions: this.newMap(),
2719
3123
  idempotency_keys: {},
2720
3124
  audit: [],
2721
3125
  chain_enabled: chainEnabled,
2722
3126
  chain_head: chainEnabled ? ZERO_HASH : void 0
2723
3127
  };
2724
3128
  this.workspaces.set(id, ws);
3129
+ this.persisted.set(ws, { seq: 0, replace: false, keys: /* @__PURE__ */ new Set() });
2725
3130
  return { result: { workspace: id, created: ws.created } };
2726
3131
  }
2727
3132
  opWorkspaceDescribe(p) {
@@ -2742,7 +3147,7 @@ var Coordinator = class {
2742
3147
  max_envelope_bytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES,
2743
3148
  step_up_window_sec: ws.step_up_window_sec,
2744
3149
  profiles: ws.profiles,
2745
- members: Array.from(ws.members.values()).map(memberToDict),
3150
+ members: Array.from(rawValues(ws.members)).map(memberToDict),
2746
3151
  audit_count: ws.audit.length,
2747
3152
  task_count: ws.tasks.size,
2748
3153
  override_count: ws.overrides.size,
@@ -2917,8 +3322,12 @@ var Coordinator = class {
2917
3322
  ws.tasks.set(taskId, task);
2918
3323
  if (typeof p.idempotency_key === "string") {
2919
3324
  ws.idempotency_keys[p.idempotency_key] = taskId;
3325
+ this.markKey(ws, p.idempotency_key);
2920
3326
  const keys = Object.keys(ws.idempotency_keys);
2921
- if (keys.length > MAX_IDEMPOTENCY_KEYS) delete ws.idempotency_keys[keys[0]];
3327
+ if (keys.length > MAX_IDEMPOTENCY_KEYS) {
3328
+ delete ws.idempotency_keys[keys[0]];
3329
+ this.markKey(ws, keys[0]);
3330
+ }
2922
3331
  }
2923
3332
  return { result: { task_id: taskId, state: "created" } };
2924
3333
  }
@@ -2969,7 +3378,7 @@ var Coordinator = class {
2969
3378
  const now = this.now();
2970
3379
  if (!task.review) {
2971
3380
  const producer = p.from;
2972
- const eligible = [...ws.members.entries()].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
3381
+ const eligible = [...rawEntries(ws.members)].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
2973
3382
  if (eligible.length === 0) {
2974
3383
  return { error: rpcError(
2975
3384
  E.NOT_AUTHORISED,
@@ -3534,6 +3943,7 @@ function wrapA2aMessageExchange(coord, workspace, options) {
3534
3943
  MemoryStore,
3535
3944
  PRIVILEGED_METHODS,
3536
3945
  PatchError,
3946
+ StoreConflictError,
3537
3947
  ZERO_HASH,
3538
3948
  applyJsonPatch,
3539
3949
  call,