@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/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: '<arg>',
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 <arg>' }
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 NotificationManager;
5224
+ var NotificationService;
4489
5225
  var init_notification = __esm({
4490
5226
  "src/plugins/notifications/notification.ts"() {
4491
5227
  "use strict";
4492
- NotificationManager = class {
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
- const { settings, terminal } = ctx;
4544
- await settings.setAll({ enabled: true });
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 manager = new NotificationManager(core.adapters);
4569
- ctx.registerService("notifications", manager);
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 = nanoid(12);
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 = nanoid(12);
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 = nanoid(12);
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_${randomBytes(12).toString("hex")}`;
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 = randomBytes(16).toString("hex");
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 ?? "api";
8712
- const turnId = body.turnId ?? nanoid2(8);
8713
- deps.core.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
8714
- sessionId,
8715
- turnId,
8716
- text: body.prompt,
8717
- sourceAdapterId,
8718
- attachments,
8719
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8720
- queueDepth: session.queueDepth
8721
- });
8722
- await session.enqueuePrompt(body.prompt, attachments, {
8723
- sourceAdapterId,
8724
- responseAdapterId: body.responseAdapterId
8725
- }, turnId);
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
- ok: true,
8728
- sessionId,
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 randomBytes2 } from "crypto";
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.${randomBytes2(4).toString("hex")}`;
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: "api",
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, { tokenStore, getJwtSecret: () => jwtSecret }));
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 randomBytes4 } from "crypto";
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_${randomBytes4(8).toString("hex")}`;
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
- /** Remove a connection from both indexes. Called automatically on client disconnect. */
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
- await session.enqueuePrompt(body.prompt, attachments, { sourceAdapterId: "sse" });
11225
- return { ok: true, sessionId, queueDepth: session.queueDepth };
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
- this.registerCommandsWithRetry();
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
- const text5 = response.type === "text" ? response.text : `\u274C ${response.message}`;
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: "Markdown" });
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
- * Register Telegram commands in the background with retries.
17657
- * Non-critical bot works fine without autocomplete commands.
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
- registerCommandsWithRetry() {
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(STATIC_COMMANDS, {
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
- this.core.handleMessage({
17957
- channelId: "telegram",
17958
- threadId: String(threadId),
17959
- userId: String(ctx.from.id),
17960
- text: forwardText
17961
- }).catch((err) => log29.error({ err }, "handleMessage error"));
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 createTurnContext(sourceAdapterId, responseAdapterId, turnId) {
22345
+ function extractSender(meta) {
22346
+ const identity = meta?.identity;
22347
+ if (!identity || !identity.userId || !identity.identityId) return null;
21313
22348
  return {
21314
- turnId: turnId ?? nanoid4(8),
21315
- sourceAdapterId,
21316
- responseAdapterId
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) return turnId;
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, { sessionId: this.id, promptText: processed.text, promptNumber: this.promptCount }, async (p2) => p2).catch(() => {
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
- if (ctx.sourceAdapterId !== "sse") {
22307
- this.deps.eventBus?.emit(BusEvent.MESSAGE_PROCESSING, {
22308
- sessionId: this.session.id,
22309
- turnId: ctx.turnId,
22310
- sourceAdapterId: ctx.sourceAdapterId,
22311
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
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 sourceAdapterId = message.routing?.sourceAdapterId ?? message.channelId;
26011
- const routing = sourceAdapterId !== message.routing?.sourceAdapterId ? { ...message.routing, sourceAdapterId } : message.routing;
26012
- if (sourceAdapterId && sourceAdapterId !== "sse" && sourceAdapterId !== "api") {
26013
- const turnId = nanoid6(8);
26014
- this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
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
- text: text5,
26018
- sourceAdapterId,
26019
- attachments: message.attachments,
26020
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
26021
- queueDepth: session.queueDepth
27238
+ reason
26022
27239
  });
26023
- await session.enqueuePrompt(text5, message.attachments, routing, turnId);
26024
- } else {
26025
- await session.enqueuePrompt(text5, message.attachments, routing);
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 <agent> <workspace>\nOr use the Assistant topic for guided setup." };
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: "<session-number>",
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: "<agent-name>",
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: "<agent-name>",
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: "<channel>",
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 init_types = __esm({
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
- init_types();
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
- init_types();
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 setup_exports = {};
28520
- __export(setup_exports, {
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 init_setup = __esm({
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(() => (init_setup(), setup_exports));
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(() => (init_setup(), setup_exports));
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(() => (init_setup(), setup_exports));
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(() => (init_setup(), setup_exports));
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(() => (init_setup(), setup_exports));
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
  }