@openacp/cli 2026.410.3 → 2026.414.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{channel-CFMUPzvH.d.ts → channel-bWC2vDOv.d.ts} +49 -6
- package/dist/cli.js +1410 -139
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +294 -25
- package/dist/index.js +448 -73
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1244,11 +1244,11 @@ mockServices.context(overrides?) // buildContext, registerProvider
|
|
|
1244
1244
|
ctx.registerCommand({
|
|
1245
1245
|
name: 'mycommand',
|
|
1246
1246
|
description: 'Does something useful',
|
|
1247
|
-
usage: '
|
|
1247
|
+
usage: '[arg]',
|
|
1248
1248
|
category: 'plugin',
|
|
1249
1249
|
async handler(args) {
|
|
1250
1250
|
const input = args.raw.trim()
|
|
1251
|
-
if (!input) return { type: 'error', message: 'Usage: /mycommand
|
|
1251
|
+
if (!input) return { type: 'error', message: 'Usage: /mycommand [arg]' }
|
|
1252
1252
|
return { type: 'text', text: \\\`Result: \\\${input}\\\` }
|
|
1253
1253
|
},
|
|
1254
1254
|
})
|
|
@@ -1811,6 +1811,8 @@ var init_events = __esm({
|
|
|
1811
1811
|
TURN_START: "turn:start",
|
|
1812
1812
|
/** Turn ended (always fires, even on error) — read-only, fire-and-forget. */
|
|
1813
1813
|
TURN_END: "turn:end",
|
|
1814
|
+
/** After a turn completes — full assembled agent text, read-only, fire-and-forget. */
|
|
1815
|
+
AGENT_AFTER_TURN: "agent:afterTurn",
|
|
1814
1816
|
// --- Session lifecycle ---
|
|
1815
1817
|
/** Before a new session is created — modifiable, can block. */
|
|
1816
1818
|
SESSION_BEFORE_CREATE: "session:beforeCreate",
|
|
@@ -1868,6 +1870,8 @@ var init_events = __esm({
|
|
|
1868
1870
|
MESSAGE_QUEUED: "message:queued",
|
|
1869
1871
|
/** Fired when a queued message starts processing. */
|
|
1870
1872
|
MESSAGE_PROCESSING: "message:processing",
|
|
1873
|
+
/** Fired when a queued message is rejected (e.g. blocked by middleware). */
|
|
1874
|
+
MESSAGE_FAILED: "message:failed",
|
|
1871
1875
|
// --- System lifecycle ---
|
|
1872
1876
|
/** Fired after kernel (core + plugin infrastructure) has booted. */
|
|
1873
1877
|
KERNEL_BOOTED: "kernel:booted",
|
|
@@ -1888,7 +1892,22 @@ var init_events = __esm({
|
|
|
1888
1892
|
PLUGIN_UNLOADED: "plugin:unloaded",
|
|
1889
1893
|
// --- Usage ---
|
|
1890
1894
|
/** Fired when a token usage record is captured (consumed by usage plugin). */
|
|
1891
|
-
USAGE_RECORDED: "usage:recorded"
|
|
1895
|
+
USAGE_RECORDED: "usage:recorded",
|
|
1896
|
+
// --- Identity lifecycle ---
|
|
1897
|
+
/** Fired when a new user+identity record is created. */
|
|
1898
|
+
IDENTITY_CREATED: "identity:created",
|
|
1899
|
+
/** Fired when user profile fields change. */
|
|
1900
|
+
IDENTITY_UPDATED: "identity:updated",
|
|
1901
|
+
/** Fired when two identities are linked (same person). */
|
|
1902
|
+
IDENTITY_LINKED: "identity:linked",
|
|
1903
|
+
/** Fired when an identity is unlinked into a new user. */
|
|
1904
|
+
IDENTITY_UNLINKED: "identity:unlinked",
|
|
1905
|
+
/** Fired when two user records are merged during a link operation. */
|
|
1906
|
+
IDENTITY_USER_MERGED: "identity:userMerged",
|
|
1907
|
+
/** Fired when a user's role changes. */
|
|
1908
|
+
IDENTITY_ROLE_CHANGED: "identity:roleChanged",
|
|
1909
|
+
/** Fired when a user is seen (throttled). */
|
|
1910
|
+
IDENTITY_SEEN: "identity:seen"
|
|
1892
1911
|
};
|
|
1893
1912
|
SessionEv = {
|
|
1894
1913
|
/** Agent produced an event (text, tool_call, etc.) during a turn. */
|
|
@@ -2014,7 +2033,16 @@ function createSecurityPlugin() {
|
|
|
2014
2033
|
handler: async (payload, next) => {
|
|
2015
2034
|
const access2 = await guard.checkAccess(payload);
|
|
2016
2035
|
if (!access2.allowed) {
|
|
2017
|
-
ctx.log.info(`Access denied: ${access2.reason}`);
|
|
2036
|
+
ctx.log.info(`Access denied for user=${payload.userId} channel=${payload.channelId}: ${access2.reason}`);
|
|
2037
|
+
const adapter = core.adapters?.get?.(payload.channelId);
|
|
2038
|
+
if (adapter?.sendMessage && payload.threadId) {
|
|
2039
|
+
adapter.sendMessage(payload.threadId, {
|
|
2040
|
+
type: "error",
|
|
2041
|
+
message: `Access denied: ${access2.reason ?? "You are not allowed to use this service."}`
|
|
2042
|
+
}).catch((err) => {
|
|
2043
|
+
ctx.log.warn(`Failed to send access-denied message to adapter: ${err}`);
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2018
2046
|
return null;
|
|
2019
2047
|
}
|
|
2020
2048
|
return next();
|
|
@@ -2035,6 +2063,711 @@ var init_security = __esm({
|
|
|
2035
2063
|
}
|
|
2036
2064
|
});
|
|
2037
2065
|
|
|
2066
|
+
// src/plugins/identity/types.ts
|
|
2067
|
+
function formatIdentityId(source, platformId) {
|
|
2068
|
+
return `${source}:${platformId}`;
|
|
2069
|
+
}
|
|
2070
|
+
var init_types = __esm({
|
|
2071
|
+
"src/plugins/identity/types.ts"() {
|
|
2072
|
+
"use strict";
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
// src/plugins/identity/identity-service.ts
|
|
2077
|
+
import { nanoid } from "nanoid";
|
|
2078
|
+
var IdentityServiceImpl;
|
|
2079
|
+
var init_identity_service = __esm({
|
|
2080
|
+
"src/plugins/identity/identity-service.ts"() {
|
|
2081
|
+
"use strict";
|
|
2082
|
+
init_types();
|
|
2083
|
+
IdentityServiceImpl = class {
|
|
2084
|
+
/**
|
|
2085
|
+
* @param store - Persistence layer for user/identity records and indexes.
|
|
2086
|
+
* @param emitEvent - Callback to publish events on the EventBus.
|
|
2087
|
+
* @param getSessionsForUser - Optional function to look up sessions for a userId.
|
|
2088
|
+
*/
|
|
2089
|
+
constructor(store, emitEvent, getSessionsForUser) {
|
|
2090
|
+
this.store = store;
|
|
2091
|
+
this.emitEvent = emitEvent;
|
|
2092
|
+
this.getSessionsForUser = getSessionsForUser;
|
|
2093
|
+
}
|
|
2094
|
+
registeredSources = /* @__PURE__ */ new Set();
|
|
2095
|
+
// ─── Lookups ───
|
|
2096
|
+
async getUser(userId) {
|
|
2097
|
+
return this.store.getUser(userId);
|
|
2098
|
+
}
|
|
2099
|
+
async getUserByUsername(username) {
|
|
2100
|
+
const userId = await this.store.getUserIdByUsername(username);
|
|
2101
|
+
if (!userId) return void 0;
|
|
2102
|
+
return this.store.getUser(userId);
|
|
2103
|
+
}
|
|
2104
|
+
async getIdentity(identityId) {
|
|
2105
|
+
return this.store.getIdentity(identityId);
|
|
2106
|
+
}
|
|
2107
|
+
async getUserByIdentity(identityId) {
|
|
2108
|
+
const identity = await this.store.getIdentity(identityId);
|
|
2109
|
+
if (!identity) return void 0;
|
|
2110
|
+
return this.store.getUser(identity.userId);
|
|
2111
|
+
}
|
|
2112
|
+
async getIdentitiesFor(userId) {
|
|
2113
|
+
return this.store.getIdentitiesForUser(userId);
|
|
2114
|
+
}
|
|
2115
|
+
async listUsers(filter) {
|
|
2116
|
+
return this.store.listUsers(filter);
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Case-insensitive substring search across displayName, username, and platform
|
|
2120
|
+
* usernames. Designed for admin tooling, not high-frequency user-facing paths.
|
|
2121
|
+
*/
|
|
2122
|
+
async searchUsers(query) {
|
|
2123
|
+
const all = await this.store.listUsers();
|
|
2124
|
+
const q = query.toLowerCase();
|
|
2125
|
+
const matched = [];
|
|
2126
|
+
for (const user of all) {
|
|
2127
|
+
const nameMatch = user.displayName.toLowerCase().includes(q) || user.username && user.username.toLowerCase().includes(q);
|
|
2128
|
+
if (nameMatch) {
|
|
2129
|
+
matched.push(user);
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
const identities = await this.store.getIdentitiesForUser(user.userId);
|
|
2133
|
+
const platformMatch = identities.some(
|
|
2134
|
+
(id) => id.platformUsername && id.platformUsername.toLowerCase().includes(q)
|
|
2135
|
+
);
|
|
2136
|
+
if (platformMatch) matched.push(user);
|
|
2137
|
+
}
|
|
2138
|
+
return matched;
|
|
2139
|
+
}
|
|
2140
|
+
async getSessionsFor(userId) {
|
|
2141
|
+
if (!this.getSessionsForUser) return [];
|
|
2142
|
+
return this.getSessionsForUser(userId);
|
|
2143
|
+
}
|
|
2144
|
+
// ─── Mutations ───
|
|
2145
|
+
/**
|
|
2146
|
+
* Creates a user + identity pair atomically.
|
|
2147
|
+
* The first ever user in the system is auto-promoted to admin — this ensures
|
|
2148
|
+
* there is always at least one admin when bootstrapping a fresh instance.
|
|
2149
|
+
*/
|
|
2150
|
+
async createUserWithIdentity(data) {
|
|
2151
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2152
|
+
const userId = `u_${nanoid(12)}`;
|
|
2153
|
+
const identityId = formatIdentityId(data.source, data.platformId);
|
|
2154
|
+
const count = await this.store.getUserCount();
|
|
2155
|
+
const role = count === 0 ? "admin" : data.role ?? "member";
|
|
2156
|
+
const user = {
|
|
2157
|
+
userId,
|
|
2158
|
+
displayName: data.displayName,
|
|
2159
|
+
username: data.username,
|
|
2160
|
+
role,
|
|
2161
|
+
identities: [identityId],
|
|
2162
|
+
pluginData: {},
|
|
2163
|
+
createdAt: now,
|
|
2164
|
+
updatedAt: now,
|
|
2165
|
+
lastSeenAt: now
|
|
2166
|
+
};
|
|
2167
|
+
const identity = {
|
|
2168
|
+
identityId,
|
|
2169
|
+
userId,
|
|
2170
|
+
source: data.source,
|
|
2171
|
+
platformId: data.platformId,
|
|
2172
|
+
platformUsername: data.platformUsername,
|
|
2173
|
+
platformDisplayName: data.platformDisplayName,
|
|
2174
|
+
createdAt: now,
|
|
2175
|
+
updatedAt: now
|
|
2176
|
+
};
|
|
2177
|
+
await this.store.putUser(user);
|
|
2178
|
+
await this.store.putIdentity(identity);
|
|
2179
|
+
await this.store.setSourceIndex(data.source, data.platformId, identityId);
|
|
2180
|
+
if (data.username) {
|
|
2181
|
+
await this.store.setUsernameIndex(data.username, userId);
|
|
2182
|
+
}
|
|
2183
|
+
this.emitEvent("identity:created", { userId, identityId, source: data.source, displayName: data.displayName });
|
|
2184
|
+
return { user, identity };
|
|
2185
|
+
}
|
|
2186
|
+
async updateUser(userId, changes) {
|
|
2187
|
+
const user = await this.store.getUser(userId);
|
|
2188
|
+
if (!user) throw new Error(`User not found: ${userId}`);
|
|
2189
|
+
if (changes.username !== void 0 && changes.username !== user.username) {
|
|
2190
|
+
if (changes.username) {
|
|
2191
|
+
const existingId = await this.store.getUserIdByUsername(changes.username);
|
|
2192
|
+
if (existingId && existingId !== userId) {
|
|
2193
|
+
throw new Error(`Username already taken: ${changes.username}`);
|
|
2194
|
+
}
|
|
2195
|
+
await this.store.setUsernameIndex(changes.username, userId);
|
|
2196
|
+
}
|
|
2197
|
+
if (user.username) {
|
|
2198
|
+
await this.store.deleteUsernameIndex(user.username);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
const updated = {
|
|
2202
|
+
...user,
|
|
2203
|
+
...changes,
|
|
2204
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2205
|
+
};
|
|
2206
|
+
await this.store.putUser(updated);
|
|
2207
|
+
this.emitEvent("identity:updated", { userId, changes: Object.keys(changes) });
|
|
2208
|
+
return updated;
|
|
2209
|
+
}
|
|
2210
|
+
async setRole(userId, role) {
|
|
2211
|
+
const user = await this.store.getUser(userId);
|
|
2212
|
+
if (!user) throw new Error(`User not found: ${userId}`);
|
|
2213
|
+
const oldRole = user.role;
|
|
2214
|
+
await this.store.putUser({ ...user, role, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2215
|
+
this.emitEvent("identity:roleChanged", { userId, oldRole, newRole: role });
|
|
2216
|
+
}
|
|
2217
|
+
async createIdentity(userId, identity) {
|
|
2218
|
+
const user = await this.store.getUser(userId);
|
|
2219
|
+
if (!user) throw new Error(`User not found: ${userId}`);
|
|
2220
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2221
|
+
const identityId = formatIdentityId(identity.source, identity.platformId);
|
|
2222
|
+
const record = {
|
|
2223
|
+
identityId,
|
|
2224
|
+
userId,
|
|
2225
|
+
source: identity.source,
|
|
2226
|
+
platformId: identity.platformId,
|
|
2227
|
+
platformUsername: identity.platformUsername,
|
|
2228
|
+
platformDisplayName: identity.platformDisplayName,
|
|
2229
|
+
createdAt: now,
|
|
2230
|
+
updatedAt: now
|
|
2231
|
+
};
|
|
2232
|
+
await this.store.putIdentity(record);
|
|
2233
|
+
await this.store.setSourceIndex(identity.source, identity.platformId, identityId);
|
|
2234
|
+
const updatedUser = {
|
|
2235
|
+
...user,
|
|
2236
|
+
identities: [...user.identities, identityId],
|
|
2237
|
+
updatedAt: now
|
|
2238
|
+
};
|
|
2239
|
+
await this.store.putUser(updatedUser);
|
|
2240
|
+
return record;
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* Links two identities into a single user.
|
|
2244
|
+
*
|
|
2245
|
+
* When identities belong to different users, the younger (more recently created)
|
|
2246
|
+
* user is merged into the older one. We keep the older user as the canonical
|
|
2247
|
+
* record because it likely has more history, sessions, and plugin data.
|
|
2248
|
+
*
|
|
2249
|
+
* Merge strategy for pluginData: per-namespace, the winning user's data takes
|
|
2250
|
+
* precedence. The younger user's data only fills in missing namespaces.
|
|
2251
|
+
*/
|
|
2252
|
+
async link(identityIdA, identityIdB) {
|
|
2253
|
+
const identityA = await this.store.getIdentity(identityIdA);
|
|
2254
|
+
const identityB = await this.store.getIdentity(identityIdB);
|
|
2255
|
+
if (!identityA) throw new Error(`Identity not found: ${identityIdA}`);
|
|
2256
|
+
if (!identityB) throw new Error(`Identity not found: ${identityIdB}`);
|
|
2257
|
+
if (identityA.userId === identityB.userId) return;
|
|
2258
|
+
const userA = await this.store.getUser(identityA.userId);
|
|
2259
|
+
const userB = await this.store.getUser(identityB.userId);
|
|
2260
|
+
if (!userA) throw new Error(`User not found: ${identityA.userId}`);
|
|
2261
|
+
if (!userB) throw new Error(`User not found: ${identityB.userId}`);
|
|
2262
|
+
const [keep, merge] = userA.createdAt <= userB.createdAt ? [userA, userB] : [userB, userA];
|
|
2263
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2264
|
+
for (const identityId of merge.identities) {
|
|
2265
|
+
const identity = await this.store.getIdentity(identityId);
|
|
2266
|
+
if (!identity) continue;
|
|
2267
|
+
const updated = { ...identity, userId: keep.userId, updatedAt: now };
|
|
2268
|
+
await this.store.putIdentity(updated);
|
|
2269
|
+
}
|
|
2270
|
+
const mergedPluginData = { ...merge.pluginData };
|
|
2271
|
+
for (const [ns, nsData] of Object.entries(keep.pluginData)) {
|
|
2272
|
+
mergedPluginData[ns] = nsData;
|
|
2273
|
+
}
|
|
2274
|
+
if (merge.username) {
|
|
2275
|
+
await this.store.deleteUsernameIndex(merge.username);
|
|
2276
|
+
}
|
|
2277
|
+
const updatedKeep = {
|
|
2278
|
+
...keep,
|
|
2279
|
+
identities: [.../* @__PURE__ */ new Set([...keep.identities, ...merge.identities])],
|
|
2280
|
+
pluginData: mergedPluginData,
|
|
2281
|
+
updatedAt: now
|
|
2282
|
+
};
|
|
2283
|
+
await this.store.putUser(updatedKeep);
|
|
2284
|
+
await this.store.deleteUser(merge.userId);
|
|
2285
|
+
const linkedIdentityId = identityA.userId === merge.userId ? identityIdA : identityIdB;
|
|
2286
|
+
this.emitEvent("identity:linked", { userId: keep.userId, identityId: linkedIdentityId, linkedFrom: merge.userId });
|
|
2287
|
+
this.emitEvent("identity:userMerged", {
|
|
2288
|
+
keptUserId: keep.userId,
|
|
2289
|
+
mergedUserId: merge.userId,
|
|
2290
|
+
movedIdentities: merge.identities
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* Separates an identity from its user into a new standalone account.
|
|
2295
|
+
* Throws if it's the user's last identity — unlinking would produce a
|
|
2296
|
+
* ghost user with no way to authenticate.
|
|
2297
|
+
*/
|
|
2298
|
+
async unlink(identityId) {
|
|
2299
|
+
const identity = await this.store.getIdentity(identityId);
|
|
2300
|
+
if (!identity) throw new Error(`Identity not found: ${identityId}`);
|
|
2301
|
+
const user = await this.store.getUser(identity.userId);
|
|
2302
|
+
if (!user) throw new Error(`User not found: ${identity.userId}`);
|
|
2303
|
+
if (user.identities.length <= 1) {
|
|
2304
|
+
throw new Error(`Cannot unlink the last identity from user ${identity.userId}`);
|
|
2305
|
+
}
|
|
2306
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2307
|
+
const newUserId = `u_${nanoid(12)}`;
|
|
2308
|
+
const newUser = {
|
|
2309
|
+
userId: newUserId,
|
|
2310
|
+
displayName: identity.platformDisplayName ?? identity.platformUsername ?? "User",
|
|
2311
|
+
role: "member",
|
|
2312
|
+
identities: [identityId],
|
|
2313
|
+
pluginData: {},
|
|
2314
|
+
createdAt: now,
|
|
2315
|
+
updatedAt: now,
|
|
2316
|
+
lastSeenAt: now
|
|
2317
|
+
};
|
|
2318
|
+
await this.store.putUser(newUser);
|
|
2319
|
+
await this.store.putIdentity({ ...identity, userId: newUserId, updatedAt: now });
|
|
2320
|
+
const updatedUser = {
|
|
2321
|
+
...user,
|
|
2322
|
+
identities: user.identities.filter((id) => id !== identityId),
|
|
2323
|
+
updatedAt: now
|
|
2324
|
+
};
|
|
2325
|
+
await this.store.putUser(updatedUser);
|
|
2326
|
+
this.emitEvent("identity:unlinked", {
|
|
2327
|
+
userId: user.userId,
|
|
2328
|
+
identityId,
|
|
2329
|
+
newUserId
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
// ─── Plugin data ───
|
|
2333
|
+
async setPluginData(userId, pluginName, key, value) {
|
|
2334
|
+
const user = await this.store.getUser(userId);
|
|
2335
|
+
if (!user) throw new Error(`User not found: ${userId}`);
|
|
2336
|
+
const pluginData = { ...user.pluginData };
|
|
2337
|
+
pluginData[pluginName] = { ...pluginData[pluginName] ?? {}, [key]: value };
|
|
2338
|
+
await this.store.putUser({ ...user, pluginData, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2339
|
+
}
|
|
2340
|
+
async getPluginData(userId, pluginName, key) {
|
|
2341
|
+
const user = await this.store.getUser(userId);
|
|
2342
|
+
if (!user) return void 0;
|
|
2343
|
+
return user.pluginData[pluginName]?.[key];
|
|
2344
|
+
}
|
|
2345
|
+
// ─── Source registry ───
|
|
2346
|
+
registerSource(source) {
|
|
2347
|
+
this.registeredSources.add(source);
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Resolves a username mention to platform-specific info for the given source.
|
|
2351
|
+
* Finds the user by username, then scans their identities for the matching source.
|
|
2352
|
+
* Returns found=false when no user or no identity for that source exists.
|
|
2353
|
+
*/
|
|
2354
|
+
async resolveCanonicalMention(username, source) {
|
|
2355
|
+
const user = await this.getUserByUsername(username);
|
|
2356
|
+
if (!user) return { found: false };
|
|
2357
|
+
const identities = await this.store.getIdentitiesForUser(user.userId);
|
|
2358
|
+
const sourceIdentity = identities.find((id) => id.source === source);
|
|
2359
|
+
if (!sourceIdentity) return { found: false };
|
|
2360
|
+
return {
|
|
2361
|
+
found: true,
|
|
2362
|
+
platformId: sourceIdentity.platformId,
|
|
2363
|
+
platformUsername: sourceIdentity.platformUsername
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
async getUserCount() {
|
|
2367
|
+
return this.store.getUserCount();
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
});
|
|
2372
|
+
|
|
2373
|
+
// src/plugins/identity/store/kv-identity-store.ts
|
|
2374
|
+
var KvIdentityStore;
|
|
2375
|
+
var init_kv_identity_store = __esm({
|
|
2376
|
+
"src/plugins/identity/store/kv-identity-store.ts"() {
|
|
2377
|
+
"use strict";
|
|
2378
|
+
KvIdentityStore = class {
|
|
2379
|
+
constructor(storage) {
|
|
2380
|
+
this.storage = storage;
|
|
2381
|
+
}
|
|
2382
|
+
// === User CRUD ===
|
|
2383
|
+
async getUser(userId) {
|
|
2384
|
+
return this.storage.get(`users/${userId}`);
|
|
2385
|
+
}
|
|
2386
|
+
async putUser(record) {
|
|
2387
|
+
await this.storage.set(`users/${record.userId}`, record);
|
|
2388
|
+
}
|
|
2389
|
+
async deleteUser(userId) {
|
|
2390
|
+
await this.storage.delete(`users/${userId}`);
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Lists all users, optionally filtered by role or source.
|
|
2394
|
+
* Filtering by source requires scanning all identity records for the user,
|
|
2395
|
+
* which is acceptable given the expected user count (hundreds, not millions).
|
|
2396
|
+
*/
|
|
2397
|
+
async listUsers(filter) {
|
|
2398
|
+
const keys = await this.storage.keys("users/");
|
|
2399
|
+
const users = [];
|
|
2400
|
+
for (const key of keys) {
|
|
2401
|
+
const user = await this.storage.get(key);
|
|
2402
|
+
if (!user) continue;
|
|
2403
|
+
if (filter?.role && user.role !== filter.role) continue;
|
|
2404
|
+
if (filter?.source) {
|
|
2405
|
+
const hasSource = user.identities.some((id) => id.startsWith(`${filter.source}:`));
|
|
2406
|
+
if (!hasSource) continue;
|
|
2407
|
+
}
|
|
2408
|
+
users.push(user);
|
|
2409
|
+
}
|
|
2410
|
+
return users;
|
|
2411
|
+
}
|
|
2412
|
+
// === Identity CRUD ===
|
|
2413
|
+
async getIdentity(identityId) {
|
|
2414
|
+
return this.storage.get(`identities/${identityId}`);
|
|
2415
|
+
}
|
|
2416
|
+
async putIdentity(record) {
|
|
2417
|
+
await this.storage.set(`identities/${record.identityId}`, record);
|
|
2418
|
+
}
|
|
2419
|
+
async deleteIdentity(identityId) {
|
|
2420
|
+
await this.storage.delete(`identities/${identityId}`);
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Fetches all identity records for a user by scanning their identities array.
|
|
2424
|
+
* Avoids a full table scan by leveraging the user record as a secondary index.
|
|
2425
|
+
*/
|
|
2426
|
+
async getIdentitiesForUser(userId) {
|
|
2427
|
+
const user = await this.getUser(userId);
|
|
2428
|
+
if (!user) return [];
|
|
2429
|
+
const records = [];
|
|
2430
|
+
for (const identityId of user.identities) {
|
|
2431
|
+
const record = await this.getIdentity(identityId);
|
|
2432
|
+
if (record) records.push(record);
|
|
2433
|
+
}
|
|
2434
|
+
return records;
|
|
2435
|
+
}
|
|
2436
|
+
// === Secondary indexes ===
|
|
2437
|
+
async getUserIdByUsername(username) {
|
|
2438
|
+
return this.storage.get(`idx/usernames/${username.toLowerCase()}`);
|
|
2439
|
+
}
|
|
2440
|
+
async getIdentityIdBySource(source, platformId) {
|
|
2441
|
+
return this.storage.get(`idx/sources/${source}/${platformId}`);
|
|
2442
|
+
}
|
|
2443
|
+
// === Index mutations ===
|
|
2444
|
+
async setUsernameIndex(username, userId) {
|
|
2445
|
+
await this.storage.set(`idx/usernames/${username.toLowerCase()}`, userId);
|
|
2446
|
+
}
|
|
2447
|
+
async deleteUsernameIndex(username) {
|
|
2448
|
+
await this.storage.delete(`idx/usernames/${username.toLowerCase()}`);
|
|
2449
|
+
}
|
|
2450
|
+
async setSourceIndex(source, platformId, identityId) {
|
|
2451
|
+
await this.storage.set(`idx/sources/${source}/${platformId}`, identityId);
|
|
2452
|
+
}
|
|
2453
|
+
async deleteSourceIndex(source, platformId) {
|
|
2454
|
+
await this.storage.delete(`idx/sources/${source}/${platformId}`);
|
|
2455
|
+
}
|
|
2456
|
+
async getUserCount() {
|
|
2457
|
+
const keys = await this.storage.keys("users/");
|
|
2458
|
+
return keys.length;
|
|
2459
|
+
}
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
});
|
|
2463
|
+
|
|
2464
|
+
// src/plugins/identity/middleware/auto-register.ts
|
|
2465
|
+
function createAutoRegisterHandler(service, store) {
|
|
2466
|
+
const lastSeenThrottle = /* @__PURE__ */ new Map();
|
|
2467
|
+
return async (payload, next) => {
|
|
2468
|
+
const { channelId, userId, meta } = payload;
|
|
2469
|
+
const identityId = formatIdentityId(channelId, userId);
|
|
2470
|
+
const channelUser = meta?.channelUser;
|
|
2471
|
+
let identity = await store.getIdentity(identityId);
|
|
2472
|
+
let user;
|
|
2473
|
+
if (!identity) {
|
|
2474
|
+
const result = await service.createUserWithIdentity({
|
|
2475
|
+
displayName: channelUser?.displayName ?? userId,
|
|
2476
|
+
username: channelUser?.username,
|
|
2477
|
+
source: channelId,
|
|
2478
|
+
platformId: userId,
|
|
2479
|
+
platformUsername: channelUser?.username,
|
|
2480
|
+
platformDisplayName: channelUser?.displayName
|
|
2481
|
+
});
|
|
2482
|
+
user = result.user;
|
|
2483
|
+
identity = result.identity;
|
|
2484
|
+
} else {
|
|
2485
|
+
user = await service.getUser(identity.userId);
|
|
2486
|
+
if (!user) return next();
|
|
2487
|
+
const now = Date.now();
|
|
2488
|
+
const lastSeen = lastSeenThrottle.get(user.userId);
|
|
2489
|
+
if (!lastSeen || now - lastSeen > LAST_SEEN_THROTTLE_MS) {
|
|
2490
|
+
lastSeenThrottle.set(user.userId, now);
|
|
2491
|
+
await store.putUser({ ...user, lastSeenAt: new Date(now).toISOString() });
|
|
2492
|
+
}
|
|
2493
|
+
if (channelUser) {
|
|
2494
|
+
const needsUpdate = channelUser.displayName !== void 0 && channelUser.displayName !== identity.platformDisplayName || channelUser.username !== void 0 && channelUser.username !== identity.platformUsername;
|
|
2495
|
+
if (needsUpdate) {
|
|
2496
|
+
await store.putIdentity({
|
|
2497
|
+
...identity,
|
|
2498
|
+
platformDisplayName: channelUser.displayName ?? identity.platformDisplayName,
|
|
2499
|
+
platformUsername: channelUser.username ?? identity.platformUsername,
|
|
2500
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
if (meta) {
|
|
2506
|
+
meta.identity = {
|
|
2507
|
+
userId: user.userId,
|
|
2508
|
+
identityId: identity.identityId,
|
|
2509
|
+
displayName: user.displayName,
|
|
2510
|
+
username: user.username,
|
|
2511
|
+
role: user.role
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
return next();
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
var LAST_SEEN_THROTTLE_MS;
|
|
2518
|
+
var init_auto_register = __esm({
|
|
2519
|
+
"src/plugins/identity/middleware/auto-register.ts"() {
|
|
2520
|
+
"use strict";
|
|
2521
|
+
init_types();
|
|
2522
|
+
LAST_SEEN_THROTTLE_MS = 5 * 60 * 1e3;
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
|
|
2526
|
+
// src/plugins/identity/routes/users.ts
|
|
2527
|
+
var users_exports = {};
|
|
2528
|
+
__export(users_exports, {
|
|
2529
|
+
registerIdentityRoutes: () => registerIdentityRoutes
|
|
2530
|
+
});
|
|
2531
|
+
function registerIdentityRoutes(app, deps) {
|
|
2532
|
+
const { service, tokenStore } = deps;
|
|
2533
|
+
function resolveUserId(request) {
|
|
2534
|
+
return tokenStore?.getUserId?.(request.auth?.tokenId);
|
|
2535
|
+
}
|
|
2536
|
+
app.get("/users", async (request) => {
|
|
2537
|
+
const { source, role, q } = request.query;
|
|
2538
|
+
if (q) return service.searchUsers(q);
|
|
2539
|
+
return service.listUsers({ source, role });
|
|
2540
|
+
});
|
|
2541
|
+
app.get("/users/me", async (request, reply) => {
|
|
2542
|
+
const userId = resolveUserId(request);
|
|
2543
|
+
if (!userId) return reply.status(403).send({ error: "Identity not set up" });
|
|
2544
|
+
const user = await service.getUser(userId);
|
|
2545
|
+
if (!user) return reply.status(404).send({ error: "User not found" });
|
|
2546
|
+
return user;
|
|
2547
|
+
});
|
|
2548
|
+
app.put("/users/me", async (request, reply) => {
|
|
2549
|
+
const userId = resolveUserId(request);
|
|
2550
|
+
if (!userId) {
|
|
2551
|
+
return reply.status(403).send({ error: "Identity not set up. Call POST /identity/setup first." });
|
|
2552
|
+
}
|
|
2553
|
+
const body = request.body;
|
|
2554
|
+
return service.updateUser(userId, {
|
|
2555
|
+
displayName: body.displayName,
|
|
2556
|
+
username: body.username,
|
|
2557
|
+
avatarUrl: body.avatarUrl,
|
|
2558
|
+
timezone: body.timezone,
|
|
2559
|
+
locale: body.locale
|
|
2560
|
+
});
|
|
2561
|
+
});
|
|
2562
|
+
app.get("/users/:userId", async (request, reply) => {
|
|
2563
|
+
const { userId } = request.params;
|
|
2564
|
+
const user = await service.getUser(userId);
|
|
2565
|
+
if (!user) return reply.status(404).send({ error: "User not found" });
|
|
2566
|
+
return user;
|
|
2567
|
+
});
|
|
2568
|
+
app.put("/users/:userId/role", async (request, reply) => {
|
|
2569
|
+
const callerUserId = resolveUserId(request);
|
|
2570
|
+
if (!callerUserId) return reply.status(403).send({ error: "Identity not set up" });
|
|
2571
|
+
const caller = await service.getUser(callerUserId);
|
|
2572
|
+
if (!caller || caller.role !== "admin") return reply.status(403).send({ error: "Admin only" });
|
|
2573
|
+
const { userId } = request.params;
|
|
2574
|
+
const { role } = request.body;
|
|
2575
|
+
await service.setRole(userId, role);
|
|
2576
|
+
return { ok: true };
|
|
2577
|
+
});
|
|
2578
|
+
app.get("/users/:userId/identities", async (request) => {
|
|
2579
|
+
const { userId } = request.params;
|
|
2580
|
+
return service.getIdentitiesFor(userId);
|
|
2581
|
+
});
|
|
2582
|
+
app.get("/resolve/:identityId", async (request, reply) => {
|
|
2583
|
+
const { identityId } = request.params;
|
|
2584
|
+
const user = await service.getUserByIdentity(identityId);
|
|
2585
|
+
if (!user) return reply.status(404).send({ error: "Identity not found" });
|
|
2586
|
+
const identity = await service.getIdentity(identityId);
|
|
2587
|
+
return { user, identity };
|
|
2588
|
+
});
|
|
2589
|
+
app.post("/link", async (request, reply) => {
|
|
2590
|
+
const callerUserId = resolveUserId(request);
|
|
2591
|
+
if (!callerUserId) return reply.status(403).send({ error: "Identity not set up" });
|
|
2592
|
+
const caller = await service.getUser(callerUserId);
|
|
2593
|
+
if (!caller || caller.role !== "admin") return reply.status(403).send({ error: "Admin only" });
|
|
2594
|
+
const { identityIdA, identityIdB } = request.body;
|
|
2595
|
+
await service.link(identityIdA, identityIdB);
|
|
2596
|
+
return { ok: true };
|
|
2597
|
+
});
|
|
2598
|
+
app.post("/unlink", async (request, reply) => {
|
|
2599
|
+
const callerUserId = resolveUserId(request);
|
|
2600
|
+
if (!callerUserId) return reply.status(403).send({ error: "Identity not set up" });
|
|
2601
|
+
const caller = await service.getUser(callerUserId);
|
|
2602
|
+
if (!caller || caller.role !== "admin") return reply.status(403).send({ error: "Admin only" });
|
|
2603
|
+
const { identityId } = request.body;
|
|
2604
|
+
await service.unlink(identityId);
|
|
2605
|
+
return { ok: true };
|
|
2606
|
+
});
|
|
2607
|
+
app.get("/search", async (request) => {
|
|
2608
|
+
const { q } = request.query;
|
|
2609
|
+
if (!q) return [];
|
|
2610
|
+
return service.searchUsers(q);
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
var init_users = __esm({
|
|
2614
|
+
"src/plugins/identity/routes/users.ts"() {
|
|
2615
|
+
"use strict";
|
|
2616
|
+
}
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2619
|
+
// src/plugins/identity/routes/setup.ts
|
|
2620
|
+
var setup_exports = {};
|
|
2621
|
+
__export(setup_exports, {
|
|
2622
|
+
registerSetupRoutes: () => registerSetupRoutes
|
|
2623
|
+
});
|
|
2624
|
+
import { randomBytes } from "crypto";
|
|
2625
|
+
function registerSetupRoutes(app, deps) {
|
|
2626
|
+
const { service, tokenStore } = deps;
|
|
2627
|
+
app.post("/setup", async (request, reply) => {
|
|
2628
|
+
const auth = request.auth;
|
|
2629
|
+
if (!auth?.tokenId) return reply.status(401).send({ error: "JWT required" });
|
|
2630
|
+
const existingUserId = tokenStore?.getUserId?.(auth.tokenId);
|
|
2631
|
+
if (existingUserId) {
|
|
2632
|
+
const user2 = await service.getUser(existingUserId);
|
|
2633
|
+
if (user2) return user2;
|
|
2634
|
+
}
|
|
2635
|
+
const body = request.body;
|
|
2636
|
+
if (body?.linkCode) {
|
|
2637
|
+
const entry = linkCodes.get(body.linkCode);
|
|
2638
|
+
if (!entry || entry.expiresAt < Date.now()) {
|
|
2639
|
+
return reply.status(401).send({ error: "Invalid or expired link code" });
|
|
2640
|
+
}
|
|
2641
|
+
linkCodes.delete(body.linkCode);
|
|
2642
|
+
await service.createIdentity(entry.userId, {
|
|
2643
|
+
source: "api",
|
|
2644
|
+
platformId: auth.tokenId
|
|
2645
|
+
});
|
|
2646
|
+
tokenStore?.setUserId?.(auth.tokenId, entry.userId);
|
|
2647
|
+
return service.getUser(entry.userId);
|
|
2648
|
+
}
|
|
2649
|
+
if (!body?.displayName) return reply.status(400).send({ error: "displayName is required" });
|
|
2650
|
+
const { user } = await service.createUserWithIdentity({
|
|
2651
|
+
displayName: body.displayName,
|
|
2652
|
+
username: body.username,
|
|
2653
|
+
source: "api",
|
|
2654
|
+
platformId: auth.tokenId
|
|
2655
|
+
});
|
|
2656
|
+
tokenStore?.setUserId?.(auth.tokenId, user.userId);
|
|
2657
|
+
return user;
|
|
2658
|
+
});
|
|
2659
|
+
app.post("/link-code", async (request, reply) => {
|
|
2660
|
+
const auth = request.auth;
|
|
2661
|
+
if (!auth?.tokenId) return reply.status(401).send({ error: "JWT required" });
|
|
2662
|
+
const userId = tokenStore?.getUserId?.(auth.tokenId);
|
|
2663
|
+
if (!userId) return reply.status(403).send({ error: "Identity not set up" });
|
|
2664
|
+
const code = randomBytes(16).toString("hex");
|
|
2665
|
+
const expiresAt = Date.now() + 5 * 60 * 1e3;
|
|
2666
|
+
for (const [k, v] of linkCodes) {
|
|
2667
|
+
if (v.expiresAt < Date.now()) linkCodes.delete(k);
|
|
2668
|
+
}
|
|
2669
|
+
linkCodes.set(code, { userId, expiresAt });
|
|
2670
|
+
return { linkCode: code, expiresAt: new Date(expiresAt).toISOString() };
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2673
|
+
var linkCodes;
|
|
2674
|
+
var init_setup = __esm({
|
|
2675
|
+
"src/plugins/identity/routes/setup.ts"() {
|
|
2676
|
+
"use strict";
|
|
2677
|
+
linkCodes = /* @__PURE__ */ new Map();
|
|
2678
|
+
}
|
|
2679
|
+
});
|
|
2680
|
+
|
|
2681
|
+
// src/plugins/identity/index.ts
|
|
2682
|
+
function createIdentityPlugin() {
|
|
2683
|
+
return {
|
|
2684
|
+
name: "@openacp/identity",
|
|
2685
|
+
version: "1.0.0",
|
|
2686
|
+
description: "User identity, cross-platform linking, and role-based access",
|
|
2687
|
+
essential: false,
|
|
2688
|
+
permissions: [
|
|
2689
|
+
"storage:read",
|
|
2690
|
+
"storage:write",
|
|
2691
|
+
"middleware:register",
|
|
2692
|
+
"services:register",
|
|
2693
|
+
"services:use",
|
|
2694
|
+
"events:emit",
|
|
2695
|
+
"events:read",
|
|
2696
|
+
"commands:register",
|
|
2697
|
+
"kernel:access"
|
|
2698
|
+
],
|
|
2699
|
+
optionalPluginDependencies: {
|
|
2700
|
+
"@openacp/api-server": ">=1.0.0"
|
|
2701
|
+
},
|
|
2702
|
+
async setup(ctx) {
|
|
2703
|
+
const store = new KvIdentityStore(ctx.storage);
|
|
2704
|
+
const service = new IdentityServiceImpl(store, (event, data) => {
|
|
2705
|
+
ctx.emit(event, data);
|
|
2706
|
+
});
|
|
2707
|
+
ctx.registerService("identity", service);
|
|
2708
|
+
ctx.registerMiddleware(Hook.MESSAGE_INCOMING, {
|
|
2709
|
+
priority: 110,
|
|
2710
|
+
handler: createAutoRegisterHandler(service, store)
|
|
2711
|
+
});
|
|
2712
|
+
ctx.registerCommand({
|
|
2713
|
+
name: "whoami",
|
|
2714
|
+
description: "Set your username and display name",
|
|
2715
|
+
usage: "@username [Display Name]",
|
|
2716
|
+
category: "plugin",
|
|
2717
|
+
async handler(args2) {
|
|
2718
|
+
const raw = args2.raw.trim();
|
|
2719
|
+
if (!raw) return { type: "error", message: "Usage: /whoami @username [Display Name]" };
|
|
2720
|
+
const tokens = raw.split(/\s+/);
|
|
2721
|
+
const first = tokens[0];
|
|
2722
|
+
const usernameRaw = first.startsWith("@") ? first.slice(1) : first;
|
|
2723
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(usernameRaw)) {
|
|
2724
|
+
return { type: "error", message: "Invalid username. Only letters, numbers, _ . - allowed." };
|
|
2725
|
+
}
|
|
2726
|
+
const username = usernameRaw;
|
|
2727
|
+
const displayName = tokens.slice(1).join(" ") || void 0;
|
|
2728
|
+
const identityId = formatIdentityId(args2.channelId, args2.userId);
|
|
2729
|
+
const user = await service.getUserByIdentity(identityId);
|
|
2730
|
+
if (!user) {
|
|
2731
|
+
return { type: "error", message: "Identity not found. Send a message first." };
|
|
2732
|
+
}
|
|
2733
|
+
try {
|
|
2734
|
+
await service.updateUser(user.userId, { username, ...displayName && { displayName } });
|
|
2735
|
+
const parts = [`@${username}`];
|
|
2736
|
+
if (displayName) parts.push(`"${displayName}"`);
|
|
2737
|
+
return { type: "text", text: `\u2705 Profile updated: ${parts.join(" ")}` };
|
|
2738
|
+
} catch (err) {
|
|
2739
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2740
|
+
return { type: "error", message };
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
});
|
|
2744
|
+
const apiServer = ctx.getService("api-server");
|
|
2745
|
+
if (apiServer) {
|
|
2746
|
+
const tokenStore = ctx.getService("token-store");
|
|
2747
|
+
const { registerIdentityRoutes: registerIdentityRoutes2 } = await Promise.resolve().then(() => (init_users(), users_exports));
|
|
2748
|
+
const { registerSetupRoutes: registerSetupRoutes2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
2749
|
+
apiServer.registerPlugin("/api/v1/identity", async (app) => {
|
|
2750
|
+
registerIdentityRoutes2(app, { service, tokenStore: tokenStore ?? void 0 });
|
|
2751
|
+
registerSetupRoutes2(app, { service, tokenStore: tokenStore ?? void 0 });
|
|
2752
|
+
}, { auth: true });
|
|
2753
|
+
}
|
|
2754
|
+
ctx.log.info(`Identity service ready (${await service.getUserCount()} users)`);
|
|
2755
|
+
}
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
var identity_default;
|
|
2759
|
+
var init_identity = __esm({
|
|
2760
|
+
"src/plugins/identity/index.ts"() {
|
|
2761
|
+
"use strict";
|
|
2762
|
+
init_identity_service();
|
|
2763
|
+
init_kv_identity_store();
|
|
2764
|
+
init_auto_register();
|
|
2765
|
+
init_types();
|
|
2766
|
+
init_events();
|
|
2767
|
+
identity_default = createIdentityPlugin();
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
2770
|
+
|
|
2038
2771
|
// src/core/utils/read-text-file.ts
|
|
2039
2772
|
var read_text_file_exports = {};
|
|
2040
2773
|
__export(read_text_file_exports, {
|
|
@@ -3631,7 +4364,7 @@ var init_history_recorder = __esm({
|
|
|
3631
4364
|
}
|
|
3632
4365
|
states = /* @__PURE__ */ new Map();
|
|
3633
4366
|
debounceTimers = /* @__PURE__ */ new Map();
|
|
3634
|
-
onBeforePrompt(sessionId, text5, attachments, sourceAdapterId) {
|
|
4367
|
+
onBeforePrompt(sessionId, text5, attachments, sourceAdapterId, meta) {
|
|
3635
4368
|
let state = this.states.get(sessionId);
|
|
3636
4369
|
if (!state) {
|
|
3637
4370
|
state = {
|
|
@@ -3652,6 +4385,9 @@ var init_history_recorder = __esm({
|
|
|
3652
4385
|
if (sourceAdapterId) {
|
|
3653
4386
|
userTurn.sourceAdapterId = sourceAdapterId;
|
|
3654
4387
|
}
|
|
4388
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
4389
|
+
userTurn.meta = meta;
|
|
4390
|
+
}
|
|
3655
4391
|
state.history.turns.push(userTurn);
|
|
3656
4392
|
const assistantTurn = {
|
|
3657
4393
|
index: state.history.turns.length,
|
|
@@ -3986,7 +4722,7 @@ var init_context = __esm({
|
|
|
3986
4722
|
ctx.registerMiddleware(Hook.AGENT_BEFORE_PROMPT, {
|
|
3987
4723
|
priority: 200,
|
|
3988
4724
|
handler: async (payload, next) => {
|
|
3989
|
-
recorder.onBeforePrompt(payload.sessionId, payload.text, payload.attachments, payload.sourceAdapterId);
|
|
4725
|
+
recorder.onBeforePrompt(payload.sessionId, payload.text, payload.attachments, payload.sourceAdapterId, payload.meta);
|
|
3990
4726
|
return next();
|
|
3991
4727
|
}
|
|
3992
4728
|
});
|
|
@@ -4485,14 +5221,20 @@ var init_speech = __esm({
|
|
|
4485
5221
|
});
|
|
4486
5222
|
|
|
4487
5223
|
// src/plugins/notifications/notification.ts
|
|
4488
|
-
var
|
|
5224
|
+
var NotificationService;
|
|
4489
5225
|
var init_notification = __esm({
|
|
4490
5226
|
"src/plugins/notifications/notification.ts"() {
|
|
4491
5227
|
"use strict";
|
|
4492
|
-
|
|
5228
|
+
NotificationService = class {
|
|
4493
5229
|
constructor(adapters) {
|
|
4494
5230
|
this.adapters = adapters;
|
|
4495
5231
|
}
|
|
5232
|
+
identityResolver;
|
|
5233
|
+
/** Inject identity resolver for user-targeted notifications. */
|
|
5234
|
+
setIdentityResolver(resolver) {
|
|
5235
|
+
this.identityResolver = resolver;
|
|
5236
|
+
}
|
|
5237
|
+
// --- Legacy API (backward compat with NotificationManager) ---
|
|
4496
5238
|
/**
|
|
4497
5239
|
* Send a notification to a specific channel adapter.
|
|
4498
5240
|
*
|
|
@@ -4521,6 +5263,62 @@ var init_notification = __esm({
|
|
|
4521
5263
|
}
|
|
4522
5264
|
}
|
|
4523
5265
|
}
|
|
5266
|
+
// --- New user-targeted API ---
|
|
5267
|
+
/**
|
|
5268
|
+
* Send a notification to a user across all their linked platforms.
|
|
5269
|
+
* Fire-and-forget — never throws, swallows all errors.
|
|
5270
|
+
*/
|
|
5271
|
+
async notifyUser(target, message, options) {
|
|
5272
|
+
try {
|
|
5273
|
+
await this._resolveAndDeliver(target, message, options);
|
|
5274
|
+
} catch {
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
5277
|
+
async _resolveAndDeliver(target, message, options) {
|
|
5278
|
+
if ("channelId" in target && "platformId" in target) {
|
|
5279
|
+
const adapter = this.adapters.get(target.channelId);
|
|
5280
|
+
if (!adapter?.sendUserNotification) return;
|
|
5281
|
+
await adapter.sendUserNotification(target.platformId, message, {
|
|
5282
|
+
via: options?.via,
|
|
5283
|
+
topicId: options?.topicId,
|
|
5284
|
+
sessionId: options?.sessionId
|
|
5285
|
+
});
|
|
5286
|
+
return;
|
|
5287
|
+
}
|
|
5288
|
+
if (!this.identityResolver) return;
|
|
5289
|
+
let identities = [];
|
|
5290
|
+
if ("identityId" in target) {
|
|
5291
|
+
const identity = await this.identityResolver.getIdentity(target.identityId);
|
|
5292
|
+
if (!identity) return;
|
|
5293
|
+
const user = await this.identityResolver.getUser(identity.userId);
|
|
5294
|
+
if (!user) return;
|
|
5295
|
+
identities = await this.identityResolver.getIdentitiesFor(user.userId);
|
|
5296
|
+
} else if ("userId" in target) {
|
|
5297
|
+
identities = await this.identityResolver.getIdentitiesFor(target.userId);
|
|
5298
|
+
}
|
|
5299
|
+
if (options?.onlyPlatforms) {
|
|
5300
|
+
identities = identities.filter((i) => options.onlyPlatforms.includes(i.source));
|
|
5301
|
+
}
|
|
5302
|
+
if (options?.excludePlatforms) {
|
|
5303
|
+
identities = identities.filter((i) => !options.excludePlatforms.includes(i.source));
|
|
5304
|
+
}
|
|
5305
|
+
for (const identity of identities) {
|
|
5306
|
+
const adapter = this.adapters.get(identity.source);
|
|
5307
|
+
if (!adapter?.sendUserNotification) continue;
|
|
5308
|
+
try {
|
|
5309
|
+
await adapter.sendUserNotification(identity.platformId, message, {
|
|
5310
|
+
via: options?.via,
|
|
5311
|
+
topicId: options?.topicId,
|
|
5312
|
+
sessionId: options?.sessionId,
|
|
5313
|
+
platformMention: {
|
|
5314
|
+
platformUsername: identity.platformUsername,
|
|
5315
|
+
platformId: identity.platformId
|
|
5316
|
+
}
|
|
5317
|
+
});
|
|
5318
|
+
} catch {
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
4524
5322
|
};
|
|
4525
5323
|
}
|
|
4526
5324
|
});
|
|
@@ -4538,11 +5336,10 @@ function createNotificationsPlugin() {
|
|
|
4538
5336
|
essential: false,
|
|
4539
5337
|
// Depends on security so the notification service is only active for authorized sessions
|
|
4540
5338
|
pluginDependencies: { "@openacp/security": "^1.0.0" },
|
|
4541
|
-
permissions: ["services:register", "kernel:access"],
|
|
5339
|
+
permissions: ["services:register", "services:use", "kernel:access", "events:read"],
|
|
4542
5340
|
async install(ctx) {
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
terminal.log.success("Notifications defaults saved");
|
|
5341
|
+
await ctx.settings.setAll({ enabled: true });
|
|
5342
|
+
ctx.terminal.log.success("Notifications defaults saved");
|
|
4546
5343
|
},
|
|
4547
5344
|
async configure(ctx) {
|
|
4548
5345
|
const { terminal, settings } = ctx;
|
|
@@ -4565,8 +5362,16 @@ function createNotificationsPlugin() {
|
|
|
4565
5362
|
},
|
|
4566
5363
|
async setup(ctx) {
|
|
4567
5364
|
const core = ctx.core;
|
|
4568
|
-
const
|
|
4569
|
-
ctx.
|
|
5365
|
+
const service = new NotificationService(core.adapters);
|
|
5366
|
+
const identity = ctx.getService("identity");
|
|
5367
|
+
if (identity) service.setIdentityResolver(identity);
|
|
5368
|
+
ctx.on("plugin:loaded", (data) => {
|
|
5369
|
+
if (data?.name === "@openacp/identity") {
|
|
5370
|
+
const id = ctx.getService("identity");
|
|
5371
|
+
if (id) service.setIdentityResolver(id);
|
|
5372
|
+
}
|
|
5373
|
+
});
|
|
5374
|
+
ctx.registerService("notifications", service);
|
|
4570
5375
|
ctx.log.info("Notifications service ready");
|
|
4571
5376
|
}
|
|
4572
5377
|
};
|
|
@@ -6866,7 +7671,7 @@ var init_viewer_routes = __esm({
|
|
|
6866
7671
|
// src/plugins/tunnel/viewer-store.ts
|
|
6867
7672
|
import * as fs17 from "fs";
|
|
6868
7673
|
import * as path20 from "path";
|
|
6869
|
-
import { nanoid } from "nanoid";
|
|
7674
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
6870
7675
|
var log10, MAX_CONTENT_SIZE, EXTENSION_LANGUAGE, ViewerStore;
|
|
6871
7676
|
var init_viewer_store = __esm({
|
|
6872
7677
|
"src/plugins/tunnel/viewer-store.ts"() {
|
|
@@ -6927,7 +7732,7 @@ var init_viewer_store = __esm({
|
|
|
6927
7732
|
log10.debug({ filePath, size: content.length }, "File too large for viewer");
|
|
6928
7733
|
return null;
|
|
6929
7734
|
}
|
|
6930
|
-
const id =
|
|
7735
|
+
const id = nanoid2(12);
|
|
6931
7736
|
const now = Date.now();
|
|
6932
7737
|
this.entries.set(id, {
|
|
6933
7738
|
id,
|
|
@@ -6953,7 +7758,7 @@ var init_viewer_store = __esm({
|
|
|
6953
7758
|
log10.debug({ filePath, size: combined }, "Diff content too large for viewer");
|
|
6954
7759
|
return null;
|
|
6955
7760
|
}
|
|
6956
|
-
const id =
|
|
7761
|
+
const id = nanoid2(12);
|
|
6957
7762
|
const now = Date.now();
|
|
6958
7763
|
this.entries.set(id, {
|
|
6959
7764
|
id,
|
|
@@ -6975,7 +7780,7 @@ var init_viewer_store = __esm({
|
|
|
6975
7780
|
log10.debug({ label, size: output.length }, "Output too large for viewer");
|
|
6976
7781
|
return null;
|
|
6977
7782
|
}
|
|
6978
|
-
const id =
|
|
7783
|
+
const id = nanoid2(12);
|
|
6979
7784
|
const now = Date.now();
|
|
6980
7785
|
this.entries.set(id, {
|
|
6981
7786
|
id,
|
|
@@ -7424,9 +8229,9 @@ __export(token_store_exports, {
|
|
|
7424
8229
|
parseDuration: () => parseDuration
|
|
7425
8230
|
});
|
|
7426
8231
|
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
7427
|
-
import { randomBytes } from "crypto";
|
|
8232
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
7428
8233
|
function generateTokenId() {
|
|
7429
|
-
return `tok_${
|
|
8234
|
+
return `tok_${randomBytes2(12).toString("hex")}`;
|
|
7430
8235
|
}
|
|
7431
8236
|
function parseDuration(duration) {
|
|
7432
8237
|
const match = duration.match(/^(\d+)(h|d|m)$/);
|
|
@@ -7573,6 +8378,18 @@ var init_token_store = __esm({
|
|
|
7573
8378
|
this.lastUsedSaveTimer = null;
|
|
7574
8379
|
}
|
|
7575
8380
|
}
|
|
8381
|
+
/** Associate a user ID with a token. Called by identity plugin after /identity/setup. */
|
|
8382
|
+
setUserId(tokenId, userId) {
|
|
8383
|
+
const token = this.tokens.get(tokenId);
|
|
8384
|
+
if (token) {
|
|
8385
|
+
token.userId = userId;
|
|
8386
|
+
this.scheduleSave();
|
|
8387
|
+
}
|
|
8388
|
+
}
|
|
8389
|
+
/** Get the user ID associated with a token. */
|
|
8390
|
+
getUserId(tokenId) {
|
|
8391
|
+
return this.tokens.get(tokenId)?.userId;
|
|
8392
|
+
}
|
|
7576
8393
|
/**
|
|
7577
8394
|
* Generates a one-time authorization code that can be exchanged for a JWT.
|
|
7578
8395
|
*
|
|
@@ -7580,7 +8397,7 @@ var init_token_store = __esm({
|
|
|
7580
8397
|
* the App, which exchanges it for a proper JWT without ever exposing the raw API secret.
|
|
7581
8398
|
*/
|
|
7582
8399
|
createCode(opts) {
|
|
7583
|
-
const code =
|
|
8400
|
+
const code = randomBytes2(16).toString("hex");
|
|
7584
8401
|
const now = /* @__PURE__ */ new Date();
|
|
7585
8402
|
const ttl = opts.codeTtlMs ?? 30 * 60 * 1e3;
|
|
7586
8403
|
const stored = {
|
|
@@ -8100,9 +8917,16 @@ async function createApiServer(options) {
|
|
|
8100
8917
|
});
|
|
8101
8918
|
const authPreHandler = createAuthPreHandler(options.getSecret, options.getJwtSecret, options.tokenStore);
|
|
8102
8919
|
app.decorateRequest("auth", null, []);
|
|
8920
|
+
let booted = false;
|
|
8921
|
+
app.addHook("onReady", async () => {
|
|
8922
|
+
booted = true;
|
|
8923
|
+
});
|
|
8103
8924
|
return {
|
|
8104
8925
|
app,
|
|
8105
8926
|
registerPlugin(prefix, plugin2, opts) {
|
|
8927
|
+
if (booted) {
|
|
8928
|
+
return;
|
|
8929
|
+
}
|
|
8106
8930
|
const wrappedPlugin = async (pluginApp, pluginOpts) => {
|
|
8107
8931
|
if (opts?.auth !== false) {
|
|
8108
8932
|
pluginApp.addHook("onRequest", authPreHandler);
|
|
@@ -8181,7 +9005,8 @@ var init_sse_manager = __esm({
|
|
|
8181
9005
|
BusEvent.PERMISSION_REQUEST,
|
|
8182
9006
|
BusEvent.PERMISSION_RESOLVED,
|
|
8183
9007
|
BusEvent.MESSAGE_QUEUED,
|
|
8184
|
-
BusEvent.MESSAGE_PROCESSING
|
|
9008
|
+
BusEvent.MESSAGE_PROCESSING,
|
|
9009
|
+
BusEvent.MESSAGE_FAILED
|
|
8185
9010
|
];
|
|
8186
9011
|
for (const eventName of events) {
|
|
8187
9012
|
const handler = (data) => {
|
|
@@ -8263,7 +9088,8 @@ data: ${JSON.stringify(data)}
|
|
|
8263
9088
|
BusEvent.PERMISSION_RESOLVED,
|
|
8264
9089
|
BusEvent.SESSION_UPDATED,
|
|
8265
9090
|
BusEvent.MESSAGE_QUEUED,
|
|
8266
|
-
BusEvent.MESSAGE_PROCESSING
|
|
9091
|
+
BusEvent.MESSAGE_PROCESSING,
|
|
9092
|
+
BusEvent.MESSAGE_FAILED
|
|
8267
9093
|
];
|
|
8268
9094
|
for (const res of this.sseConnections) {
|
|
8269
9095
|
const filter = res.sessionFilter;
|
|
@@ -8533,7 +9359,6 @@ var sessions_exports = {};
|
|
|
8533
9359
|
__export(sessions_exports, {
|
|
8534
9360
|
sessionRoutes: () => sessionRoutes
|
|
8535
9361
|
});
|
|
8536
|
-
import { nanoid as nanoid2 } from "nanoid";
|
|
8537
9362
|
async function sessionRoutes(app, deps) {
|
|
8538
9363
|
app.get("/", { preHandler: requireScopes("sessions:read") }, async () => {
|
|
8539
9364
|
const summaries = deps.core.sessionManager.listAllSessions();
|
|
@@ -8708,26 +9533,37 @@ async function sessionRoutes(app, deps) {
|
|
|
8708
9533
|
}
|
|
8709
9534
|
attachments = await resolveAttachments(fileService, sessionId, body.attachments);
|
|
8710
9535
|
}
|
|
8711
|
-
const sourceAdapterId = body.sourceAdapterId ?? "
|
|
8712
|
-
const
|
|
8713
|
-
deps.core.
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
}
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
9536
|
+
const sourceAdapterId = body.sourceAdapterId ?? "sse";
|
|
9537
|
+
const userId = request.auth?.tokenId ?? "api";
|
|
9538
|
+
const result = await deps.core.handleMessageInSession(
|
|
9539
|
+
session,
|
|
9540
|
+
{ channelId: sourceAdapterId, userId, text: body.prompt, attachments },
|
|
9541
|
+
{ channelUser: { channelId: "sse", userId } },
|
|
9542
|
+
{ externalTurnId: body.turnId, responseAdapterId: body.responseAdapterId }
|
|
9543
|
+
);
|
|
9544
|
+
if (!result) {
|
|
9545
|
+
throw new AuthError("MESSAGE_BLOCKED", "Message was blocked by a middleware plugin.", 403);
|
|
9546
|
+
}
|
|
9547
|
+
return { ok: true, sessionId, queueDepth: result.queueDepth, turnId: result.turnId };
|
|
9548
|
+
}
|
|
9549
|
+
);
|
|
9550
|
+
app.get(
|
|
9551
|
+
"/:sessionId/queue",
|
|
9552
|
+
{ preHandler: requireScopes("sessions:read") },
|
|
9553
|
+
async (request) => {
|
|
9554
|
+
const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
|
|
9555
|
+
const sessionId = decodeURIComponent(rawId);
|
|
9556
|
+
const session = await deps.core.getOrResumeSessionById(sessionId);
|
|
9557
|
+
if (!session) {
|
|
9558
|
+
throw new NotFoundError(
|
|
9559
|
+
"SESSION_NOT_FOUND",
|
|
9560
|
+
`Session "${sessionId}" not found`
|
|
9561
|
+
);
|
|
9562
|
+
}
|
|
8726
9563
|
return {
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
queueDepth: session.queueDepth
|
|
8730
|
-
turnId
|
|
9564
|
+
pending: session.queueItems,
|
|
9565
|
+
processing: session.promptRunning,
|
|
9566
|
+
queueDepth: session.queueDepth
|
|
8731
9567
|
};
|
|
8732
9568
|
}
|
|
8733
9569
|
);
|
|
@@ -8995,7 +9831,6 @@ var init_sessions2 = __esm({
|
|
|
8995
9831
|
init_error_handler();
|
|
8996
9832
|
init_auth();
|
|
8997
9833
|
init_attachment_utils();
|
|
8998
|
-
init_events();
|
|
8999
9834
|
init_sessions();
|
|
9000
9835
|
}
|
|
9001
9836
|
});
|
|
@@ -9180,7 +10015,7 @@ import { z as z4 } from "zod";
|
|
|
9180
10015
|
import * as fs21 from "fs";
|
|
9181
10016
|
import * as path24 from "path";
|
|
9182
10017
|
import * as os5 from "os";
|
|
9183
|
-
import { randomBytes as
|
|
10018
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
9184
10019
|
import { EventEmitter } from "events";
|
|
9185
10020
|
function expandHome2(p2) {
|
|
9186
10021
|
if (p2.startsWith("~")) {
|
|
@@ -9310,7 +10145,7 @@ var init_config2 = __esm({
|
|
|
9310
10145
|
log14.error({ errors: result.error.issues }, "Config validation failed, not saving");
|
|
9311
10146
|
return;
|
|
9312
10147
|
}
|
|
9313
|
-
const tmpPath = this.configPath + `.tmp.${
|
|
10148
|
+
const tmpPath = this.configPath + `.tmp.${randomBytes3(4).toString("hex")}`;
|
|
9314
10149
|
fs21.writeFileSync(tmpPath, JSON.stringify(raw, null, 2), "utf-8");
|
|
9315
10150
|
fs21.renameSync(tmpPath, this.configPath);
|
|
9316
10151
|
this.config = result.data;
|
|
@@ -9879,8 +10714,8 @@ async function commandRoutes(app, deps) {
|
|
|
9879
10714
|
const result = await deps.commandRegistry.execute(commandString, {
|
|
9880
10715
|
raw: "",
|
|
9881
10716
|
sessionId: body.sessionId ?? null,
|
|
9882
|
-
channelId: "
|
|
9883
|
-
userId: "api",
|
|
10717
|
+
channelId: "sse",
|
|
10718
|
+
userId: request.auth?.tokenId ?? "api",
|
|
9884
10719
|
reply: async () => {
|
|
9885
10720
|
}
|
|
9886
10721
|
});
|
|
@@ -10040,11 +10875,23 @@ async function authRoutes(app, deps) {
|
|
|
10040
10875
|
});
|
|
10041
10876
|
app.get("/me", async (request) => {
|
|
10042
10877
|
const { auth } = request;
|
|
10878
|
+
const userId = auth.tokenId ? deps.tokenStore.getUserId(auth.tokenId) : void 0;
|
|
10879
|
+
let displayName = null;
|
|
10880
|
+
if (userId && deps.getIdentityService) {
|
|
10881
|
+
const identityService = deps.getIdentityService();
|
|
10882
|
+
if (identityService) {
|
|
10883
|
+
const user = await identityService.getUser(userId);
|
|
10884
|
+
displayName = user?.displayName ?? null;
|
|
10885
|
+
}
|
|
10886
|
+
}
|
|
10043
10887
|
return {
|
|
10044
10888
|
type: auth.type,
|
|
10045
10889
|
tokenId: auth.tokenId,
|
|
10046
10890
|
role: auth.role,
|
|
10047
|
-
scopes: auth.scopes
|
|
10891
|
+
scopes: auth.scopes,
|
|
10892
|
+
userId: userId ?? null,
|
|
10893
|
+
displayName,
|
|
10894
|
+
claimed: !!userId
|
|
10048
10895
|
};
|
|
10049
10896
|
});
|
|
10050
10897
|
app.post("/codes", {
|
|
@@ -10624,6 +11471,7 @@ function createApiServerPlugin() {
|
|
|
10624
11471
|
const tokenStore = new TokenStore2(tokensFilePath);
|
|
10625
11472
|
await tokenStore.load();
|
|
10626
11473
|
tokenStoreRef = tokenStore;
|
|
11474
|
+
ctx.registerService("token-store", tokenStore);
|
|
10627
11475
|
const { createApiServer: createApiServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
10628
11476
|
const { SSEManager: SSEManager2 } = await Promise.resolve().then(() => (init_sse_manager(), sse_manager_exports));
|
|
10629
11477
|
const { StaticServer: StaticServer2 } = await Promise.resolve().then(() => (init_static_server(), static_server_exports));
|
|
@@ -10677,7 +11525,12 @@ function createApiServerPlugin() {
|
|
|
10677
11525
|
server.registerPlugin("/api/v1/tunnel", async (app) => tunnelRoutes2(app, deps));
|
|
10678
11526
|
server.registerPlugin("/api/v1/notify", async (app) => notifyRoutes2(app, deps));
|
|
10679
11527
|
server.registerPlugin("/api/v1/commands", async (app) => commandRoutes2(app, deps));
|
|
10680
|
-
server.registerPlugin("/api/v1/auth", async (app) => authRoutes2(app, {
|
|
11528
|
+
server.registerPlugin("/api/v1/auth", async (app) => authRoutes2(app, {
|
|
11529
|
+
tokenStore,
|
|
11530
|
+
getJwtSecret: () => jwtSecret,
|
|
11531
|
+
// Lazy resolver: identity plugin may not be loaded, so we fetch it on demand
|
|
11532
|
+
getIdentityService: () => ctx.getService("identity") ?? void 0
|
|
11533
|
+
}));
|
|
10681
11534
|
server.registerPlugin("/api/v1/plugins", async (app) => pluginRoutes2(app, deps));
|
|
10682
11535
|
const appConfig = core.configManager.get();
|
|
10683
11536
|
const workspaceName = appConfig.instanceName ?? "Main";
|
|
@@ -10826,7 +11679,7 @@ var init_api_server = __esm({
|
|
|
10826
11679
|
});
|
|
10827
11680
|
|
|
10828
11681
|
// src/plugins/sse-adapter/connection-manager.ts
|
|
10829
|
-
import { randomBytes as
|
|
11682
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
10830
11683
|
var ConnectionManager;
|
|
10831
11684
|
var init_connection_manager = __esm({
|
|
10832
11685
|
"src/plugins/sse-adapter/connection-manager.ts"() {
|
|
@@ -10835,6 +11688,8 @@ var init_connection_manager = __esm({
|
|
|
10835
11688
|
connections = /* @__PURE__ */ new Map();
|
|
10836
11689
|
// Secondary index: sessionId → Set of connection IDs for O(1) broadcast targeting
|
|
10837
11690
|
sessionIndex = /* @__PURE__ */ new Map();
|
|
11691
|
+
// Secondary index: userId → Set of connection IDs for user-level event delivery
|
|
11692
|
+
userIndex = /* @__PURE__ */ new Map();
|
|
10838
11693
|
maxConnectionsPerSession;
|
|
10839
11694
|
maxTotalConnections;
|
|
10840
11695
|
constructor(opts) {
|
|
@@ -10857,7 +11712,7 @@ var init_connection_manager = __esm({
|
|
|
10857
11712
|
if (sessionConns && sessionConns.size >= this.maxConnectionsPerSession) {
|
|
10858
11713
|
throw new Error("Maximum connections per session reached");
|
|
10859
11714
|
}
|
|
10860
|
-
const id = `conn_${
|
|
11715
|
+
const id = `conn_${randomBytes5(8).toString("hex")}`;
|
|
10861
11716
|
const connection = { id, sessionId, tokenId, response, connectedAt: /* @__PURE__ */ new Date() };
|
|
10862
11717
|
this.connections.set(id, connection);
|
|
10863
11718
|
let sessionConnsSet = this.sessionIndex.get(sessionId);
|
|
@@ -10869,7 +11724,66 @@ var init_connection_manager = __esm({
|
|
|
10869
11724
|
response.on("close", () => this.removeConnection(id));
|
|
10870
11725
|
return connection;
|
|
10871
11726
|
}
|
|
10872
|
-
/**
|
|
11727
|
+
/**
|
|
11728
|
+
* Registers a user-level SSE connection (not tied to a specific session).
|
|
11729
|
+
* Used for notifications and system events delivered to a user.
|
|
11730
|
+
*
|
|
11731
|
+
* @throws if the global connection limit is reached.
|
|
11732
|
+
*/
|
|
11733
|
+
addUserConnection(userId, tokenId, response) {
|
|
11734
|
+
if (this.connections.size >= this.maxTotalConnections) {
|
|
11735
|
+
throw new Error("Maximum total connections reached");
|
|
11736
|
+
}
|
|
11737
|
+
const id = `conn_${randomBytes5(8).toString("hex")}`;
|
|
11738
|
+
const connection = {
|
|
11739
|
+
id,
|
|
11740
|
+
sessionId: "",
|
|
11741
|
+
tokenId,
|
|
11742
|
+
userId,
|
|
11743
|
+
response,
|
|
11744
|
+
connectedAt: /* @__PURE__ */ new Date()
|
|
11745
|
+
};
|
|
11746
|
+
this.connections.set(id, connection);
|
|
11747
|
+
let userConns = this.userIndex.get(userId);
|
|
11748
|
+
if (!userConns) {
|
|
11749
|
+
userConns = /* @__PURE__ */ new Set();
|
|
11750
|
+
this.userIndex.set(userId, userConns);
|
|
11751
|
+
}
|
|
11752
|
+
userConns.add(id);
|
|
11753
|
+
response.on("close", () => this.removeConnection(id));
|
|
11754
|
+
return connection;
|
|
11755
|
+
}
|
|
11756
|
+
/**
|
|
11757
|
+
* Writes a serialized SSE event to all connections for a given user.
|
|
11758
|
+
*
|
|
11759
|
+
* Uses the same backpressure strategy as `broadcast`: flag on first overflow,
|
|
11760
|
+
* forcibly close if still backpressured on the next write.
|
|
11761
|
+
*/
|
|
11762
|
+
pushToUser(userId, serializedEvent) {
|
|
11763
|
+
const connIds = this.userIndex.get(userId);
|
|
11764
|
+
if (!connIds) return;
|
|
11765
|
+
for (const connId of connIds) {
|
|
11766
|
+
const conn = this.connections.get(connId);
|
|
11767
|
+
if (!conn || conn.response.writableEnded) continue;
|
|
11768
|
+
try {
|
|
11769
|
+
const ok3 = conn.response.write(serializedEvent);
|
|
11770
|
+
if (!ok3) {
|
|
11771
|
+
if (conn.backpressured) {
|
|
11772
|
+
conn.response.end();
|
|
11773
|
+
this.removeConnection(conn.id);
|
|
11774
|
+
} else {
|
|
11775
|
+
conn.backpressured = true;
|
|
11776
|
+
conn.response.once("drain", () => {
|
|
11777
|
+
conn.backpressured = false;
|
|
11778
|
+
});
|
|
11779
|
+
}
|
|
11780
|
+
}
|
|
11781
|
+
} catch {
|
|
11782
|
+
this.removeConnection(conn.id);
|
|
11783
|
+
}
|
|
11784
|
+
}
|
|
11785
|
+
}
|
|
11786
|
+
/** Remove a connection from all indexes. Called automatically on client disconnect. */
|
|
10873
11787
|
removeConnection(connectionId) {
|
|
10874
11788
|
const conn = this.connections.get(connectionId);
|
|
10875
11789
|
if (!conn) return;
|
|
@@ -10879,6 +11793,13 @@ var init_connection_manager = __esm({
|
|
|
10879
11793
|
sessionConns.delete(connectionId);
|
|
10880
11794
|
if (sessionConns.size === 0) this.sessionIndex.delete(conn.sessionId);
|
|
10881
11795
|
}
|
|
11796
|
+
if (conn.userId) {
|
|
11797
|
+
const userConns = this.userIndex.get(conn.userId);
|
|
11798
|
+
if (userConns) {
|
|
11799
|
+
userConns.delete(connectionId);
|
|
11800
|
+
if (userConns.size === 0) this.userIndex.delete(conn.userId);
|
|
11801
|
+
}
|
|
11802
|
+
}
|
|
10882
11803
|
}
|
|
10883
11804
|
/** Returns all active connections for a session. */
|
|
10884
11805
|
getConnectionsBySession(sessionId) {
|
|
@@ -10938,6 +11859,7 @@ var init_connection_manager = __esm({
|
|
|
10938
11859
|
}
|
|
10939
11860
|
this.connections.clear();
|
|
10940
11861
|
this.sessionIndex.clear();
|
|
11862
|
+
this.userIndex.clear();
|
|
10941
11863
|
}
|
|
10942
11864
|
};
|
|
10943
11865
|
}
|
|
@@ -11131,6 +12053,22 @@ var init_adapter = __esm({
|
|
|
11131
12053
|
this.connectionManager.broadcast(notification.sessionId, serialized);
|
|
11132
12054
|
}
|
|
11133
12055
|
}
|
|
12056
|
+
/**
|
|
12057
|
+
* Delivers a push notification to a specific user's SSE connections.
|
|
12058
|
+
*
|
|
12059
|
+
* `platformId` is the userId for the SSE adapter — SSE has no concept of
|
|
12060
|
+
* platform-specific user handles, so we use the internal userId directly.
|
|
12061
|
+
*/
|
|
12062
|
+
async sendUserNotification(platformId, message, options) {
|
|
12063
|
+
const serialized = `event: notification:text
|
|
12064
|
+
data: ${JSON.stringify({
|
|
12065
|
+
text: message.text ?? message.summary ?? "",
|
|
12066
|
+
...options ?? {}
|
|
12067
|
+
})}
|
|
12068
|
+
|
|
12069
|
+
`;
|
|
12070
|
+
this.connectionManager.pushToUser(platformId, serialized);
|
|
12071
|
+
}
|
|
11134
12072
|
/** SSE has no concept of threads — return sessionId as the threadId */
|
|
11135
12073
|
async createSessionThread(sessionId, _name) {
|
|
11136
12074
|
return sessionId;
|
|
@@ -11221,8 +12159,13 @@ async function sseRoutes(app, deps) {
|
|
|
11221
12159
|
}
|
|
11222
12160
|
attachments = await resolveAttachments(fileService, sessionId, body.attachments);
|
|
11223
12161
|
}
|
|
11224
|
-
|
|
11225
|
-
|
|
12162
|
+
const userId = request.auth?.tokenId ?? "api";
|
|
12163
|
+
const { turnId, queueDepth } = await deps.core.handleMessageInSession(
|
|
12164
|
+
session,
|
|
12165
|
+
{ channelId: "sse", userId, text: body.prompt, attachments },
|
|
12166
|
+
{ channelUser: { channelId: "sse", userId } }
|
|
12167
|
+
);
|
|
12168
|
+
return { ok: true, sessionId, queueDepth, turnId };
|
|
11226
12169
|
}
|
|
11227
12170
|
);
|
|
11228
12171
|
app.post(
|
|
@@ -11300,6 +12243,45 @@ async function sseRoutes(app, deps) {
|
|
|
11300
12243
|
total: connections.length
|
|
11301
12244
|
};
|
|
11302
12245
|
});
|
|
12246
|
+
app.get("/events", async (request, reply) => {
|
|
12247
|
+
const auth = request.auth;
|
|
12248
|
+
if (!auth?.tokenId) {
|
|
12249
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
12250
|
+
}
|
|
12251
|
+
const userId = deps.getUserId?.(auth.tokenId);
|
|
12252
|
+
if (!userId) {
|
|
12253
|
+
return reply.status(403).send({ error: "Identity not set up. Complete /identity/setup first." });
|
|
12254
|
+
}
|
|
12255
|
+
try {
|
|
12256
|
+
deps.connectionManager.addUserConnection(userId, auth.tokenId, reply.raw);
|
|
12257
|
+
} catch (err) {
|
|
12258
|
+
return reply.status(503).send({ error: err.message });
|
|
12259
|
+
}
|
|
12260
|
+
reply.hijack();
|
|
12261
|
+
const raw = reply.raw;
|
|
12262
|
+
raw.writeHead(200, {
|
|
12263
|
+
"Content-Type": "text/event-stream",
|
|
12264
|
+
"Cache-Control": "no-cache",
|
|
12265
|
+
"Connection": "keep-alive",
|
|
12266
|
+
// Disable buffering in Nginx/Cloudflare so events arrive without delay
|
|
12267
|
+
"X-Accel-Buffering": "no"
|
|
12268
|
+
});
|
|
12269
|
+
raw.write(`event: heartbeat
|
|
12270
|
+
data: ${JSON.stringify({ ts: Date.now() })}
|
|
12271
|
+
|
|
12272
|
+
`);
|
|
12273
|
+
const heartbeat = setInterval(() => {
|
|
12274
|
+
if (raw.writableEnded) {
|
|
12275
|
+
clearInterval(heartbeat);
|
|
12276
|
+
return;
|
|
12277
|
+
}
|
|
12278
|
+
raw.write(`event: heartbeat
|
|
12279
|
+
data: ${JSON.stringify({ ts: Date.now() })}
|
|
12280
|
+
|
|
12281
|
+
`);
|
|
12282
|
+
}, 3e4);
|
|
12283
|
+
raw.on("close", () => clearInterval(heartbeat));
|
|
12284
|
+
});
|
|
11303
12285
|
}
|
|
11304
12286
|
var init_routes = __esm({
|
|
11305
12287
|
"src/plugins/sse-adapter/routes.ts"() {
|
|
@@ -11353,6 +12335,7 @@ var init_sse_adapter = __esm({
|
|
|
11353
12335
|
_connectionManager = connectionManager;
|
|
11354
12336
|
ctx.registerService("adapter:sse", adapter);
|
|
11355
12337
|
const commandRegistry = ctx.getService("command-registry");
|
|
12338
|
+
const tokenStore = ctx.getService("token-store");
|
|
11356
12339
|
ctx.on(BusEvent.SESSION_DELETED, (data) => {
|
|
11357
12340
|
const { sessionId } = data;
|
|
11358
12341
|
eventBuffer.cleanup(sessionId);
|
|
@@ -11366,7 +12349,8 @@ var init_sse_adapter = __esm({
|
|
|
11366
12349
|
core,
|
|
11367
12350
|
connectionManager,
|
|
11368
12351
|
eventBuffer,
|
|
11369
|
-
commandRegistry: commandRegistry ?? void 0
|
|
12352
|
+
commandRegistry: commandRegistry ?? void 0,
|
|
12353
|
+
getUserId: tokenStore ? (id) => tokenStore.getUserId(id) : void 0
|
|
11370
12354
|
});
|
|
11371
12355
|
}, { auth: true });
|
|
11372
12356
|
ctx.log.info("SSE adapter registered");
|
|
@@ -17436,7 +18420,11 @@ var init_adapter2 = __esm({
|
|
|
17436
18420
|
}
|
|
17437
18421
|
return prev(method, payload, signal);
|
|
17438
18422
|
});
|
|
17439
|
-
|
|
18423
|
+
const onCommandsReady = ({ commands }) => {
|
|
18424
|
+
this.core.eventBus.off(BusEvent.SYSTEM_COMMANDS_READY, onCommandsReady);
|
|
18425
|
+
this.syncCommandsWithRetry(commands);
|
|
18426
|
+
};
|
|
18427
|
+
this.core.eventBus.on(BusEvent.SYSTEM_COMMANDS_READY, onCommandsReady);
|
|
17440
18428
|
this.bot.use((ctx, next) => {
|
|
17441
18429
|
const chatId = ctx.chat?.id ?? ctx.callbackQuery?.message?.chat?.id;
|
|
17442
18430
|
if (chatId !== this.telegramConfig.chatId) return;
|
|
@@ -17560,10 +18548,19 @@ var init_adapter2 = __esm({
|
|
|
17560
18548
|
});
|
|
17561
18549
|
} catch {
|
|
17562
18550
|
}
|
|
17563
|
-
} else if (response.type === "text" || response.type === "error") {
|
|
17564
|
-
|
|
18551
|
+
} else if (response.type === "text" || response.type === "error" || response.type === "adaptive") {
|
|
18552
|
+
let text5;
|
|
18553
|
+
let parseMode;
|
|
18554
|
+
if (response.type === "adaptive") {
|
|
18555
|
+
const variant = response.variants?.["telegram"];
|
|
18556
|
+
text5 = variant?.text ?? response.fallback;
|
|
18557
|
+
parseMode = variant?.parse_mode;
|
|
18558
|
+
} else {
|
|
18559
|
+
text5 = response.type === "text" ? response.text : `\u274C ${response.message}`;
|
|
18560
|
+
parseMode = "Markdown";
|
|
18561
|
+
}
|
|
17565
18562
|
try {
|
|
17566
|
-
await ctx.editMessageText(text5, { parse_mode:
|
|
18563
|
+
await ctx.editMessageText(text5, { ...parseMode && { parse_mode: parseMode } });
|
|
17567
18564
|
} catch {
|
|
17568
18565
|
}
|
|
17569
18566
|
}
|
|
@@ -17653,12 +18650,16 @@ ${p2}` : p2;
|
|
|
17653
18650
|
throw new Error("unreachable");
|
|
17654
18651
|
}
|
|
17655
18652
|
/**
|
|
17656
|
-
*
|
|
17657
|
-
*
|
|
18653
|
+
* Sync Telegram autocomplete commands after all plugins are ready.
|
|
18654
|
+
* Merges STATIC_COMMANDS (hardcoded system commands) with plugin commands
|
|
18655
|
+
* from the registry, deduplicating by command name. Non-critical.
|
|
17658
18656
|
*/
|
|
17659
|
-
|
|
18657
|
+
syncCommandsWithRetry(registryCommands) {
|
|
18658
|
+
const staticNames = new Set(STATIC_COMMANDS.map((c3) => c3.command));
|
|
18659
|
+
const pluginCommands = registryCommands.filter((c3) => c3.category === "plugin" && !staticNames.has(c3.name) && /^[a-z0-9_]+$/.test(c3.name)).map((c3) => ({ command: c3.name, description: c3.description.slice(0, 256) }));
|
|
18660
|
+
const allCommands = [...STATIC_COMMANDS, ...pluginCommands].slice(0, 100);
|
|
17660
18661
|
this.retryWithBackoff(
|
|
17661
|
-
() => this.bot.api.setMyCommands(
|
|
18662
|
+
() => this.bot.api.setMyCommands(allCommands, {
|
|
17662
18663
|
scope: { type: "chat", chat_id: this.telegramConfig.chatId }
|
|
17663
18664
|
}),
|
|
17664
18665
|
"setMyCommands"
|
|
@@ -17845,6 +18846,15 @@ OpenACP will automatically retry until this is resolved.`;
|
|
|
17845
18846
|
message_thread_id: topicId
|
|
17846
18847
|
});
|
|
17847
18848
|
break;
|
|
18849
|
+
case "adaptive": {
|
|
18850
|
+
const variant = response.variants?.["telegram"];
|
|
18851
|
+
const text5 = variant?.text ?? response.fallback;
|
|
18852
|
+
await this.bot.api.sendMessage(chatId, text5, {
|
|
18853
|
+
message_thread_id: topicId,
|
|
18854
|
+
...variant?.parse_mode && { parse_mode: variant.parse_mode }
|
|
18855
|
+
});
|
|
18856
|
+
break;
|
|
18857
|
+
}
|
|
17848
18858
|
case "error":
|
|
17849
18859
|
await this.bot.api.sendMessage(
|
|
17850
18860
|
chatId,
|
|
@@ -17953,12 +18963,25 @@ ${lines.join("\n")}`;
|
|
|
17953
18963
|
}
|
|
17954
18964
|
ctx.replyWithChatAction("typing").catch(() => {
|
|
17955
18965
|
});
|
|
17956
|
-
|
|
17957
|
-
|
|
17958
|
-
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
18966
|
+
const fromName = [ctx.from.first_name, ctx.from.last_name].filter(Boolean).join(" ") || void 0;
|
|
18967
|
+
this.core.handleMessage(
|
|
18968
|
+
{
|
|
18969
|
+
channelId: "telegram",
|
|
18970
|
+
threadId: String(threadId),
|
|
18971
|
+
userId: String(ctx.from.id),
|
|
18972
|
+
text: forwardText
|
|
18973
|
+
},
|
|
18974
|
+
// Inject structured channel user info into TurnMeta so plugins can identify
|
|
18975
|
+
// the sender by name without adapter-specific fields on IncomingMessage.
|
|
18976
|
+
{
|
|
18977
|
+
channelUser: {
|
|
18978
|
+
channelId: "telegram",
|
|
18979
|
+
userId: String(ctx.from.id),
|
|
18980
|
+
displayName: fromName,
|
|
18981
|
+
username: ctx.from.username
|
|
18982
|
+
}
|
|
18983
|
+
}
|
|
18984
|
+
).catch((err) => log29.error({ err }, "handleMessage error"));
|
|
17962
18985
|
});
|
|
17963
18986
|
this.bot.on("message:photo", async (ctx) => {
|
|
17964
18987
|
const threadId = ctx.message.message_thread_id;
|
|
@@ -18922,6 +19945,7 @@ var init_core_plugins = __esm({
|
|
|
18922
19945
|
"src/plugins/core-plugins.ts"() {
|
|
18923
19946
|
"use strict";
|
|
18924
19947
|
init_security();
|
|
19948
|
+
init_identity();
|
|
18925
19949
|
init_file_service2();
|
|
18926
19950
|
init_context();
|
|
18927
19951
|
init_speech();
|
|
@@ -18933,6 +19957,8 @@ var init_core_plugins = __esm({
|
|
|
18933
19957
|
corePlugins = [
|
|
18934
19958
|
// Service plugins (no adapter dependencies)
|
|
18935
19959
|
security_default,
|
|
19960
|
+
identity_default,
|
|
19961
|
+
// Must boot after security (blocked users rejected before identity records are created)
|
|
18936
19962
|
file_service_default,
|
|
18937
19963
|
context_default,
|
|
18938
19964
|
speech_default,
|
|
@@ -21162,16 +22188,16 @@ var init_prompt_queue = __esm({
|
|
|
21162
22188
|
* immediately. Otherwise, it's buffered and the returned promise resolves
|
|
21163
22189
|
* only after the prompt finishes processing.
|
|
21164
22190
|
*/
|
|
21165
|
-
async enqueue(text5, attachments, routing, turnId) {
|
|
22191
|
+
async enqueue(text5, userPrompt, attachments, routing, turnId, meta) {
|
|
21166
22192
|
if (this.processing) {
|
|
21167
22193
|
return new Promise((resolve9) => {
|
|
21168
|
-
this.queue.push({ text: text5, attachments, routing, turnId, resolve: resolve9 });
|
|
22194
|
+
this.queue.push({ text: text5, userPrompt, attachments, routing, turnId, meta, resolve: resolve9 });
|
|
21169
22195
|
});
|
|
21170
22196
|
}
|
|
21171
|
-
await this.process(text5, attachments, routing, turnId);
|
|
22197
|
+
await this.process(text5, userPrompt, attachments, routing, turnId, meta);
|
|
21172
22198
|
}
|
|
21173
22199
|
/** Run a single prompt through the processor, then drain the next queued item. */
|
|
21174
|
-
async process(text5, attachments, routing, turnId) {
|
|
22200
|
+
async process(text5, userPrompt, attachments, routing, turnId, meta) {
|
|
21175
22201
|
this.processing = true;
|
|
21176
22202
|
this.abortController = new AbortController();
|
|
21177
22203
|
const { signal } = this.abortController;
|
|
@@ -21181,7 +22207,7 @@ var init_prompt_queue = __esm({
|
|
|
21181
22207
|
});
|
|
21182
22208
|
try {
|
|
21183
22209
|
await Promise.race([
|
|
21184
|
-
this.processor(text5, attachments, routing, turnId),
|
|
22210
|
+
this.processor(text5, userPrompt, attachments, routing, turnId, meta),
|
|
21185
22211
|
new Promise((_, reject) => {
|
|
21186
22212
|
signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
|
|
21187
22213
|
})
|
|
@@ -21202,7 +22228,7 @@ var init_prompt_queue = __esm({
|
|
|
21202
22228
|
drainNext() {
|
|
21203
22229
|
const next = this.queue.shift();
|
|
21204
22230
|
if (next) {
|
|
21205
|
-
this.process(next.text, next.attachments, next.routing, next.turnId).then(next.resolve);
|
|
22231
|
+
this.process(next.text, next.userPrompt, next.attachments, next.routing, next.turnId, next.meta).then(next.resolve);
|
|
21206
22232
|
}
|
|
21207
22233
|
}
|
|
21208
22234
|
/**
|
|
@@ -21224,6 +22250,13 @@ var init_prompt_queue = __esm({
|
|
|
21224
22250
|
get isProcessing() {
|
|
21225
22251
|
return this.processing;
|
|
21226
22252
|
}
|
|
22253
|
+
/** Snapshot of queued (not yet processing) items — used for queue inspection by callers. */
|
|
22254
|
+
get pendingItems() {
|
|
22255
|
+
return this.queue.map((item) => ({
|
|
22256
|
+
userPrompt: item.userPrompt,
|
|
22257
|
+
turnId: item.turnId
|
|
22258
|
+
}));
|
|
22259
|
+
}
|
|
21227
22260
|
};
|
|
21228
22261
|
}
|
|
21229
22262
|
});
|
|
@@ -21309,17 +22342,31 @@ var init_permission_gate = __esm({
|
|
|
21309
22342
|
|
|
21310
22343
|
// src/core/sessions/turn-context.ts
|
|
21311
22344
|
import { nanoid as nanoid4 } from "nanoid";
|
|
21312
|
-
function
|
|
22345
|
+
function extractSender(meta) {
|
|
22346
|
+
const identity = meta?.identity;
|
|
22347
|
+
if (!identity || !identity.userId || !identity.identityId) return null;
|
|
21313
22348
|
return {
|
|
21314
|
-
|
|
21315
|
-
|
|
21316
|
-
|
|
22349
|
+
userId: identity.userId,
|
|
22350
|
+
identityId: identity.identityId,
|
|
22351
|
+
displayName: identity.displayName,
|
|
22352
|
+
username: identity.username
|
|
21317
22353
|
};
|
|
21318
22354
|
}
|
|
21319
22355
|
function getEffectiveTarget(ctx) {
|
|
21320
22356
|
if (ctx.responseAdapterId === null) return null;
|
|
21321
22357
|
return ctx.responseAdapterId ?? ctx.sourceAdapterId;
|
|
21322
22358
|
}
|
|
22359
|
+
function createTurnContext(sourceAdapterId, responseAdapterId, turnId, userPrompt, finalPrompt, attachments, meta) {
|
|
22360
|
+
return {
|
|
22361
|
+
turnId: turnId ?? nanoid4(8),
|
|
22362
|
+
sourceAdapterId,
|
|
22363
|
+
responseAdapterId,
|
|
22364
|
+
userPrompt,
|
|
22365
|
+
finalPrompt,
|
|
22366
|
+
attachments,
|
|
22367
|
+
meta
|
|
22368
|
+
};
|
|
22369
|
+
}
|
|
21323
22370
|
function isSystemEvent(event) {
|
|
21324
22371
|
return SYSTEM_EVENT_TYPES.has(event.type);
|
|
21325
22372
|
}
|
|
@@ -21431,7 +22478,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
|
|
|
21431
22478
|
this.log = createSessionLogger(this.id, moduleLog);
|
|
21432
22479
|
this.log.info({ agentName: this.agentName }, "Session created");
|
|
21433
22480
|
this.queue = new PromptQueue(
|
|
21434
|
-
(text5, attachments, routing, turnId) => this.processPrompt(text5, attachments, routing, turnId),
|
|
22481
|
+
(text5, userPrompt, attachments, routing, turnId, meta) => this.processPrompt(text5, userPrompt, attachments, routing, turnId, meta),
|
|
21435
22482
|
(err) => {
|
|
21436
22483
|
this.log.error({ err }, "Prompt execution failed");
|
|
21437
22484
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -21511,6 +22558,10 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
|
|
|
21511
22558
|
get promptRunning() {
|
|
21512
22559
|
return this.queue.isProcessing;
|
|
21513
22560
|
}
|
|
22561
|
+
/** Snapshot of queued (not yet processing) items — for inspection by API consumers. */
|
|
22562
|
+
get queueItems() {
|
|
22563
|
+
return this.queue.pendingItems;
|
|
22564
|
+
}
|
|
21514
22565
|
// --- Context Injection ---
|
|
21515
22566
|
/** Store context markdown to be prepended to the next prompt (used for session resume with history). */
|
|
21516
22567
|
setContext(markdown) {
|
|
@@ -21530,24 +22581,31 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
|
|
|
21530
22581
|
* then adds it to the PromptQueue. Returns a turnId that callers can use to correlate
|
|
21531
22582
|
* queued/processing events before the prompt actually runs.
|
|
21532
22583
|
*/
|
|
21533
|
-
async enqueuePrompt(text5, attachments, routing, externalTurnId) {
|
|
22584
|
+
async enqueuePrompt(text5, attachments, routing, externalTurnId, meta) {
|
|
21534
22585
|
const turnId = externalTurnId ?? nanoid5(8);
|
|
22586
|
+
const turnMeta = meta ?? { turnId };
|
|
22587
|
+
const userPrompt = text5;
|
|
21535
22588
|
if (this.middlewareChain) {
|
|
21536
|
-
const payload = { text: text5, attachments, sessionId: this.id, sourceAdapterId: routing?.sourceAdapterId };
|
|
22589
|
+
const payload = { text: text5, attachments, sessionId: this.id, sourceAdapterId: routing?.sourceAdapterId, meta: turnMeta };
|
|
21537
22590
|
const result = await this.middlewareChain.execute(Hook.AGENT_BEFORE_PROMPT, payload, async (p2) => p2);
|
|
21538
|
-
if (!result)
|
|
22591
|
+
if (!result) throw new Error("PROMPT_BLOCKED");
|
|
21539
22592
|
text5 = result.text;
|
|
21540
22593
|
attachments = result.attachments;
|
|
21541
22594
|
}
|
|
21542
|
-
await this.queue.enqueue(text5, attachments, routing, turnId);
|
|
22595
|
+
await this.queue.enqueue(text5, userPrompt, attachments, routing, turnId, turnMeta);
|
|
21543
22596
|
return turnId;
|
|
21544
22597
|
}
|
|
21545
|
-
async processPrompt(text5, attachments, routing, turnId) {
|
|
22598
|
+
async processPrompt(text5, userPrompt, attachments, routing, turnId, meta) {
|
|
21546
22599
|
if (this._status === "finished") return;
|
|
21547
22600
|
this.activeTurnContext = createTurnContext(
|
|
21548
22601
|
routing?.sourceAdapterId ?? this.channelId,
|
|
21549
22602
|
routing?.responseAdapterId,
|
|
21550
|
-
turnId
|
|
22603
|
+
turnId,
|
|
22604
|
+
userPrompt,
|
|
22605
|
+
text5,
|
|
22606
|
+
// finalPrompt (after middleware transformations)
|
|
22607
|
+
attachments,
|
|
22608
|
+
meta
|
|
21551
22609
|
);
|
|
21552
22610
|
this.emit(SessionEv.TURN_STARTED, this.activeTurnContext);
|
|
21553
22611
|
this.promptCount++;
|
|
@@ -21582,6 +22640,13 @@ ${text5}`;
|
|
|
21582
22640
|
if (accumulatorListener) {
|
|
21583
22641
|
this.on(SessionEv.AGENT_EVENT, accumulatorListener);
|
|
21584
22642
|
}
|
|
22643
|
+
const turnTextBuffer = [];
|
|
22644
|
+
const turnTextListener = (event) => {
|
|
22645
|
+
if (event.type === "text" && typeof event.content === "string") {
|
|
22646
|
+
turnTextBuffer.push(event.content);
|
|
22647
|
+
}
|
|
22648
|
+
};
|
|
22649
|
+
this.on(SessionEv.AGENT_EVENT, turnTextListener);
|
|
21585
22650
|
const mw = this.middlewareChain;
|
|
21586
22651
|
const afterEventListener = mw ? (event) => {
|
|
21587
22652
|
mw.execute(Hook.AGENT_AFTER_EVENT, { sessionId: this.id, event, outgoingMessage: { type: "text", text: "" } }, async (e) => e).catch(() => {
|
|
@@ -21591,7 +22656,16 @@ ${text5}`;
|
|
|
21591
22656
|
this.agentInstance.on(SessionEv.AGENT_EVENT, afterEventListener);
|
|
21592
22657
|
}
|
|
21593
22658
|
if (this.middlewareChain) {
|
|
21594
|
-
this.middlewareChain.execute(Hook.TURN_START, {
|
|
22659
|
+
this.middlewareChain.execute(Hook.TURN_START, {
|
|
22660
|
+
sessionId: this.id,
|
|
22661
|
+
promptText: processed.text,
|
|
22662
|
+
promptNumber: this.promptCount,
|
|
22663
|
+
turnId: this.activeTurnContext?.turnId ?? turnId ?? "",
|
|
22664
|
+
meta,
|
|
22665
|
+
userPrompt: this.activeTurnContext?.userPrompt,
|
|
22666
|
+
sourceAdapterId: this.activeTurnContext?.sourceAdapterId,
|
|
22667
|
+
responseAdapterId: this.activeTurnContext?.responseAdapterId
|
|
22668
|
+
}, async (p2) => p2).catch(() => {
|
|
21595
22669
|
});
|
|
21596
22670
|
}
|
|
21597
22671
|
let stopReason = "end_turn";
|
|
@@ -21617,8 +22691,20 @@ ${text5}`;
|
|
|
21617
22691
|
if (afterEventListener) {
|
|
21618
22692
|
this.agentInstance.off(SessionEv.AGENT_EVENT, afterEventListener);
|
|
21619
22693
|
}
|
|
22694
|
+
this.off(SessionEv.AGENT_EVENT, turnTextListener);
|
|
22695
|
+
const finalTurnId = this.activeTurnContext?.turnId ?? turnId ?? "";
|
|
21620
22696
|
if (this.middlewareChain) {
|
|
21621
|
-
this.middlewareChain.execute(Hook.TURN_END, { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart }, async (p2) => p2).catch(() => {
|
|
22697
|
+
this.middlewareChain.execute(Hook.TURN_END, { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart, turnId: finalTurnId, meta }, async (p2) => p2).catch(() => {
|
|
22698
|
+
});
|
|
22699
|
+
}
|
|
22700
|
+
if (this.middlewareChain) {
|
|
22701
|
+
this.middlewareChain.execute(Hook.AGENT_AFTER_TURN, {
|
|
22702
|
+
sessionId: this.id,
|
|
22703
|
+
turnId: finalTurnId,
|
|
22704
|
+
fullText: turnTextBuffer.join(""),
|
|
22705
|
+
stopReason,
|
|
22706
|
+
meta
|
|
22707
|
+
}, async (p2) => p2).catch(() => {
|
|
21622
22708
|
});
|
|
21623
22709
|
}
|
|
21624
22710
|
this.activeTurnContext = null;
|
|
@@ -22250,7 +23336,7 @@ var init_session_bridge = __esm({
|
|
|
22250
23336
|
if (this.shouldForward(event)) {
|
|
22251
23337
|
this.dispatchAgentEvent(event);
|
|
22252
23338
|
} else {
|
|
22253
|
-
this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, { sessionId: this.session.id, event });
|
|
23339
|
+
this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, { sessionId: this.session.id, turnId: "", event });
|
|
22254
23340
|
}
|
|
22255
23341
|
});
|
|
22256
23342
|
if (!this.session.agentInstance.onPermissionRequest || this.session.agentInstance.onPermissionRequest.__bridgeId === void 0) {
|
|
@@ -22303,14 +23389,16 @@ var init_session_bridge = __esm({
|
|
|
22303
23389
|
this.deps.sessionManager.patchRecord(this.session.id, { currentPromptCount: count });
|
|
22304
23390
|
});
|
|
22305
23391
|
this.listen(this.session, SessionEv.TURN_STARTED, (ctx) => {
|
|
22306
|
-
|
|
22307
|
-
this.
|
|
22308
|
-
|
|
22309
|
-
|
|
22310
|
-
|
|
22311
|
-
|
|
22312
|
-
|
|
22313
|
-
|
|
23392
|
+
this.deps.eventBus?.emit(BusEvent.MESSAGE_PROCESSING, {
|
|
23393
|
+
sessionId: this.session.id,
|
|
23394
|
+
turnId: ctx.turnId,
|
|
23395
|
+
sourceAdapterId: ctx.sourceAdapterId,
|
|
23396
|
+
userPrompt: ctx.userPrompt,
|
|
23397
|
+
finalPrompt: ctx.finalPrompt,
|
|
23398
|
+
attachments: ctx.attachments,
|
|
23399
|
+
sender: extractSender(ctx.meta),
|
|
23400
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
23401
|
+
});
|
|
22314
23402
|
});
|
|
22315
23403
|
if (this.session.latestCommands !== null) {
|
|
22316
23404
|
this.session.emit(SessionEv.AGENT_EVENT, { type: "commands_update", commands: this.session.latestCommands });
|
|
@@ -22476,6 +23564,7 @@ var init_session_bridge = __esm({
|
|
|
22476
23564
|
}
|
|
22477
23565
|
this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, {
|
|
22478
23566
|
sessionId: this.session.id,
|
|
23567
|
+
turnId: this.session.activeTurnContext?.turnId ?? "",
|
|
22479
23568
|
event
|
|
22480
23569
|
});
|
|
22481
23570
|
return outgoing;
|
|
@@ -23315,8 +24404,7 @@ var init_session_factory = __esm({
|
|
|
23315
24404
|
const payload = {
|
|
23316
24405
|
agentName: params.agentName,
|
|
23317
24406
|
workingDir: params.workingDirectory,
|
|
23318
|
-
userId: "",
|
|
23319
|
-
// userId is not part of SessionCreateParams — resolved upstream
|
|
24407
|
+
userId: params.userId ?? "",
|
|
23320
24408
|
channelId: params.channelId,
|
|
23321
24409
|
threadId: ""
|
|
23322
24410
|
// threadId is assigned after session creation
|
|
@@ -23390,6 +24478,7 @@ var init_session_factory = __esm({
|
|
|
23390
24478
|
const failedSessionId = createParams.existingSessionId ?? `failed-${Date.now()}`;
|
|
23391
24479
|
this.eventBus.emit(BusEvent.AGENT_EVENT, {
|
|
23392
24480
|
sessionId: failedSessionId,
|
|
24481
|
+
turnId: "",
|
|
23393
24482
|
event: guidance
|
|
23394
24483
|
});
|
|
23395
24484
|
throw err;
|
|
@@ -23418,7 +24507,8 @@ var init_session_factory = __esm({
|
|
|
23418
24507
|
this.eventBus.emit(BusEvent.SESSION_CREATED, {
|
|
23419
24508
|
sessionId: session.id,
|
|
23420
24509
|
agent: session.agentName,
|
|
23421
|
-
status: session.status
|
|
24510
|
+
status: session.status,
|
|
24511
|
+
userId: createParams.userId
|
|
23422
24512
|
});
|
|
23423
24513
|
}
|
|
23424
24514
|
return session;
|
|
@@ -23759,7 +24849,7 @@ var init_agent_switch_handler = __esm({
|
|
|
23759
24849
|
message: `Switching from ${fromAgent} to ${toAgent}...`
|
|
23760
24850
|
};
|
|
23761
24851
|
session.emit(SessionEv.AGENT_EVENT, startEvent);
|
|
23762
|
-
eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: startEvent });
|
|
24852
|
+
eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, turnId: "", event: startEvent });
|
|
23763
24853
|
eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
|
|
23764
24854
|
sessionId,
|
|
23765
24855
|
fromAgent,
|
|
@@ -23822,7 +24912,7 @@ var init_agent_switch_handler = __esm({
|
|
|
23822
24912
|
message: resumed ? `Switched to ${toAgent} (resumed previous session).` : `Switched to ${toAgent} (new session).`
|
|
23823
24913
|
};
|
|
23824
24914
|
session.emit(SessionEv.AGENT_EVENT, successEvent);
|
|
23825
|
-
eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: successEvent });
|
|
24915
|
+
eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, turnId: "", event: successEvent });
|
|
23826
24916
|
eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
|
|
23827
24917
|
sessionId,
|
|
23828
24918
|
fromAgent,
|
|
@@ -23837,7 +24927,7 @@ var init_agent_switch_handler = __esm({
|
|
|
23837
24927
|
message: `Failed to switch to ${toAgent}: ${errorMessage}`
|
|
23838
24928
|
};
|
|
23839
24929
|
session.emit(SessionEv.AGENT_EVENT, failedEvent);
|
|
23840
|
-
eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: failedEvent });
|
|
24930
|
+
eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, turnId: "", event: failedEvent });
|
|
23841
24931
|
eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
|
|
23842
24932
|
sessionId,
|
|
23843
24933
|
fromAgent,
|
|
@@ -24663,11 +25753,55 @@ var init_plugin_storage = __esm({
|
|
|
24663
25753
|
async list() {
|
|
24664
25754
|
return Object.keys(this.readKv());
|
|
24665
25755
|
}
|
|
25756
|
+
async keys(prefix) {
|
|
25757
|
+
const all = Object.keys(this.readKv());
|
|
25758
|
+
return prefix ? all.filter((k) => k.startsWith(prefix)) : all;
|
|
25759
|
+
}
|
|
25760
|
+
async clear() {
|
|
25761
|
+
this.writeChain = this.writeChain.then(() => {
|
|
25762
|
+
this.writeKv({});
|
|
25763
|
+
});
|
|
25764
|
+
return this.writeChain;
|
|
25765
|
+
}
|
|
24666
25766
|
/** Returns the plugin's data directory, creating it lazily on first access. */
|
|
24667
25767
|
getDataDir() {
|
|
24668
25768
|
fs45.mkdirSync(this.dataDir, { recursive: true });
|
|
24669
25769
|
return this.dataDir;
|
|
24670
25770
|
}
|
|
25771
|
+
/**
|
|
25772
|
+
* Creates a namespaced storage instance scoped to a session.
|
|
25773
|
+
* Keys are prefixed with `session:{sessionId}:` to isolate session data
|
|
25774
|
+
* from global plugin storage in the same backing file.
|
|
25775
|
+
*/
|
|
25776
|
+
forSession(sessionId) {
|
|
25777
|
+
const prefix = `session:${sessionId}:`;
|
|
25778
|
+
return {
|
|
25779
|
+
get: (key) => this.get(`${prefix}${key}`),
|
|
25780
|
+
set: (key, value) => this.set(`${prefix}${key}`, value),
|
|
25781
|
+
delete: (key) => this.delete(`${prefix}${key}`),
|
|
25782
|
+
list: async () => {
|
|
25783
|
+
const all = await this.keys(prefix);
|
|
25784
|
+
return all.map((k) => k.slice(prefix.length));
|
|
25785
|
+
},
|
|
25786
|
+
keys: async (p2) => {
|
|
25787
|
+
const full = p2 ? `${prefix}${p2}` : prefix;
|
|
25788
|
+
const all = await this.keys(full);
|
|
25789
|
+
return all.map((k) => k.slice(prefix.length));
|
|
25790
|
+
},
|
|
25791
|
+
clear: async () => {
|
|
25792
|
+
this.writeChain = this.writeChain.then(() => {
|
|
25793
|
+
const data = this.readKv();
|
|
25794
|
+
for (const key of Object.keys(data)) {
|
|
25795
|
+
if (key.startsWith(prefix)) delete data[key];
|
|
25796
|
+
}
|
|
25797
|
+
this.writeKv(data);
|
|
25798
|
+
});
|
|
25799
|
+
return this.writeChain;
|
|
25800
|
+
},
|
|
25801
|
+
getDataDir: () => this.getDataDir(),
|
|
25802
|
+
forSession: (nestedId) => this.forSession(`${sessionId}:${nestedId}`)
|
|
25803
|
+
};
|
|
25804
|
+
}
|
|
24671
25805
|
};
|
|
24672
25806
|
}
|
|
24673
25807
|
});
|
|
@@ -24733,9 +25867,52 @@ function createPluginContext(opts) {
|
|
|
24733
25867
|
requirePermission(permissions, "storage:read", "storage.list");
|
|
24734
25868
|
return storageImpl.list();
|
|
24735
25869
|
},
|
|
25870
|
+
async keys(prefix) {
|
|
25871
|
+
requirePermission(permissions, "storage:read", "storage.keys");
|
|
25872
|
+
return storageImpl.keys(prefix);
|
|
25873
|
+
},
|
|
25874
|
+
async clear() {
|
|
25875
|
+
requirePermission(permissions, "storage:write", "storage.clear");
|
|
25876
|
+
return storageImpl.clear();
|
|
25877
|
+
},
|
|
24736
25878
|
getDataDir() {
|
|
24737
25879
|
requirePermission(permissions, "storage:read", "storage.getDataDir");
|
|
24738
25880
|
return storageImpl.getDataDir();
|
|
25881
|
+
},
|
|
25882
|
+
forSession(sessionId) {
|
|
25883
|
+
requirePermission(permissions, "storage:read", "storage.forSession");
|
|
25884
|
+
const scoped = storageImpl.forSession(sessionId);
|
|
25885
|
+
return {
|
|
25886
|
+
get: (key) => {
|
|
25887
|
+
requirePermission(permissions, "storage:read", "storage.get");
|
|
25888
|
+
return scoped.get(key);
|
|
25889
|
+
},
|
|
25890
|
+
set: (key, value) => {
|
|
25891
|
+
requirePermission(permissions, "storage:write", "storage.set");
|
|
25892
|
+
return scoped.set(key, value);
|
|
25893
|
+
},
|
|
25894
|
+
delete: (key) => {
|
|
25895
|
+
requirePermission(permissions, "storage:write", "storage.delete");
|
|
25896
|
+
return scoped.delete(key);
|
|
25897
|
+
},
|
|
25898
|
+
list: () => {
|
|
25899
|
+
requirePermission(permissions, "storage:read", "storage.list");
|
|
25900
|
+
return scoped.list();
|
|
25901
|
+
},
|
|
25902
|
+
keys: (prefix) => {
|
|
25903
|
+
requirePermission(permissions, "storage:read", "storage.keys");
|
|
25904
|
+
return scoped.keys(prefix);
|
|
25905
|
+
},
|
|
25906
|
+
clear: () => {
|
|
25907
|
+
requirePermission(permissions, "storage:write", "storage.clear");
|
|
25908
|
+
return scoped.clear();
|
|
25909
|
+
},
|
|
25910
|
+
getDataDir: () => {
|
|
25911
|
+
requirePermission(permissions, "storage:read", "storage.getDataDir");
|
|
25912
|
+
return scoped.getDataDir();
|
|
25913
|
+
},
|
|
25914
|
+
forSession: (nestedId) => storage.forSession(`${sessionId}:${nestedId}`)
|
|
25915
|
+
};
|
|
24739
25916
|
}
|
|
24740
25917
|
};
|
|
24741
25918
|
const ctx = {
|
|
@@ -24786,6 +25963,26 @@ function createPluginContext(opts) {
|
|
|
24786
25963
|
await router.send(_sessionId, _content);
|
|
24787
25964
|
}
|
|
24788
25965
|
},
|
|
25966
|
+
notify(target, message, options) {
|
|
25967
|
+
requirePermission(permissions, "notifications:send", "notify()");
|
|
25968
|
+
const svc = serviceRegistry.get("notifications");
|
|
25969
|
+
if (svc?.notifyUser) {
|
|
25970
|
+
svc.notifyUser(target, message, options).catch(() => {
|
|
25971
|
+
});
|
|
25972
|
+
}
|
|
25973
|
+
},
|
|
25974
|
+
defineHook(_name) {
|
|
25975
|
+
},
|
|
25976
|
+
async emitHook(name, payload) {
|
|
25977
|
+
const qualifiedName = `plugin:${pluginName}:${name}`;
|
|
25978
|
+
return middlewareChain.execute(qualifiedName, payload, (p2) => p2);
|
|
25979
|
+
},
|
|
25980
|
+
async getSessionInfo(sessionId) {
|
|
25981
|
+
requirePermission(permissions, "sessions:read", "getSessionInfo()");
|
|
25982
|
+
const sessionMgr = serviceRegistry.get("session-info");
|
|
25983
|
+
if (!sessionMgr) return void 0;
|
|
25984
|
+
return sessionMgr.getSessionInfo(sessionId);
|
|
25985
|
+
},
|
|
24789
25986
|
registerMenuItem(item) {
|
|
24790
25987
|
requirePermission(permissions, "commands:register", "registerMenuItem()");
|
|
24791
25988
|
const menuRegistry = serviceRegistry.get("menu-registry");
|
|
@@ -25669,6 +26866,7 @@ var init_core = __esm({
|
|
|
25669
26866
|
init_error_tracker();
|
|
25670
26867
|
init_log();
|
|
25671
26868
|
init_events();
|
|
26869
|
+
init_turn_context();
|
|
25672
26870
|
log44 = createChildLogger({ module: "core" });
|
|
25673
26871
|
OpenACPCore = class {
|
|
25674
26872
|
configManager;
|
|
@@ -25945,7 +27143,7 @@ var init_core = __esm({
|
|
|
25945
27143
|
*
|
|
25946
27144
|
* If no session is found, the user is told to start one with /new.
|
|
25947
27145
|
*/
|
|
25948
|
-
async handleMessage(message) {
|
|
27146
|
+
async handleMessage(message, initialMeta) {
|
|
25949
27147
|
log44.debug(
|
|
25950
27148
|
{
|
|
25951
27149
|
channelId: message.channelId,
|
|
@@ -25954,10 +27152,12 @@ var init_core = __esm({
|
|
|
25954
27152
|
},
|
|
25955
27153
|
"Incoming message"
|
|
25956
27154
|
);
|
|
27155
|
+
const turnId = nanoid6(8);
|
|
27156
|
+
const meta = { turnId, ...initialMeta };
|
|
25957
27157
|
if (this.lifecycleManager?.middlewareChain) {
|
|
25958
27158
|
const result = await this.lifecycleManager.middlewareChain.execute(
|
|
25959
27159
|
Hook.MESSAGE_INCOMING,
|
|
25960
|
-
message,
|
|
27160
|
+
{ ...message, meta },
|
|
25961
27161
|
async (msg) => msg
|
|
25962
27162
|
);
|
|
25963
27163
|
if (!result) return;
|
|
@@ -25992,9 +27192,6 @@ var init_core = __esm({
|
|
|
25992
27192
|
}
|
|
25993
27193
|
return;
|
|
25994
27194
|
}
|
|
25995
|
-
this.sessionManager.patchRecord(session.id, {
|
|
25996
|
-
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
25997
|
-
});
|
|
25998
27195
|
let text5 = message.text;
|
|
25999
27196
|
if (this.assistantManager?.isAssistant(session.id)) {
|
|
26000
27197
|
const pending = this.assistantManager.consumePendingSystemPrompt(message.channelId);
|
|
@@ -26007,23 +27204,85 @@ User message:
|
|
|
26007
27204
|
${text5}`;
|
|
26008
27205
|
}
|
|
26009
27206
|
}
|
|
26010
|
-
const
|
|
26011
|
-
|
|
26012
|
-
|
|
26013
|
-
|
|
26014
|
-
|
|
27207
|
+
const enrichedMeta = message.meta ?? meta;
|
|
27208
|
+
await this._dispatchToSession(session, text5, message.attachments, {
|
|
27209
|
+
sourceAdapterId: message.routing?.sourceAdapterId ?? message.channelId,
|
|
27210
|
+
responseAdapterId: message.routing?.responseAdapterId
|
|
27211
|
+
}, turnId, enrichedMeta);
|
|
27212
|
+
}
|
|
27213
|
+
/**
|
|
27214
|
+
* Shared dispatch path for sending a prompt to a session.
|
|
27215
|
+
* Called by both handleMessage (Telegram) and handleMessageInSession (SSE/API)
|
|
27216
|
+
* after their respective middleware/enrichment steps.
|
|
27217
|
+
*/
|
|
27218
|
+
async _dispatchToSession(session, text5, attachments, routing, turnId, meta) {
|
|
27219
|
+
this.sessionManager.patchRecord(session.id, {
|
|
27220
|
+
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
27221
|
+
});
|
|
27222
|
+
this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
|
|
27223
|
+
sessionId: session.id,
|
|
27224
|
+
turnId,
|
|
27225
|
+
text: text5,
|
|
27226
|
+
sourceAdapterId: routing.sourceAdapterId,
|
|
27227
|
+
attachments,
|
|
27228
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27229
|
+
queueDepth: session.queueDepth + 1,
|
|
27230
|
+
sender: extractSender(meta)
|
|
27231
|
+
});
|
|
27232
|
+
session.enqueuePrompt(text5, attachments, routing, turnId, meta).catch((err) => {
|
|
27233
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
27234
|
+
log44.warn({ err, sessionId: session.id, turnId, reason }, "enqueuePrompt failed \u2014 emitting message:failed");
|
|
27235
|
+
this.eventBus.emit(BusEvent.MESSAGE_FAILED, {
|
|
26015
27236
|
sessionId: session.id,
|
|
26016
27237
|
turnId,
|
|
26017
|
-
|
|
26018
|
-
sourceAdapterId,
|
|
26019
|
-
attachments: message.attachments,
|
|
26020
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26021
|
-
queueDepth: session.queueDepth
|
|
27238
|
+
reason
|
|
26022
27239
|
});
|
|
26023
|
-
|
|
26024
|
-
|
|
26025
|
-
|
|
27240
|
+
});
|
|
27241
|
+
}
|
|
27242
|
+
/**
|
|
27243
|
+
* Send a message to a known session, running the full message:incoming → agent:beforePrompt
|
|
27244
|
+
* middleware chain (same as handleMessage) but without the threadId-based session lookup.
|
|
27245
|
+
*
|
|
27246
|
+
* Used by channels that already hold a direct session reference (e.g. SSE adapter, api-server),
|
|
27247
|
+
* where looking up by channelId+threadId is unreliable (API sessions may have no threadId).
|
|
27248
|
+
*
|
|
27249
|
+
* @param session The target session — caller is responsible for validating its status.
|
|
27250
|
+
* @param message Sender context and message content.
|
|
27251
|
+
* @param initialMeta Optional adapter-specific context to seed the TurnMeta bag
|
|
27252
|
+
* (e.g. channelUser with display name/username).
|
|
27253
|
+
* @param options Optional turnId override and response routing.
|
|
27254
|
+
*/
|
|
27255
|
+
async handleMessageInSession(session, message, initialMeta, options) {
|
|
27256
|
+
const turnId = options?.externalTurnId ?? nanoid6(8);
|
|
27257
|
+
const meta = { turnId, ...initialMeta };
|
|
27258
|
+
let text5 = message.text;
|
|
27259
|
+
let { attachments } = message;
|
|
27260
|
+
let enrichedMeta = meta;
|
|
27261
|
+
if (this.lifecycleManager?.middlewareChain) {
|
|
27262
|
+
const payload = {
|
|
27263
|
+
channelId: message.channelId,
|
|
27264
|
+
threadId: session.id,
|
|
27265
|
+
userId: message.userId,
|
|
27266
|
+
text: text5,
|
|
27267
|
+
attachments,
|
|
27268
|
+
meta
|
|
27269
|
+
};
|
|
27270
|
+
const result = await this.lifecycleManager.middlewareChain.execute(
|
|
27271
|
+
Hook.MESSAGE_INCOMING,
|
|
27272
|
+
payload,
|
|
27273
|
+
async (p2) => p2
|
|
27274
|
+
);
|
|
27275
|
+
if (!result) return { turnId, queueDepth: session.queueDepth };
|
|
27276
|
+
text5 = result.text;
|
|
27277
|
+
attachments = result.attachments;
|
|
27278
|
+
enrichedMeta = result.meta ?? meta;
|
|
26026
27279
|
}
|
|
27280
|
+
const routing = {
|
|
27281
|
+
sourceAdapterId: message.channelId,
|
|
27282
|
+
responseAdapterId: options?.responseAdapterId
|
|
27283
|
+
};
|
|
27284
|
+
await this._dispatchToSession(session, text5, attachments, routing, turnId, enrichedMeta);
|
|
27285
|
+
return { turnId, queueDepth: session.queueDepth };
|
|
26027
27286
|
}
|
|
26028
27287
|
// --- Unified Session Creation Pipeline ---
|
|
26029
27288
|
/**
|
|
@@ -26135,7 +27394,7 @@ ${text5}`;
|
|
|
26135
27394
|
} else if (processedEvent.type === "error") {
|
|
26136
27395
|
session.fail(processedEvent.message);
|
|
26137
27396
|
}
|
|
26138
|
-
this.eventBus.emit(BusEvent.AGENT_EVENT, { sessionId: session.id, event: processedEvent });
|
|
27397
|
+
this.eventBus.emit(BusEvent.AGENT_EVENT, { sessionId: session.id, turnId: session.activeTurnContext?.turnId ?? "", event: processedEvent });
|
|
26139
27398
|
});
|
|
26140
27399
|
session.on(SessionEv.STATUS_CHANGE, (_from, to) => {
|
|
26141
27400
|
this.sessionManager.patchRecord(session.id, {
|
|
@@ -26147,6 +27406,18 @@ ${text5}`;
|
|
|
26147
27406
|
session.on(SessionEv.PROMPT_COUNT_CHANGED, (count) => {
|
|
26148
27407
|
this.sessionManager.patchRecord(session.id, { currentPromptCount: count });
|
|
26149
27408
|
});
|
|
27409
|
+
session.on(SessionEv.TURN_STARTED, (ctx) => {
|
|
27410
|
+
this.eventBus.emit(BusEvent.MESSAGE_PROCESSING, {
|
|
27411
|
+
sessionId: session.id,
|
|
27412
|
+
turnId: ctx.turnId,
|
|
27413
|
+
sourceAdapterId: ctx.sourceAdapterId,
|
|
27414
|
+
userPrompt: ctx.userPrompt,
|
|
27415
|
+
finalPrompt: ctx.finalPrompt,
|
|
27416
|
+
attachments: ctx.attachments,
|
|
27417
|
+
sender: extractSender(ctx.meta),
|
|
27418
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
27419
|
+
});
|
|
27420
|
+
});
|
|
26150
27421
|
}
|
|
26151
27422
|
this.sessionFactory.wireSideEffects(session, {
|
|
26152
27423
|
eventBus: this.eventBus,
|
|
@@ -26584,7 +27855,7 @@ function registerSessionCommands(registry, _core) {
|
|
|
26584
27855
|
await assistant.enqueuePrompt(prompt);
|
|
26585
27856
|
return { type: "delegated" };
|
|
26586
27857
|
}
|
|
26587
|
-
return { type: "text", text: "Usage: /new
|
|
27858
|
+
return { type: "text", text: "Usage: /new [agent] [workspace]\nOr use the Assistant topic for guided setup." };
|
|
26588
27859
|
}
|
|
26589
27860
|
});
|
|
26590
27861
|
registry.register({
|
|
@@ -26656,7 +27927,7 @@ Prompts: ${session.promptCount}` };
|
|
|
26656
27927
|
registry.register({
|
|
26657
27928
|
name: "resume",
|
|
26658
27929
|
description: "Resume a previous session",
|
|
26659
|
-
usage: "
|
|
27930
|
+
usage: "[session-number]",
|
|
26660
27931
|
category: "system",
|
|
26661
27932
|
handler: async (args2) => {
|
|
26662
27933
|
const assistant = core.assistantManager?.get(args2.channelId);
|
|
@@ -26670,7 +27941,7 @@ Prompts: ${session.promptCount}` };
|
|
|
26670
27941
|
registry.register({
|
|
26671
27942
|
name: "handoff",
|
|
26672
27943
|
description: "Hand off session to another agent",
|
|
26673
|
-
usage: "
|
|
27944
|
+
usage: "[agent-name]",
|
|
26674
27945
|
category: "system",
|
|
26675
27946
|
handler: async (args2) => {
|
|
26676
27947
|
if (!args2.sessionId) return { type: "text", text: "Use /handoff inside a session topic." };
|
|
@@ -26856,7 +28127,7 @@ function registerAgentCommands(registry, _core) {
|
|
|
26856
28127
|
registry.register({
|
|
26857
28128
|
name: "install",
|
|
26858
28129
|
description: "Install an agent",
|
|
26859
|
-
usage: "
|
|
28130
|
+
usage: "[agent-name]",
|
|
26860
28131
|
category: "system",
|
|
26861
28132
|
handler: async (args2) => {
|
|
26862
28133
|
const agentName = args2.raw.trim();
|
|
@@ -26931,7 +28202,7 @@ function registerAdminCommands(registry, _core) {
|
|
|
26931
28202
|
registry.register({
|
|
26932
28203
|
name: "integrate",
|
|
26933
28204
|
description: "Set up a new channel integration",
|
|
26934
|
-
usage: "
|
|
28205
|
+
usage: "[channel]",
|
|
26935
28206
|
category: "system",
|
|
26936
28207
|
handler: async (args2) => {
|
|
26937
28208
|
const channel = args2.raw.trim();
|
|
@@ -27334,7 +28605,7 @@ var init_plugin_field_registry = __esm({
|
|
|
27334
28605
|
|
|
27335
28606
|
// src/core/setup/types.ts
|
|
27336
28607
|
var ONBOARD_SECTION_OPTIONS, CHANNEL_META;
|
|
27337
|
-
var
|
|
28608
|
+
var init_types2 = __esm({
|
|
27338
28609
|
"src/core/setup/types.ts"() {
|
|
27339
28610
|
"use strict";
|
|
27340
28611
|
ONBOARD_SECTION_OPTIONS = [
|
|
@@ -27843,7 +29114,7 @@ var CHANNEL_PLUGIN_NAME;
|
|
|
27843
29114
|
var init_setup_channels = __esm({
|
|
27844
29115
|
"src/core/setup/setup-channels.ts"() {
|
|
27845
29116
|
"use strict";
|
|
27846
|
-
|
|
29117
|
+
init_types2();
|
|
27847
29118
|
init_helpers2();
|
|
27848
29119
|
CHANNEL_PLUGIN_NAME = {
|
|
27849
29120
|
discord: "@openacp/discord-adapter"
|
|
@@ -28501,7 +29772,7 @@ async function runReconfigure(configManager, settingsManager) {
|
|
|
28501
29772
|
var init_wizard = __esm({
|
|
28502
29773
|
"src/core/setup/wizard.ts"() {
|
|
28503
29774
|
"use strict";
|
|
28504
|
-
|
|
29775
|
+
init_types2();
|
|
28505
29776
|
init_helpers2();
|
|
28506
29777
|
init_setup_agents();
|
|
28507
29778
|
init_setup_run_mode();
|
|
@@ -28516,8 +29787,8 @@ var init_wizard = __esm({
|
|
|
28516
29787
|
});
|
|
28517
29788
|
|
|
28518
29789
|
// src/core/setup/index.ts
|
|
28519
|
-
var
|
|
28520
|
-
__export(
|
|
29790
|
+
var setup_exports2 = {};
|
|
29791
|
+
__export(setup_exports2, {
|
|
28521
29792
|
detectAgents: () => detectAgents,
|
|
28522
29793
|
printStartBanner: () => printStartBanner,
|
|
28523
29794
|
runReconfigure: () => runReconfigure,
|
|
@@ -28529,7 +29800,7 @@ __export(setup_exports, {
|
|
|
28529
29800
|
validateBotToken: () => validateBotToken,
|
|
28530
29801
|
validateChatId: () => validateChatId
|
|
28531
29802
|
});
|
|
28532
|
-
var
|
|
29803
|
+
var init_setup2 = __esm({
|
|
28533
29804
|
"src/core/setup/index.ts"() {
|
|
28534
29805
|
"use strict";
|
|
28535
29806
|
init_wizard();
|
|
@@ -28778,7 +30049,7 @@ async function startServer(opts) {
|
|
|
28778
30049
|
const configManager = new ConfigManager(ctx.paths.config);
|
|
28779
30050
|
const configExists = await configManager.exists();
|
|
28780
30051
|
if (!configExists) {
|
|
28781
|
-
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (
|
|
30052
|
+
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
|
|
28782
30053
|
const shouldStart = await runSetup2(configManager, { settingsManager, pluginRegistry });
|
|
28783
30054
|
if (!shouldStart) process.exit(0);
|
|
28784
30055
|
}
|
|
@@ -28792,7 +30063,7 @@ async function startServer(opts) {
|
|
|
28792
30063
|
}
|
|
28793
30064
|
const isForegroundTTY = !!(process.stdout.isTTY && !process.env.NO_COLOR && config.runMode !== "daemon");
|
|
28794
30065
|
if (isForegroundTTY) {
|
|
28795
|
-
const { printStartBanner: printStartBanner2 } = await Promise.resolve().then(() => (
|
|
30066
|
+
const { printStartBanner: printStartBanner2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
|
|
28796
30067
|
await printStartBanner2();
|
|
28797
30068
|
}
|
|
28798
30069
|
let spinner4;
|
|
@@ -33307,10 +34578,10 @@ async function cmdOnboard(instanceRoot) {
|
|
|
33307
34578
|
const pluginRegistry = new PluginRegistry2(REGISTRY_PATH);
|
|
33308
34579
|
await pluginRegistry.load();
|
|
33309
34580
|
if (await cm.exists()) {
|
|
33310
|
-
const { runReconfigure: runReconfigure2 } = await Promise.resolve().then(() => (
|
|
34581
|
+
const { runReconfigure: runReconfigure2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
|
|
33311
34582
|
await runReconfigure2(cm, settingsManager);
|
|
33312
34583
|
} else {
|
|
33313
|
-
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (
|
|
34584
|
+
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
|
|
33314
34585
|
await runSetup2(cm, { skipRunMode: true, settingsManager, pluginRegistry, instanceRoot: OPENACP_DIR });
|
|
33315
34586
|
}
|
|
33316
34587
|
}
|
|
@@ -33370,7 +34641,7 @@ async function cmdDefault(command2, instanceRoot) {
|
|
|
33370
34641
|
const settingsManager = new SettingsManager2(pluginsDataDir);
|
|
33371
34642
|
const pluginRegistry = new PluginRegistry2(registryPath);
|
|
33372
34643
|
await pluginRegistry.load();
|
|
33373
|
-
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (
|
|
34644
|
+
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
|
|
33374
34645
|
const shouldStart = await runSetup2(cm, { settingsManager, pluginRegistry, instanceRoot: root });
|
|
33375
34646
|
if (!shouldStart) process.exit(0);
|
|
33376
34647
|
}
|