@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,673 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { getSessionBindingService, __testing } from "openclaw/plugin-sdk/conversation-runtime";
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import type { PluginRuntime } from "../../runtime-api.js";
|
|
8
|
+
import { setMatrixRuntime } from "../runtime.js";
|
|
9
|
+
import {
|
|
10
|
+
resolveMatrixStateFilePath,
|
|
11
|
+
resolveMatrixStoragePaths,
|
|
12
|
+
writeStorageMeta,
|
|
13
|
+
} from "./client/storage.js";
|
|
14
|
+
import type { MatrixAuth, MatrixStoragePaths } from "./client/types.js";
|
|
15
|
+
import {
|
|
16
|
+
createMatrixThreadBindingManager,
|
|
17
|
+
resetMatrixThreadBindingsForTests,
|
|
18
|
+
setMatrixThreadBindingIdleTimeoutBySessionKey,
|
|
19
|
+
setMatrixThreadBindingMaxAgeBySessionKey,
|
|
20
|
+
} from "./thread-bindings.js";
|
|
21
|
+
|
|
22
|
+
const sendMessageMatrixMock = vi.hoisted(() =>
|
|
23
|
+
vi.fn(async (_to: string, _message: string, opts?: { threadId?: string }) => ({
|
|
24
|
+
messageId: opts?.threadId ? "$reply" : "$root",
|
|
25
|
+
roomId: "!room:example",
|
|
26
|
+
})),
|
|
27
|
+
);
|
|
28
|
+
const actualRename = fs.rename.bind(fs);
|
|
29
|
+
const renameMock = vi.spyOn(fs, "rename");
|
|
30
|
+
|
|
31
|
+
vi.mock("./send.js", () => {
|
|
32
|
+
return {
|
|
33
|
+
sendMessageMatrix: sendMessageMatrixMock,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("matrix thread bindings", () => {
|
|
38
|
+
let stateDir: string;
|
|
39
|
+
const auth = {
|
|
40
|
+
accountId: "ops",
|
|
41
|
+
homeserver: "https://matrix.example.org",
|
|
42
|
+
userId: "@bot:example.org",
|
|
43
|
+
accessToken: "token",
|
|
44
|
+
} as const;
|
|
45
|
+
const accountId = "ops";
|
|
46
|
+
const idleTimeoutMs = 24 * 60 * 60 * 1000;
|
|
47
|
+
const matrixClient = {} as never;
|
|
48
|
+
|
|
49
|
+
function resetThreadBindingAdapters() {
|
|
50
|
+
__testing.resetSessionBindingAdaptersForTests();
|
|
51
|
+
resetMatrixThreadBindingsForTests();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function currentThreadConversation(params?: {
|
|
55
|
+
conversationId?: string;
|
|
56
|
+
parentConversationId?: string;
|
|
57
|
+
}) {
|
|
58
|
+
return {
|
|
59
|
+
channel: "matrix" as const,
|
|
60
|
+
accountId,
|
|
61
|
+
conversationId: params?.conversationId ?? "$thread",
|
|
62
|
+
parentConversationId: params?.parentConversationId ?? "!room:example",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createBindingManager(
|
|
67
|
+
params: {
|
|
68
|
+
auth?: MatrixAuth;
|
|
69
|
+
stateDir?: string;
|
|
70
|
+
idleTimeoutMs?: number;
|
|
71
|
+
maxAgeMs?: number;
|
|
72
|
+
enableSweeper?: boolean;
|
|
73
|
+
logVerboseMessage?: (message: string) => void;
|
|
74
|
+
} = {},
|
|
75
|
+
) {
|
|
76
|
+
return createMatrixThreadBindingManager({
|
|
77
|
+
accountId,
|
|
78
|
+
auth: params.auth ?? auth,
|
|
79
|
+
client: matrixClient,
|
|
80
|
+
...(params.stateDir ? { stateDir: params.stateDir } : {}),
|
|
81
|
+
idleTimeoutMs: params.idleTimeoutMs ?? idleTimeoutMs,
|
|
82
|
+
maxAgeMs: params.maxAgeMs ?? 0,
|
|
83
|
+
enableSweeper: params.enableSweeper ?? false,
|
|
84
|
+
...(params.logVerboseMessage ? { logVerboseMessage: params.logVerboseMessage } : {}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function createStaticThreadBindingManager() {
|
|
89
|
+
return createBindingManager();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function bindCurrentThread(params?: {
|
|
93
|
+
targetSessionKey?: string;
|
|
94
|
+
conversationId?: string;
|
|
95
|
+
parentConversationId?: string;
|
|
96
|
+
metadata?: { introText?: string };
|
|
97
|
+
}) {
|
|
98
|
+
return getSessionBindingService().bind({
|
|
99
|
+
targetSessionKey: params?.targetSessionKey ?? "agent:ops:subagent:child",
|
|
100
|
+
targetKind: "subagent",
|
|
101
|
+
conversation: currentThreadConversation({
|
|
102
|
+
conversationId: params?.conversationId,
|
|
103
|
+
parentConversationId: params?.parentConversationId,
|
|
104
|
+
}),
|
|
105
|
+
placement: "current",
|
|
106
|
+
...(params?.metadata ? { metadata: params.metadata } : {}),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveBindingsFilePath(customStateDir?: string) {
|
|
111
|
+
return resolveMatrixStateFilePath({
|
|
112
|
+
auth,
|
|
113
|
+
env: process.env,
|
|
114
|
+
...(customStateDir ? { stateDir: customStateDir } : {}),
|
|
115
|
+
filename: "thread-bindings.json",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function writeAuthStorageMeta(authForMeta: MatrixAuth, storagePaths: MatrixStoragePaths) {
|
|
120
|
+
writeStorageMeta({
|
|
121
|
+
storagePaths,
|
|
122
|
+
homeserver: authForMeta.homeserver,
|
|
123
|
+
userId: authForMeta.userId,
|
|
124
|
+
accountId: authForMeta.accountId,
|
|
125
|
+
deviceId: authForMeta.deviceId ?? null,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readPersistedLastActivityAt(bindingsPath: string) {
|
|
130
|
+
const raw = await fs.readFile(bindingsPath, "utf-8");
|
|
131
|
+
const parsed = JSON.parse(raw) as {
|
|
132
|
+
bindings?: Array<{ lastActivityAt?: number }>;
|
|
133
|
+
};
|
|
134
|
+
return parsed.bindings?.[0]?.lastActivityAt;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function expectPersistedThreadBinding(
|
|
138
|
+
bindingsPath: string,
|
|
139
|
+
expected: {
|
|
140
|
+
conversationId: string;
|
|
141
|
+
targetSessionKey: string;
|
|
142
|
+
parentConversationId?: string;
|
|
143
|
+
},
|
|
144
|
+
) {
|
|
145
|
+
await vi.waitFor(async () => {
|
|
146
|
+
const persistedRaw = await fs.readFile(bindingsPath, "utf-8");
|
|
147
|
+
expect(JSON.parse(persistedRaw)).toMatchObject({
|
|
148
|
+
version: 1,
|
|
149
|
+
bindings: [
|
|
150
|
+
expect.objectContaining({
|
|
151
|
+
conversationId: expected.conversationId,
|
|
152
|
+
parentConversationId: expected.parentConversationId ?? "!room:example",
|
|
153
|
+
targetSessionKey: expected.targetSessionKey,
|
|
154
|
+
}),
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
beforeEach(() => {
|
|
161
|
+
stateDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "matrix-thread-bindings-"));
|
|
162
|
+
resetThreadBindingAdapters();
|
|
163
|
+
sendMessageMatrixMock.mockClear();
|
|
164
|
+
renameMock.mockReset();
|
|
165
|
+
renameMock.mockImplementation(actualRename);
|
|
166
|
+
setMatrixRuntime({
|
|
167
|
+
state: {
|
|
168
|
+
resolveStateDir: () => stateDir,
|
|
169
|
+
},
|
|
170
|
+
} as PluginRuntime);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("creates child Matrix thread bindings from a top-level room context", async () => {
|
|
174
|
+
await createMatrixThreadBindingManager({
|
|
175
|
+
accountId,
|
|
176
|
+
auth,
|
|
177
|
+
client: matrixClient,
|
|
178
|
+
idleTimeoutMs,
|
|
179
|
+
maxAgeMs: 0,
|
|
180
|
+
enableSweeper: false,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const binding = await getSessionBindingService().bind({
|
|
184
|
+
targetSessionKey: "agent:ops:subagent:child",
|
|
185
|
+
targetKind: "subagent",
|
|
186
|
+
conversation: {
|
|
187
|
+
channel: "matrix",
|
|
188
|
+
accountId: "ops",
|
|
189
|
+
conversationId: "!room:example",
|
|
190
|
+
},
|
|
191
|
+
placement: "child",
|
|
192
|
+
metadata: {
|
|
193
|
+
introText: "intro root",
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(sendMessageMatrixMock).toHaveBeenCalledWith("room:!room:example", "intro root", {
|
|
198
|
+
client: {},
|
|
199
|
+
accountId: "ops",
|
|
200
|
+
});
|
|
201
|
+
expect(binding.conversation).toEqual({
|
|
202
|
+
channel: "matrix",
|
|
203
|
+
accountId: "ops",
|
|
204
|
+
conversationId: "$root",
|
|
205
|
+
parentConversationId: "!room:example",
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("posts intro messages inside existing Matrix threads for current placement", async () => {
|
|
210
|
+
await createStaticThreadBindingManager();
|
|
211
|
+
|
|
212
|
+
const binding = await bindCurrentThread({
|
|
213
|
+
metadata: {
|
|
214
|
+
introText: "intro thread",
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(sendMessageMatrixMock).toHaveBeenCalledWith("room:!room:example", "intro thread", {
|
|
219
|
+
client: {},
|
|
220
|
+
accountId: "ops",
|
|
221
|
+
threadId: "$thread",
|
|
222
|
+
});
|
|
223
|
+
expect(
|
|
224
|
+
getSessionBindingService().resolveByConversation({
|
|
225
|
+
channel: "matrix",
|
|
226
|
+
accountId: "ops",
|
|
227
|
+
conversationId: "$thread",
|
|
228
|
+
parentConversationId: "!room:example",
|
|
229
|
+
}),
|
|
230
|
+
).toMatchObject({
|
|
231
|
+
bindingId: binding.bindingId,
|
|
232
|
+
targetSessionKey: "agent:ops:subagent:child",
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("expires idle bindings via the sweeper", async () => {
|
|
237
|
+
vi.useFakeTimers();
|
|
238
|
+
vi.setSystemTime(new Date("2026-03-08T12:00:00.000Z"));
|
|
239
|
+
try {
|
|
240
|
+
await createMatrixThreadBindingManager({
|
|
241
|
+
accountId: "ops",
|
|
242
|
+
auth,
|
|
243
|
+
client: {} as never,
|
|
244
|
+
idleTimeoutMs: 1_000,
|
|
245
|
+
maxAgeMs: 0,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await getSessionBindingService().bind({
|
|
249
|
+
targetSessionKey: "agent:ops:subagent:child",
|
|
250
|
+
targetKind: "subagent",
|
|
251
|
+
conversation: {
|
|
252
|
+
channel: "matrix",
|
|
253
|
+
accountId: "ops",
|
|
254
|
+
conversationId: "$thread",
|
|
255
|
+
parentConversationId: "!room:example",
|
|
256
|
+
},
|
|
257
|
+
placement: "current",
|
|
258
|
+
metadata: {
|
|
259
|
+
introText: "intro thread",
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
sendMessageMatrixMock.mockClear();
|
|
264
|
+
await vi.advanceTimersByTimeAsync(61_000);
|
|
265
|
+
await Promise.resolve();
|
|
266
|
+
|
|
267
|
+
expect(
|
|
268
|
+
getSessionBindingService().resolveByConversation({
|
|
269
|
+
channel: "matrix",
|
|
270
|
+
accountId: "ops",
|
|
271
|
+
conversationId: "$thread",
|
|
272
|
+
parentConversationId: "!room:example",
|
|
273
|
+
}),
|
|
274
|
+
).toBeNull();
|
|
275
|
+
} finally {
|
|
276
|
+
vi.useRealTimers();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("persists expired bindings after a sweep", async () => {
|
|
281
|
+
vi.useFakeTimers();
|
|
282
|
+
vi.setSystemTime(new Date("2026-03-08T12:00:00.000Z"));
|
|
283
|
+
try {
|
|
284
|
+
await createMatrixThreadBindingManager({
|
|
285
|
+
accountId: "ops",
|
|
286
|
+
auth,
|
|
287
|
+
client: {} as never,
|
|
288
|
+
idleTimeoutMs: 1_000,
|
|
289
|
+
maxAgeMs: 0,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await getSessionBindingService().bind({
|
|
293
|
+
targetSessionKey: "agent:ops:subagent:first",
|
|
294
|
+
targetKind: "subagent",
|
|
295
|
+
conversation: {
|
|
296
|
+
channel: "matrix",
|
|
297
|
+
accountId: "ops",
|
|
298
|
+
conversationId: "$thread-1",
|
|
299
|
+
parentConversationId: "!room:example",
|
|
300
|
+
},
|
|
301
|
+
placement: "current",
|
|
302
|
+
});
|
|
303
|
+
await getSessionBindingService().bind({
|
|
304
|
+
targetSessionKey: "agent:ops:subagent:second",
|
|
305
|
+
targetKind: "subagent",
|
|
306
|
+
conversation: {
|
|
307
|
+
channel: "matrix",
|
|
308
|
+
accountId: "ops",
|
|
309
|
+
conversationId: "$thread-2",
|
|
310
|
+
parentConversationId: "!room:example",
|
|
311
|
+
},
|
|
312
|
+
placement: "current",
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await vi.advanceTimersByTimeAsync(61_000);
|
|
316
|
+
await Promise.resolve();
|
|
317
|
+
|
|
318
|
+
await vi.waitFor(async () => {
|
|
319
|
+
const persistedRaw = await fs.readFile(resolveBindingsFilePath(), "utf-8");
|
|
320
|
+
expect(JSON.parse(persistedRaw)).toMatchObject({
|
|
321
|
+
version: 1,
|
|
322
|
+
bindings: [],
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
} finally {
|
|
326
|
+
vi.useRealTimers();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("logs and survives sweeper persistence failures", async () => {
|
|
331
|
+
vi.useFakeTimers();
|
|
332
|
+
vi.setSystemTime(new Date("2026-03-08T12:00:00.000Z"));
|
|
333
|
+
const logVerboseMessage = vi.fn();
|
|
334
|
+
try {
|
|
335
|
+
await createMatrixThreadBindingManager({
|
|
336
|
+
accountId: "ops",
|
|
337
|
+
auth,
|
|
338
|
+
client: {} as never,
|
|
339
|
+
idleTimeoutMs: 1_000,
|
|
340
|
+
maxAgeMs: 0,
|
|
341
|
+
logVerboseMessage,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await getSessionBindingService().bind({
|
|
345
|
+
targetSessionKey: "agent:ops:subagent:child",
|
|
346
|
+
targetKind: "subagent",
|
|
347
|
+
conversation: {
|
|
348
|
+
channel: "matrix",
|
|
349
|
+
accountId: "ops",
|
|
350
|
+
conversationId: "$thread",
|
|
351
|
+
parentConversationId: "!room:example",
|
|
352
|
+
},
|
|
353
|
+
placement: "current",
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
renameMock.mockRejectedValueOnce(new Error("disk full"));
|
|
357
|
+
await vi.advanceTimersByTimeAsync(61_000);
|
|
358
|
+
await Promise.resolve();
|
|
359
|
+
|
|
360
|
+
await vi.waitFor(() => {
|
|
361
|
+
expect(
|
|
362
|
+
logVerboseMessage.mock.calls.some(
|
|
363
|
+
([message]) =>
|
|
364
|
+
typeof message === "string" &&
|
|
365
|
+
message.includes("failed auto-unbinding expired bindings"),
|
|
366
|
+
),
|
|
367
|
+
).toBe(true);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
await vi.waitFor(() => {
|
|
371
|
+
expect(logVerboseMessage).toHaveBeenCalledWith(
|
|
372
|
+
expect.stringContaining("matrix: auto-unbinding $thread due to idle-expired"),
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
expect(
|
|
377
|
+
getSessionBindingService().resolveByConversation({
|
|
378
|
+
channel: "matrix",
|
|
379
|
+
accountId: "ops",
|
|
380
|
+
conversationId: "$thread",
|
|
381
|
+
parentConversationId: "!room:example",
|
|
382
|
+
}),
|
|
383
|
+
).toBeNull();
|
|
384
|
+
} finally {
|
|
385
|
+
vi.useRealTimers();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("sends threaded farewell messages when bindings are unbound", async () => {
|
|
390
|
+
await createMatrixThreadBindingManager({
|
|
391
|
+
accountId: "ops",
|
|
392
|
+
auth,
|
|
393
|
+
client: {} as never,
|
|
394
|
+
idleTimeoutMs: 1_000,
|
|
395
|
+
maxAgeMs: 0,
|
|
396
|
+
enableSweeper: false,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const binding = await getSessionBindingService().bind({
|
|
400
|
+
targetSessionKey: "agent:ops:subagent:child",
|
|
401
|
+
targetKind: "subagent",
|
|
402
|
+
conversation: {
|
|
403
|
+
channel: "matrix",
|
|
404
|
+
accountId: "ops",
|
|
405
|
+
conversationId: "$thread",
|
|
406
|
+
parentConversationId: "!room:example",
|
|
407
|
+
},
|
|
408
|
+
placement: "current",
|
|
409
|
+
metadata: {
|
|
410
|
+
introText: "intro thread",
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
sendMessageMatrixMock.mockClear();
|
|
415
|
+
await getSessionBindingService().unbind({
|
|
416
|
+
bindingId: binding.bindingId,
|
|
417
|
+
reason: "idle-expired",
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
expect(sendMessageMatrixMock).toHaveBeenCalledWith(
|
|
421
|
+
"room:!room:example",
|
|
422
|
+
expect.stringContaining("Session ended automatically"),
|
|
423
|
+
expect.objectContaining({
|
|
424
|
+
accountId: "ops",
|
|
425
|
+
threadId: "$thread",
|
|
426
|
+
}),
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("does not reload persisted bindings after the Matrix access token changes while deviceId is unknown", async () => {
|
|
431
|
+
const initialAuth = {
|
|
432
|
+
...auth,
|
|
433
|
+
accessToken: "token-old",
|
|
434
|
+
};
|
|
435
|
+
const rotatedAuth = {
|
|
436
|
+
...auth,
|
|
437
|
+
accessToken: "token-new",
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const initialManager = await createBindingManager({ auth: initialAuth });
|
|
441
|
+
|
|
442
|
+
await bindCurrentThread();
|
|
443
|
+
const initialStoragePaths = resolveMatrixStoragePaths({
|
|
444
|
+
...initialAuth,
|
|
445
|
+
env: process.env,
|
|
446
|
+
});
|
|
447
|
+
writeAuthStorageMeta(initialAuth, initialStoragePaths);
|
|
448
|
+
|
|
449
|
+
initialManager.stop();
|
|
450
|
+
resetThreadBindingAdapters();
|
|
451
|
+
|
|
452
|
+
await createBindingManager({ auth: rotatedAuth });
|
|
453
|
+
|
|
454
|
+
expect(
|
|
455
|
+
getSessionBindingService().resolveByConversation({
|
|
456
|
+
channel: "matrix",
|
|
457
|
+
accountId: "ops",
|
|
458
|
+
conversationId: "$thread",
|
|
459
|
+
parentConversationId: "!room:example",
|
|
460
|
+
}),
|
|
461
|
+
).toBeNull();
|
|
462
|
+
|
|
463
|
+
const initialBindingsPath = path.join(initialStoragePaths.rootDir, "thread-bindings.json");
|
|
464
|
+
const rotatedBindingsPath = path.join(
|
|
465
|
+
resolveMatrixStoragePaths({
|
|
466
|
+
...rotatedAuth,
|
|
467
|
+
env: process.env,
|
|
468
|
+
}).rootDir,
|
|
469
|
+
"thread-bindings.json",
|
|
470
|
+
);
|
|
471
|
+
expect(rotatedBindingsPath).not.toBe(initialBindingsPath);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("reloads persisted bindings after the Matrix access token changes when deviceId is known", async () => {
|
|
475
|
+
const initialAuth = {
|
|
476
|
+
...auth,
|
|
477
|
+
accessToken: "token-old",
|
|
478
|
+
deviceId: "DEVICE123",
|
|
479
|
+
};
|
|
480
|
+
const rotatedAuth = {
|
|
481
|
+
...auth,
|
|
482
|
+
accessToken: "token-new",
|
|
483
|
+
deviceId: "DEVICE123",
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const initialManager = await createBindingManager({ auth: initialAuth });
|
|
487
|
+
|
|
488
|
+
await bindCurrentThread();
|
|
489
|
+
const initialStoragePaths = resolveMatrixStoragePaths({
|
|
490
|
+
...initialAuth,
|
|
491
|
+
env: process.env,
|
|
492
|
+
});
|
|
493
|
+
writeAuthStorageMeta(initialAuth, initialStoragePaths);
|
|
494
|
+
const initialBindingsPath = path.join(initialStoragePaths.rootDir, "thread-bindings.json");
|
|
495
|
+
await expectPersistedThreadBinding(initialBindingsPath, {
|
|
496
|
+
conversationId: "$thread",
|
|
497
|
+
targetSessionKey: "agent:ops:subagent:child",
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
initialManager.stop();
|
|
501
|
+
resetThreadBindingAdapters();
|
|
502
|
+
|
|
503
|
+
await createBindingManager({ auth: rotatedAuth });
|
|
504
|
+
|
|
505
|
+
expect(
|
|
506
|
+
getSessionBindingService().resolveByConversation({
|
|
507
|
+
channel: "matrix",
|
|
508
|
+
accountId: "ops",
|
|
509
|
+
conversationId: "$thread",
|
|
510
|
+
parentConversationId: "!room:example",
|
|
511
|
+
}),
|
|
512
|
+
).toMatchObject({
|
|
513
|
+
targetSessionKey: "agent:ops:subagent:child",
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const rotatedBindingsPath = path.join(
|
|
517
|
+
resolveMatrixStoragePaths({
|
|
518
|
+
...rotatedAuth,
|
|
519
|
+
env: process.env,
|
|
520
|
+
}).rootDir,
|
|
521
|
+
"thread-bindings.json",
|
|
522
|
+
);
|
|
523
|
+
expect(rotatedBindingsPath).toBe(initialBindingsPath);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("replaces reused account managers when the bindings stateDir changes", async () => {
|
|
527
|
+
const initialStateDir = stateDir;
|
|
528
|
+
const replacementStateDir = await fs.mkdtemp(
|
|
529
|
+
path.join(os.tmpdir(), "matrix-thread-bindings-replacement-"),
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const initialManager = await createBindingManager({
|
|
533
|
+
stateDir: initialStateDir,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
await bindCurrentThread();
|
|
537
|
+
|
|
538
|
+
const replacementManager = await createBindingManager({
|
|
539
|
+
stateDir: replacementStateDir,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
expect(replacementManager).not.toBe(initialManager);
|
|
543
|
+
expect(replacementManager.listBindings()).toEqual([]);
|
|
544
|
+
expect(
|
|
545
|
+
getSessionBindingService().resolveByConversation({
|
|
546
|
+
channel: "matrix",
|
|
547
|
+
accountId: "ops",
|
|
548
|
+
conversationId: "$thread",
|
|
549
|
+
parentConversationId: "!room:example",
|
|
550
|
+
}),
|
|
551
|
+
).toBeNull();
|
|
552
|
+
|
|
553
|
+
await bindCurrentThread({
|
|
554
|
+
targetSessionKey: "agent:ops:subagent:replacement",
|
|
555
|
+
conversationId: "$thread-2",
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
await expectPersistedThreadBinding(resolveBindingsFilePath(replacementStateDir), {
|
|
559
|
+
conversationId: "$thread-2",
|
|
560
|
+
targetSessionKey: "agent:ops:subagent:replacement",
|
|
561
|
+
});
|
|
562
|
+
await expectPersistedThreadBinding(resolveBindingsFilePath(initialStateDir), {
|
|
563
|
+
conversationId: "$thread",
|
|
564
|
+
targetSessionKey: "agent:ops:subagent:child",
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it("updates lifecycle windows by session key and refreshes activity", async () => {
|
|
569
|
+
vi.useFakeTimers();
|
|
570
|
+
vi.setSystemTime(new Date("2026-03-06T10:00:00.000Z"));
|
|
571
|
+
try {
|
|
572
|
+
const manager = await createMatrixThreadBindingManager({
|
|
573
|
+
accountId: "ops",
|
|
574
|
+
auth,
|
|
575
|
+
client: {} as never,
|
|
576
|
+
idleTimeoutMs: 24 * 60 * 60 * 1000,
|
|
577
|
+
maxAgeMs: 0,
|
|
578
|
+
enableSweeper: false,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
await getSessionBindingService().bind({
|
|
582
|
+
targetSessionKey: "agent:ops:subagent:child",
|
|
583
|
+
targetKind: "subagent",
|
|
584
|
+
conversation: {
|
|
585
|
+
channel: "matrix",
|
|
586
|
+
accountId: "ops",
|
|
587
|
+
conversationId: "$thread",
|
|
588
|
+
parentConversationId: "!room:example",
|
|
589
|
+
},
|
|
590
|
+
placement: "current",
|
|
591
|
+
});
|
|
592
|
+
const original = manager.listBySessionKey("agent:ops:subagent:child")[0];
|
|
593
|
+
expect(original).toBeDefined();
|
|
594
|
+
|
|
595
|
+
const idleUpdated = setMatrixThreadBindingIdleTimeoutBySessionKey({
|
|
596
|
+
accountId: "ops",
|
|
597
|
+
targetSessionKey: "agent:ops:subagent:child",
|
|
598
|
+
idleTimeoutMs: 2 * 60 * 60 * 1000,
|
|
599
|
+
});
|
|
600
|
+
vi.setSystemTime(new Date("2026-03-06T12:00:00.000Z"));
|
|
601
|
+
const maxAgeUpdated = setMatrixThreadBindingMaxAgeBySessionKey({
|
|
602
|
+
accountId: "ops",
|
|
603
|
+
targetSessionKey: "agent:ops:subagent:child",
|
|
604
|
+
maxAgeMs: 6 * 60 * 60 * 1000,
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
expect(idleUpdated).toHaveLength(1);
|
|
608
|
+
expect(idleUpdated[0]?.metadata?.idleTimeoutMs).toBe(2 * 60 * 60 * 1000);
|
|
609
|
+
expect(maxAgeUpdated).toHaveLength(1);
|
|
610
|
+
expect(maxAgeUpdated[0]?.metadata?.maxAgeMs).toBe(6 * 60 * 60 * 1000);
|
|
611
|
+
expect(maxAgeUpdated[0]?.boundAt).toBe(original?.boundAt);
|
|
612
|
+
expect(maxAgeUpdated[0]?.metadata?.lastActivityAt).toBe(
|
|
613
|
+
Date.parse("2026-03-06T12:00:00.000Z"),
|
|
614
|
+
);
|
|
615
|
+
expect(manager.listBySessionKey("agent:ops:subagent:child")[0]?.maxAgeMs).toBe(
|
|
616
|
+
6 * 60 * 60 * 1000,
|
|
617
|
+
);
|
|
618
|
+
expect(manager.listBySessionKey("agent:ops:subagent:child")[0]?.lastActivityAt).toBe(
|
|
619
|
+
Date.parse("2026-03-06T12:00:00.000Z"),
|
|
620
|
+
);
|
|
621
|
+
} finally {
|
|
622
|
+
vi.useRealTimers();
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it("persists the latest touched activity only after the debounce window", async () => {
|
|
627
|
+
vi.useFakeTimers();
|
|
628
|
+
vi.setSystemTime(new Date("2026-03-06T10:00:00.000Z"));
|
|
629
|
+
try {
|
|
630
|
+
await createStaticThreadBindingManager();
|
|
631
|
+
const binding = await bindCurrentThread();
|
|
632
|
+
|
|
633
|
+
const bindingsPath = resolveBindingsFilePath();
|
|
634
|
+
const originalLastActivityAt = await readPersistedLastActivityAt(bindingsPath);
|
|
635
|
+
const firstTouchedAt = Date.parse("2026-03-06T10:05:00.000Z");
|
|
636
|
+
const secondTouchedAt = Date.parse("2026-03-06T10:10:00.000Z");
|
|
637
|
+
|
|
638
|
+
getSessionBindingService().touch(binding.bindingId, firstTouchedAt);
|
|
639
|
+
getSessionBindingService().touch(binding.bindingId, secondTouchedAt);
|
|
640
|
+
|
|
641
|
+
await vi.advanceTimersByTimeAsync(29_000);
|
|
642
|
+
expect(await readPersistedLastActivityAt(bindingsPath)).toBe(originalLastActivityAt);
|
|
643
|
+
|
|
644
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
645
|
+
await vi.waitFor(async () => {
|
|
646
|
+
expect(await readPersistedLastActivityAt(bindingsPath)).toBe(secondTouchedAt);
|
|
647
|
+
});
|
|
648
|
+
} finally {
|
|
649
|
+
vi.useRealTimers();
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("flushes pending touch persistence on stop", async () => {
|
|
654
|
+
vi.useFakeTimers();
|
|
655
|
+
vi.setSystemTime(new Date("2026-03-06T10:00:00.000Z"));
|
|
656
|
+
try {
|
|
657
|
+
const manager = await createStaticThreadBindingManager();
|
|
658
|
+
const binding = await bindCurrentThread();
|
|
659
|
+
const touchedAt = Date.parse("2026-03-06T12:00:00.000Z");
|
|
660
|
+
getSessionBindingService().touch(binding.bindingId, touchedAt);
|
|
661
|
+
|
|
662
|
+
manager.stop();
|
|
663
|
+
vi.useRealTimers();
|
|
664
|
+
|
|
665
|
+
const bindingsPath = resolveBindingsFilePath();
|
|
666
|
+
await vi.waitFor(async () => {
|
|
667
|
+
expect(await readPersistedLastActivityAt(bindingsPath)).toBe(touchedAt);
|
|
668
|
+
});
|
|
669
|
+
} finally {
|
|
670
|
+
vi.useRealTimers();
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
});
|