agents-can-communicate 0.6.0 → 0.6.1
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/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-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/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +1 -2
- 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/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/retention.mjs +23 -13
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +18 -1
- package/package.json +1 -1
|
@@ -58,8 +58,7 @@ export function createSessionService(ports) {
|
|
|
58
58
|
const ephemeral = await store.ephemeral.get("session", sessionId);
|
|
59
59
|
const resolved = workspaceId ?? store.workspaceId ?? ephemeral?.workspaceId;
|
|
60
60
|
if (resolved === undefined) return null;
|
|
61
|
-
const durable =
|
|
62
|
-
.find(session => session.sessionId === sessionId) ?? null;
|
|
61
|
+
const durable = await store.stateRecord(resolved, "session", sessionId);
|
|
63
62
|
if (durable !== null) return { record: durable, durable: true };
|
|
64
63
|
return ephemeral !== null && ephemeral.workspaceId === resolved
|
|
65
64
|
? { record: ephemeral, durable: false } : null;
|
|
@@ -58,28 +58,34 @@ function ephemeralDirectory(paths, kind, id) {
|
|
|
58
58
|
return path.join(paths.retained, "ephemeral", kind, id);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// The newest marker decides a record's state, so only that one is read and
|
|
62
|
+
// checked. Every name is still validated first - an unparsable sequence fails
|
|
63
|
+
// closed before anything is selected - and names are ordered as numbers, since
|
|
64
|
+
// a sequence can outgrow its padding. Reading every marker made each lookup
|
|
65
|
+
// cost an open and a full ancestor check per marker ever written: thousands
|
|
66
|
+
// for a delivery binding renewed every forty seconds, and seconds for one
|
|
67
|
+
// status in a workspace that had been in use for weeks.
|
|
61
68
|
async function latestEphemeralMarker(paths, root, kind, id) {
|
|
62
69
|
const directory = ephemeralDirectory(paths, kind, id);
|
|
63
|
-
const
|
|
64
|
-
for (const filePath of await listJsonFiles(directory, { root })) {
|
|
70
|
+
const named = (await listJsonFiles(directory, { root })).map(filePath => {
|
|
65
71
|
const sequence = path.basename(filePath, ".json");
|
|
72
|
+
if (typeof sequence !== "string" || !/^\d+$/.test(sequence)
|
|
73
|
+
|| BigInt(sequence) < 1n || sequence !== pad(BigInt(sequence))) {
|
|
74
|
+
data("invalid ephemeral retention marker", { filePath });
|
|
75
|
+
}
|
|
76
|
+
return { filePath, sequence, order: BigInt(sequence) };
|
|
77
|
+
}).sort((left, right) => (left.order < right.order ? 1 : left.order > right.order ? -1 : 0));
|
|
78
|
+
for (const { filePath, sequence } of named) {
|
|
66
79
|
const found = await readJsonIfPresent(filePath, root);
|
|
67
80
|
if (found === null) continue;
|
|
68
81
|
const marker = found.value;
|
|
69
|
-
if (
|
|
70
|
-
|| BigInt(sequence) < 1n || sequence !== pad(BigInt(sequence))
|
|
71
|
-
|| !(marker?.state === "present" || marker?.state === "deleted")) {
|
|
82
|
+
if (!(marker?.state === "present" || marker?.state === "deleted")) {
|
|
72
83
|
data("invalid ephemeral retention marker", { filePath });
|
|
73
84
|
}
|
|
74
|
-
assertMarker(found, { area: "ephemeral", kind, id, sequence, state: marker.state },
|
|
75
|
-
|
|
76
|
-
markers.push(marker);
|
|
85
|
+
assertMarker(found, { area: "ephemeral", kind, id, sequence, state: marker.state }, filePath);
|
|
86
|
+
return marker;
|
|
77
87
|
}
|
|
78
|
-
return
|
|
79
|
-
const leftSequence = BigInt(left.sequence);
|
|
80
|
-
const rightSequence = BigInt(right.sequence);
|
|
81
|
-
return leftSequence < rightSequence ? -1 : leftSequence > rightSequence ? 1 : 0;
|
|
82
|
-
}).at(-1) ?? null;
|
|
88
|
+
return null;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
export async function ephemeralIsDeleted(paths, root, kind, id) {
|
|
@@ -91,6 +97,10 @@ export async function markEphemeral(paths, options, kind, id, state) {
|
|
|
91
97
|
data("invalid ephemeral retention state", { state });
|
|
92
98
|
}
|
|
93
99
|
const previous = await latestEphemeralMarker(paths, options.root, kind, id);
|
|
100
|
+
// A marker that repeats the current state changes nothing, and every renewal
|
|
101
|
+
// of a live record used to append one. History now grows only with a real
|
|
102
|
+
// change: published after a deletion, or deleted.
|
|
103
|
+
if (previous?.state === state) return previous;
|
|
94
104
|
const sequence = pad(previous === null ? 1n : BigInt(previous.sequence) + 1n);
|
|
95
105
|
const record = { retentionVersion: RETENTION_VERSION, area: "ephemeral", kind, id,
|
|
96
106
|
sequence, state };
|
|
@@ -265,6 +265,23 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
|
|
|
265
265
|
* cost grow with the number of messages the workspace had ever carried, and
|
|
266
266
|
* the hook budget is five seconds after which it allows the write.
|
|
267
267
|
*/
|
|
268
|
+
/**
|
|
269
|
+
* One state record by id, or null - the checks a listing applies to each
|
|
270
|
+
* record (the path names it, a deleted generation is absent, the record
|
|
271
|
+
* validates, it belongs to this workspace) without reading every other
|
|
272
|
+
* record of its kind. Looking one session up by listing them all cost a full
|
|
273
|
+
* pass over every session the workspace ever had, several times a hook.
|
|
274
|
+
*/
|
|
275
|
+
async function stateRecord(workspace, kind, id) {
|
|
276
|
+
const filePath = statePath(paths, kind, id);
|
|
277
|
+
const found = await readJsonIfPresent(filePath, root);
|
|
278
|
+
if (found === null) return null;
|
|
279
|
+
const envelope = assertStateBinding(found.value, kind, id, filePath);
|
|
280
|
+
if (await stateGenerationIsDeleted(paths, root, kind, envelope.id, envelope.generation)) return null;
|
|
281
|
+
validateRecord(kind, envelope.record);
|
|
282
|
+
return envelope.record.workspaceId === workspace ? envelope.record : null;
|
|
283
|
+
}
|
|
284
|
+
|
|
268
285
|
async function snapshot(workspace, { kinds } = {}) {
|
|
269
286
|
const wanted = kinds === undefined ? null : new Set(kinds);
|
|
270
287
|
const of = async kind => {
|
|
@@ -348,6 +365,6 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
|
|
|
348
365
|
},
|
|
349
366
|
});
|
|
350
367
|
|
|
351
|
-
return Object.freeze({ transaction, eventsSince, snapshot, ephemeral, paths, root,
|
|
368
|
+
return Object.freeze({ transaction, eventsSince, snapshot, stateRecord, ephemeral, paths, root,
|
|
352
369
|
workspaceId });
|
|
353
370
|
}
|