@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.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
|
-
|
|
804
|
+
held = /* @__PURE__ */ new Map();
|
|
711
805
|
load() {
|
|
712
|
-
|
|
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.
|
|
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.
|
|
851
|
+
this.held.delete(id);
|
|
719
852
|
}
|
|
720
|
-
|
|
721
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
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
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
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:
|
|
2712
|
-
tasks:
|
|
2713
|
-
overrides:
|
|
2714
|
-
whispers:
|
|
2715
|
-
deliberations:
|
|
2716
|
-
handoffs:
|
|
2717
|
-
snapshots:
|
|
2718
|
-
route_decisions:
|
|
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
|
|
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)
|
|
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
|
|
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,
|