@archipelagolab/lobi 1.0.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/CHANGELOG.md +164 -0
- package/ENDOFFILE +0 -0
- package/EOF +0 -0
- package/LICENSE +21 -0
- package/SPEC-SUPPORT.md +116 -0
- package/YAMLEND +0 -0
- package/api.ts +18 -0
- package/archipelagolab-lobi-1.0.0.tgz +0 -0
- package/auth-presence.ts +56 -0
- package/channel-plugin-api.ts +3 -0
- package/cli-metadata.ts +11 -0
- package/contract-api.ts +17 -0
- package/docs/CHECKLIST.md +83 -0
- package/docs/FORK_SDK_GUIDE.md +279 -0
- package/helper-api.ts +3 -0
- package/index.test.ts +61 -0
- package/index.ts +65 -0
- package/openclaw.plugin.json +23 -0
- package/package.json +52 -0
- package/plugin-entry.handlers.runtime.ts +1 -0
- package/runtime-api.ts +54 -0
- package/runtime-heavy-api.ts +1 -0
- package/scripts/migrate-to-lobi.sh +72 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/src/account-selection.test.ts +124 -0
- package/src/account-selection.ts +226 -0
- package/src/actions.account-propagation.test.ts +251 -0
- package/src/actions.test.ts +251 -0
- package/src/actions.ts +336 -0
- package/src/approval-auth.test.ts +23 -0
- package/src/approval-auth.ts +25 -0
- package/src/approval-handler.runtime.test.ts +46 -0
- package/src/approval-handler.runtime.ts +400 -0
- package/src/approval-ids.ts +6 -0
- package/src/approval-native.test.ts +329 -0
- package/src/approval-native.ts +336 -0
- package/src/approval-reactions.test.ts +107 -0
- package/src/approval-reactions.ts +158 -0
- package/src/auth-precedence.ts +61 -0
- package/src/channel-account-paths.ts +92 -0
- package/src/channel.account-paths.test.ts +102 -0
- package/src/channel.directory.test.ts +601 -0
- package/src/channel.resolve.test.ts +38 -0
- package/src/channel.runtime.ts +16 -0
- package/src/channel.setup.test.ts +269 -0
- package/src/channel.ts +570 -0
- package/src/cli-metadata.ts +19 -0
- package/src/cli.test.ts +1015 -0
- package/src/cli.ts +1198 -0
- package/src/config-adapter.ts +41 -0
- package/src/config-schema.test.ts +90 -0
- package/src/config-schema.ts +114 -0
- package/src/directory-live.test.ts +200 -0
- package/src/directory-live.ts +238 -0
- package/src/doctor-contract.ts +287 -0
- package/src/doctor.test.ts +440 -0
- package/src/doctor.ts +262 -0
- package/src/env-vars.ts +92 -0
- package/src/exec-approval-resolver.test.ts +68 -0
- package/src/exec-approval-resolver.ts +23 -0
- package/src/exec-approvals.test.ts +483 -0
- package/src/exec-approvals.ts +290 -0
- package/src/group-mentions.ts +41 -0
- package/src/legacy-crypto-inspector-availability.test.ts +81 -0
- package/src/legacy-crypto-inspector-availability.ts +60 -0
- package/src/legacy-crypto.test.ts +234 -0
- package/src/legacy-crypto.ts +549 -0
- package/src/legacy-state.test.ts +86 -0
- package/src/legacy-state.ts +156 -0
- package/src/matrix/account-config.ts +150 -0
- package/src/matrix/accounts.readiness.test.ts +27 -0
- package/src/matrix/accounts.test.ts +757 -0
- package/src/matrix/accounts.ts +194 -0
- package/src/matrix/actions/client.test.ts +215 -0
- package/src/matrix/actions/client.ts +31 -0
- package/src/matrix/actions/devices.test.ts +114 -0
- package/src/matrix/actions/devices.ts +34 -0
- package/src/matrix/actions/limits.test.ts +15 -0
- package/src/matrix/actions/limits.ts +6 -0
- package/src/matrix/actions/messages.test.ts +289 -0
- package/src/matrix/actions/messages.ts +123 -0
- package/src/matrix/actions/pins.test.ts +74 -0
- package/src/matrix/actions/pins.ts +64 -0
- package/src/matrix/actions/polls.test.ts +71 -0
- package/src/matrix/actions/polls.ts +109 -0
- package/src/matrix/actions/profile.test.ts +109 -0
- package/src/matrix/actions/profile.ts +37 -0
- package/src/matrix/actions/reactions.test.ts +135 -0
- package/src/matrix/actions/reactions.ts +59 -0
- package/src/matrix/actions/room.test.ts +79 -0
- package/src/matrix/actions/room.ts +71 -0
- package/src/matrix/actions/summary.test.ts +87 -0
- package/src/matrix/actions/summary.ts +88 -0
- package/src/matrix/actions/types.ts +82 -0
- package/src/matrix/actions/verification.test.ts +105 -0
- package/src/matrix/actions/verification.ts +237 -0
- package/src/matrix/actions.ts +37 -0
- package/src/matrix/active-client.ts +26 -0
- package/src/matrix/async-lock.ts +18 -0
- package/src/matrix/backup-health.ts +115 -0
- package/src/matrix/client/config-runtime-api.ts +14 -0
- package/src/matrix/client/config-secret-input.runtime.ts +1 -0
- package/src/matrix/client/config.ts +982 -0
- package/src/matrix/client/create-client.test.ts +115 -0
- package/src/matrix/client/create-client.ts +101 -0
- package/src/matrix/client/env-auth.ts +6 -0
- package/src/matrix/client/file-sync-store.test.ts +265 -0
- package/src/matrix/client/file-sync-store.ts +289 -0
- package/src/matrix/client/logging.ts +123 -0
- package/src/matrix/client/migration-snapshot.runtime.ts +1 -0
- package/src/matrix/client/private-network-host.ts +56 -0
- package/src/matrix/client/runtime.ts +4 -0
- package/src/matrix/client/shared.test.ts +344 -0
- package/src/matrix/client/shared.ts +306 -0
- package/src/matrix/client/storage.test.ts +634 -0
- package/src/matrix/client/storage.ts +544 -0
- package/src/matrix/client/types.ts +50 -0
- package/src/matrix/client-bootstrap.test.ts +84 -0
- package/src/matrix/client-bootstrap.ts +164 -0
- package/src/matrix/client-resolver.test-helpers.ts +147 -0
- package/src/matrix/client.test.ts +1521 -0
- package/src/matrix/client.ts +23 -0
- package/src/matrix/config-paths.ts +31 -0
- package/src/matrix/config-update.test.ts +237 -0
- package/src/matrix/config-update.ts +291 -0
- package/src/matrix/credentials-read.ts +206 -0
- package/src/matrix/credentials-write.runtime.ts +26 -0
- package/src/matrix/credentials.test.ts +501 -0
- package/src/matrix/credentials.ts +95 -0
- package/src/matrix/deps.test.ts +74 -0
- package/src/matrix/deps.ts +225 -0
- package/src/matrix/device-health.test.ts +45 -0
- package/src/matrix/device-health.ts +31 -0
- package/src/matrix/direct-management.test.ts +350 -0
- package/src/matrix/direct-management.ts +347 -0
- package/src/matrix/direct-room.test.ts +61 -0
- package/src/matrix/direct-room.ts +128 -0
- package/src/matrix/draft-stream.test.ts +406 -0
- package/src/matrix/draft-stream.ts +216 -0
- package/src/matrix/encryption-guidance.ts +27 -0
- package/src/matrix/errors.ts +21 -0
- package/src/matrix/format.test.ts +340 -0
- package/src/matrix/format.ts +428 -0
- package/src/matrix/legacy-crypto-inspector.ts +95 -0
- package/src/matrix/media-errors.ts +20 -0
- package/src/matrix/media-text.ts +169 -0
- package/src/matrix/monitor/access-state.test.ts +45 -0
- package/src/matrix/monitor/access-state.ts +77 -0
- package/src/matrix/monitor/ack-config.test.ts +57 -0
- package/src/matrix/monitor/ack-config.ts +26 -0
- package/src/matrix/monitor/allowlist.test.ts +45 -0
- package/src/matrix/monitor/allowlist.ts +94 -0
- package/src/matrix/monitor/auto-join.test.ts +203 -0
- package/src/matrix/monitor/auto-join.ts +86 -0
- package/src/matrix/monitor/config.test.ts +197 -0
- package/src/matrix/monitor/config.ts +303 -0
- package/src/matrix/monitor/context-summary.ts +43 -0
- package/src/matrix/monitor/direct.test.ts +529 -0
- package/src/matrix/monitor/direct.ts +270 -0
- package/src/matrix/monitor/events.test.ts +1524 -0
- package/src/matrix/monitor/events.ts +213 -0
- package/src/matrix/monitor/handler.body-for-agent.test.ts +396 -0
- package/src/matrix/monitor/handler.group-history.test.ts +648 -0
- package/src/matrix/monitor/handler.media-failure.test.ts +267 -0
- package/src/matrix/monitor/handler.test-helpers.ts +308 -0
- package/src/matrix/monitor/handler.test.ts +2952 -0
- package/src/matrix/monitor/handler.thread-root-media.test.ts +82 -0
- package/src/matrix/monitor/handler.ts +1679 -0
- package/src/matrix/monitor/inbound-dedupe.test.ts +146 -0
- package/src/matrix/monitor/inbound-dedupe.ts +267 -0
- package/src/matrix/monitor/index.test.ts +920 -0
- package/src/matrix/monitor/index.ts +434 -0
- package/src/matrix/monitor/legacy-crypto-restore.test.ts +206 -0
- package/src/matrix/monitor/legacy-crypto-restore.ts +139 -0
- package/src/matrix/monitor/location.ts +100 -0
- package/src/matrix/monitor/media.test.ts +159 -0
- package/src/matrix/monitor/media.ts +119 -0
- package/src/matrix/monitor/mentions.test.ts +289 -0
- package/src/matrix/monitor/mentions.ts +177 -0
- package/src/matrix/monitor/reaction-events.test.ts +326 -0
- package/src/matrix/monitor/reaction-events.ts +187 -0
- package/src/matrix/monitor/recent-invite.test.ts +92 -0
- package/src/matrix/monitor/recent-invite.ts +30 -0
- package/src/matrix/monitor/replies.test.ts +265 -0
- package/src/matrix/monitor/replies.ts +136 -0
- package/src/matrix/monitor/reply-context.test.ts +276 -0
- package/src/matrix/monitor/reply-context.ts +92 -0
- package/src/matrix/monitor/room-history.test.ts +258 -0
- package/src/matrix/monitor/room-history.ts +301 -0
- package/src/matrix/monitor/room-info.test.ts +201 -0
- package/src/matrix/monitor/room-info.ts +126 -0
- package/src/matrix/monitor/rooms.test.ts +121 -0
- package/src/matrix/monitor/rooms.ts +52 -0
- package/src/matrix/monitor/route.test.ts +255 -0
- package/src/matrix/monitor/route.ts +178 -0
- package/src/matrix/monitor/runtime-api.ts +31 -0
- package/src/matrix/monitor/startup-verification.test.ts +294 -0
- package/src/matrix/monitor/startup-verification.ts +237 -0
- package/src/matrix/monitor/startup.test.ts +257 -0
- package/src/matrix/monitor/startup.ts +218 -0
- package/src/matrix/monitor/status.ts +111 -0
- package/src/matrix/monitor/sync-lifecycle.test.ts +224 -0
- package/src/matrix/monitor/sync-lifecycle.ts +91 -0
- package/src/matrix/monitor/task-runner.ts +38 -0
- package/src/matrix/monitor/thread-context.test.ts +149 -0
- package/src/matrix/monitor/thread-context.ts +108 -0
- package/src/matrix/monitor/threads.test.ts +68 -0
- package/src/matrix/monitor/threads.ts +85 -0
- package/src/matrix/monitor/types.ts +30 -0
- package/src/matrix/monitor/verification-events.ts +627 -0
- package/src/matrix/monitor/verification-utils.test.ts +47 -0
- package/src/matrix/monitor/verification-utils.ts +46 -0
- package/src/matrix/outbound-media-runtime.ts +1 -0
- package/src/matrix/poll-summary.ts +110 -0
- package/src/matrix/poll-types.test.ts +205 -0
- package/src/matrix/poll-types.ts +433 -0
- package/src/matrix/probe.runtime.ts +4 -0
- package/src/matrix/probe.test.ts +154 -0
- package/src/matrix/probe.ts +96 -0
- package/src/matrix/profile.test.ts +154 -0
- package/src/matrix/profile.ts +184 -0
- package/src/matrix/reaction-common.test.ts +96 -0
- package/src/matrix/reaction-common.ts +147 -0
- package/src/matrix/sdk/crypto-bootstrap.test.ts +505 -0
- package/src/matrix/sdk/crypto-bootstrap.ts +341 -0
- package/src/matrix/sdk/crypto-facade.test.ts +197 -0
- package/src/matrix/sdk/crypto-facade.ts +207 -0
- package/src/matrix/sdk/crypto-node.runtime.test.ts +27 -0
- package/src/matrix/sdk/crypto-node.runtime.ts +9 -0
- package/src/matrix/sdk/crypto-runtime.ts +11 -0
- package/src/matrix/sdk/decrypt-bridge.ts +356 -0
- package/src/matrix/sdk/event-helpers.test.ts +60 -0
- package/src/matrix/sdk/event-helpers.ts +71 -0
- package/src/matrix/sdk/http-client.test.ts +134 -0
- package/src/matrix/sdk/http-client.ts +87 -0
- package/src/matrix/sdk/idb-persistence-lock.ts +51 -0
- package/src/matrix/sdk/idb-persistence.lock-order.test.ts +108 -0
- package/src/matrix/sdk/idb-persistence.test-helpers.ts +88 -0
- package/src/matrix/sdk/idb-persistence.test.ts +149 -0
- package/src/matrix/sdk/idb-persistence.ts +283 -0
- package/src/matrix/sdk/logger.test.ts +25 -0
- package/src/matrix/sdk/logger.ts +108 -0
- package/src/matrix/sdk/read-response-with-limit.ts +19 -0
- package/src/matrix/sdk/recovery-key-store.test.ts +385 -0
- package/src/matrix/sdk/recovery-key-store.ts +430 -0
- package/src/matrix/sdk/transport.test.ts +161 -0
- package/src/matrix/sdk/transport.ts +344 -0
- package/src/matrix/sdk/types.ts +236 -0
- package/src/matrix/sdk/verification-manager.test.ts +509 -0
- package/src/matrix/sdk/verification-manager.ts +694 -0
- package/src/matrix/sdk/verification-status.ts +23 -0
- package/src/matrix/sdk.test.ts +2568 -0
- package/src/matrix/sdk.ts +1789 -0
- package/src/matrix/send/client.test.ts +174 -0
- package/src/matrix/send/client.ts +90 -0
- package/src/matrix/send/formatting.ts +189 -0
- package/src/matrix/send/media.ts +244 -0
- package/src/matrix/send/targets.test.ts +254 -0
- package/src/matrix/send/targets.ts +104 -0
- package/src/matrix/send/types.ts +134 -0
- package/src/matrix/send.test.ts +958 -0
- package/src/matrix/send.ts +609 -0
- package/src/matrix/session-store-metadata.ts +108 -0
- package/src/matrix/startup-abort.ts +44 -0
- package/src/matrix/sync-state.ts +27 -0
- package/src/matrix/target-ids.ts +102 -0
- package/src/matrix/thread-bindings-shared.ts +201 -0
- package/src/matrix/thread-bindings.test.ts +673 -0
- package/src/matrix/thread-bindings.ts +577 -0
- package/src/matrix-migration.runtime.ts +9 -0
- package/src/migration-config.test.ts +228 -0
- package/src/migration-config.ts +243 -0
- package/src/migration-snapshot-backup.ts +117 -0
- package/src/migration-snapshot.test.ts +184 -0
- package/src/migration-snapshot.ts +55 -0
- package/src/onboarding.resolve.test.ts +55 -0
- package/src/onboarding.test-harness.ts +158 -0
- package/src/onboarding.test.ts +665 -0
- package/src/onboarding.ts +773 -0
- package/src/outbound.test.ts +173 -0
- package/src/outbound.ts +78 -0
- package/src/plugin-entry.runtime.js +159 -0
- package/src/plugin-entry.runtime.test.ts +108 -0
- package/src/plugin-entry.runtime.ts +68 -0
- package/src/profile-update.ts +68 -0
- package/src/record-shared.ts +3 -0
- package/src/resolve-targets.test.ts +178 -0
- package/src/resolve-targets.ts +175 -0
- package/src/resolver.ts +21 -0
- package/src/runtime-api.ts +144 -0
- package/src/runtime.ts +7 -0
- package/src/secret-contract.ts +174 -0
- package/src/session-route.test.ts +315 -0
- package/src/session-route.ts +113 -0
- package/src/setup-bootstrap.ts +94 -0
- package/src/setup-config.ts +222 -0
- package/src/setup-contract.ts +89 -0
- package/src/setup-core.test.ts +326 -0
- package/src/setup-core.ts +50 -0
- package/src/setup-surface.ts +4 -0
- package/src/startup-maintenance.test.ts +227 -0
- package/src/startup-maintenance.ts +114 -0
- package/src/storage-paths.ts +92 -0
- package/src/test-helpers.ts +42 -0
- package/src/test-mocks.ts +55 -0
- package/src/test-runtime.ts +72 -0
- package/src/test-support/monitor-route-test-support.ts +8 -0
- package/src/tool-actions.runtime.ts +1 -0
- package/src/tool-actions.test.ts +422 -0
- package/src/tool-actions.ts +498 -0
- package/src/types.ts +230 -0
- package/test-api.ts +2 -0
- package/thread-bindings-runtime.ts +4 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { MatrixClient } from "../sdk.js";
|
|
2
|
+
import { summarizeMatrixMessageContextEvent, trimMatrixMaybeString } from "./context-summary.js";
|
|
3
|
+
import type { MatrixRawEvent } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const MAX_CACHED_REPLY_CONTEXTS = 256;
|
|
6
|
+
const MAX_REPLY_BODY_LENGTH = 500;
|
|
7
|
+
|
|
8
|
+
export type MatrixReplyContext = {
|
|
9
|
+
replyToBody?: string;
|
|
10
|
+
replyToSender?: string;
|
|
11
|
+
replyToSenderId?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function truncateReplyBody(value: string): string {
|
|
15
|
+
if (value.length <= MAX_REPLY_BODY_LENGTH) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
return `${value.slice(0, MAX_REPLY_BODY_LENGTH - 3)}...`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function summarizeMatrixReplyEvent(event: MatrixRawEvent): string | undefined {
|
|
22
|
+
const body = summarizeMatrixMessageContextEvent(event);
|
|
23
|
+
return body ? truncateReplyBody(body) : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a cached resolver that fetches the body and sender of a replied-to
|
|
28
|
+
* Matrix event. This allows the agent to see the content of the message being
|
|
29
|
+
* replied to, not just its event ID.
|
|
30
|
+
*/
|
|
31
|
+
export function createMatrixReplyContextResolver(params: {
|
|
32
|
+
client: MatrixClient;
|
|
33
|
+
getMemberDisplayName: (roomId: string, userId: string) => Promise<string>;
|
|
34
|
+
logVerboseMessage: (message: string) => void;
|
|
35
|
+
}) {
|
|
36
|
+
const cache = new Map<string, MatrixReplyContext>();
|
|
37
|
+
|
|
38
|
+
const remember = (key: string, value: MatrixReplyContext): MatrixReplyContext => {
|
|
39
|
+
cache.set(key, value);
|
|
40
|
+
if (cache.size > MAX_CACHED_REPLY_CONTEXTS) {
|
|
41
|
+
const oldest = cache.keys().next().value;
|
|
42
|
+
if (typeof oldest === "string") {
|
|
43
|
+
cache.delete(oldest);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return async (input: { roomId: string; eventId: string }): Promise<MatrixReplyContext> => {
|
|
50
|
+
const cacheKey = `${input.roomId}:${input.eventId}`;
|
|
51
|
+
const cached = cache.get(cacheKey);
|
|
52
|
+
if (cached) {
|
|
53
|
+
// Move to end for LRU semantics so frequently accessed entries survive eviction.
|
|
54
|
+
cache.delete(cacheKey);
|
|
55
|
+
cache.set(cacheKey, cached);
|
|
56
|
+
return cached;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const event = await params.client.getEvent(input.roomId, input.eventId).catch((err) => {
|
|
60
|
+
params.logVerboseMessage(
|
|
61
|
+
`matrix: failed resolving reply context room=${input.roomId} id=${input.eventId}: ${String(err)}`,
|
|
62
|
+
);
|
|
63
|
+
return null;
|
|
64
|
+
});
|
|
65
|
+
if (!event) {
|
|
66
|
+
// Do not cache failures so transient errors can be retried on the next
|
|
67
|
+
// message that references the same event.
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rawEvent = event as MatrixRawEvent;
|
|
72
|
+
if (rawEvent.unsigned?.redacted_because) {
|
|
73
|
+
return remember(cacheKey, {});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const replyToBody = summarizeMatrixReplyEvent(rawEvent);
|
|
77
|
+
if (!replyToBody) {
|
|
78
|
+
return remember(cacheKey, {});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const senderId = trimMatrixMaybeString(rawEvent.sender);
|
|
82
|
+
const senderName =
|
|
83
|
+
senderId &&
|
|
84
|
+
(await params.getMemberDisplayName(input.roomId, senderId).catch(() => undefined));
|
|
85
|
+
|
|
86
|
+
return remember(cacheKey, {
|
|
87
|
+
replyToBody,
|
|
88
|
+
replyToSender: senderName ?? senderId,
|
|
89
|
+
replyToSenderId: senderId,
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for createRoomHistoryTracker.
|
|
3
|
+
*
|
|
4
|
+
* Covers correctness properties that are hard to observe through the handler harness:
|
|
5
|
+
* - Monotone watermark advancement (out-of-order consumeHistory must not regress)
|
|
6
|
+
* - roomQueues FIFO eviction when the room count exceeds the cap
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import { createRoomHistoryTrackerForTests } from "./room-history.js";
|
|
11
|
+
|
|
12
|
+
const ROOM = "!room:test";
|
|
13
|
+
const AGENT = "agent_a";
|
|
14
|
+
|
|
15
|
+
function entry(body: string) {
|
|
16
|
+
return { sender: "user", body };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("createRoomHistoryTracker — watermark monotonicity", () => {
|
|
20
|
+
it("consumeHistory is monotone: out-of-order completion does not regress the watermark", () => {
|
|
21
|
+
const tracker = createRoomHistoryTrackerForTests();
|
|
22
|
+
|
|
23
|
+
// Queue: [msg1, msg2, trigger1, msg3, trigger2]
|
|
24
|
+
tracker.recordPending(ROOM, entry("msg1"));
|
|
25
|
+
tracker.recordPending(ROOM, entry("msg2"));
|
|
26
|
+
const snap1 = tracker.recordTrigger(ROOM, entry("trigger1")); // snap=3
|
|
27
|
+
tracker.recordPending(ROOM, entry("msg3"));
|
|
28
|
+
const snap2 = tracker.recordTrigger(ROOM, entry("trigger2")); // snap=5
|
|
29
|
+
|
|
30
|
+
// trigger2 completes first (higher index)
|
|
31
|
+
tracker.consumeHistory(AGENT, ROOM, snap2); // watermark → 5
|
|
32
|
+
expect(tracker.getPendingHistory(AGENT, ROOM, 100)).toHaveLength(0);
|
|
33
|
+
|
|
34
|
+
// trigger1 completes later (lower index) — must NOT regress to 3
|
|
35
|
+
tracker.consumeHistory(AGENT, ROOM, snap1);
|
|
36
|
+
// If regressed: [msg3, trigger2] would be visible (2 entries); must stay at 0
|
|
37
|
+
expect(tracker.getPendingHistory(AGENT, ROOM, 100)).toHaveLength(0);
|
|
38
|
+
|
|
39
|
+
// In-order advancement still works
|
|
40
|
+
tracker.recordPending(ROOM, entry("msg4"));
|
|
41
|
+
const snap3 = tracker.recordTrigger(ROOM, entry("trigger3")); // snap=7
|
|
42
|
+
tracker.consumeHistory(AGENT, ROOM, snap3); // watermark → 7
|
|
43
|
+
expect(tracker.getPendingHistory(AGENT, ROOM, 100)).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("prepareTrigger reuses the original history window for a retried event", () => {
|
|
47
|
+
const tracker = createRoomHistoryTrackerForTests();
|
|
48
|
+
|
|
49
|
+
tracker.recordPending(ROOM, { sender: "user", body: "msg1", messageId: "$m1" });
|
|
50
|
+
const first = tracker.prepareTrigger(AGENT, ROOM, 100, {
|
|
51
|
+
sender: "user",
|
|
52
|
+
body: "trigger",
|
|
53
|
+
messageId: "$trigger",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
tracker.recordPending(ROOM, { sender: "user", body: "msg2", messageId: "$m2" });
|
|
57
|
+
const retried = tracker.prepareTrigger(AGENT, ROOM, 100, {
|
|
58
|
+
sender: "user",
|
|
59
|
+
body: "trigger",
|
|
60
|
+
messageId: "$trigger",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(first.history.map((entry) => entry.body)).toEqual(["msg1"]);
|
|
64
|
+
expect(retried.history.map((entry) => entry.body)).toEqual(["msg1"]);
|
|
65
|
+
expect(retried.snapshotIdx).toBe(first.snapshotIdx);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("refreshes watermark recency before capped-map eviction", () => {
|
|
69
|
+
const tracker = createRoomHistoryTrackerForTests(200, 10, 2);
|
|
70
|
+
const room1 = "!room1:test";
|
|
71
|
+
const room2 = "!room2:test";
|
|
72
|
+
const room3 = "!room3:test";
|
|
73
|
+
|
|
74
|
+
tracker.recordPending(room1, entry("old msg in room1"));
|
|
75
|
+
const snap1 = tracker.recordTrigger(room1, entry("trigger in room1"));
|
|
76
|
+
tracker.consumeHistory(AGENT, room1, snap1);
|
|
77
|
+
|
|
78
|
+
tracker.recordPending(room2, entry("old msg in room2"));
|
|
79
|
+
const snap2 = tracker.recordTrigger(room2, entry("trigger in room2"));
|
|
80
|
+
tracker.consumeHistory(AGENT, room2, snap2);
|
|
81
|
+
|
|
82
|
+
// Refresh room1 so room2 becomes the stalest watermark entry.
|
|
83
|
+
tracker.consumeHistory(AGENT, room1, snap1);
|
|
84
|
+
|
|
85
|
+
tracker.recordPending(room3, entry("old msg in room3"));
|
|
86
|
+
const snap3 = tracker.recordTrigger(room3, entry("trigger in room3"));
|
|
87
|
+
tracker.consumeHistory(AGENT, room3, snap3);
|
|
88
|
+
|
|
89
|
+
tracker.recordPending(room1, entry("new msg in room1"));
|
|
90
|
+
const room1History = tracker.getPendingHistory(AGENT, room1, 100);
|
|
91
|
+
expect(room1History).toHaveLength(1);
|
|
92
|
+
expect(room1History[0]?.body).toBe("new msg in room1");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("refreshes prepared-trigger recency before capped eviction on retry hits", () => {
|
|
96
|
+
const tracker = createRoomHistoryTrackerForTests(200, 10, 5000, 2);
|
|
97
|
+
const room1 = "!room1:test";
|
|
98
|
+
|
|
99
|
+
tracker.prepareTrigger(AGENT, room1, 100, {
|
|
100
|
+
sender: "user",
|
|
101
|
+
body: "trigger1",
|
|
102
|
+
messageId: "$trigger1",
|
|
103
|
+
});
|
|
104
|
+
tracker.prepareTrigger(AGENT, room1, 100, {
|
|
105
|
+
sender: "user",
|
|
106
|
+
body: "trigger2",
|
|
107
|
+
messageId: "$trigger2",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Retry hit should refresh trigger1 so trigger2 becomes the stale entry.
|
|
111
|
+
const retried = tracker.prepareTrigger(AGENT, room1, 100, {
|
|
112
|
+
sender: "user",
|
|
113
|
+
body: "trigger1",
|
|
114
|
+
messageId: "$trigger1",
|
|
115
|
+
});
|
|
116
|
+
tracker.prepareTrigger(AGENT, room1, 100, {
|
|
117
|
+
sender: "user",
|
|
118
|
+
body: "trigger3",
|
|
119
|
+
messageId: "$trigger3",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const reused = tracker.prepareTrigger(AGENT, room1, 100, {
|
|
123
|
+
sender: "user",
|
|
124
|
+
body: "trigger1",
|
|
125
|
+
messageId: "$trigger1",
|
|
126
|
+
});
|
|
127
|
+
expect(reused.snapshotIdx).toBe(retried.snapshotIdx);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("createRoomHistoryTracker — roomQueues eviction", () => {
|
|
132
|
+
it("evicts the oldest room (FIFO) when the room count exceeds the cap", () => {
|
|
133
|
+
const tracker = createRoomHistoryTrackerForTests(200, 3);
|
|
134
|
+
|
|
135
|
+
const room1 = "!room1:test";
|
|
136
|
+
const room2 = "!room2:test";
|
|
137
|
+
const room3 = "!room3:test";
|
|
138
|
+
const room4 = "!room4:test";
|
|
139
|
+
|
|
140
|
+
tracker.recordPending(room1, entry("msg in room1"));
|
|
141
|
+
tracker.recordPending(room2, entry("msg in room2"));
|
|
142
|
+
tracker.recordPending(room3, entry("msg in room3"));
|
|
143
|
+
|
|
144
|
+
// At cap (3 rooms) — no eviction yet
|
|
145
|
+
expect(tracker.getPendingHistory(AGENT, room1, 100)).toHaveLength(1);
|
|
146
|
+
|
|
147
|
+
// room4 pushes count to 4 > cap=3 → room1 (oldest) evicted
|
|
148
|
+
tracker.recordPending(room4, entry("msg in room4"));
|
|
149
|
+
expect(tracker.getPendingHistory(AGENT, room1, 100)).toHaveLength(0);
|
|
150
|
+
expect(tracker.getPendingHistory(AGENT, room2, 100)).toHaveLength(1);
|
|
151
|
+
expect(tracker.getPendingHistory(AGENT, room3, 100)).toHaveLength(1);
|
|
152
|
+
expect(tracker.getPendingHistory(AGENT, room4, 100)).toHaveLength(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("re-accessing an evicted room starts a fresh empty queue", () => {
|
|
156
|
+
const tracker = createRoomHistoryTrackerForTests(200, 2);
|
|
157
|
+
|
|
158
|
+
const room1 = "!room1:test";
|
|
159
|
+
const room2 = "!room2:test";
|
|
160
|
+
const room3 = "!room3:test";
|
|
161
|
+
|
|
162
|
+
tracker.recordPending(room1, entry("old msg in room1"));
|
|
163
|
+
tracker.recordPending(room2, entry("msg in room2"));
|
|
164
|
+
tracker.recordPending(room3, entry("msg in room3")); // evicts room1
|
|
165
|
+
|
|
166
|
+
tracker.recordPending(room1, entry("new msg in room1"));
|
|
167
|
+
const history = tracker.getPendingHistory(AGENT, room1, 100);
|
|
168
|
+
expect(history).toHaveLength(1);
|
|
169
|
+
expect(history[0]?.body).toBe("new msg in room1");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("clears stale room watermarks when an evicted room is recreated", () => {
|
|
173
|
+
const tracker = createRoomHistoryTrackerForTests(200, 1);
|
|
174
|
+
const room1 = "!room1:test";
|
|
175
|
+
const room2 = "!room2:test";
|
|
176
|
+
|
|
177
|
+
tracker.recordPending(room1, entry("old msg in room1"));
|
|
178
|
+
const firstSnapshot = tracker.recordTrigger(room1, entry("trigger in room1"));
|
|
179
|
+
tracker.consumeHistory(AGENT, room1, firstSnapshot);
|
|
180
|
+
|
|
181
|
+
// room2 creation evicts room1 (maxRoomQueues=1)
|
|
182
|
+
tracker.recordPending(room2, entry("msg in room2"));
|
|
183
|
+
|
|
184
|
+
// Recreate room1 and add fresh content.
|
|
185
|
+
tracker.recordPending(room1, entry("new msg in room1"));
|
|
186
|
+
const history = tracker.getPendingHistory(AGENT, room1, 100);
|
|
187
|
+
expect(history).toHaveLength(1);
|
|
188
|
+
expect(history[0]?.body).toBe("new msg in room1");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("ignores late consumeHistory calls after the room queue was evicted", () => {
|
|
192
|
+
const tracker = createRoomHistoryTrackerForTests(200, 1);
|
|
193
|
+
const room1 = "!room1:test";
|
|
194
|
+
const room2 = "!room2:test";
|
|
195
|
+
|
|
196
|
+
tracker.recordPending(room1, entry("old msg in room1"));
|
|
197
|
+
const prepared = tracker.prepareTrigger(AGENT, room1, 100, {
|
|
198
|
+
sender: "user",
|
|
199
|
+
body: "trigger in room1",
|
|
200
|
+
messageId: "$trigger",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// room2 creation evicts room1 (maxRoomQueues=1) while the trigger is still in flight.
|
|
204
|
+
tracker.recordPending(room2, entry("msg in room2"));
|
|
205
|
+
|
|
206
|
+
// Late completion for the evicted room must not recreate a stale watermark.
|
|
207
|
+
tracker.consumeHistory(AGENT, room1, prepared, "$trigger");
|
|
208
|
+
|
|
209
|
+
// Recreate room1 and add fresh content.
|
|
210
|
+
tracker.recordPending(room1, entry("new msg in room1"));
|
|
211
|
+
const history = tracker.getPendingHistory(AGENT, room1, 100);
|
|
212
|
+
expect(history).toHaveLength(1);
|
|
213
|
+
expect(history[0]?.body).toBe("new msg in room1");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("rejects stale snapshots after the room queue is recreated", () => {
|
|
217
|
+
const tracker = createRoomHistoryTrackerForTests(200, 1);
|
|
218
|
+
const room1 = "!room1:test";
|
|
219
|
+
const room2 = "!room2:test";
|
|
220
|
+
|
|
221
|
+
tracker.recordPending(room1, entry("old msg in room1"));
|
|
222
|
+
const staleSnapshot = tracker.recordTrigger(room1, entry("trigger in room1"));
|
|
223
|
+
|
|
224
|
+
tracker.recordPending(room2, entry("msg in room2")); // evicts room1
|
|
225
|
+
tracker.recordPending(room1, entry("new msg in room1")); // recreates room1 with new generation
|
|
226
|
+
|
|
227
|
+
tracker.consumeHistory(AGENT, room1, staleSnapshot);
|
|
228
|
+
|
|
229
|
+
const history = tracker.getPendingHistory(AGENT, room1, 100);
|
|
230
|
+
expect(history).toHaveLength(1);
|
|
231
|
+
expect(history[0]?.body).toBe("new msg in room1");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("preserves newer watermarks when an older snapshot finishes after room recreation", () => {
|
|
235
|
+
const tracker = createRoomHistoryTrackerForTests(200, 1);
|
|
236
|
+
const room1 = "!room1:test";
|
|
237
|
+
const room2 = "!room2:test";
|
|
238
|
+
|
|
239
|
+
tracker.recordPending(room1, entry("old msg in room1"));
|
|
240
|
+
const staleSnapshot = tracker.recordTrigger(room1, entry("old trigger in room1"));
|
|
241
|
+
|
|
242
|
+
tracker.recordPending(room2, entry("msg in room2")); // evicts room1
|
|
243
|
+
|
|
244
|
+
tracker.recordPending(room1, entry("new msg in room1"));
|
|
245
|
+
const freshSnapshot = tracker.recordTrigger(room1, entry("new trigger in room1"));
|
|
246
|
+
tracker.consumeHistory(AGENT, room1, freshSnapshot);
|
|
247
|
+
|
|
248
|
+
// Late completion from the old generation must be ignored and must not clear the
|
|
249
|
+
// watermark already written by the newer trigger.
|
|
250
|
+
tracker.consumeHistory(AGENT, room1, staleSnapshot);
|
|
251
|
+
|
|
252
|
+
tracker.recordPending(room1, entry("fresh msg after consume"));
|
|
253
|
+
|
|
254
|
+
const history = tracker.getPendingHistory(AGENT, room1, 100);
|
|
255
|
+
expect(history).toHaveLength(1);
|
|
256
|
+
expect(history[0]?.body).toBe("fresh msg after consume");
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-room group chat history tracking for Matrix.
|
|
3
|
+
*
|
|
4
|
+
* Maintains a shared per-room message queue and per-(agentId, roomId) watermarks so
|
|
5
|
+
* each agent independently tracks which messages it has already consumed. This design
|
|
6
|
+
* lets multiple agents in the same room see independent history windows:
|
|
7
|
+
*
|
|
8
|
+
* - dev replies to @dev msgB (watermark advances to B) → room queue still has [A, B]
|
|
9
|
+
* - spark replies to @spark msgC → spark watermark starts at 0 and sees [A, B, C]
|
|
10
|
+
*
|
|
11
|
+
* Race-condition safety: the watermark only advances to the snapshot index taken at
|
|
12
|
+
* dispatch time, NOT to the queue's end at reply time. Messages that land in the queue
|
|
13
|
+
* while the agent is processing stay visible to the next trigger for that agent.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
|
|
17
|
+
|
|
18
|
+
/** Maximum entries retained per room (hard cap to bound memory). */
|
|
19
|
+
const DEFAULT_MAX_QUEUE_SIZE = 200;
|
|
20
|
+
/** Maximum number of rooms to retain queues for (FIFO eviction beyond this). */
|
|
21
|
+
const DEFAULT_MAX_ROOM_QUEUES = 1000;
|
|
22
|
+
/** Maximum number of (agentId, roomId) watermark entries to retain. */
|
|
23
|
+
const MAX_WATERMARK_ENTRIES = 5000;
|
|
24
|
+
/** Maximum prepared trigger snapshots retained per room for retry reuse. */
|
|
25
|
+
const MAX_PREPARED_TRIGGER_ENTRIES = 500;
|
|
26
|
+
|
|
27
|
+
export type { HistoryEntry };
|
|
28
|
+
|
|
29
|
+
export type HistorySnapshotToken = {
|
|
30
|
+
snapshotIdx: number;
|
|
31
|
+
queueGeneration: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type PreparedTriggerResult = {
|
|
35
|
+
history: HistoryEntry[];
|
|
36
|
+
} & HistorySnapshotToken;
|
|
37
|
+
|
|
38
|
+
export type RoomHistoryTracker = {
|
|
39
|
+
/**
|
|
40
|
+
* Record a non-trigger message for future context.
|
|
41
|
+
* Call this when a room message arrives but does not mention the bot.
|
|
42
|
+
*/
|
|
43
|
+
recordPending: (roomId: string, entry: HistoryEntry) => void;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Capture pending history and append the trigger as one idempotent operation.
|
|
47
|
+
* Retries of the same Matrix event reuse the original prepared history window.
|
|
48
|
+
*/
|
|
49
|
+
prepareTrigger: (
|
|
50
|
+
agentId: string,
|
|
51
|
+
roomId: string,
|
|
52
|
+
limit: number,
|
|
53
|
+
entry: HistoryEntry,
|
|
54
|
+
) => PreparedTriggerResult;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Advance the agent's watermark to the snapshot index returned by prepareTrigger
|
|
58
|
+
* (or the lower-level recordTrigger helper used in tests).
|
|
59
|
+
* Only messages appended after that snapshot remain visible on the next trigger.
|
|
60
|
+
*/
|
|
61
|
+
consumeHistory: (
|
|
62
|
+
agentId: string,
|
|
63
|
+
roomId: string,
|
|
64
|
+
snapshot: HistorySnapshotToken,
|
|
65
|
+
messageId?: string,
|
|
66
|
+
) => void;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type RoomHistoryTrackerTestApi = RoomHistoryTracker & {
|
|
70
|
+
/**
|
|
71
|
+
* Test-only helper for inspecting pending room history directly.
|
|
72
|
+
*/
|
|
73
|
+
getPendingHistory: (agentId: string, roomId: string, limit: number) => HistoryEntry[];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Test-only helper for manually appending a trigger entry and snapshot index.
|
|
77
|
+
*/
|
|
78
|
+
recordTrigger: (roomId: string, entry: HistoryEntry) => HistorySnapshotToken;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type RoomQueue = {
|
|
82
|
+
entries: HistoryEntry[];
|
|
83
|
+
/** Absolute index of entries[0] — increases as old entries are trimmed. */
|
|
84
|
+
baseIndex: number;
|
|
85
|
+
generation: number;
|
|
86
|
+
preparedTriggers: Map<string, PreparedTriggerResult>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function createRoomHistoryTrackerInternal(
|
|
90
|
+
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
|
|
91
|
+
maxRoomQueues = DEFAULT_MAX_ROOM_QUEUES,
|
|
92
|
+
maxWatermarkEntries = MAX_WATERMARK_ENTRIES,
|
|
93
|
+
maxPreparedTriggerEntries = MAX_PREPARED_TRIGGER_ENTRIES,
|
|
94
|
+
): RoomHistoryTrackerTestApi {
|
|
95
|
+
const roomQueues = new Map<string, RoomQueue>();
|
|
96
|
+
/** Maps `${agentId}:${roomId}` → absolute consumed-up-to index */
|
|
97
|
+
const agentWatermarks = new Map<string, number>();
|
|
98
|
+
let nextQueueGeneration = 1;
|
|
99
|
+
|
|
100
|
+
function clearRoomWatermarks(roomId: string): void {
|
|
101
|
+
const roomSuffix = `:${roomId}`;
|
|
102
|
+
for (const key of agentWatermarks.keys()) {
|
|
103
|
+
if (key.endsWith(roomSuffix)) {
|
|
104
|
+
agentWatermarks.delete(key);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getOrCreateQueue(roomId: string): RoomQueue {
|
|
110
|
+
let queue = roomQueues.get(roomId);
|
|
111
|
+
if (!queue) {
|
|
112
|
+
queue = {
|
|
113
|
+
entries: [],
|
|
114
|
+
baseIndex: 0,
|
|
115
|
+
generation: nextQueueGeneration++,
|
|
116
|
+
preparedTriggers: new Map(),
|
|
117
|
+
};
|
|
118
|
+
roomQueues.set(roomId, queue);
|
|
119
|
+
// FIFO eviction to prevent unbounded growth across many rooms
|
|
120
|
+
if (roomQueues.size > maxRoomQueues) {
|
|
121
|
+
const oldest = roomQueues.keys().next().value;
|
|
122
|
+
if (oldest !== undefined) {
|
|
123
|
+
roomQueues.delete(oldest);
|
|
124
|
+
clearRoomWatermarks(oldest);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return queue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function appendToQueue(queue: RoomQueue, entry: HistoryEntry): HistorySnapshotToken {
|
|
132
|
+
queue.entries.push(entry);
|
|
133
|
+
if (queue.entries.length > maxQueueSize) {
|
|
134
|
+
const overflow = queue.entries.length - maxQueueSize;
|
|
135
|
+
queue.entries.splice(0, overflow);
|
|
136
|
+
queue.baseIndex += overflow;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
snapshotIdx: queue.baseIndex + queue.entries.length,
|
|
140
|
+
queueGeneration: queue.generation,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function wmKey(agentId: string, roomId: string): string {
|
|
145
|
+
return `${agentId}:${roomId}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function preparedTriggerKey(agentId: string, messageId?: string): string | null {
|
|
149
|
+
if (!messageId?.trim()) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return `${agentId}:${messageId.trim()}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function rememberWatermark(key: string, snapshotIdx: number): void {
|
|
156
|
+
const nextSnapshotIdx = Math.max(agentWatermarks.get(key) ?? 0, snapshotIdx);
|
|
157
|
+
if (agentWatermarks.has(key)) {
|
|
158
|
+
// Refresh insertion order so capped-map eviction removes the stalest pair, not an active one.
|
|
159
|
+
agentWatermarks.delete(key);
|
|
160
|
+
}
|
|
161
|
+
agentWatermarks.set(key, nextSnapshotIdx);
|
|
162
|
+
if (agentWatermarks.size > maxWatermarkEntries) {
|
|
163
|
+
const oldest = agentWatermarks.keys().next().value;
|
|
164
|
+
if (oldest !== undefined) {
|
|
165
|
+
agentWatermarks.delete(oldest);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function rememberPreparedTrigger(
|
|
171
|
+
queue: RoomQueue,
|
|
172
|
+
retryKey: string,
|
|
173
|
+
prepared: PreparedTriggerResult,
|
|
174
|
+
): PreparedTriggerResult {
|
|
175
|
+
if (queue.preparedTriggers.has(retryKey)) {
|
|
176
|
+
// Refresh insertion order so capped eviction keeps actively retried events hot.
|
|
177
|
+
queue.preparedTriggers.delete(retryKey);
|
|
178
|
+
}
|
|
179
|
+
queue.preparedTriggers.set(retryKey, prepared);
|
|
180
|
+
if (queue.preparedTriggers.size > maxPreparedTriggerEntries) {
|
|
181
|
+
const oldest = queue.preparedTriggers.keys().next().value;
|
|
182
|
+
if (oldest !== undefined) {
|
|
183
|
+
queue.preparedTriggers.delete(oldest);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return prepared;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function computePendingHistory(
|
|
190
|
+
queue: RoomQueue,
|
|
191
|
+
agentId: string,
|
|
192
|
+
roomId: string,
|
|
193
|
+
limit: number,
|
|
194
|
+
): HistoryEntry[] {
|
|
195
|
+
if (limit <= 0 || queue.entries.length === 0) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
const wm = agentWatermarks.get(wmKey(agentId, roomId)) ?? 0;
|
|
199
|
+
// startAbs: the first absolute index the agent hasn't seen yet
|
|
200
|
+
const startAbs = Math.max(wm, queue.baseIndex);
|
|
201
|
+
const startRel = startAbs - queue.baseIndex;
|
|
202
|
+
const available = queue.entries.slice(startRel);
|
|
203
|
+
return available.length > limit ? available.slice(-limit) : available;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
recordPending(roomId, entry) {
|
|
208
|
+
const queue = getOrCreateQueue(roomId);
|
|
209
|
+
appendToQueue(queue, entry);
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
getPendingHistory(agentId, roomId, limit) {
|
|
213
|
+
const queue = roomQueues.get(roomId);
|
|
214
|
+
if (!queue) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
return computePendingHistory(queue, agentId, roomId, limit);
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
recordTrigger(roomId, entry) {
|
|
221
|
+
const queue = getOrCreateQueue(roomId);
|
|
222
|
+
return appendToQueue(queue, entry);
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
prepareTrigger(agentId, roomId, limit, entry) {
|
|
226
|
+
const queue = getOrCreateQueue(roomId);
|
|
227
|
+
const retryKey = preparedTriggerKey(agentId, entry.messageId);
|
|
228
|
+
if (retryKey) {
|
|
229
|
+
const prepared = queue.preparedTriggers.get(retryKey);
|
|
230
|
+
if (prepared) {
|
|
231
|
+
return rememberPreparedTrigger(queue, retryKey, prepared);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const prepared = {
|
|
235
|
+
history: computePendingHistory(queue, agentId, roomId, limit),
|
|
236
|
+
...appendToQueue(queue, entry),
|
|
237
|
+
};
|
|
238
|
+
if (retryKey) {
|
|
239
|
+
return rememberPreparedTrigger(queue, retryKey, prepared);
|
|
240
|
+
}
|
|
241
|
+
return prepared;
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
consumeHistory(agentId, roomId, snapshot, messageId) {
|
|
245
|
+
const key = wmKey(agentId, roomId);
|
|
246
|
+
const queue = roomQueues.get(roomId);
|
|
247
|
+
if (!queue) {
|
|
248
|
+
// The room was evicted while this trigger was in flight. Keep eviction authoritative
|
|
249
|
+
// so a late completion cannot recreate a stale watermark against a fresh queue.
|
|
250
|
+
agentWatermarks.delete(key);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (queue.generation !== snapshot.queueGeneration) {
|
|
254
|
+
// The room was evicted and recreated before this trigger completed. Reject the stale
|
|
255
|
+
// snapshot so it cannot advance or erase state for the new queue generation.
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Monotone write: never regress an already-advanced watermark.
|
|
259
|
+
// Guards against out-of-order completion when two triggers for the same
|
|
260
|
+
// (agentId, roomId) are in-flight concurrently.
|
|
261
|
+
rememberWatermark(key, snapshot.snapshotIdx);
|
|
262
|
+
const retryKey = preparedTriggerKey(agentId, messageId);
|
|
263
|
+
if (queue && retryKey) {
|
|
264
|
+
queue.preparedTriggers.delete(retryKey);
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createRoomHistoryTracker(
|
|
271
|
+
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
|
|
272
|
+
maxRoomQueues = DEFAULT_MAX_ROOM_QUEUES,
|
|
273
|
+
maxWatermarkEntries = MAX_WATERMARK_ENTRIES,
|
|
274
|
+
maxPreparedTriggerEntries = MAX_PREPARED_TRIGGER_ENTRIES,
|
|
275
|
+
): RoomHistoryTracker {
|
|
276
|
+
const tracker = createRoomHistoryTrackerInternal(
|
|
277
|
+
maxQueueSize,
|
|
278
|
+
maxRoomQueues,
|
|
279
|
+
maxWatermarkEntries,
|
|
280
|
+
maxPreparedTriggerEntries,
|
|
281
|
+
);
|
|
282
|
+
return {
|
|
283
|
+
recordPending: tracker.recordPending,
|
|
284
|
+
prepareTrigger: tracker.prepareTrigger,
|
|
285
|
+
consumeHistory: tracker.consumeHistory,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function createRoomHistoryTrackerForTests(
|
|
290
|
+
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
|
|
291
|
+
maxRoomQueues = DEFAULT_MAX_ROOM_QUEUES,
|
|
292
|
+
maxWatermarkEntries = MAX_WATERMARK_ENTRIES,
|
|
293
|
+
maxPreparedTriggerEntries = MAX_PREPARED_TRIGGER_ENTRIES,
|
|
294
|
+
): RoomHistoryTrackerTestApi {
|
|
295
|
+
return createRoomHistoryTrackerInternal(
|
|
296
|
+
maxQueueSize,
|
|
297
|
+
maxRoomQueues,
|
|
298
|
+
maxWatermarkEntries,
|
|
299
|
+
maxPreparedTriggerEntries,
|
|
300
|
+
);
|
|
301
|
+
}
|