@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/{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 +537 -126
- 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 +536 -126
- 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,18 +2265,181 @@ var Coordinator = class {
|
|
|
2055
2265
|
this.wsVersions.clear();
|
|
2056
2266
|
this.restore(records.map((r) => r.data));
|
|
2057
2267
|
for (const r of records) this.wsVersions.set(r.id, r.version);
|
|
2268
|
+
for (const ws of this.workspaces.values()) this.markWritten(ws);
|
|
2058
2269
|
}
|
|
2059
|
-
/**
|
|
2270
|
+
/** A map for a workspace collection, tracked for stores that implement `apply`. */
|
|
2271
|
+
newMap() {
|
|
2272
|
+
return new TrackedMap(this.tracker);
|
|
2273
|
+
}
|
|
2274
|
+
/** Record that the store holds this workspace exactly as it is in memory. */
|
|
2275
|
+
markWritten(ws) {
|
|
2276
|
+
for (const name of COLLECTIONS) {
|
|
2277
|
+
const map = ws[name];
|
|
2278
|
+
if (map instanceof TrackedMap) map.dirty.clear();
|
|
2279
|
+
}
|
|
2280
|
+
this.persisted.set(ws, { seq: ws.audit.length, replace: false, keys: /* @__PURE__ */ new Set() });
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* What the store holds of a workspace. A workspace this instance did not
|
|
2284
|
+
* create or load, such as one an application put in `workspaces` itself,
|
|
2285
|
+
* or one whose maps an application replaced, is tracked from here on and
|
|
2286
|
+
* written whole once.
|
|
2287
|
+
*/
|
|
2288
|
+
persistedOf(ws) {
|
|
2289
|
+
let p = this.persisted.get(ws);
|
|
2290
|
+
let untracked = false;
|
|
2291
|
+
for (const name of COLLECTIONS) {
|
|
2292
|
+
const map = ws[name];
|
|
2293
|
+
if (map instanceof TrackedMap && map.tracker === this.tracker) continue;
|
|
2294
|
+
untracked = true;
|
|
2295
|
+
const tracked = new TrackedMap(this.tracker);
|
|
2296
|
+
for (const [key, value] of Map.prototype.entries.call(map)) {
|
|
2297
|
+
tracked.set(key, value);
|
|
2298
|
+
}
|
|
2299
|
+
ws[name] = tracked;
|
|
2300
|
+
}
|
|
2301
|
+
if (!p || untracked) {
|
|
2302
|
+
p = { seq: p?.seq ?? 0, replace: true, keys: /* @__PURE__ */ new Set(), updatedAt: p?.updatedAt };
|
|
2303
|
+
this.persisted.set(ws, p);
|
|
2304
|
+
}
|
|
2305
|
+
return p;
|
|
2306
|
+
}
|
|
2307
|
+
/** Record that a task.create idempotency key was written or removed. */
|
|
2308
|
+
markKey(ws, key) {
|
|
2309
|
+
if (this.tracker.enabled) this.persistedOf(ws).keys.add(key);
|
|
2310
|
+
}
|
|
2311
|
+
/** After an entry is appended: write now, or when the call being handled ends. */
|
|
2060
2312
|
persist(ws) {
|
|
2313
|
+
if (!this.tracker.enabled) {
|
|
2314
|
+
this.persistSnapshot(ws);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
this.persistedOf(ws).updatedAt = this.now();
|
|
2318
|
+
if (this.tracker.depth > 0) this.unflushed.add(ws);
|
|
2319
|
+
else this.flush(ws);
|
|
2320
|
+
}
|
|
2321
|
+
flushUnflushed() {
|
|
2322
|
+
if (!this.unflushed.size) return;
|
|
2323
|
+
const list = [...this.unflushed];
|
|
2324
|
+
this.unflushed.clear();
|
|
2325
|
+
for (const ws of list) this.flush(ws);
|
|
2326
|
+
}
|
|
2327
|
+
/** Write what changed in one workspace since its last write. */
|
|
2328
|
+
flush(ws) {
|
|
2329
|
+
const pending = this.saveChains.get(ws.id);
|
|
2330
|
+
if (pending) {
|
|
2331
|
+
if (this.flushQueued.has(ws)) return;
|
|
2332
|
+
this.flushQueued.add(ws);
|
|
2333
|
+
this.chainWrite(ws.id, pending.then(() => {
|
|
2334
|
+
this.flushQueued.delete(ws);
|
|
2335
|
+
return this.writeChange(ws);
|
|
2336
|
+
}));
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
const result = this.writeChange(ws);
|
|
2340
|
+
if (result) this.chainWrite(ws.id, result);
|
|
2341
|
+
}
|
|
2342
|
+
chainWrite(id, work) {
|
|
2343
|
+
const guarded = work.catch(() => void 0);
|
|
2344
|
+
this.saveChains.set(id, guarded);
|
|
2345
|
+
void guarded.then(() => {
|
|
2346
|
+
if (this.saveChains.get(id) === guarded) this.saveChains.delete(id);
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
writeChange(ws) {
|
|
2350
|
+
const p = this.persistedOf(ws);
|
|
2351
|
+
let change;
|
|
2352
|
+
try {
|
|
2353
|
+
change = this.takeChange(ws, p);
|
|
2354
|
+
} catch (err) {
|
|
2355
|
+
p.replace = true;
|
|
2356
|
+
this.reportStoreError(err, { id: ws.id, version: ws.audit.length });
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
if (!change) return;
|
|
2360
|
+
const taken = change;
|
|
2361
|
+
const done = () => {
|
|
2362
|
+
p.seq = taken.version;
|
|
2363
|
+
if (taken.replace) p.replace = false;
|
|
2364
|
+
};
|
|
2365
|
+
const failed = (err) => {
|
|
2366
|
+
this.giveBack(ws, p, taken);
|
|
2367
|
+
this.reportStoreError(err, { id: taken.id, version: taken.version });
|
|
2368
|
+
};
|
|
2369
|
+
let result;
|
|
2370
|
+
try {
|
|
2371
|
+
result = this.store.apply(taken);
|
|
2372
|
+
} catch (err) {
|
|
2373
|
+
failed(err);
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
if (isThenable(result)) return Promise.resolve(result).then(done, failed);
|
|
2377
|
+
done();
|
|
2378
|
+
}
|
|
2379
|
+
/** The change since the last write, taking the recorded keys with it. */
|
|
2380
|
+
takeChange(ws, p) {
|
|
2381
|
+
const replace = p.replace;
|
|
2382
|
+
const objects = {};
|
|
2383
|
+
for (const name of COLLECTIONS) {
|
|
2384
|
+
const map = ws[name];
|
|
2385
|
+
if (replace) {
|
|
2386
|
+
const all = {};
|
|
2387
|
+
for (const [key, value] of Map.prototype.entries.call(map)) {
|
|
2388
|
+
all[key] = value;
|
|
2389
|
+
}
|
|
2390
|
+
objects[name] = all;
|
|
2391
|
+
} else if (map.dirty.size) {
|
|
2392
|
+
const changed = {};
|
|
2393
|
+
for (const key of map.dirty) changed[key] = map.peek(key) ?? null;
|
|
2394
|
+
objects[name] = changed;
|
|
2395
|
+
}
|
|
2396
|
+
map.dirty.clear();
|
|
2397
|
+
}
|
|
2398
|
+
if (replace) {
|
|
2399
|
+
objects[IDEMPOTENCY_KEYS] = { ...ws.idempotency_keys };
|
|
2400
|
+
} else if (p.keys.size) {
|
|
2401
|
+
const changed = {};
|
|
2402
|
+
for (const key of p.keys) changed[key] = ws.idempotency_keys[key] ?? null;
|
|
2403
|
+
objects[IDEMPOTENCY_KEYS] = changed;
|
|
2404
|
+
}
|
|
2405
|
+
p.keys.clear();
|
|
2406
|
+
const entries = ws.audit.slice(replace ? 0 : p.seq);
|
|
2407
|
+
if (!replace && !entries.length && !Object.keys(objects).length) return void 0;
|
|
2408
|
+
return {
|
|
2409
|
+
id: ws.id,
|
|
2410
|
+
base: replace ? 0 : p.seq,
|
|
2411
|
+
version: ws.audit.length,
|
|
2412
|
+
updated_at: p.updatedAt ?? ws.audit[ws.audit.length - 1]?.arrived ?? ws.created,
|
|
2413
|
+
meta: this.metaOf(ws),
|
|
2414
|
+
entries,
|
|
2415
|
+
objects,
|
|
2416
|
+
...replace ? { replace: true } : {}
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
/** A failed write's keys go back, so the next write carries them again. */
|
|
2420
|
+
giveBack(ws, p, change) {
|
|
2421
|
+
if (change.replace) {
|
|
2422
|
+
p.replace = true;
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
for (const [name, items] of Object.entries(change.objects)) {
|
|
2426
|
+
if (name === IDEMPOTENCY_KEYS) {
|
|
2427
|
+
for (const key of Object.keys(items)) p.keys.add(key);
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
const map = ws[name];
|
|
2431
|
+
if (map instanceof TrackedMap) for (const key of Object.keys(items)) map.dirty.add(key);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
/** Persist the whole workspace, for a store that implements only `save`. */
|
|
2435
|
+
persistSnapshot(ws) {
|
|
2061
2436
|
const next = (this.wsVersions.get(ws.id) ?? 0) + 1;
|
|
2062
2437
|
this.wsVersions.set(ws.id, next);
|
|
2063
2438
|
let record;
|
|
2064
2439
|
try {
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
record = { id: ws.id, data, version: next, updated_at: this.now() };
|
|
2069
|
-
} catch {
|
|
2440
|
+
record = { id: ws.id, data: this.snapshotOf(ws), version: next, updated_at: this.now() };
|
|
2441
|
+
} catch (err) {
|
|
2442
|
+
this.reportStoreError(err, { id: ws.id, version: next });
|
|
2070
2443
|
return;
|
|
2071
2444
|
}
|
|
2072
2445
|
const pending = this.saveChains.get(ws.id);
|
|
@@ -2081,8 +2454,8 @@ var Coordinator = class {
|
|
|
2081
2454
|
this.reportStoreError(err, record);
|
|
2082
2455
|
return;
|
|
2083
2456
|
}
|
|
2084
|
-
if (result
|
|
2085
|
-
this.trackSave(result, record);
|
|
2457
|
+
if (isThenable(result)) {
|
|
2458
|
+
this.trackSave(Promise.resolve(result), record);
|
|
2086
2459
|
}
|
|
2087
2460
|
}
|
|
2088
2461
|
trackSave(save, record) {
|
|
@@ -2100,7 +2473,10 @@ var Coordinator = class {
|
|
|
2100
2473
|
this.options.onStoreError(err, { id: record.id, version: record.version });
|
|
2101
2474
|
} catch {
|
|
2102
2475
|
}
|
|
2476
|
+
return;
|
|
2103
2477
|
}
|
|
2478
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2479
|
+
console.error(`chap coordinator: writing workspace ${record.id} to the store failed: ${message}`);
|
|
2104
2480
|
}
|
|
2105
2481
|
async drainSaves() {
|
|
2106
2482
|
while (this.saveChains.size) {
|
|
@@ -2174,122 +2550,150 @@ var Coordinator = class {
|
|
|
2174
2550
|
/** Serialise all workspaces to a JSON-safe structure for persistence. */
|
|
2175
2551
|
snapshot() {
|
|
2176
2552
|
const out = [];
|
|
2177
|
-
for (const ws of this.workspaces.values())
|
|
2178
|
-
out.push({
|
|
2179
|
-
id: ws.id,
|
|
2180
|
-
created: ws.created,
|
|
2181
|
-
state: ws.state,
|
|
2182
|
-
profiles: ws.profiles,
|
|
2183
|
-
mode: ws.mode,
|
|
2184
|
-
mode_ceiling: ws.mode_ceiling,
|
|
2185
|
-
routing_policy_uri: ws.routing_policy_uri,
|
|
2186
|
-
step_up_window_sec: ws.step_up_window_sec,
|
|
2187
|
-
min_acr: ws.min_acr,
|
|
2188
|
-
members: Array.from(ws.members.values()),
|
|
2189
|
-
tasks: Array.from(ws.tasks.values()),
|
|
2190
|
-
overrides: Array.from(ws.overrides.values()),
|
|
2191
|
-
whispers: Array.from(ws.whispers.values()),
|
|
2192
|
-
deliberations: Array.from(ws.deliberations.values()),
|
|
2193
|
-
handoffs: Array.from(ws.handoffs.values()),
|
|
2194
|
-
snapshots: Array.from(ws.snapshots.values()),
|
|
2195
|
-
route_decisions: Array.from(ws.route_decisions.values()),
|
|
2196
|
-
idempotency_keys: ws.idempotency_keys,
|
|
2197
|
-
audit: ws.audit,
|
|
2198
|
-
chain_head: ws.chain_head,
|
|
2199
|
-
chain_enabled: ws.chain_enabled
|
|
2200
|
-
});
|
|
2201
|
-
}
|
|
2553
|
+
for (const ws of this.workspaces.values()) out.push(this.snapshotOf(ws));
|
|
2202
2554
|
return out;
|
|
2203
2555
|
}
|
|
2556
|
+
/** The workspace's own fields: everything except its collections and log. */
|
|
2557
|
+
metaOf(ws) {
|
|
2558
|
+
return {
|
|
2559
|
+
id: ws.id,
|
|
2560
|
+
created: ws.created,
|
|
2561
|
+
state: ws.state,
|
|
2562
|
+
profiles: ws.profiles,
|
|
2563
|
+
mode: ws.mode,
|
|
2564
|
+
mode_ceiling: ws.mode_ceiling,
|
|
2565
|
+
routing_policy_uri: ws.routing_policy_uri,
|
|
2566
|
+
step_up_window_sec: ws.step_up_window_sec,
|
|
2567
|
+
min_acr: ws.min_acr,
|
|
2568
|
+
chain_head: ws.chain_head,
|
|
2569
|
+
chain_enabled: ws.chain_enabled
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
/** One workspace as `snapshot()` holds it. Reads nothing as a change. */
|
|
2573
|
+
snapshotOf(ws) {
|
|
2574
|
+
const { chain_head, chain_enabled, ...own } = this.metaOf(ws);
|
|
2575
|
+
return {
|
|
2576
|
+
...own,
|
|
2577
|
+
members: Array.from(rawValues(ws.members)),
|
|
2578
|
+
tasks: Array.from(rawValues(ws.tasks)),
|
|
2579
|
+
overrides: Array.from(rawValues(ws.overrides)),
|
|
2580
|
+
whispers: Array.from(rawValues(ws.whispers)),
|
|
2581
|
+
deliberations: Array.from(rawValues(ws.deliberations)),
|
|
2582
|
+
handoffs: Array.from(rawValues(ws.handoffs)),
|
|
2583
|
+
snapshots: Array.from(rawValues(ws.snapshots)),
|
|
2584
|
+
route_decisions: Array.from(rawValues(ws.route_decisions)),
|
|
2585
|
+
idempotency_keys: { ...ws.idempotency_keys },
|
|
2586
|
+
audit: ws.audit,
|
|
2587
|
+
chain_head,
|
|
2588
|
+
chain_enabled
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2204
2591
|
/** Restore from a snapshot produced by ``snapshot()``. Replaces
|
|
2205
2592
|
* any existing in-memory workspaces. */
|
|
2206
2593
|
restore(data) {
|
|
2207
2594
|
this.workspaces.clear();
|
|
2595
|
+
this.unflushed.clear();
|
|
2208
2596
|
if (!Array.isArray(data)) return;
|
|
2209
2597
|
for (const w of data) {
|
|
2210
|
-
|
|
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
|
-
|
|
2598
|
+
let ws;
|
|
2599
|
+
try {
|
|
2600
|
+
ws = this.rebuild(w);
|
|
2601
|
+
} catch (err) {
|
|
2602
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2603
|
+
console.warn(`chap coordinator: workspace ${String(w?.id)} could not be restored: ${message}`);
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
this.workspaces.set(ws.id, ws);
|
|
2607
|
+
for (const name of COLLECTIONS) ws[name].dirty.clear();
|
|
2608
|
+
this.persisted.set(ws, { seq: 0, replace: true, keys: /* @__PURE__ */ new Set() });
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
/** One workspace from a snapshot entry, its maps tracked. */
|
|
2612
|
+
rebuild(w) {
|
|
2613
|
+
const ws = {
|
|
2614
|
+
id: w.id,
|
|
2615
|
+
created: w.created,
|
|
2616
|
+
state: w.state ?? "active",
|
|
2617
|
+
profiles: w.profiles ?? [],
|
|
2618
|
+
mode: w.mode ?? "trial",
|
|
2619
|
+
mode_ceiling: w.mode_ceiling ?? "production",
|
|
2620
|
+
routing_policy_uri: w.routing_policy_uri,
|
|
2621
|
+
step_up_window_sec: w.step_up_window_sec ?? 300,
|
|
2622
|
+
min_acr: w.min_acr,
|
|
2623
|
+
members: this.newMap(),
|
|
2624
|
+
tasks: this.newMap(),
|
|
2625
|
+
overrides: this.newMap(),
|
|
2626
|
+
whispers: this.newMap(),
|
|
2627
|
+
deliberations: this.newMap(),
|
|
2628
|
+
handoffs: this.newMap(),
|
|
2629
|
+
snapshots: this.newMap(),
|
|
2630
|
+
route_decisions: this.newMap(),
|
|
2631
|
+
idempotency_keys: w.idempotency_keys ?? {},
|
|
2632
|
+
audit: w.audit ?? [],
|
|
2633
|
+
chain_head: w.chain_head,
|
|
2634
|
+
chain_enabled: !!w.chain_enabled
|
|
2635
|
+
};
|
|
2636
|
+
for (const m of w.members ?? []) {
|
|
2637
|
+
if (!Array.isArray(m.keys)) m.keys = [];
|
|
2638
|
+
if (m.paused === void 0) m.paused = false;
|
|
2639
|
+
ws.members.set(m.uri, m);
|
|
2640
|
+
}
|
|
2641
|
+
for (const t of w.tasks ?? []) ws.tasks.set(t.id, t);
|
|
2642
|
+
for (const o of w.overrides ?? []) ws.overrides.set(o.id, o);
|
|
2643
|
+
for (const x of w.whispers ?? [])
|
|
2644
|
+
ws.whispers.set(x.id, x);
|
|
2645
|
+
for (const x of w.deliberations ?? [])
|
|
2646
|
+
ws.deliberations.set(x.id, x);
|
|
2647
|
+
for (const x of w.handoffs ?? [])
|
|
2648
|
+
ws.handoffs.set(x.id, x);
|
|
2649
|
+
for (const x of w.snapshots ?? []) {
|
|
2650
|
+
const content = structuredClone(x.content ?? {
|
|
2651
|
+
workspace: x.workspace,
|
|
2652
|
+
audit_seq: x.audit_seq,
|
|
2653
|
+
include: x.include,
|
|
2654
|
+
state: x.state,
|
|
2655
|
+
...x.label ? { label: x.label } : {}
|
|
2656
|
+
});
|
|
2657
|
+
if (!x.content) {
|
|
2658
|
+
const oldState = content.state;
|
|
2659
|
+
const state = {};
|
|
2660
|
+
for (const key of ["mode_ceiling", "routing_policy_uri", "audit_seq"]) {
|
|
2661
|
+
if (oldState[key] != null && oldState[key] !== "") state[key] = oldState[key];
|
|
2662
|
+
}
|
|
2663
|
+
for (const [key, fields] of [
|
|
2664
|
+
["members", ["uri", "type", "role", "scopes"]],
|
|
2665
|
+
["open_tasks", ["id", "kind", "state", "assignee"]]
|
|
2666
|
+
]) {
|
|
2667
|
+
const items = oldState[key];
|
|
2668
|
+
if (Array.isArray(items) && items.length) {
|
|
2669
|
+
state[key] = items.map((item) => Object.fromEntries(fields.filter((field) => item[field] != null && item[field] !== "" && (!Array.isArray(item[field]) || item[field].length > 0)).map((field) => [field, item[field]])));
|
|
2268
2670
|
}
|
|
2269
|
-
content.state = state;
|
|
2270
2671
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
ws.route_decisions.set(x.id, x);
|
|
2282
|
-
this.workspaces.set(ws.id, ws);
|
|
2672
|
+
content.state = state;
|
|
2673
|
+
}
|
|
2674
|
+
ws.snapshots.set(x.id, {
|
|
2675
|
+
id: x.id,
|
|
2676
|
+
kind: "snapshot",
|
|
2677
|
+
produced_by: x.produced_by ?? x.by,
|
|
2678
|
+
produced_at: x.produced_at ?? x.ts,
|
|
2679
|
+
content_hash: x.content_hash ?? contentHash(content),
|
|
2680
|
+
content
|
|
2681
|
+
});
|
|
2283
2682
|
}
|
|
2683
|
+
for (const x of w.route_decisions ?? [])
|
|
2684
|
+
ws.route_decisions.set(x.id, x);
|
|
2685
|
+
return ws;
|
|
2284
2686
|
}
|
|
2285
2687
|
// -- dispatch ------------------------------------------------------
|
|
2286
2688
|
dispatch(envelope) {
|
|
2287
2689
|
this.frozenNow = this.advanceClock();
|
|
2690
|
+
this.tracker.depth++;
|
|
2288
2691
|
try {
|
|
2289
2692
|
const response = this._dispatch(envelope);
|
|
2290
2693
|
if (response.error) this.recordRefusal(envelope, response.error);
|
|
2291
2694
|
return response;
|
|
2292
2695
|
} finally {
|
|
2696
|
+
if (--this.tracker.depth === 0) this.flushUnflushed();
|
|
2293
2697
|
this.frozenNow = void 0;
|
|
2294
2698
|
}
|
|
2295
2699
|
}
|
|
@@ -2401,7 +2805,7 @@ var Coordinator = class {
|
|
|
2401
2805
|
}
|
|
2402
2806
|
let out;
|
|
2403
2807
|
try {
|
|
2404
|
-
out = handler(params);
|
|
2808
|
+
out = handler(ownCopy(params));
|
|
2405
2809
|
} catch (e) {
|
|
2406
2810
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2407
2811
|
return reply(envelope, { error: rpcError(E.INTERNAL, "Internal error", { detail: msg }) });
|
|
@@ -2412,7 +2816,7 @@ var Coordinator = class {
|
|
|
2412
2816
|
const ws = this.workspaces.get(wsId);
|
|
2413
2817
|
if (ws) this.recordAudit(ws, envelope);
|
|
2414
2818
|
}
|
|
2415
|
-
return reply(envelope, { result: out.result });
|
|
2819
|
+
return reply(envelope, { result: ownCopy(out.result) });
|
|
2416
2820
|
}
|
|
2417
2821
|
// -- audit ---------------------------------------------------------
|
|
2418
2822
|
recordAudit(ws, envelope) {
|
|
@@ -2656,20 +3060,21 @@ var Coordinator = class {
|
|
|
2656
3060
|
routing_policy_uri: p.routing_policy_uri,
|
|
2657
3061
|
step_up_window_sec: typeof p.step_up_window_sec === "number" ? p.step_up_window_sec : 300,
|
|
2658
3062
|
min_acr: p.min_acr,
|
|
2659
|
-
members:
|
|
2660
|
-
tasks:
|
|
2661
|
-
overrides:
|
|
2662
|
-
whispers:
|
|
2663
|
-
deliberations:
|
|
2664
|
-
handoffs:
|
|
2665
|
-
snapshots:
|
|
2666
|
-
route_decisions:
|
|
3063
|
+
members: this.newMap(),
|
|
3064
|
+
tasks: this.newMap(),
|
|
3065
|
+
overrides: this.newMap(),
|
|
3066
|
+
whispers: this.newMap(),
|
|
3067
|
+
deliberations: this.newMap(),
|
|
3068
|
+
handoffs: this.newMap(),
|
|
3069
|
+
snapshots: this.newMap(),
|
|
3070
|
+
route_decisions: this.newMap(),
|
|
2667
3071
|
idempotency_keys: {},
|
|
2668
3072
|
audit: [],
|
|
2669
3073
|
chain_enabled: chainEnabled,
|
|
2670
3074
|
chain_head: chainEnabled ? ZERO_HASH : void 0
|
|
2671
3075
|
};
|
|
2672
3076
|
this.workspaces.set(id, ws);
|
|
3077
|
+
this.persisted.set(ws, { seq: 0, replace: false, keys: /* @__PURE__ */ new Set() });
|
|
2673
3078
|
return { result: { workspace: id, created: ws.created } };
|
|
2674
3079
|
}
|
|
2675
3080
|
opWorkspaceDescribe(p) {
|
|
@@ -2690,7 +3095,7 @@ var Coordinator = class {
|
|
|
2690
3095
|
max_envelope_bytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES,
|
|
2691
3096
|
step_up_window_sec: ws.step_up_window_sec,
|
|
2692
3097
|
profiles: ws.profiles,
|
|
2693
|
-
members: Array.from(ws.members
|
|
3098
|
+
members: Array.from(rawValues(ws.members)).map(memberToDict),
|
|
2694
3099
|
audit_count: ws.audit.length,
|
|
2695
3100
|
task_count: ws.tasks.size,
|
|
2696
3101
|
override_count: ws.overrides.size,
|
|
@@ -2865,8 +3270,12 @@ var Coordinator = class {
|
|
|
2865
3270
|
ws.tasks.set(taskId, task);
|
|
2866
3271
|
if (typeof p.idempotency_key === "string") {
|
|
2867
3272
|
ws.idempotency_keys[p.idempotency_key] = taskId;
|
|
3273
|
+
this.markKey(ws, p.idempotency_key);
|
|
2868
3274
|
const keys = Object.keys(ws.idempotency_keys);
|
|
2869
|
-
if (keys.length > MAX_IDEMPOTENCY_KEYS)
|
|
3275
|
+
if (keys.length > MAX_IDEMPOTENCY_KEYS) {
|
|
3276
|
+
delete ws.idempotency_keys[keys[0]];
|
|
3277
|
+
this.markKey(ws, keys[0]);
|
|
3278
|
+
}
|
|
2870
3279
|
}
|
|
2871
3280
|
return { result: { task_id: taskId, state: "created" } };
|
|
2872
3281
|
}
|
|
@@ -2917,7 +3326,7 @@ var Coordinator = class {
|
|
|
2917
3326
|
const now = this.now();
|
|
2918
3327
|
if (!task.review) {
|
|
2919
3328
|
const producer = p.from;
|
|
2920
|
-
const eligible = [...ws.members
|
|
3329
|
+
const eligible = [...rawEntries(ws.members)].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
|
|
2921
3330
|
if (eligible.length === 0) {
|
|
2922
3331
|
return { error: rpcError(
|
|
2923
3332
|
E.NOT_AUTHORISED,
|
|
@@ -3481,6 +3890,7 @@ export {
|
|
|
3481
3890
|
MemoryStore,
|
|
3482
3891
|
PRIVILEGED_METHODS,
|
|
3483
3892
|
PatchError,
|
|
3893
|
+
StoreConflictError,
|
|
3484
3894
|
ZERO_HASH,
|
|
3485
3895
|
applyJsonPatch,
|
|
3486
3896
|
call,
|