@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.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,18 +2318,181 @@ var Coordinator = class {
|
|
|
2107
2318
|
this.wsVersions.clear();
|
|
2108
2319
|
this.restore(records.map((r) => r.data));
|
|
2109
2320
|
for (const r of records) this.wsVersions.set(r.id, r.version);
|
|
2321
|
+
for (const ws of this.workspaces.values()) this.markWritten(ws);
|
|
2110
2322
|
}
|
|
2111
|
-
/**
|
|
2323
|
+
/** A map for a workspace collection, tracked for stores that implement `apply`. */
|
|
2324
|
+
newMap() {
|
|
2325
|
+
return new TrackedMap(this.tracker);
|
|
2326
|
+
}
|
|
2327
|
+
/** Record that the store holds this workspace exactly as it is in memory. */
|
|
2328
|
+
markWritten(ws) {
|
|
2329
|
+
for (const name of COLLECTIONS) {
|
|
2330
|
+
const map = ws[name];
|
|
2331
|
+
if (map instanceof TrackedMap) map.dirty.clear();
|
|
2332
|
+
}
|
|
2333
|
+
this.persisted.set(ws, { seq: ws.audit.length, replace: false, keys: /* @__PURE__ */ new Set() });
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* What the store holds of a workspace. A workspace this instance did not
|
|
2337
|
+
* create or load, such as one an application put in `workspaces` itself,
|
|
2338
|
+
* or one whose maps an application replaced, is tracked from here on and
|
|
2339
|
+
* written whole once.
|
|
2340
|
+
*/
|
|
2341
|
+
persistedOf(ws) {
|
|
2342
|
+
let p = this.persisted.get(ws);
|
|
2343
|
+
let untracked = false;
|
|
2344
|
+
for (const name of COLLECTIONS) {
|
|
2345
|
+
const map = ws[name];
|
|
2346
|
+
if (map instanceof TrackedMap && map.tracker === this.tracker) continue;
|
|
2347
|
+
untracked = true;
|
|
2348
|
+
const tracked = new TrackedMap(this.tracker);
|
|
2349
|
+
for (const [key, value] of Map.prototype.entries.call(map)) {
|
|
2350
|
+
tracked.set(key, value);
|
|
2351
|
+
}
|
|
2352
|
+
ws[name] = tracked;
|
|
2353
|
+
}
|
|
2354
|
+
if (!p || untracked) {
|
|
2355
|
+
p = { seq: p?.seq ?? 0, replace: true, keys: /* @__PURE__ */ new Set(), updatedAt: p?.updatedAt };
|
|
2356
|
+
this.persisted.set(ws, p);
|
|
2357
|
+
}
|
|
2358
|
+
return p;
|
|
2359
|
+
}
|
|
2360
|
+
/** Record that a task.create idempotency key was written or removed. */
|
|
2361
|
+
markKey(ws, key) {
|
|
2362
|
+
if (this.tracker.enabled) this.persistedOf(ws).keys.add(key);
|
|
2363
|
+
}
|
|
2364
|
+
/** After an entry is appended: write now, or when the call being handled ends. */
|
|
2112
2365
|
persist(ws) {
|
|
2366
|
+
if (!this.tracker.enabled) {
|
|
2367
|
+
this.persistSnapshot(ws);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
this.persistedOf(ws).updatedAt = this.now();
|
|
2371
|
+
if (this.tracker.depth > 0) this.unflushed.add(ws);
|
|
2372
|
+
else this.flush(ws);
|
|
2373
|
+
}
|
|
2374
|
+
flushUnflushed() {
|
|
2375
|
+
if (!this.unflushed.size) return;
|
|
2376
|
+
const list = [...this.unflushed];
|
|
2377
|
+
this.unflushed.clear();
|
|
2378
|
+
for (const ws of list) this.flush(ws);
|
|
2379
|
+
}
|
|
2380
|
+
/** Write what changed in one workspace since its last write. */
|
|
2381
|
+
flush(ws) {
|
|
2382
|
+
const pending = this.saveChains.get(ws.id);
|
|
2383
|
+
if (pending) {
|
|
2384
|
+
if (this.flushQueued.has(ws)) return;
|
|
2385
|
+
this.flushQueued.add(ws);
|
|
2386
|
+
this.chainWrite(ws.id, pending.then(() => {
|
|
2387
|
+
this.flushQueued.delete(ws);
|
|
2388
|
+
return this.writeChange(ws);
|
|
2389
|
+
}));
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
const result = this.writeChange(ws);
|
|
2393
|
+
if (result) this.chainWrite(ws.id, result);
|
|
2394
|
+
}
|
|
2395
|
+
chainWrite(id, work) {
|
|
2396
|
+
const guarded = work.catch(() => void 0);
|
|
2397
|
+
this.saveChains.set(id, guarded);
|
|
2398
|
+
void guarded.then(() => {
|
|
2399
|
+
if (this.saveChains.get(id) === guarded) this.saveChains.delete(id);
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
writeChange(ws) {
|
|
2403
|
+
const p = this.persistedOf(ws);
|
|
2404
|
+
let change;
|
|
2405
|
+
try {
|
|
2406
|
+
change = this.takeChange(ws, p);
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
p.replace = true;
|
|
2409
|
+
this.reportStoreError(err, { id: ws.id, version: ws.audit.length });
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
if (!change) return;
|
|
2413
|
+
const taken = change;
|
|
2414
|
+
const done = () => {
|
|
2415
|
+
p.seq = taken.version;
|
|
2416
|
+
if (taken.replace) p.replace = false;
|
|
2417
|
+
};
|
|
2418
|
+
const failed = (err) => {
|
|
2419
|
+
this.giveBack(ws, p, taken);
|
|
2420
|
+
this.reportStoreError(err, { id: taken.id, version: taken.version });
|
|
2421
|
+
};
|
|
2422
|
+
let result;
|
|
2423
|
+
try {
|
|
2424
|
+
result = this.store.apply(taken);
|
|
2425
|
+
} catch (err) {
|
|
2426
|
+
failed(err);
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
if (isThenable(result)) return Promise.resolve(result).then(done, failed);
|
|
2430
|
+
done();
|
|
2431
|
+
}
|
|
2432
|
+
/** The change since the last write, taking the recorded keys with it. */
|
|
2433
|
+
takeChange(ws, p) {
|
|
2434
|
+
const replace = p.replace;
|
|
2435
|
+
const objects = {};
|
|
2436
|
+
for (const name of COLLECTIONS) {
|
|
2437
|
+
const map = ws[name];
|
|
2438
|
+
if (replace) {
|
|
2439
|
+
const all = {};
|
|
2440
|
+
for (const [key, value] of Map.prototype.entries.call(map)) {
|
|
2441
|
+
all[key] = value;
|
|
2442
|
+
}
|
|
2443
|
+
objects[name] = all;
|
|
2444
|
+
} else if (map.dirty.size) {
|
|
2445
|
+
const changed = {};
|
|
2446
|
+
for (const key of map.dirty) changed[key] = map.peek(key) ?? null;
|
|
2447
|
+
objects[name] = changed;
|
|
2448
|
+
}
|
|
2449
|
+
map.dirty.clear();
|
|
2450
|
+
}
|
|
2451
|
+
if (replace) {
|
|
2452
|
+
objects[IDEMPOTENCY_KEYS] = { ...ws.idempotency_keys };
|
|
2453
|
+
} else if (p.keys.size) {
|
|
2454
|
+
const changed = {};
|
|
2455
|
+
for (const key of p.keys) changed[key] = ws.idempotency_keys[key] ?? null;
|
|
2456
|
+
objects[IDEMPOTENCY_KEYS] = changed;
|
|
2457
|
+
}
|
|
2458
|
+
p.keys.clear();
|
|
2459
|
+
const entries = ws.audit.slice(replace ? 0 : p.seq);
|
|
2460
|
+
if (!replace && !entries.length && !Object.keys(objects).length) return void 0;
|
|
2461
|
+
return {
|
|
2462
|
+
id: ws.id,
|
|
2463
|
+
base: replace ? 0 : p.seq,
|
|
2464
|
+
version: ws.audit.length,
|
|
2465
|
+
updated_at: p.updatedAt ?? ws.audit[ws.audit.length - 1]?.arrived ?? ws.created,
|
|
2466
|
+
meta: this.metaOf(ws),
|
|
2467
|
+
entries,
|
|
2468
|
+
objects,
|
|
2469
|
+
...replace ? { replace: true } : {}
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
/** A failed write's keys go back, so the next write carries them again. */
|
|
2473
|
+
giveBack(ws, p, change) {
|
|
2474
|
+
if (change.replace) {
|
|
2475
|
+
p.replace = true;
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
for (const [name, items] of Object.entries(change.objects)) {
|
|
2479
|
+
if (name === IDEMPOTENCY_KEYS) {
|
|
2480
|
+
for (const key of Object.keys(items)) p.keys.add(key);
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
const map = ws[name];
|
|
2484
|
+
if (map instanceof TrackedMap) for (const key of Object.keys(items)) map.dirty.add(key);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
/** Persist the whole workspace, for a store that implements only `save`. */
|
|
2488
|
+
persistSnapshot(ws) {
|
|
2113
2489
|
const next = (this.wsVersions.get(ws.id) ?? 0) + 1;
|
|
2114
2490
|
this.wsVersions.set(ws.id, next);
|
|
2115
2491
|
let record;
|
|
2116
2492
|
try {
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
record = { id: ws.id, data, version: next, updated_at: this.now() };
|
|
2121
|
-
} catch {
|
|
2493
|
+
record = { id: ws.id, data: this.snapshotOf(ws), version: next, updated_at: this.now() };
|
|
2494
|
+
} catch (err) {
|
|
2495
|
+
this.reportStoreError(err, { id: ws.id, version: next });
|
|
2122
2496
|
return;
|
|
2123
2497
|
}
|
|
2124
2498
|
const pending = this.saveChains.get(ws.id);
|
|
@@ -2133,8 +2507,8 @@ var Coordinator = class {
|
|
|
2133
2507
|
this.reportStoreError(err, record);
|
|
2134
2508
|
return;
|
|
2135
2509
|
}
|
|
2136
|
-
if (result
|
|
2137
|
-
this.trackSave(result, record);
|
|
2510
|
+
if (isThenable(result)) {
|
|
2511
|
+
this.trackSave(Promise.resolve(result), record);
|
|
2138
2512
|
}
|
|
2139
2513
|
}
|
|
2140
2514
|
trackSave(save, record) {
|
|
@@ -2152,7 +2526,10 @@ var Coordinator = class {
|
|
|
2152
2526
|
this.options.onStoreError(err, { id: record.id, version: record.version });
|
|
2153
2527
|
} catch {
|
|
2154
2528
|
}
|
|
2529
|
+
return;
|
|
2155
2530
|
}
|
|
2531
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2532
|
+
console.error(`chap coordinator: writing workspace ${record.id} to the store failed: ${message}`);
|
|
2156
2533
|
}
|
|
2157
2534
|
async drainSaves() {
|
|
2158
2535
|
while (this.saveChains.size) {
|
|
@@ -2226,122 +2603,150 @@ var Coordinator = class {
|
|
|
2226
2603
|
/** Serialise all workspaces to a JSON-safe structure for persistence. */
|
|
2227
2604
|
snapshot() {
|
|
2228
2605
|
const out = [];
|
|
2229
|
-
for (const ws of this.workspaces.values())
|
|
2230
|
-
out.push({
|
|
2231
|
-
id: ws.id,
|
|
2232
|
-
created: ws.created,
|
|
2233
|
-
state: ws.state,
|
|
2234
|
-
profiles: ws.profiles,
|
|
2235
|
-
mode: ws.mode,
|
|
2236
|
-
mode_ceiling: ws.mode_ceiling,
|
|
2237
|
-
routing_policy_uri: ws.routing_policy_uri,
|
|
2238
|
-
step_up_window_sec: ws.step_up_window_sec,
|
|
2239
|
-
min_acr: ws.min_acr,
|
|
2240
|
-
members: Array.from(ws.members.values()),
|
|
2241
|
-
tasks: Array.from(ws.tasks.values()),
|
|
2242
|
-
overrides: Array.from(ws.overrides.values()),
|
|
2243
|
-
whispers: Array.from(ws.whispers.values()),
|
|
2244
|
-
deliberations: Array.from(ws.deliberations.values()),
|
|
2245
|
-
handoffs: Array.from(ws.handoffs.values()),
|
|
2246
|
-
snapshots: Array.from(ws.snapshots.values()),
|
|
2247
|
-
route_decisions: Array.from(ws.route_decisions.values()),
|
|
2248
|
-
idempotency_keys: ws.idempotency_keys,
|
|
2249
|
-
audit: ws.audit,
|
|
2250
|
-
chain_head: ws.chain_head,
|
|
2251
|
-
chain_enabled: ws.chain_enabled
|
|
2252
|
-
});
|
|
2253
|
-
}
|
|
2606
|
+
for (const ws of this.workspaces.values()) out.push(this.snapshotOf(ws));
|
|
2254
2607
|
return out;
|
|
2255
2608
|
}
|
|
2609
|
+
/** The workspace's own fields: everything except its collections and log. */
|
|
2610
|
+
metaOf(ws) {
|
|
2611
|
+
return {
|
|
2612
|
+
id: ws.id,
|
|
2613
|
+
created: ws.created,
|
|
2614
|
+
state: ws.state,
|
|
2615
|
+
profiles: ws.profiles,
|
|
2616
|
+
mode: ws.mode,
|
|
2617
|
+
mode_ceiling: ws.mode_ceiling,
|
|
2618
|
+
routing_policy_uri: ws.routing_policy_uri,
|
|
2619
|
+
step_up_window_sec: ws.step_up_window_sec,
|
|
2620
|
+
min_acr: ws.min_acr,
|
|
2621
|
+
chain_head: ws.chain_head,
|
|
2622
|
+
chain_enabled: ws.chain_enabled
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
/** One workspace as `snapshot()` holds it. Reads nothing as a change. */
|
|
2626
|
+
snapshotOf(ws) {
|
|
2627
|
+
const { chain_head, chain_enabled, ...own } = this.metaOf(ws);
|
|
2628
|
+
return {
|
|
2629
|
+
...own,
|
|
2630
|
+
members: Array.from(rawValues(ws.members)),
|
|
2631
|
+
tasks: Array.from(rawValues(ws.tasks)),
|
|
2632
|
+
overrides: Array.from(rawValues(ws.overrides)),
|
|
2633
|
+
whispers: Array.from(rawValues(ws.whispers)),
|
|
2634
|
+
deliberations: Array.from(rawValues(ws.deliberations)),
|
|
2635
|
+
handoffs: Array.from(rawValues(ws.handoffs)),
|
|
2636
|
+
snapshots: Array.from(rawValues(ws.snapshots)),
|
|
2637
|
+
route_decisions: Array.from(rawValues(ws.route_decisions)),
|
|
2638
|
+
idempotency_keys: { ...ws.idempotency_keys },
|
|
2639
|
+
audit: ws.audit,
|
|
2640
|
+
chain_head,
|
|
2641
|
+
chain_enabled
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2256
2644
|
/** Restore from a snapshot produced by ``snapshot()``. Replaces
|
|
2257
2645
|
* any existing in-memory workspaces. */
|
|
2258
2646
|
restore(data) {
|
|
2259
2647
|
this.workspaces.clear();
|
|
2648
|
+
this.unflushed.clear();
|
|
2260
2649
|
if (!Array.isArray(data)) return;
|
|
2261
2650
|
for (const w of data) {
|
|
2262
|
-
|
|
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
|
-
|
|
2651
|
+
let ws;
|
|
2652
|
+
try {
|
|
2653
|
+
ws = this.rebuild(w);
|
|
2654
|
+
} catch (err) {
|
|
2655
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2656
|
+
console.warn(`chap coordinator: workspace ${String(w?.id)} could not be restored: ${message}`);
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
this.workspaces.set(ws.id, ws);
|
|
2660
|
+
for (const name of COLLECTIONS) ws[name].dirty.clear();
|
|
2661
|
+
this.persisted.set(ws, { seq: 0, replace: true, keys: /* @__PURE__ */ new Set() });
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
/** One workspace from a snapshot entry, its maps tracked. */
|
|
2665
|
+
rebuild(w) {
|
|
2666
|
+
const ws = {
|
|
2667
|
+
id: w.id,
|
|
2668
|
+
created: w.created,
|
|
2669
|
+
state: w.state ?? "active",
|
|
2670
|
+
profiles: w.profiles ?? [],
|
|
2671
|
+
mode: w.mode ?? "trial",
|
|
2672
|
+
mode_ceiling: w.mode_ceiling ?? "production",
|
|
2673
|
+
routing_policy_uri: w.routing_policy_uri,
|
|
2674
|
+
step_up_window_sec: w.step_up_window_sec ?? 300,
|
|
2675
|
+
min_acr: w.min_acr,
|
|
2676
|
+
members: this.newMap(),
|
|
2677
|
+
tasks: this.newMap(),
|
|
2678
|
+
overrides: this.newMap(),
|
|
2679
|
+
whispers: this.newMap(),
|
|
2680
|
+
deliberations: this.newMap(),
|
|
2681
|
+
handoffs: this.newMap(),
|
|
2682
|
+
snapshots: this.newMap(),
|
|
2683
|
+
route_decisions: this.newMap(),
|
|
2684
|
+
idempotency_keys: w.idempotency_keys ?? {},
|
|
2685
|
+
audit: w.audit ?? [],
|
|
2686
|
+
chain_head: w.chain_head,
|
|
2687
|
+
chain_enabled: !!w.chain_enabled
|
|
2688
|
+
};
|
|
2689
|
+
for (const m of w.members ?? []) {
|
|
2690
|
+
if (!Array.isArray(m.keys)) m.keys = [];
|
|
2691
|
+
if (m.paused === void 0) m.paused = false;
|
|
2692
|
+
ws.members.set(m.uri, m);
|
|
2693
|
+
}
|
|
2694
|
+
for (const t of w.tasks ?? []) ws.tasks.set(t.id, t);
|
|
2695
|
+
for (const o of w.overrides ?? []) ws.overrides.set(o.id, o);
|
|
2696
|
+
for (const x of w.whispers ?? [])
|
|
2697
|
+
ws.whispers.set(x.id, x);
|
|
2698
|
+
for (const x of w.deliberations ?? [])
|
|
2699
|
+
ws.deliberations.set(x.id, x);
|
|
2700
|
+
for (const x of w.handoffs ?? [])
|
|
2701
|
+
ws.handoffs.set(x.id, x);
|
|
2702
|
+
for (const x of w.snapshots ?? []) {
|
|
2703
|
+
const content = structuredClone(x.content ?? {
|
|
2704
|
+
workspace: x.workspace,
|
|
2705
|
+
audit_seq: x.audit_seq,
|
|
2706
|
+
include: x.include,
|
|
2707
|
+
state: x.state,
|
|
2708
|
+
...x.label ? { label: x.label } : {}
|
|
2709
|
+
});
|
|
2710
|
+
if (!x.content) {
|
|
2711
|
+
const oldState = content.state;
|
|
2712
|
+
const state = {};
|
|
2713
|
+
for (const key of ["mode_ceiling", "routing_policy_uri", "audit_seq"]) {
|
|
2714
|
+
if (oldState[key] != null && oldState[key] !== "") state[key] = oldState[key];
|
|
2715
|
+
}
|
|
2716
|
+
for (const [key, fields] of [
|
|
2717
|
+
["members", ["uri", "type", "role", "scopes"]],
|
|
2718
|
+
["open_tasks", ["id", "kind", "state", "assignee"]]
|
|
2719
|
+
]) {
|
|
2720
|
+
const items = oldState[key];
|
|
2721
|
+
if (Array.isArray(items) && items.length) {
|
|
2722
|
+
state[key] = items.map((item) => Object.fromEntries(fields.filter((field) => item[field] != null && item[field] !== "" && (!Array.isArray(item[field]) || item[field].length > 0)).map((field) => [field, item[field]])));
|
|
2320
2723
|
}
|
|
2321
|
-
content.state = state;
|
|
2322
2724
|
}
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
ws.route_decisions.set(x.id, x);
|
|
2334
|
-
this.workspaces.set(ws.id, ws);
|
|
2725
|
+
content.state = state;
|
|
2726
|
+
}
|
|
2727
|
+
ws.snapshots.set(x.id, {
|
|
2728
|
+
id: x.id,
|
|
2729
|
+
kind: "snapshot",
|
|
2730
|
+
produced_by: x.produced_by ?? x.by,
|
|
2731
|
+
produced_at: x.produced_at ?? x.ts,
|
|
2732
|
+
content_hash: x.content_hash ?? contentHash(content),
|
|
2733
|
+
content
|
|
2734
|
+
});
|
|
2335
2735
|
}
|
|
2736
|
+
for (const x of w.route_decisions ?? [])
|
|
2737
|
+
ws.route_decisions.set(x.id, x);
|
|
2738
|
+
return ws;
|
|
2336
2739
|
}
|
|
2337
2740
|
// -- dispatch ------------------------------------------------------
|
|
2338
2741
|
dispatch(envelope) {
|
|
2339
2742
|
this.frozenNow = this.advanceClock();
|
|
2743
|
+
this.tracker.depth++;
|
|
2340
2744
|
try {
|
|
2341
2745
|
const response = this._dispatch(envelope);
|
|
2342
2746
|
if (response.error) this.recordRefusal(envelope, response.error);
|
|
2343
2747
|
return response;
|
|
2344
2748
|
} finally {
|
|
2749
|
+
if (--this.tracker.depth === 0) this.flushUnflushed();
|
|
2345
2750
|
this.frozenNow = void 0;
|
|
2346
2751
|
}
|
|
2347
2752
|
}
|
|
@@ -2453,7 +2858,7 @@ var Coordinator = class {
|
|
|
2453
2858
|
}
|
|
2454
2859
|
let out;
|
|
2455
2860
|
try {
|
|
2456
|
-
out = handler(params);
|
|
2861
|
+
out = handler(ownCopy(params));
|
|
2457
2862
|
} catch (e) {
|
|
2458
2863
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2459
2864
|
return reply(envelope, { error: rpcError(E.INTERNAL, "Internal error", { detail: msg }) });
|
|
@@ -2464,7 +2869,7 @@ var Coordinator = class {
|
|
|
2464
2869
|
const ws = this.workspaces.get(wsId);
|
|
2465
2870
|
if (ws) this.recordAudit(ws, envelope);
|
|
2466
2871
|
}
|
|
2467
|
-
return reply(envelope, { result: out.result });
|
|
2872
|
+
return reply(envelope, { result: ownCopy(out.result) });
|
|
2468
2873
|
}
|
|
2469
2874
|
// -- audit ---------------------------------------------------------
|
|
2470
2875
|
recordAudit(ws, envelope) {
|
|
@@ -2708,20 +3113,21 @@ var Coordinator = class {
|
|
|
2708
3113
|
routing_policy_uri: p.routing_policy_uri,
|
|
2709
3114
|
step_up_window_sec: typeof p.step_up_window_sec === "number" ? p.step_up_window_sec : 300,
|
|
2710
3115
|
min_acr: p.min_acr,
|
|
2711
|
-
members:
|
|
2712
|
-
tasks:
|
|
2713
|
-
overrides:
|
|
2714
|
-
whispers:
|
|
2715
|
-
deliberations:
|
|
2716
|
-
handoffs:
|
|
2717
|
-
snapshots:
|
|
2718
|
-
route_decisions:
|
|
3116
|
+
members: this.newMap(),
|
|
3117
|
+
tasks: this.newMap(),
|
|
3118
|
+
overrides: this.newMap(),
|
|
3119
|
+
whispers: this.newMap(),
|
|
3120
|
+
deliberations: this.newMap(),
|
|
3121
|
+
handoffs: this.newMap(),
|
|
3122
|
+
snapshots: this.newMap(),
|
|
3123
|
+
route_decisions: this.newMap(),
|
|
2719
3124
|
idempotency_keys: {},
|
|
2720
3125
|
audit: [],
|
|
2721
3126
|
chain_enabled: chainEnabled,
|
|
2722
3127
|
chain_head: chainEnabled ? ZERO_HASH : void 0
|
|
2723
3128
|
};
|
|
2724
3129
|
this.workspaces.set(id, ws);
|
|
3130
|
+
this.persisted.set(ws, { seq: 0, replace: false, keys: /* @__PURE__ */ new Set() });
|
|
2725
3131
|
return { result: { workspace: id, created: ws.created } };
|
|
2726
3132
|
}
|
|
2727
3133
|
opWorkspaceDescribe(p) {
|
|
@@ -2742,7 +3148,7 @@ var Coordinator = class {
|
|
|
2742
3148
|
max_envelope_bytes: this.options.maxEnvelopeBytes ?? DEFAULT_MAX_ENVELOPE_BYTES,
|
|
2743
3149
|
step_up_window_sec: ws.step_up_window_sec,
|
|
2744
3150
|
profiles: ws.profiles,
|
|
2745
|
-
members: Array.from(ws.members
|
|
3151
|
+
members: Array.from(rawValues(ws.members)).map(memberToDict),
|
|
2746
3152
|
audit_count: ws.audit.length,
|
|
2747
3153
|
task_count: ws.tasks.size,
|
|
2748
3154
|
override_count: ws.overrides.size,
|
|
@@ -2917,8 +3323,12 @@ var Coordinator = class {
|
|
|
2917
3323
|
ws.tasks.set(taskId, task);
|
|
2918
3324
|
if (typeof p.idempotency_key === "string") {
|
|
2919
3325
|
ws.idempotency_keys[p.idempotency_key] = taskId;
|
|
3326
|
+
this.markKey(ws, p.idempotency_key);
|
|
2920
3327
|
const keys = Object.keys(ws.idempotency_keys);
|
|
2921
|
-
if (keys.length > MAX_IDEMPOTENCY_KEYS)
|
|
3328
|
+
if (keys.length > MAX_IDEMPOTENCY_KEYS) {
|
|
3329
|
+
delete ws.idempotency_keys[keys[0]];
|
|
3330
|
+
this.markKey(ws, keys[0]);
|
|
3331
|
+
}
|
|
2922
3332
|
}
|
|
2923
3333
|
return { result: { task_id: taskId, state: "created" } };
|
|
2924
3334
|
}
|
|
@@ -2969,7 +3379,7 @@ var Coordinator = class {
|
|
|
2969
3379
|
const now = this.now();
|
|
2970
3380
|
if (!task.review) {
|
|
2971
3381
|
const producer = p.from;
|
|
2972
|
-
const eligible = [...ws.members
|
|
3382
|
+
const eligible = [...rawEntries(ws.members)].filter(([uri, member]) => uri !== producer && uri !== task.assignee && member.type === "human").map(([uri]) => uri);
|
|
2973
3383
|
if (eligible.length === 0) {
|
|
2974
3384
|
return { error: rpcError(
|
|
2975
3385
|
E.NOT_AUTHORISED,
|
|
@@ -3534,6 +3944,7 @@ function wrapA2aMessageExchange(coord, workspace, options) {
|
|
|
3534
3944
|
MemoryStore,
|
|
3535
3945
|
PRIVILEGED_METHODS,
|
|
3536
3946
|
PatchError,
|
|
3947
|
+
StoreConflictError,
|
|
3537
3948
|
ZERO_HASH,
|
|
3538
3949
|
applyJsonPatch,
|
|
3539
3950
|
call,
|