Skip to content

Commit ff6541f

Browse files
committed
Matrix: fix Jiti runtime API boundary
1 parent 5a41229 commit ff6541f

8 files changed

Lines changed: 273 additions & 233 deletions

File tree

extensions/matrix/runtime-api.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,4 @@
1-
export * from "openclaw/plugin-sdk/matrix";
1+
// Keep the external runtime API light so Jiti callers can resolve Matrix config
2+
// helpers without traversing the full plugin-sdk/runtime graph.
23
export * from "./src/auth-precedence.js";
3-
export {
4-
findMatrixAccountEntry,
5-
hashMatrixAccessToken,
6-
listMatrixEnvAccountIds,
7-
resolveConfiguredMatrixAccountIds,
8-
resolveMatrixChannelConfig,
9-
resolveMatrixCredentialsFilename,
10-
resolveMatrixEnvAccountToken,
11-
resolveMatrixHomeserverKey,
12-
resolveMatrixLegacyFlatStoreRoot,
13-
sanitizeMatrixPathSegment,
14-
} from "./helper-api.js";
4+
export * from "./helper-api.js";

extensions/matrix/src/channel.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,6 @@ import {
1717
} from "openclaw/plugin-sdk/channel-runtime";
1818
import { buildTrafficStatusSummary } from "openclaw/plugin-sdk/extension-shared";
1919
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
20-
import {
21-
buildChannelConfigSchema,
22-
buildProbeChannelStatusSummary,
23-
collectStatusIssuesFromLastError,
24-
DEFAULT_ACCOUNT_ID,
25-
PAIRING_APPROVED_MESSAGE,
26-
type ChannelPlugin,
27-
} from "../runtime-api.js";
2820
import { matrixMessageActions } from "./actions.js";
2921
import { MatrixConfigSchema } from "./config-schema.js";
3022
import {
@@ -44,6 +36,14 @@ import {
4436
resolveMatrixDirectUserId,
4537
resolveMatrixTargetIdentity,
4638
} from "./matrix/target-ids.js";
39+
import {
40+
buildChannelConfigSchema,
41+
buildProbeChannelStatusSummary,
42+
collectStatusIssuesFromLastError,
43+
DEFAULT_ACCOUNT_ID,
44+
PAIRING_APPROVED_MESSAGE,
45+
type ChannelPlugin,
46+
} from "./runtime-api.js";
4747
import { getMatrixRuntime } from "./runtime.js";
4848
import { resolveMatrixOutboundSessionRoute } from "./session-route.js";
4949
import { matrixSetupAdapter } from "./setup-core.js";
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import type {
2+
BindingTargetKind,
3+
SessionBindingRecord,
4+
} from "openclaw/plugin-sdk/conversation-runtime";
5+
6+
export type MatrixThreadBindingTargetKind = "subagent" | "acp";
7+
8+
export type MatrixThreadBindingRecord = {
9+
accountId: string;
10+
conversationId: string;
11+
parentConversationId?: string;
12+
targetKind: MatrixThreadBindingTargetKind;
13+
targetSessionKey: string;
14+
agentId?: string;
15+
label?: string;
16+
boundBy?: string;
17+
boundAt: number;
18+
lastActivityAt: number;
19+
idleTimeoutMs?: number;
20+
maxAgeMs?: number;
21+
};
22+
23+
export type MatrixThreadBindingManager = {
24+
accountId: string;
25+
getIdleTimeoutMs: () => number;
26+
getMaxAgeMs: () => number;
27+
getByConversation: (params: {
28+
conversationId: string;
29+
parentConversationId?: string;
30+
}) => MatrixThreadBindingRecord | undefined;
31+
listBySessionKey: (targetSessionKey: string) => MatrixThreadBindingRecord[];
32+
listBindings: () => MatrixThreadBindingRecord[];
33+
touchBinding: (bindingId: string, at?: number) => MatrixThreadBindingRecord | null;
34+
setIdleTimeoutBySessionKey: (params: {
35+
targetSessionKey: string;
36+
idleTimeoutMs: number;
37+
}) => MatrixThreadBindingRecord[];
38+
setMaxAgeBySessionKey: (params: {
39+
targetSessionKey: string;
40+
maxAgeMs: number;
41+
}) => MatrixThreadBindingRecord[];
42+
stop: () => void;
43+
};
44+
45+
export type MatrixThreadBindingManagerCacheEntry = {
46+
filePath: string;
47+
manager: MatrixThreadBindingManager;
48+
};
49+
50+
const MANAGERS_BY_ACCOUNT_ID = new Map<string, MatrixThreadBindingManagerCacheEntry>();
51+
const BINDINGS_BY_ACCOUNT_CONVERSATION = new Map<string, MatrixThreadBindingRecord>();
52+
53+
export function resolveBindingKey(params: {
54+
accountId: string;
55+
conversationId: string;
56+
parentConversationId?: string;
57+
}): string {
58+
return `${params.accountId}:${params.parentConversationId?.trim() || "-"}:${params.conversationId}`;
59+
}
60+
61+
function toSessionBindingTargetKind(raw: MatrixThreadBindingTargetKind): BindingTargetKind {
62+
return raw === "subagent" ? "subagent" : "session";
63+
}
64+
65+
export function toMatrixBindingTargetKind(raw: BindingTargetKind): MatrixThreadBindingTargetKind {
66+
return raw === "subagent" ? "subagent" : "acp";
67+
}
68+
69+
export function resolveEffectiveBindingExpiry(params: {
70+
record: MatrixThreadBindingRecord;
71+
defaultIdleTimeoutMs: number;
72+
defaultMaxAgeMs: number;
73+
}): {
74+
expiresAt?: number;
75+
reason?: "idle-expired" | "max-age-expired";
76+
} {
77+
const idleTimeoutMs =
78+
typeof params.record.idleTimeoutMs === "number"
79+
? Math.max(0, Math.floor(params.record.idleTimeoutMs))
80+
: params.defaultIdleTimeoutMs;
81+
const maxAgeMs =
82+
typeof params.record.maxAgeMs === "number"
83+
? Math.max(0, Math.floor(params.record.maxAgeMs))
84+
: params.defaultMaxAgeMs;
85+
const inactivityExpiresAt =
86+
idleTimeoutMs > 0
87+
? Math.max(params.record.lastActivityAt, params.record.boundAt) + idleTimeoutMs
88+
: undefined;
89+
const maxAgeExpiresAt = maxAgeMs > 0 ? params.record.boundAt + maxAgeMs : undefined;
90+
91+
if (inactivityExpiresAt != null && maxAgeExpiresAt != null) {
92+
return inactivityExpiresAt <= maxAgeExpiresAt
93+
? { expiresAt: inactivityExpiresAt, reason: "idle-expired" }
94+
: { expiresAt: maxAgeExpiresAt, reason: "max-age-expired" };
95+
}
96+
if (inactivityExpiresAt != null) {
97+
return { expiresAt: inactivityExpiresAt, reason: "idle-expired" };
98+
}
99+
if (maxAgeExpiresAt != null) {
100+
return { expiresAt: maxAgeExpiresAt, reason: "max-age-expired" };
101+
}
102+
return {};
103+
}
104+
105+
export function toSessionBindingRecord(
106+
record: MatrixThreadBindingRecord,
107+
defaults: { idleTimeoutMs: number; maxAgeMs: number },
108+
): SessionBindingRecord {
109+
const lifecycle = resolveEffectiveBindingExpiry({
110+
record,
111+
defaultIdleTimeoutMs: defaults.idleTimeoutMs,
112+
defaultMaxAgeMs: defaults.maxAgeMs,
113+
});
114+
const idleTimeoutMs =
115+
typeof record.idleTimeoutMs === "number" ? record.idleTimeoutMs : defaults.idleTimeoutMs;
116+
const maxAgeMs = typeof record.maxAgeMs === "number" ? record.maxAgeMs : defaults.maxAgeMs;
117+
return {
118+
bindingId: resolveBindingKey(record),
119+
targetSessionKey: record.targetSessionKey,
120+
targetKind: toSessionBindingTargetKind(record.targetKind),
121+
conversation: {
122+
channel: "matrix",
123+
accountId: record.accountId,
124+
conversationId: record.conversationId,
125+
parentConversationId: record.parentConversationId,
126+
},
127+
status: "active",
128+
boundAt: record.boundAt,
129+
expiresAt: lifecycle.expiresAt,
130+
metadata: {
131+
agentId: record.agentId,
132+
label: record.label,
133+
boundBy: record.boundBy,
134+
lastActivityAt: record.lastActivityAt,
135+
idleTimeoutMs,
136+
maxAgeMs,
137+
},
138+
};
139+
}
140+
141+
export function setBindingRecord(record: MatrixThreadBindingRecord): void {
142+
BINDINGS_BY_ACCOUNT_CONVERSATION.set(resolveBindingKey(record), record);
143+
}
144+
145+
export function removeBindingRecord(
146+
record: MatrixThreadBindingRecord,
147+
): MatrixThreadBindingRecord | null {
148+
const key = resolveBindingKey(record);
149+
const removed = BINDINGS_BY_ACCOUNT_CONVERSATION.get(key) ?? null;
150+
if (removed) {
151+
BINDINGS_BY_ACCOUNT_CONVERSATION.delete(key);
152+
}
153+
return removed;
154+
}
155+
156+
export function listBindingsForAccount(accountId: string): MatrixThreadBindingRecord[] {
157+
return [...BINDINGS_BY_ACCOUNT_CONVERSATION.values()].filter(
158+
(entry) => entry.accountId === accountId,
159+
);
160+
}
161+
162+
export function getMatrixThreadBindingManagerEntry(
163+
accountId: string,
164+
): MatrixThreadBindingManagerCacheEntry | null {
165+
return MANAGERS_BY_ACCOUNT_ID.get(accountId) ?? null;
166+
}
167+
168+
export function setMatrixThreadBindingManagerEntry(
169+
accountId: string,
170+
entry: MatrixThreadBindingManagerCacheEntry,
171+
): void {
172+
MANAGERS_BY_ACCOUNT_ID.set(accountId, entry);
173+
}
174+
175+
export function deleteMatrixThreadBindingManagerEntry(accountId: string): void {
176+
MANAGERS_BY_ACCOUNT_ID.delete(accountId);
177+
}
178+
179+
export function getMatrixThreadBindingManager(
180+
accountId: string,
181+
): MatrixThreadBindingManager | null {
182+
return MANAGERS_BY_ACCOUNT_ID.get(accountId)?.manager ?? null;
183+
}
184+
185+
export function setMatrixThreadBindingIdleTimeoutBySessionKey(params: {
186+
accountId: string;
187+
targetSessionKey: string;
188+
idleTimeoutMs: number;
189+
}): SessionBindingRecord[] {
190+
const manager = MANAGERS_BY_ACCOUNT_ID.get(params.accountId)?.manager;
191+
if (!manager) {
192+
return [];
193+
}
194+
return manager.setIdleTimeoutBySessionKey(params).map((record) =>
195+
toSessionBindingRecord(record, {
196+
idleTimeoutMs: manager.getIdleTimeoutMs(),
197+
maxAgeMs: manager.getMaxAgeMs(),
198+
}),
199+
);
200+
}
201+
202+
export function setMatrixThreadBindingMaxAgeBySessionKey(params: {
203+
accountId: string;
204+
targetSessionKey: string;
205+
maxAgeMs: number;
206+
}): SessionBindingRecord[] {
207+
const manager = MANAGERS_BY_ACCOUNT_ID.get(params.accountId)?.manager;
208+
if (!manager) {
209+
return [];
210+
}
211+
return manager.setMaxAgeBySessionKey(params).map((record) =>
212+
toSessionBindingRecord(record, {
213+
idleTimeoutMs: manager.getIdleTimeoutMs(),
214+
maxAgeMs: manager.getMaxAgeMs(),
215+
}),
216+
);
217+
}
218+
219+
export function resetMatrixThreadBindingsForTests(): void {
220+
for (const { manager } of MANAGERS_BY_ACCOUNT_ID.values()) {
221+
manager.stop();
222+
}
223+
MANAGERS_BY_ACCOUNT_ID.clear();
224+
BINDINGS_BY_ACCOUNT_CONVERSATION.clear();
225+
}

0 commit comments

Comments
 (0)