agents-can-communicate 0.6.3 → 0.7.0
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/README.md +3 -1
- package/bin/entrypoints/hook-output.mjs +12 -0
- package/docs/CAPABILITIES.md +1 -1
- package/docs/CLI.md +37 -0
- package/docs/GETTING_STARTED.md +2 -2
- package/docs/TROUBLESHOOTING.md +4 -3
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +57 -11
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt-writer.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +3 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +6 -0
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +13 -1
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -1
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +18 -0
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/attention.mjs +7 -2
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +16 -0
- package/node_modules/@agents-can-communicate/core/src/prune.mjs +241 -0
- package/node_modules/@agents-can-communicate/core/src/service.mjs +3 -0
- package/node_modules/@agents-can-communicate/core/src/status.mjs +6 -1
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +4 -0
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +60 -7
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +42 -16
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +26 -7
- package/node_modules/@agents-can-communicate/storage-filesystem/src/doomed-directory.mjs +98 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/event-floor.mjs +52 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/reclaim.mjs +272 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/recovery.mjs +54 -7
- package/node_modules/@agents-can-communicate/storage-filesystem/src/stage-sweep.mjs +174 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +97 -5
- package/package.json +1 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { rename } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { EXIT } from "@agents-can-communicate/protocol";
|
|
5
|
+
|
|
6
|
+
import { encode, listDirectoryEntries, publishAtomic, readJsonIfPresent } from "./atomic-json.mjs";
|
|
7
|
+
import { discard, discardLeftovers, doomedName, expired, syncDirectory }
|
|
8
|
+
from "./doomed-directory.mjs";
|
|
9
|
+
import { reclaimRetired } from "./reclaim.mjs";
|
|
10
|
+
import { ensureManagedDirectory } from "./safe-directory.mjs";
|
|
11
|
+
|
|
12
|
+
// One pass never touches more than this many entries, counting both the legacy
|
|
13
|
+
// entries it moves and the entries it discards. A store left unopened can hold
|
|
14
|
+
// as many accepted stages as an unswept tmp/ did - tens of thousands - and an
|
|
15
|
+
// unbounded removal inside a hook is the budget failure this exists to avoid.
|
|
16
|
+
export const SWEEP_BUDGET = 512;
|
|
17
|
+
|
|
18
|
+
// A day between passes. A count-based threshold costs either a full listing of
|
|
19
|
+
// the directory this keeps small, or an fsync'ed write on every transaction. A
|
|
20
|
+
// timestamp costs one read.
|
|
21
|
+
export const SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
// An older ACC keeps renaming accepted stages onto tmp, because it does not
|
|
24
|
+
// know stage exists, so this is a standing rule rather than a one-off
|
|
25
|
+
// migration. They are moved rather than unlinked: joining the doomed directory
|
|
26
|
+
// costs one rename and keeps the no-unlink rule intact. A partial from a failed
|
|
27
|
+
// publication does not end in .published and never moves.
|
|
28
|
+
async function reclaimLegacy(paths, root, detached, budget, deadlineAt) {
|
|
29
|
+
let spent = 0;
|
|
30
|
+
for (const entry of await listDirectoryEntries(paths.tmp, { root })) {
|
|
31
|
+
if (spent >= budget || expired(deadlineAt)) return { spent, drained: false };
|
|
32
|
+
if (!entry.isFile() || !entry.name.endsWith(".published")) continue;
|
|
33
|
+
await rename(path.join(paths.tmp, entry.name), path.join(detached, entry.name));
|
|
34
|
+
spent += 1;
|
|
35
|
+
}
|
|
36
|
+
return { spent, drained: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Empty the store's accepted staging directory.
|
|
41
|
+
*
|
|
42
|
+
* `spent` is what the budget bought, which the caller needs to give whatever
|
|
43
|
+
* runs next the part of the budget this pass did not use.
|
|
44
|
+
*
|
|
45
|
+
* @returns {Promise<{ swept: number, remaining: boolean, spent: number }>}
|
|
46
|
+
*/
|
|
47
|
+
export async function sweepAcceptedStages(paths,
|
|
48
|
+
{ root, limit = SWEEP_BUDGET, deadlineAt } = {}) {
|
|
49
|
+
// Two counters, because they answer different questions. `spent` is what the
|
|
50
|
+
// budget bought: moving a legacy entry and removing one both cost a syscall
|
|
51
|
+
// on the same directory. `swept` is what left the store, and a legacy entry
|
|
52
|
+
// that was only moved has not left it yet. Counting a move as a sweep would
|
|
53
|
+
// report two reclaimed records where one record was reclaimed.
|
|
54
|
+
let spent = 0;
|
|
55
|
+
let swept = 0;
|
|
56
|
+
let remaining = false;
|
|
57
|
+
|
|
58
|
+
// Finish what an interrupted pass left before starting another one. This runs
|
|
59
|
+
// first for a second reason: a pass that stops here never detaches a further
|
|
60
|
+
// directory, so an exhausted budget cannot make them accumulate.
|
|
61
|
+
const leftovers = await discardLeftovers(root, limit, deadlineAt);
|
|
62
|
+
spent += leftovers.spent;
|
|
63
|
+
swept += leftovers.spent;
|
|
64
|
+
if (!leftovers.drained) return { swept, remaining: true, spent };
|
|
65
|
+
|
|
66
|
+
const detached = doomedName(root);
|
|
67
|
+
// A store written by an older version has no stage directory at all, because
|
|
68
|
+
// nothing ever published into one. Creating it before the detach makes that
|
|
69
|
+
// the ordinary case rather than an error, and such a store is exactly the one
|
|
70
|
+
// whose accepted stages are all still in tmp for the reclamation below.
|
|
71
|
+
await ensureManagedDirectory(root, paths.stage);
|
|
72
|
+
await rename(paths.stage, detached);
|
|
73
|
+
await ensureManagedDirectory(root, paths.stage);
|
|
74
|
+
await syncDirectory(root);
|
|
75
|
+
|
|
76
|
+
const reclaimed = await reclaimLegacy(paths, root, detached, limit - spent, deadlineAt);
|
|
77
|
+
spent += reclaimed.spent;
|
|
78
|
+
if (!reclaimed.drained) return { swept, remaining: true, spent };
|
|
79
|
+
|
|
80
|
+
const removed = await discard(detached, root, limit - spent, deadlineAt);
|
|
81
|
+
spent += removed.spent;
|
|
82
|
+
swept += removed.spent;
|
|
83
|
+
if (!removed.drained) remaining = true;
|
|
84
|
+
return { swept, remaining, spent };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const markerPath = paths => path.join(paths.locks, "stage-sweep.json");
|
|
88
|
+
|
|
89
|
+
// A lost lock and a lapsed budget are the same answer: not now, next open. They
|
|
90
|
+
// both arrive as CONFLICT, and neither is a reason to fail the caller.
|
|
91
|
+
async function withLockTolerantly(withLock, operation) {
|
|
92
|
+
try {
|
|
93
|
+
return await withLock(operation);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error?.code !== EXIT.CONFLICT) throw error;
|
|
96
|
+
return { swept: 0, remaining: true };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// An unreadable marker means the interval is unknown, and an unknown interval
|
|
101
|
+
// is treated as due: maintenance that refuses to run because its own bookkeeping
|
|
102
|
+
// is damaged is the failure it exists to prevent.
|
|
103
|
+
async function sweptAt(paths, root) {
|
|
104
|
+
const found = await readJsonIfPresent(markerPath(paths), root).catch(() => null);
|
|
105
|
+
const value = Date.parse(found?.value?.sweptAt ?? "");
|
|
106
|
+
return Number.isNaN(value) ? null : value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sweep only when the recorded pass is older than SWEEP_INTERVAL_MS.
|
|
111
|
+
*
|
|
112
|
+
* The due check is one read and it happens before the lock. Sweeping is a
|
|
113
|
+
* write and needs the writer mutex, but taking that mutex is itself a mkdir, a
|
|
114
|
+
* write, an fsync and a rename, and this runs on every store open - which means
|
|
115
|
+
* inside every hook. Paying for the lock on the days there is nothing to sweep
|
|
116
|
+
* would put that cost on every hook to do nothing, which is the shape of the
|
|
117
|
+
* slowdown that made hooks time out in 0.6.1.
|
|
118
|
+
*
|
|
119
|
+
* `withLock` is the seam: production passes the writer mutex, tests count calls.
|
|
120
|
+
*
|
|
121
|
+
* @returns {Promise<{ swept: number, remaining: boolean }>}
|
|
122
|
+
*/
|
|
123
|
+
export async function sweepIfDue(paths,
|
|
124
|
+
{ root, clock, limit = SWEEP_BUDGET, deadlineAt, withLock = operation => operation() } = {}) {
|
|
125
|
+
// Nothing below tolerates an expired budget: withWriterMutex refuses one, and
|
|
126
|
+
// so does the publishAtomic that writes the marker. Leaving the work for the
|
|
127
|
+
// next open is the whole point of a bounded pass, so leave before the first
|
|
128
|
+
// call that would throw instead of failing the open this runs inside.
|
|
129
|
+
if (expired(deadlineAt)) return { swept: 0, remaining: true };
|
|
130
|
+
|
|
131
|
+
const now = Date.parse(clock.now());
|
|
132
|
+
const last = await sweptAt(paths, root);
|
|
133
|
+
if (last !== null && now - last < SWEEP_INTERVAL_MS) return { swept: 0, remaining: false };
|
|
134
|
+
|
|
135
|
+
// The budget can lapse after the check above: the lock may be granted late,
|
|
136
|
+
// or granted to someone else first. Both arrive as CONFLICT, from the mutex
|
|
137
|
+
// or from the publishAtomic that writes the marker. Leaving that pass for the
|
|
138
|
+
// next open is the bounded behaviour working, so it is answered here rather
|
|
139
|
+
// than left for whoever called - maintenance never decides whether a store
|
|
140
|
+
// can be opened. Every other fault is a real store problem and propagates.
|
|
141
|
+
return withLockTolerantly(withLock, async () => {
|
|
142
|
+
// Re-read under the lock: another process may have swept while this one
|
|
143
|
+
// waited for it, and two sweeps in a row would detach an empty directory
|
|
144
|
+
// for nothing.
|
|
145
|
+
const holder = await sweptAt(paths, root);
|
|
146
|
+
if (holder !== null && holder !== last && Date.parse(clock.now()) - holder < SWEEP_INTERVAL_MS) {
|
|
147
|
+
return { swept: 0, remaining: false };
|
|
148
|
+
}
|
|
149
|
+
const stages = await sweepAcceptedStages(paths, { root, limit, deadlineAt });
|
|
150
|
+
// One budget for the whole pass. The reclaimer gets what the stage sweep
|
|
151
|
+
// did not spend, so opening a store never pays the bound twice, and a stage
|
|
152
|
+
// sweep that ran out leaves the reclaimer for the next open rather than
|
|
153
|
+
// borrowing against it.
|
|
154
|
+
const retired = stages.remaining
|
|
155
|
+
? { reclaimed: 0, remaining: true }
|
|
156
|
+
: await reclaimRetired(paths, { root, limit: limit - stages.spent, deadlineAt });
|
|
157
|
+
const result = { swept: stages.swept + retired.reclaimed,
|
|
158
|
+
remaining: stages.remaining || retired.remaining };
|
|
159
|
+
// The marker records a *finished* pass, never an attempted one. A pass that
|
|
160
|
+
// stopped at its entry budget or ran out of time leaves it unwritten, so
|
|
161
|
+
// the next open carries on immediately. Recording an unfinished pass as
|
|
162
|
+
// done would park the remainder for a whole interval: a store holding
|
|
163
|
+
// 17,880 entries would drain over weeks rather than over a few opens.
|
|
164
|
+
//
|
|
165
|
+
// Writing it after an expired deadline would also throw, since
|
|
166
|
+
// publishAtomic refuses one - out of the store open this runs inside.
|
|
167
|
+
if (result.remaining || expired(deadlineAt)) {
|
|
168
|
+
return { swept: result.swept, remaining: true };
|
|
169
|
+
}
|
|
170
|
+
await publishAtomic(markerPath(paths), encode({ sweptAt: clock.now() }),
|
|
171
|
+
{ root, tmpDir: paths.tmp, stageDir: paths.stage, replace: true, deadlineAt });
|
|
172
|
+
return result;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -16,6 +16,9 @@ import { assertEventBinding, assertStateBinding, eventPath, stateEnvelope, state
|
|
|
16
16
|
import { ephemeralIsDeleted, markEphemeral, stateDeletionPublication,
|
|
17
17
|
stateGenerationIsDeleted } from "./retention.mjs";
|
|
18
18
|
import { ensureManagedDirectory } from "./safe-directory.mjs";
|
|
19
|
+
import { readEventFloor } from "./event-floor.mjs";
|
|
20
|
+
import { reclaimStateRecords, trimEventLog } from "./reclaim.mjs";
|
|
21
|
+
import { sweepIfDue } from "./stage-sweep.mjs";
|
|
19
22
|
import { withWriterMutex } from "./writer-mutex.mjs";
|
|
20
23
|
|
|
21
24
|
// Kept cohesive above 300 lines because durable transactions and ephemeral
|
|
@@ -29,7 +32,15 @@ export const ZERO_CURSOR = "0".repeat(SEQUENCE_WIDTH);
|
|
|
29
32
|
// corrupt record, so nothing ever had a reason to put one aside. An empty
|
|
30
33
|
// directory that reads as a feature is the same mistake as an attention kind
|
|
31
34
|
// with no rule behind it. If quarantining is ever built, it comes back with it.
|
|
32
|
-
|
|
35
|
+
//
|
|
36
|
+
// `stage` is the counter-example that keeps that rule rather than breaking it:
|
|
37
|
+
// every immutable publication fills it and stage-sweep.mjs empties it. Holding
|
|
38
|
+
// accepted stages apart from the partials in `tmp` is what lets the sweep
|
|
39
|
+
// remove a whole directory instead of deciding file by file from a filename
|
|
40
|
+
// suffix. A `stage` ever found empty by design rather than by sweeping is the
|
|
41
|
+
// mistake described above, and should go.
|
|
42
|
+
const DIRECTORIES = ["state", "events", "journal", "locks", "ephemeral", "retained", "tmp",
|
|
43
|
+
"stage"];
|
|
33
44
|
|
|
34
45
|
const pad = value => String(value).padStart(SEQUENCE_WIDTH, "0");
|
|
35
46
|
|
|
@@ -68,7 +79,24 @@ async function loadAllState(paths, root, wanted = null) {
|
|
|
68
79
|
|
|
69
80
|
async function nextSequence(paths, root) {
|
|
70
81
|
const last = (await listJsonFiles(paths.events, { root })).at(-1);
|
|
71
|
-
|
|
82
|
+
// Only an empty directory consults the floor, so the path every transaction
|
|
83
|
+
// runs pays nothing for retention. Trimming removes the oldest events, so
|
|
84
|
+
// while any file remains the newest one still answers this. A log trimmed
|
|
85
|
+
// away entirely is the case that would otherwise restart at 1 and hand out a
|
|
86
|
+
// sequence a peer already holds a cursor for.
|
|
87
|
+
const highest = last === undefined
|
|
88
|
+
? await readEventFloor(paths, root)
|
|
89
|
+
: path.basename(last, ".json");
|
|
90
|
+
if (highest === null) return 1;
|
|
91
|
+
// Counted as a BigInt whichever side it came from. A 16-digit sequence
|
|
92
|
+
// reaches past Number.MAX_SAFE_INTEGER, where adding one stops changing the
|
|
93
|
+
// value, so a Number here would eventually hand out a sequence twice.
|
|
94
|
+
// Refusing is the only honest answer left at that point.
|
|
95
|
+
const next = BigInt(highest) + 1n;
|
|
96
|
+
if (next > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
97
|
+
throw new AccError(EXIT.DATA, "event sequence is exhausted", { highest });
|
|
98
|
+
}
|
|
99
|
+
return Number(next);
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
export async function openFilesystemStore({ root, clock, ids, workspaceId, failAt,
|
|
@@ -93,6 +121,18 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
|
|
|
93
121
|
// must not roll the same journal forward concurrently.
|
|
94
122
|
await recoverOpenJournals();
|
|
95
123
|
|
|
124
|
+
// Sweeping is a write, so it holds the same mutex recovery does and no
|
|
125
|
+
// publisher is in flight while the stage directory is detached. It is bounded
|
|
126
|
+
// per pass: a store carrying a large accumulation drains over several opens
|
|
127
|
+
// rather than spending one hook's whole budget on it. The mutex is taken only
|
|
128
|
+
// on the opens that actually sweep - see sweepIfDue, which decides before it
|
|
129
|
+
// locks, because this runs inside every hook.
|
|
130
|
+
// Maintenance never decides whether a store can be opened: sweepIfDue answers
|
|
131
|
+
// a lost lock and a lapsed budget itself, and raises only real store faults.
|
|
132
|
+
await sweepIfDue(paths, { root, clock, deadlineAt: storeDeadline,
|
|
133
|
+
withLock: operation => withWriterMutex(paths,
|
|
134
|
+
{ root, tmpDir: paths.tmp, clock, deadlineAt: storeDeadline }, operation) });
|
|
135
|
+
|
|
96
136
|
async function recoverOpenJournals() {
|
|
97
137
|
const open = await readOpenJournals(paths, root);
|
|
98
138
|
if (open.length === 0) return [];
|
|
@@ -253,7 +293,11 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
|
|
|
253
293
|
events.push(event);
|
|
254
294
|
if (events.length === limit) break;
|
|
255
295
|
}
|
|
256
|
-
|
|
296
|
+
// A caller whose cursor precedes the boundary is served what is left and
|
|
297
|
+
// told where the log now starts. A short page that reads like a complete
|
|
298
|
+
// one is the failure trimming must not introduce.
|
|
299
|
+
return { cursor: events.at(-1)?.sequence ?? after, events,
|
|
300
|
+
trimmedThrough: await readEventFloor(paths, root) };
|
|
257
301
|
}
|
|
258
302
|
|
|
259
303
|
/**
|
|
@@ -365,6 +409,54 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
|
|
|
365
409
|
},
|
|
366
410
|
});
|
|
367
411
|
|
|
368
|
-
|
|
369
|
-
|
|
412
|
+
/**
|
|
413
|
+
* Every live state record with the generation that identifies it.
|
|
414
|
+
*
|
|
415
|
+
* `snapshot` deliberately hands back records alone, because that is all any
|
|
416
|
+
* reader of coordination state needs. Reclaiming is the one caller that needs
|
|
417
|
+
* the envelope generation too: it decides what is eligible from a reading
|
|
418
|
+
* taken outside the writer mutex and applies it inside, and the generation is
|
|
419
|
+
* what proves the record did not change in between.
|
|
420
|
+
*/
|
|
421
|
+
async function stateEnvelopes(workspace, { kinds } = {}) {
|
|
422
|
+
const wanted = kinds === undefined ? null : new Set(kinds);
|
|
423
|
+
return [...(await loadAllState(paths, root, wanted)).values()]
|
|
424
|
+
.filter(envelope => envelope.record.workspaceId === workspace);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Remove named state records and the retention markers they own.
|
|
429
|
+
*
|
|
430
|
+
* Under the writer mutex, because it competes with every transaction; the
|
|
431
|
+
* store deadline still bounds it, so an operator waiting on a busy store is
|
|
432
|
+
* told rather than blocked forever.
|
|
433
|
+
*/
|
|
434
|
+
async function reclaimRecords(plan, { limit, deadlineAt } = {}) {
|
|
435
|
+
const bounded = Math.min(deadlineAt ?? Infinity, storeDeadline ?? Infinity);
|
|
436
|
+
return withWriterMutex(paths, { ...publishOptions, deadlineAt: bounded }, async () => {
|
|
437
|
+
// A function is decided here, holding the mutex, because eligibility is a
|
|
438
|
+
// statement about relationships between records and not only about each
|
|
439
|
+
// record. The generation on an entry proves that record did not change;
|
|
440
|
+
// it says nothing about a session opening for a participant this was
|
|
441
|
+
// about to remove. Deciding inside the lock is what closes that.
|
|
442
|
+
const entries = typeof plan === "function" ? await plan() : plan;
|
|
443
|
+
return reclaimStateRecords(paths, entries, { root, limit, deadlineAt: bounded });
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Trim the event log to a boundary and report where it now starts.
|
|
449
|
+
*
|
|
450
|
+
* Under the writer mutex, because it competes with the sequence every
|
|
451
|
+
* transaction allocates.
|
|
452
|
+
*/
|
|
453
|
+
async function trimHistory(boundary, { limit, deadlineAt } = {}) {
|
|
454
|
+
const bounded = Math.min(deadlineAt ?? Infinity, storeDeadline ?? Infinity);
|
|
455
|
+
const publish = { ...publishOptions, deadlineAt: bounded };
|
|
456
|
+
return withWriterMutex(paths, publish,
|
|
457
|
+
() => trimEventLog(paths, boundary, { root, publish, limit, deadlineAt: bounded }));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return Object.freeze({ transaction, eventsSince, snapshot, stateRecord, stateEnvelopes,
|
|
461
|
+
reclaimRecords, trimHistory, ephemeral, paths, root, workspaceId });
|
|
370
462
|
}
|