@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/{api-BkvwN42r.d.cts → api-59aU01Bs.d.cts} +48 -3
- package/dist/{api-Ddy9fqkY.d.ts → api-CxciGFHA.d.ts} +48 -3
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/index.cjs +535 -125
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +534 -125
- package/dist/index.js.map +1 -1
- package/dist/storage/sqlite.cjs +270 -34
- package/dist/storage/sqlite.cjs.map +1 -1
- package/dist/storage/sqlite.d.cts +60 -10
- package/dist/storage/sqlite.d.ts +60 -10
- package/dist/storage/sqlite.js +270 -34
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/storage/store.cjs +150 -9
- package/dist/storage/store.cjs.map +1 -1
- package/dist/storage/store.d.cts +118 -40
- package/dist/storage/store.d.ts +118 -40
- package/dist/storage/store.js +143 -8
- package/dist/storage/store.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
751
|
+
held = /* @__PURE__ */ new Map();
|
|
659
752
|
load() {
|
|
660
|
-
|
|
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.
|
|
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.
|
|
798
|
+
this.held.delete(id);
|
|
667
799
|
}
|
|
668
|
-
|
|
669
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
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
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
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:
|
|
2660
|
-
tasks:
|
|
2661
|
-
overrides:
|
|
2662
|
-
whispers:
|
|
2663
|
-
deliberations:
|
|
2664
|
-
handoffs:
|
|
2665
|
-
snapshots:
|
|
2666
|
-
route_decisions:
|
|
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
|
|
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)
|
|
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
|
|
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,
|