@openacp/cli 2026.410.3 → 2026.413.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",
@@ -1888,7 +1890,22 @@ var init_events = __esm({
1888
1890
  PLUGIN_UNLOADED: "plugin:unloaded",
1889
1891
  // --- Usage ---
1890
1892
  /** Fired when a token usage record is captured (consumed by usage plugin). */
1891
- USAGE_RECORDED: "usage:recorded"
1893
+ USAGE_RECORDED: "usage:recorded",
1894
+ // --- Identity lifecycle ---
1895
+ /** Fired when a new user+identity record is created. */
1896
+ IDENTITY_CREATED: "identity:created",
1897
+ /** Fired when user profile fields change. */
1898
+ IDENTITY_UPDATED: "identity:updated",
1899
+ /** Fired when two identities are linked (same person). */
1900
+ IDENTITY_LINKED: "identity:linked",
1901
+ /** Fired when an identity is unlinked into a new user. */
1902
+ IDENTITY_UNLINKED: "identity:unlinked",
1903
+ /** Fired when two user records are merged during a link operation. */
1904
+ IDENTITY_USER_MERGED: "identity:userMerged",
1905
+ /** Fired when a user's role changes. */
1906
+ IDENTITY_ROLE_CHANGED: "identity:roleChanged",
1907
+ /** Fired when a user is seen (throttled). */
1908
+ IDENTITY_SEEN: "identity:seen"
1892
1909
  };
1893
1910
  SessionEv = {
1894
1911
  /** Agent produced an event (text, tool_call, etc.) during a turn. */
@@ -2035,6 +2052,704 @@ var init_security = __esm({
2035
2052
  }
2036
2053
  });
2037
2054
 
2055
+ // src/plugins/identity/types.ts
2056
+ function formatIdentityId(source, platformId) {
2057
+ return `${source}:${platformId}`;
2058
+ }
2059
+ var init_types = __esm({
2060
+ "src/plugins/identity/types.ts"() {
2061
+ "use strict";
2062
+ }
2063
+ });
2064
+
2065
+ // src/plugins/identity/identity-service.ts
2066
+ import { nanoid } from "nanoid";
2067
+ var IdentityServiceImpl;
2068
+ var init_identity_service = __esm({
2069
+ "src/plugins/identity/identity-service.ts"() {
2070
+ "use strict";
2071
+ init_types();
2072
+ IdentityServiceImpl = class {
2073
+ /**
2074
+ * @param store - Persistence layer for user/identity records and indexes.
2075
+ * @param emitEvent - Callback to publish events on the EventBus.
2076
+ * @param getSessionsForUser - Optional function to look up sessions for a userId.
2077
+ */
2078
+ constructor(store, emitEvent, getSessionsForUser) {
2079
+ this.store = store;
2080
+ this.emitEvent = emitEvent;
2081
+ this.getSessionsForUser = getSessionsForUser;
2082
+ }
2083
+ registeredSources = /* @__PURE__ */ new Set();
2084
+ // ─── Lookups ───
2085
+ async getUser(userId) {
2086
+ return this.store.getUser(userId);
2087
+ }
2088
+ async getUserByUsername(username) {
2089
+ const userId = await this.store.getUserIdByUsername(username);
2090
+ if (!userId) return void 0;
2091
+ return this.store.getUser(userId);
2092
+ }
2093
+ async getIdentity(identityId) {
2094
+ return this.store.getIdentity(identityId);
2095
+ }
2096
+ async getUserByIdentity(identityId) {
2097
+ const identity = await this.store.getIdentity(identityId);
2098
+ if (!identity) return void 0;
2099
+ return this.store.getUser(identity.userId);
2100
+ }
2101
+ async getIdentitiesFor(userId) {
2102
+ return this.store.getIdentitiesForUser(userId);
2103
+ }
2104
+ async listUsers(filter) {
2105
+ return this.store.listUsers(filter);
2106
+ }
2107
+ /**
2108
+ * Case-insensitive substring search across displayName, username, and platform
2109
+ * usernames. Designed for admin tooling, not high-frequency user-facing paths.
2110
+ */
2111
+ async searchUsers(query) {
2112
+ const all = await this.store.listUsers();
2113
+ const q = query.toLowerCase();
2114
+ const matched = [];
2115
+ for (const user of all) {
2116
+ const nameMatch = user.displayName.toLowerCase().includes(q) || user.username && user.username.toLowerCase().includes(q);
2117
+ if (nameMatch) {
2118
+ matched.push(user);
2119
+ continue;
2120
+ }
2121
+ const identities = await this.store.getIdentitiesForUser(user.userId);
2122
+ const platformMatch = identities.some(
2123
+ (id) => id.platformUsername && id.platformUsername.toLowerCase().includes(q)
2124
+ );
2125
+ if (platformMatch) matched.push(user);
2126
+ }
2127
+ return matched;
2128
+ }
2129
+ async getSessionsFor(userId) {
2130
+ if (!this.getSessionsForUser) return [];
2131
+ return this.getSessionsForUser(userId);
2132
+ }
2133
+ // ─── Mutations ───
2134
+ /**
2135
+ * Creates a user + identity pair atomically.
2136
+ * The first ever user in the system is auto-promoted to admin — this ensures
2137
+ * there is always at least one admin when bootstrapping a fresh instance.
2138
+ */
2139
+ async createUserWithIdentity(data) {
2140
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2141
+ const userId = `u_${nanoid(12)}`;
2142
+ const identityId = formatIdentityId(data.source, data.platformId);
2143
+ const count = await this.store.getUserCount();
2144
+ const role = count === 0 ? "admin" : data.role ?? "member";
2145
+ const user = {
2146
+ userId,
2147
+ displayName: data.displayName,
2148
+ username: data.username,
2149
+ role,
2150
+ identities: [identityId],
2151
+ pluginData: {},
2152
+ createdAt: now,
2153
+ updatedAt: now,
2154
+ lastSeenAt: now
2155
+ };
2156
+ const identity = {
2157
+ identityId,
2158
+ userId,
2159
+ source: data.source,
2160
+ platformId: data.platformId,
2161
+ platformUsername: data.platformUsername,
2162
+ platformDisplayName: data.platformDisplayName,
2163
+ createdAt: now,
2164
+ updatedAt: now
2165
+ };
2166
+ await this.store.putUser(user);
2167
+ await this.store.putIdentity(identity);
2168
+ await this.store.setSourceIndex(data.source, data.platformId, identityId);
2169
+ if (data.username) {
2170
+ await this.store.setUsernameIndex(data.username, userId);
2171
+ }
2172
+ this.emitEvent("identity:created", { userId, identityId, source: data.source, displayName: data.displayName });
2173
+ return { user, identity };
2174
+ }
2175
+ async updateUser(userId, changes) {
2176
+ const user = await this.store.getUser(userId);
2177
+ if (!user) throw new Error(`User not found: ${userId}`);
2178
+ if (changes.username !== void 0 && changes.username !== user.username) {
2179
+ if (changes.username) {
2180
+ const existingId = await this.store.getUserIdByUsername(changes.username);
2181
+ if (existingId && existingId !== userId) {
2182
+ throw new Error(`Username already taken: ${changes.username}`);
2183
+ }
2184
+ await this.store.setUsernameIndex(changes.username, userId);
2185
+ }
2186
+ if (user.username) {
2187
+ await this.store.deleteUsernameIndex(user.username);
2188
+ }
2189
+ }
2190
+ const updated = {
2191
+ ...user,
2192
+ ...changes,
2193
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2194
+ };
2195
+ await this.store.putUser(updated);
2196
+ this.emitEvent("identity:updated", { userId, changes: Object.keys(changes) });
2197
+ return updated;
2198
+ }
2199
+ async setRole(userId, role) {
2200
+ const user = await this.store.getUser(userId);
2201
+ if (!user) throw new Error(`User not found: ${userId}`);
2202
+ const oldRole = user.role;
2203
+ await this.store.putUser({ ...user, role, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2204
+ this.emitEvent("identity:roleChanged", { userId, oldRole, newRole: role });
2205
+ }
2206
+ async createIdentity(userId, identity) {
2207
+ const user = await this.store.getUser(userId);
2208
+ if (!user) throw new Error(`User not found: ${userId}`);
2209
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2210
+ const identityId = formatIdentityId(identity.source, identity.platformId);
2211
+ const record = {
2212
+ identityId,
2213
+ userId,
2214
+ source: identity.source,
2215
+ platformId: identity.platformId,
2216
+ platformUsername: identity.platformUsername,
2217
+ platformDisplayName: identity.platformDisplayName,
2218
+ createdAt: now,
2219
+ updatedAt: now
2220
+ };
2221
+ await this.store.putIdentity(record);
2222
+ await this.store.setSourceIndex(identity.source, identity.platformId, identityId);
2223
+ const updatedUser = {
2224
+ ...user,
2225
+ identities: [...user.identities, identityId],
2226
+ updatedAt: now
2227
+ };
2228
+ await this.store.putUser(updatedUser);
2229
+ return record;
2230
+ }
2231
+ /**
2232
+ * Links two identities into a single user.
2233
+ *
2234
+ * When identities belong to different users, the younger (more recently created)
2235
+ * user is merged into the older one. We keep the older user as the canonical
2236
+ * record because it likely has more history, sessions, and plugin data.
2237
+ *
2238
+ * Merge strategy for pluginData: per-namespace, the winning user's data takes
2239
+ * precedence. The younger user's data only fills in missing namespaces.
2240
+ */
2241
+ async link(identityIdA, identityIdB) {
2242
+ const identityA = await this.store.getIdentity(identityIdA);
2243
+ const identityB = await this.store.getIdentity(identityIdB);
2244
+ if (!identityA) throw new Error(`Identity not found: ${identityIdA}`);
2245
+ if (!identityB) throw new Error(`Identity not found: ${identityIdB}`);
2246
+ if (identityA.userId === identityB.userId) return;
2247
+ const userA = await this.store.getUser(identityA.userId);
2248
+ const userB = await this.store.getUser(identityB.userId);
2249
+ if (!userA) throw new Error(`User not found: ${identityA.userId}`);
2250
+ if (!userB) throw new Error(`User not found: ${identityB.userId}`);
2251
+ const [keep, merge] = userA.createdAt <= userB.createdAt ? [userA, userB] : [userB, userA];
2252
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2253
+ for (const identityId of merge.identities) {
2254
+ const identity = await this.store.getIdentity(identityId);
2255
+ if (!identity) continue;
2256
+ const updated = { ...identity, userId: keep.userId, updatedAt: now };
2257
+ await this.store.putIdentity(updated);
2258
+ }
2259
+ const mergedPluginData = { ...merge.pluginData };
2260
+ for (const [ns, nsData] of Object.entries(keep.pluginData)) {
2261
+ mergedPluginData[ns] = nsData;
2262
+ }
2263
+ if (merge.username) {
2264
+ await this.store.deleteUsernameIndex(merge.username);
2265
+ }
2266
+ const updatedKeep = {
2267
+ ...keep,
2268
+ identities: [.../* @__PURE__ */ new Set([...keep.identities, ...merge.identities])],
2269
+ pluginData: mergedPluginData,
2270
+ updatedAt: now
2271
+ };
2272
+ await this.store.putUser(updatedKeep);
2273
+ await this.store.deleteUser(merge.userId);
2274
+ const linkedIdentityId = identityA.userId === merge.userId ? identityIdA : identityIdB;
2275
+ this.emitEvent("identity:linked", { userId: keep.userId, identityId: linkedIdentityId, linkedFrom: merge.userId });
2276
+ this.emitEvent("identity:userMerged", {
2277
+ keptUserId: keep.userId,
2278
+ mergedUserId: merge.userId,
2279
+ movedIdentities: merge.identities
2280
+ });
2281
+ }
2282
+ /**
2283
+ * Separates an identity from its user into a new standalone account.
2284
+ * Throws if it's the user's last identity — unlinking would produce a
2285
+ * ghost user with no way to authenticate.
2286
+ */
2287
+ async unlink(identityId) {
2288
+ const identity = await this.store.getIdentity(identityId);
2289
+ if (!identity) throw new Error(`Identity not found: ${identityId}`);
2290
+ const user = await this.store.getUser(identity.userId);
2291
+ if (!user) throw new Error(`User not found: ${identity.userId}`);
2292
+ if (user.identities.length <= 1) {
2293
+ throw new Error(`Cannot unlink the last identity from user ${identity.userId}`);
2294
+ }
2295
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2296
+ const newUserId = `u_${nanoid(12)}`;
2297
+ const newUser = {
2298
+ userId: newUserId,
2299
+ displayName: identity.platformDisplayName ?? identity.platformUsername ?? "User",
2300
+ role: "member",
2301
+ identities: [identityId],
2302
+ pluginData: {},
2303
+ createdAt: now,
2304
+ updatedAt: now,
2305
+ lastSeenAt: now
2306
+ };
2307
+ await this.store.putUser(newUser);
2308
+ await this.store.putIdentity({ ...identity, userId: newUserId, updatedAt: now });
2309
+ const updatedUser = {
2310
+ ...user,
2311
+ identities: user.identities.filter((id) => id !== identityId),
2312
+ updatedAt: now
2313
+ };
2314
+ await this.store.putUser(updatedUser);
2315
+ this.emitEvent("identity:unlinked", {
2316
+ userId: user.userId,
2317
+ identityId,
2318
+ newUserId
2319
+ });
2320
+ }
2321
+ // ─── Plugin data ───
2322
+ async setPluginData(userId, pluginName, key, value) {
2323
+ const user = await this.store.getUser(userId);
2324
+ if (!user) throw new Error(`User not found: ${userId}`);
2325
+ const pluginData = { ...user.pluginData };
2326
+ pluginData[pluginName] = { ...pluginData[pluginName] ?? {}, [key]: value };
2327
+ await this.store.putUser({ ...user, pluginData, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2328
+ }
2329
+ async getPluginData(userId, pluginName, key) {
2330
+ const user = await this.store.getUser(userId);
2331
+ if (!user) return void 0;
2332
+ return user.pluginData[pluginName]?.[key];
2333
+ }
2334
+ // ─── Source registry ───
2335
+ registerSource(source) {
2336
+ this.registeredSources.add(source);
2337
+ }
2338
+ /**
2339
+ * Resolves a username mention to platform-specific info for the given source.
2340
+ * Finds the user by username, then scans their identities for the matching source.
2341
+ * Returns found=false when no user or no identity for that source exists.
2342
+ */
2343
+ async resolveCanonicalMention(username, source) {
2344
+ const user = await this.getUserByUsername(username);
2345
+ if (!user) return { found: false };
2346
+ const identities = await this.store.getIdentitiesForUser(user.userId);
2347
+ const sourceIdentity = identities.find((id) => id.source === source);
2348
+ if (!sourceIdentity) return { found: false };
2349
+ return {
2350
+ found: true,
2351
+ platformId: sourceIdentity.platformId,
2352
+ platformUsername: sourceIdentity.platformUsername
2353
+ };
2354
+ }
2355
+ async getUserCount() {
2356
+ return this.store.getUserCount();
2357
+ }
2358
+ };
2359
+ }
2360
+ });
2361
+
2362
+ // src/plugins/identity/store/kv-identity-store.ts
2363
+ var KvIdentityStore;
2364
+ var init_kv_identity_store = __esm({
2365
+ "src/plugins/identity/store/kv-identity-store.ts"() {
2366
+ "use strict";
2367
+ KvIdentityStore = class {
2368
+ constructor(storage) {
2369
+ this.storage = storage;
2370
+ }
2371
+ // === User CRUD ===
2372
+ async getUser(userId) {
2373
+ return this.storage.get(`users/${userId}`);
2374
+ }
2375
+ async putUser(record) {
2376
+ await this.storage.set(`users/${record.userId}`, record);
2377
+ }
2378
+ async deleteUser(userId) {
2379
+ await this.storage.delete(`users/${userId}`);
2380
+ }
2381
+ /**
2382
+ * Lists all users, optionally filtered by role or source.
2383
+ * Filtering by source requires scanning all identity records for the user,
2384
+ * which is acceptable given the expected user count (hundreds, not millions).
2385
+ */
2386
+ async listUsers(filter) {
2387
+ const keys = await this.storage.keys("users/");
2388
+ const users = [];
2389
+ for (const key of keys) {
2390
+ const user = await this.storage.get(key);
2391
+ if (!user) continue;
2392
+ if (filter?.role && user.role !== filter.role) continue;
2393
+ if (filter?.source) {
2394
+ const hasSource = user.identities.some((id) => id.startsWith(`${filter.source}:`));
2395
+ if (!hasSource) continue;
2396
+ }
2397
+ users.push(user);
2398
+ }
2399
+ return users;
2400
+ }
2401
+ // === Identity CRUD ===
2402
+ async getIdentity(identityId) {
2403
+ return this.storage.get(`identities/${identityId}`);
2404
+ }
2405
+ async putIdentity(record) {
2406
+ await this.storage.set(`identities/${record.identityId}`, record);
2407
+ }
2408
+ async deleteIdentity(identityId) {
2409
+ await this.storage.delete(`identities/${identityId}`);
2410
+ }
2411
+ /**
2412
+ * Fetches all identity records for a user by scanning their identities array.
2413
+ * Avoids a full table scan by leveraging the user record as a secondary index.
2414
+ */
2415
+ async getIdentitiesForUser(userId) {
2416
+ const user = await this.getUser(userId);
2417
+ if (!user) return [];
2418
+ const records = [];
2419
+ for (const identityId of user.identities) {
2420
+ const record = await this.getIdentity(identityId);
2421
+ if (record) records.push(record);
2422
+ }
2423
+ return records;
2424
+ }
2425
+ // === Secondary indexes ===
2426
+ async getUserIdByUsername(username) {
2427
+ return this.storage.get(`idx/usernames/${username.toLowerCase()}`);
2428
+ }
2429
+ async getIdentityIdBySource(source, platformId) {
2430
+ return this.storage.get(`idx/sources/${source}/${platformId}`);
2431
+ }
2432
+ // === Index mutations ===
2433
+ async setUsernameIndex(username, userId) {
2434
+ await this.storage.set(`idx/usernames/${username.toLowerCase()}`, userId);
2435
+ }
2436
+ async deleteUsernameIndex(username) {
2437
+ await this.storage.delete(`idx/usernames/${username.toLowerCase()}`);
2438
+ }
2439
+ async setSourceIndex(source, platformId, identityId) {
2440
+ await this.storage.set(`idx/sources/${source}/${platformId}`, identityId);
2441
+ }
2442
+ async deleteSourceIndex(source, platformId) {
2443
+ await this.storage.delete(`idx/sources/${source}/${platformId}`);
2444
+ }
2445
+ async getUserCount() {
2446
+ const keys = await this.storage.keys("users/");
2447
+ return keys.length;
2448
+ }
2449
+ };
2450
+ }
2451
+ });
2452
+
2453
+ // src/plugins/identity/middleware/auto-register.ts
2454
+ function createAutoRegisterHandler(service, store) {
2455
+ const lastSeenThrottle = /* @__PURE__ */ new Map();
2456
+ return async (payload, next) => {
2457
+ const { channelId, userId, meta } = payload;
2458
+ const identityId = formatIdentityId(channelId, userId);
2459
+ const channelUser = meta?.channelUser;
2460
+ let identity = await store.getIdentity(identityId);
2461
+ let user;
2462
+ if (!identity) {
2463
+ const result = await service.createUserWithIdentity({
2464
+ displayName: channelUser?.displayName ?? userId,
2465
+ username: channelUser?.username,
2466
+ source: channelId,
2467
+ platformId: userId,
2468
+ platformUsername: channelUser?.username,
2469
+ platformDisplayName: channelUser?.displayName
2470
+ });
2471
+ user = result.user;
2472
+ identity = result.identity;
2473
+ } else {
2474
+ user = await service.getUser(identity.userId);
2475
+ if (!user) return next();
2476
+ const now = Date.now();
2477
+ const lastSeen = lastSeenThrottle.get(user.userId);
2478
+ if (!lastSeen || now - lastSeen > LAST_SEEN_THROTTLE_MS) {
2479
+ lastSeenThrottle.set(user.userId, now);
2480
+ await store.putUser({ ...user, lastSeenAt: new Date(now).toISOString() });
2481
+ }
2482
+ if (channelUser) {
2483
+ const needsUpdate = channelUser.displayName !== void 0 && channelUser.displayName !== identity.platformDisplayName || channelUser.username !== void 0 && channelUser.username !== identity.platformUsername;
2484
+ if (needsUpdate) {
2485
+ await store.putIdentity({
2486
+ ...identity,
2487
+ platformDisplayName: channelUser.displayName ?? identity.platformDisplayName,
2488
+ platformUsername: channelUser.username ?? identity.platformUsername,
2489
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2490
+ });
2491
+ }
2492
+ }
2493
+ }
2494
+ if (meta) {
2495
+ meta.identity = {
2496
+ userId: user.userId,
2497
+ identityId: identity.identityId,
2498
+ displayName: user.displayName,
2499
+ username: user.username,
2500
+ role: user.role
2501
+ };
2502
+ }
2503
+ return next();
2504
+ };
2505
+ }
2506
+ var LAST_SEEN_THROTTLE_MS;
2507
+ var init_auto_register = __esm({
2508
+ "src/plugins/identity/middleware/auto-register.ts"() {
2509
+ "use strict";
2510
+ init_types();
2511
+ LAST_SEEN_THROTTLE_MS = 5 * 60 * 1e3;
2512
+ }
2513
+ });
2514
+
2515
+ // src/plugins/identity/routes/users.ts
2516
+ var users_exports = {};
2517
+ __export(users_exports, {
2518
+ registerIdentityRoutes: () => registerIdentityRoutes
2519
+ });
2520
+ function registerIdentityRoutes(app, deps) {
2521
+ const { service, tokenStore } = deps;
2522
+ function resolveUserId(request) {
2523
+ return tokenStore?.getUserId?.(request.auth?.tokenId);
2524
+ }
2525
+ app.get("/users", async (request) => {
2526
+ const { source, role, q } = request.query;
2527
+ if (q) return service.searchUsers(q);
2528
+ return service.listUsers({ source, role });
2529
+ });
2530
+ app.get("/users/me", async (request, reply) => {
2531
+ const userId = resolveUserId(request);
2532
+ if (!userId) return reply.status(403).send({ error: "Identity not set up" });
2533
+ const user = await service.getUser(userId);
2534
+ if (!user) return reply.status(404).send({ error: "User not found" });
2535
+ return user;
2536
+ });
2537
+ app.put("/users/me", async (request, reply) => {
2538
+ const userId = resolveUserId(request);
2539
+ if (!userId) {
2540
+ return reply.status(403).send({ error: "Identity not set up. Call POST /identity/setup first." });
2541
+ }
2542
+ const body = request.body;
2543
+ return service.updateUser(userId, {
2544
+ displayName: body.displayName,
2545
+ username: body.username,
2546
+ avatarUrl: body.avatarUrl,
2547
+ timezone: body.timezone,
2548
+ locale: body.locale
2549
+ });
2550
+ });
2551
+ app.get("/users/:userId", async (request, reply) => {
2552
+ const { userId } = request.params;
2553
+ const user = await service.getUser(userId);
2554
+ if (!user) return reply.status(404).send({ error: "User not found" });
2555
+ return user;
2556
+ });
2557
+ app.put("/users/:userId/role", async (request, reply) => {
2558
+ const callerUserId = resolveUserId(request);
2559
+ if (!callerUserId) return reply.status(403).send({ error: "Identity not set up" });
2560
+ const caller = await service.getUser(callerUserId);
2561
+ if (!caller || caller.role !== "admin") return reply.status(403).send({ error: "Admin only" });
2562
+ const { userId } = request.params;
2563
+ const { role } = request.body;
2564
+ await service.setRole(userId, role);
2565
+ return { ok: true };
2566
+ });
2567
+ app.get("/users/:userId/identities", async (request) => {
2568
+ const { userId } = request.params;
2569
+ return service.getIdentitiesFor(userId);
2570
+ });
2571
+ app.get("/resolve/:identityId", async (request, reply) => {
2572
+ const { identityId } = request.params;
2573
+ const user = await service.getUserByIdentity(identityId);
2574
+ if (!user) return reply.status(404).send({ error: "Identity not found" });
2575
+ const identity = await service.getIdentity(identityId);
2576
+ return { user, identity };
2577
+ });
2578
+ app.post("/link", async (request, reply) => {
2579
+ const callerUserId = resolveUserId(request);
2580
+ if (!callerUserId) return reply.status(403).send({ error: "Identity not set up" });
2581
+ const caller = await service.getUser(callerUserId);
2582
+ if (!caller || caller.role !== "admin") return reply.status(403).send({ error: "Admin only" });
2583
+ const { identityIdA, identityIdB } = request.body;
2584
+ await service.link(identityIdA, identityIdB);
2585
+ return { ok: true };
2586
+ });
2587
+ app.post("/unlink", async (request, reply) => {
2588
+ const callerUserId = resolveUserId(request);
2589
+ if (!callerUserId) return reply.status(403).send({ error: "Identity not set up" });
2590
+ const caller = await service.getUser(callerUserId);
2591
+ if (!caller || caller.role !== "admin") return reply.status(403).send({ error: "Admin only" });
2592
+ const { identityId } = request.body;
2593
+ await service.unlink(identityId);
2594
+ return { ok: true };
2595
+ });
2596
+ app.get("/search", async (request) => {
2597
+ const { q } = request.query;
2598
+ if (!q) return [];
2599
+ return service.searchUsers(q);
2600
+ });
2601
+ }
2602
+ var init_users = __esm({
2603
+ "src/plugins/identity/routes/users.ts"() {
2604
+ "use strict";
2605
+ }
2606
+ });
2607
+
2608
+ // src/plugins/identity/routes/setup.ts
2609
+ var setup_exports = {};
2610
+ __export(setup_exports, {
2611
+ registerSetupRoutes: () => registerSetupRoutes
2612
+ });
2613
+ import { randomBytes } from "crypto";
2614
+ function registerSetupRoutes(app, deps) {
2615
+ const { service, tokenStore } = deps;
2616
+ app.post("/setup", async (request, reply) => {
2617
+ const auth = request.auth;
2618
+ if (!auth?.tokenId) return reply.status(401).send({ error: "JWT required" });
2619
+ const existingUserId = tokenStore?.getUserId?.(auth.tokenId);
2620
+ if (existingUserId) {
2621
+ const user2 = await service.getUser(existingUserId);
2622
+ if (user2) return user2;
2623
+ }
2624
+ const body = request.body;
2625
+ if (body?.linkCode) {
2626
+ const entry = linkCodes.get(body.linkCode);
2627
+ if (!entry || entry.expiresAt < Date.now()) {
2628
+ return reply.status(401).send({ error: "Invalid or expired link code" });
2629
+ }
2630
+ linkCodes.delete(body.linkCode);
2631
+ await service.createIdentity(entry.userId, {
2632
+ source: "api",
2633
+ platformId: auth.tokenId
2634
+ });
2635
+ tokenStore?.setUserId?.(auth.tokenId, entry.userId);
2636
+ return service.getUser(entry.userId);
2637
+ }
2638
+ if (!body?.displayName) return reply.status(400).send({ error: "displayName is required" });
2639
+ const { user } = await service.createUserWithIdentity({
2640
+ displayName: body.displayName,
2641
+ username: body.username,
2642
+ source: "api",
2643
+ platformId: auth.tokenId
2644
+ });
2645
+ tokenStore?.setUserId?.(auth.tokenId, user.userId);
2646
+ return user;
2647
+ });
2648
+ app.post("/link-code", async (request, reply) => {
2649
+ const auth = request.auth;
2650
+ if (!auth?.tokenId) return reply.status(401).send({ error: "JWT required" });
2651
+ const userId = tokenStore?.getUserId?.(auth.tokenId);
2652
+ if (!userId) return reply.status(403).send({ error: "Identity not set up" });
2653
+ const code = randomBytes(16).toString("hex");
2654
+ const expiresAt = Date.now() + 5 * 60 * 1e3;
2655
+ for (const [k, v] of linkCodes) {
2656
+ if (v.expiresAt < Date.now()) linkCodes.delete(k);
2657
+ }
2658
+ linkCodes.set(code, { userId, expiresAt });
2659
+ return { linkCode: code, expiresAt: new Date(expiresAt).toISOString() };
2660
+ });
2661
+ }
2662
+ var linkCodes;
2663
+ var init_setup = __esm({
2664
+ "src/plugins/identity/routes/setup.ts"() {
2665
+ "use strict";
2666
+ linkCodes = /* @__PURE__ */ new Map();
2667
+ }
2668
+ });
2669
+
2670
+ // src/plugins/identity/index.ts
2671
+ function createIdentityPlugin() {
2672
+ return {
2673
+ name: "@openacp/identity",
2674
+ version: "1.0.0",
2675
+ description: "User identity, cross-platform linking, and role-based access",
2676
+ essential: false,
2677
+ permissions: [
2678
+ "storage:read",
2679
+ "storage:write",
2680
+ "middleware:register",
2681
+ "services:register",
2682
+ "services:use",
2683
+ "events:emit",
2684
+ "events:read",
2685
+ "commands:register",
2686
+ "kernel:access"
2687
+ ],
2688
+ optionalPluginDependencies: {
2689
+ "@openacp/api-server": ">=1.0.0"
2690
+ },
2691
+ async setup(ctx) {
2692
+ const store = new KvIdentityStore(ctx.storage);
2693
+ const service = new IdentityServiceImpl(store, (event, data) => {
2694
+ ctx.emit(event, data);
2695
+ });
2696
+ ctx.registerService("identity", service);
2697
+ ctx.registerMiddleware(Hook.MESSAGE_INCOMING, {
2698
+ priority: 110,
2699
+ handler: createAutoRegisterHandler(service, store)
2700
+ });
2701
+ ctx.registerCommand({
2702
+ name: "whoami",
2703
+ description: "Set your display name and username",
2704
+ usage: "[name]",
2705
+ category: "plugin",
2706
+ async handler(args2) {
2707
+ const name = args2.raw.trim();
2708
+ if (!name) {
2709
+ return { type: "text", text: "Usage: /whoami <name>" };
2710
+ }
2711
+ const identityId = formatIdentityId(args2.channelId, args2.userId);
2712
+ const user = await service.getUserByIdentity(identityId);
2713
+ if (!user) {
2714
+ return { type: "error", message: "User not found \u2014 send a message first." };
2715
+ }
2716
+ try {
2717
+ const username = name.toLowerCase().replace(/[^a-z0-9_]/g, "");
2718
+ await service.updateUser(user.userId, { displayName: name, username });
2719
+ return { type: "text", text: `Display name set to "${name}", username: @${username}` };
2720
+ } catch (err) {
2721
+ const message = err instanceof Error ? err.message : String(err);
2722
+ return { type: "error", message };
2723
+ }
2724
+ }
2725
+ });
2726
+ const apiServer = ctx.getService("api-server");
2727
+ if (apiServer) {
2728
+ const tokenStore = ctx.getService("token-store");
2729
+ const { registerIdentityRoutes: registerIdentityRoutes2 } = await Promise.resolve().then(() => (init_users(), users_exports));
2730
+ const { registerSetupRoutes: registerSetupRoutes2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
2731
+ apiServer.registerPlugin("/api/v1/identity", async (app) => {
2732
+ registerIdentityRoutes2(app, { service, tokenStore: tokenStore ?? void 0 });
2733
+ registerSetupRoutes2(app, { service, tokenStore: tokenStore ?? void 0 });
2734
+ }, { auth: true });
2735
+ }
2736
+ ctx.log.info(`Identity service ready (${await service.getUserCount()} users)`);
2737
+ }
2738
+ };
2739
+ }
2740
+ var identity_default;
2741
+ var init_identity = __esm({
2742
+ "src/plugins/identity/index.ts"() {
2743
+ "use strict";
2744
+ init_identity_service();
2745
+ init_kv_identity_store();
2746
+ init_auto_register();
2747
+ init_types();
2748
+ init_events();
2749
+ identity_default = createIdentityPlugin();
2750
+ }
2751
+ });
2752
+
2038
2753
  // src/core/utils/read-text-file.ts
2039
2754
  var read_text_file_exports = {};
2040
2755
  __export(read_text_file_exports, {
@@ -3631,7 +4346,7 @@ var init_history_recorder = __esm({
3631
4346
  }
3632
4347
  states = /* @__PURE__ */ new Map();
3633
4348
  debounceTimers = /* @__PURE__ */ new Map();
3634
- onBeforePrompt(sessionId, text5, attachments, sourceAdapterId) {
4349
+ onBeforePrompt(sessionId, text5, attachments, sourceAdapterId, meta) {
3635
4350
  let state = this.states.get(sessionId);
3636
4351
  if (!state) {
3637
4352
  state = {
@@ -3652,6 +4367,9 @@ var init_history_recorder = __esm({
3652
4367
  if (sourceAdapterId) {
3653
4368
  userTurn.sourceAdapterId = sourceAdapterId;
3654
4369
  }
4370
+ if (meta && Object.keys(meta).length > 0) {
4371
+ userTurn.meta = meta;
4372
+ }
3655
4373
  state.history.turns.push(userTurn);
3656
4374
  const assistantTurn = {
3657
4375
  index: state.history.turns.length,
@@ -3986,7 +4704,7 @@ var init_context = __esm({
3986
4704
  ctx.registerMiddleware(Hook.AGENT_BEFORE_PROMPT, {
3987
4705
  priority: 200,
3988
4706
  handler: async (payload, next) => {
3989
- recorder.onBeforePrompt(payload.sessionId, payload.text, payload.attachments, payload.sourceAdapterId);
4707
+ recorder.onBeforePrompt(payload.sessionId, payload.text, payload.attachments, payload.sourceAdapterId, payload.meta);
3990
4708
  return next();
3991
4709
  }
3992
4710
  });
@@ -4485,14 +5203,20 @@ var init_speech = __esm({
4485
5203
  });
4486
5204
 
4487
5205
  // src/plugins/notifications/notification.ts
4488
- var NotificationManager;
5206
+ var NotificationService;
4489
5207
  var init_notification = __esm({
4490
5208
  "src/plugins/notifications/notification.ts"() {
4491
5209
  "use strict";
4492
- NotificationManager = class {
5210
+ NotificationService = class {
4493
5211
  constructor(adapters) {
4494
5212
  this.adapters = adapters;
4495
5213
  }
5214
+ identityResolver;
5215
+ /** Inject identity resolver for user-targeted notifications. */
5216
+ setIdentityResolver(resolver) {
5217
+ this.identityResolver = resolver;
5218
+ }
5219
+ // --- Legacy API (backward compat with NotificationManager) ---
4496
5220
  /**
4497
5221
  * Send a notification to a specific channel adapter.
4498
5222
  *
@@ -4521,6 +5245,62 @@ var init_notification = __esm({
4521
5245
  }
4522
5246
  }
4523
5247
  }
5248
+ // --- New user-targeted API ---
5249
+ /**
5250
+ * Send a notification to a user across all their linked platforms.
5251
+ * Fire-and-forget — never throws, swallows all errors.
5252
+ */
5253
+ async notifyUser(target, message, options) {
5254
+ try {
5255
+ await this._resolveAndDeliver(target, message, options);
5256
+ } catch {
5257
+ }
5258
+ }
5259
+ async _resolveAndDeliver(target, message, options) {
5260
+ if ("channelId" in target && "platformId" in target) {
5261
+ const adapter = this.adapters.get(target.channelId);
5262
+ if (!adapter?.sendUserNotification) return;
5263
+ await adapter.sendUserNotification(target.platformId, message, {
5264
+ via: options?.via,
5265
+ topicId: options?.topicId,
5266
+ sessionId: options?.sessionId
5267
+ });
5268
+ return;
5269
+ }
5270
+ if (!this.identityResolver) return;
5271
+ let identities = [];
5272
+ if ("identityId" in target) {
5273
+ const identity = await this.identityResolver.getIdentity(target.identityId);
5274
+ if (!identity) return;
5275
+ const user = await this.identityResolver.getUser(identity.userId);
5276
+ if (!user) return;
5277
+ identities = await this.identityResolver.getIdentitiesFor(user.userId);
5278
+ } else if ("userId" in target) {
5279
+ identities = await this.identityResolver.getIdentitiesFor(target.userId);
5280
+ }
5281
+ if (options?.onlyPlatforms) {
5282
+ identities = identities.filter((i) => options.onlyPlatforms.includes(i.source));
5283
+ }
5284
+ if (options?.excludePlatforms) {
5285
+ identities = identities.filter((i) => !options.excludePlatforms.includes(i.source));
5286
+ }
5287
+ for (const identity of identities) {
5288
+ const adapter = this.adapters.get(identity.source);
5289
+ if (!adapter?.sendUserNotification) continue;
5290
+ try {
5291
+ await adapter.sendUserNotification(identity.platformId, message, {
5292
+ via: options?.via,
5293
+ topicId: options?.topicId,
5294
+ sessionId: options?.sessionId,
5295
+ platformMention: {
5296
+ platformUsername: identity.platformUsername,
5297
+ platformId: identity.platformId
5298
+ }
5299
+ });
5300
+ } catch {
5301
+ }
5302
+ }
5303
+ }
4524
5304
  };
4525
5305
  }
4526
5306
  });
@@ -4538,11 +5318,10 @@ function createNotificationsPlugin() {
4538
5318
  essential: false,
4539
5319
  // Depends on security so the notification service is only active for authorized sessions
4540
5320
  pluginDependencies: { "@openacp/security": "^1.0.0" },
4541
- permissions: ["services:register", "kernel:access"],
5321
+ permissions: ["services:register", "services:use", "kernel:access", "events:read"],
4542
5322
  async install(ctx) {
4543
- const { settings, terminal } = ctx;
4544
- await settings.setAll({ enabled: true });
4545
- terminal.log.success("Notifications defaults saved");
5323
+ await ctx.settings.setAll({ enabled: true });
5324
+ ctx.terminal.log.success("Notifications defaults saved");
4546
5325
  },
4547
5326
  async configure(ctx) {
4548
5327
  const { terminal, settings } = ctx;
@@ -4565,8 +5344,16 @@ function createNotificationsPlugin() {
4565
5344
  },
4566
5345
  async setup(ctx) {
4567
5346
  const core = ctx.core;
4568
- const manager = new NotificationManager(core.adapters);
4569
- ctx.registerService("notifications", manager);
5347
+ const service = new NotificationService(core.adapters);
5348
+ const identity = ctx.getService("identity");
5349
+ if (identity) service.setIdentityResolver(identity);
5350
+ ctx.on("plugin:loaded", (data) => {
5351
+ if (data?.name === "@openacp/identity") {
5352
+ const id = ctx.getService("identity");
5353
+ if (id) service.setIdentityResolver(id);
5354
+ }
5355
+ });
5356
+ ctx.registerService("notifications", service);
4570
5357
  ctx.log.info("Notifications service ready");
4571
5358
  }
4572
5359
  };
@@ -6866,7 +7653,7 @@ var init_viewer_routes = __esm({
6866
7653
  // src/plugins/tunnel/viewer-store.ts
6867
7654
  import * as fs17 from "fs";
6868
7655
  import * as path20 from "path";
6869
- import { nanoid } from "nanoid";
7656
+ import { nanoid as nanoid2 } from "nanoid";
6870
7657
  var log10, MAX_CONTENT_SIZE, EXTENSION_LANGUAGE, ViewerStore;
6871
7658
  var init_viewer_store = __esm({
6872
7659
  "src/plugins/tunnel/viewer-store.ts"() {
@@ -6927,7 +7714,7 @@ var init_viewer_store = __esm({
6927
7714
  log10.debug({ filePath, size: content.length }, "File too large for viewer");
6928
7715
  return null;
6929
7716
  }
6930
- const id = nanoid(12);
7717
+ const id = nanoid2(12);
6931
7718
  const now = Date.now();
6932
7719
  this.entries.set(id, {
6933
7720
  id,
@@ -6953,7 +7740,7 @@ var init_viewer_store = __esm({
6953
7740
  log10.debug({ filePath, size: combined }, "Diff content too large for viewer");
6954
7741
  return null;
6955
7742
  }
6956
- const id = nanoid(12);
7743
+ const id = nanoid2(12);
6957
7744
  const now = Date.now();
6958
7745
  this.entries.set(id, {
6959
7746
  id,
@@ -6975,7 +7762,7 @@ var init_viewer_store = __esm({
6975
7762
  log10.debug({ label, size: output.length }, "Output too large for viewer");
6976
7763
  return null;
6977
7764
  }
6978
- const id = nanoid(12);
7765
+ const id = nanoid2(12);
6979
7766
  const now = Date.now();
6980
7767
  this.entries.set(id, {
6981
7768
  id,
@@ -7424,9 +8211,9 @@ __export(token_store_exports, {
7424
8211
  parseDuration: () => parseDuration
7425
8212
  });
7426
8213
  import { readFile as readFile2, writeFile } from "fs/promises";
7427
- import { randomBytes } from "crypto";
8214
+ import { randomBytes as randomBytes2 } from "crypto";
7428
8215
  function generateTokenId() {
7429
- return `tok_${randomBytes(12).toString("hex")}`;
8216
+ return `tok_${randomBytes2(12).toString("hex")}`;
7430
8217
  }
7431
8218
  function parseDuration(duration) {
7432
8219
  const match = duration.match(/^(\d+)(h|d|m)$/);
@@ -7573,6 +8360,18 @@ var init_token_store = __esm({
7573
8360
  this.lastUsedSaveTimer = null;
7574
8361
  }
7575
8362
  }
8363
+ /** Associate a user ID with a token. Called by identity plugin after /identity/setup. */
8364
+ setUserId(tokenId, userId) {
8365
+ const token = this.tokens.get(tokenId);
8366
+ if (token) {
8367
+ token.userId = userId;
8368
+ this.scheduleSave();
8369
+ }
8370
+ }
8371
+ /** Get the user ID associated with a token. */
8372
+ getUserId(tokenId) {
8373
+ return this.tokens.get(tokenId)?.userId;
8374
+ }
7576
8375
  /**
7577
8376
  * Generates a one-time authorization code that can be exchanged for a JWT.
7578
8377
  *
@@ -7580,7 +8379,7 @@ var init_token_store = __esm({
7580
8379
  * the App, which exchanges it for a proper JWT without ever exposing the raw API secret.
7581
8380
  */
7582
8381
  createCode(opts) {
7583
- const code = randomBytes(16).toString("hex");
8382
+ const code = randomBytes2(16).toString("hex");
7584
8383
  const now = /* @__PURE__ */ new Date();
7585
8384
  const ttl = opts.codeTtlMs ?? 30 * 60 * 1e3;
7586
8385
  const stored = {
@@ -8100,9 +8899,16 @@ async function createApiServer(options) {
8100
8899
  });
8101
8900
  const authPreHandler = createAuthPreHandler(options.getSecret, options.getJwtSecret, options.tokenStore);
8102
8901
  app.decorateRequest("auth", null, []);
8902
+ let booted = false;
8903
+ app.addHook("onReady", async () => {
8904
+ booted = true;
8905
+ });
8103
8906
  return {
8104
8907
  app,
8105
8908
  registerPlugin(prefix, plugin2, opts) {
8909
+ if (booted) {
8910
+ return;
8911
+ }
8106
8912
  const wrappedPlugin = async (pluginApp, pluginOpts) => {
8107
8913
  if (opts?.auth !== false) {
8108
8914
  pluginApp.addHook("onRequest", authPreHandler);
@@ -8533,7 +9339,7 @@ var sessions_exports = {};
8533
9339
  __export(sessions_exports, {
8534
9340
  sessionRoutes: () => sessionRoutes
8535
9341
  });
8536
- import { nanoid as nanoid2 } from "nanoid";
9342
+ import { nanoid as nanoid3 } from "nanoid";
8537
9343
  async function sessionRoutes(app, deps) {
8538
9344
  app.get("/", { preHandler: requireScopes("sessions:read") }, async () => {
8539
9345
  const summaries = deps.core.sessionManager.listAllSessions();
@@ -8708,8 +9514,17 @@ async function sessionRoutes(app, deps) {
8708
9514
  }
8709
9515
  attachments = await resolveAttachments(fileService, sessionId, body.attachments);
8710
9516
  }
8711
- const sourceAdapterId = body.sourceAdapterId ?? "api";
8712
- const turnId = body.turnId ?? nanoid2(8);
9517
+ const sourceAdapterId = body.sourceAdapterId ?? "sse";
9518
+ const turnId = body.turnId ?? nanoid3(8);
9519
+ const userId = request.auth?.tokenId ?? "api";
9520
+ const meta = { turnId, channelUser: { channelId: "sse", userId } };
9521
+ if (deps.lifecycleManager?.middlewareChain) {
9522
+ await deps.lifecycleManager.middlewareChain.execute(
9523
+ Hook.MESSAGE_INCOMING,
9524
+ { channelId: sourceAdapterId, threadId: session.id, userId, text: body.prompt, attachments, meta },
9525
+ async (p2) => p2
9526
+ );
9527
+ }
8713
9528
  deps.core.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
8714
9529
  sessionId,
8715
9530
  turnId,
@@ -8722,7 +9537,7 @@ async function sessionRoutes(app, deps) {
8722
9537
  await session.enqueuePrompt(body.prompt, attachments, {
8723
9538
  sourceAdapterId,
8724
9539
  responseAdapterId: body.responseAdapterId
8725
- }, turnId);
9540
+ }, turnId, meta);
8726
9541
  return {
8727
9542
  ok: true,
8728
9543
  sessionId,
@@ -9180,7 +9995,7 @@ import { z as z4 } from "zod";
9180
9995
  import * as fs21 from "fs";
9181
9996
  import * as path24 from "path";
9182
9997
  import * as os5 from "os";
9183
- import { randomBytes as randomBytes2 } from "crypto";
9998
+ import { randomBytes as randomBytes3 } from "crypto";
9184
9999
  import { EventEmitter } from "events";
9185
10000
  function expandHome2(p2) {
9186
10001
  if (p2.startsWith("~")) {
@@ -9310,7 +10125,7 @@ var init_config2 = __esm({
9310
10125
  log14.error({ errors: result.error.issues }, "Config validation failed, not saving");
9311
10126
  return;
9312
10127
  }
9313
- const tmpPath = this.configPath + `.tmp.${randomBytes2(4).toString("hex")}`;
10128
+ const tmpPath = this.configPath + `.tmp.${randomBytes3(4).toString("hex")}`;
9314
10129
  fs21.writeFileSync(tmpPath, JSON.stringify(raw, null, 2), "utf-8");
9315
10130
  fs21.renameSync(tmpPath, this.configPath);
9316
10131
  this.config = result.data;
@@ -9879,8 +10694,8 @@ async function commandRoutes(app, deps) {
9879
10694
  const result = await deps.commandRegistry.execute(commandString, {
9880
10695
  raw: "",
9881
10696
  sessionId: body.sessionId ?? null,
9882
- channelId: "api",
9883
- userId: "api",
10697
+ channelId: "sse",
10698
+ userId: request.auth?.tokenId ?? "api",
9884
10699
  reply: async () => {
9885
10700
  }
9886
10701
  });
@@ -10040,11 +10855,23 @@ async function authRoutes(app, deps) {
10040
10855
  });
10041
10856
  app.get("/me", async (request) => {
10042
10857
  const { auth } = request;
10858
+ const userId = auth.tokenId ? deps.tokenStore.getUserId(auth.tokenId) : void 0;
10859
+ let displayName = null;
10860
+ if (userId && deps.getIdentityService) {
10861
+ const identityService = deps.getIdentityService();
10862
+ if (identityService) {
10863
+ const user = await identityService.getUser(userId);
10864
+ displayName = user?.displayName ?? null;
10865
+ }
10866
+ }
10043
10867
  return {
10044
10868
  type: auth.type,
10045
10869
  tokenId: auth.tokenId,
10046
10870
  role: auth.role,
10047
- scopes: auth.scopes
10871
+ scopes: auth.scopes,
10872
+ userId: userId ?? null,
10873
+ displayName,
10874
+ claimed: !!userId
10048
10875
  };
10049
10876
  });
10050
10877
  app.post("/codes", {
@@ -10624,6 +11451,7 @@ function createApiServerPlugin() {
10624
11451
  const tokenStore = new TokenStore2(tokensFilePath);
10625
11452
  await tokenStore.load();
10626
11453
  tokenStoreRef = tokenStore;
11454
+ ctx.registerService("token-store", tokenStore);
10627
11455
  const { createApiServer: createApiServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
10628
11456
  const { SSEManager: SSEManager2 } = await Promise.resolve().then(() => (init_sse_manager(), sse_manager_exports));
10629
11457
  const { StaticServer: StaticServer2 } = await Promise.resolve().then(() => (init_static_server(), static_server_exports));
@@ -10677,7 +11505,12 @@ function createApiServerPlugin() {
10677
11505
  server.registerPlugin("/api/v1/tunnel", async (app) => tunnelRoutes2(app, deps));
10678
11506
  server.registerPlugin("/api/v1/notify", async (app) => notifyRoutes2(app, deps));
10679
11507
  server.registerPlugin("/api/v1/commands", async (app) => commandRoutes2(app, deps));
10680
- server.registerPlugin("/api/v1/auth", async (app) => authRoutes2(app, { tokenStore, getJwtSecret: () => jwtSecret }));
11508
+ server.registerPlugin("/api/v1/auth", async (app) => authRoutes2(app, {
11509
+ tokenStore,
11510
+ getJwtSecret: () => jwtSecret,
11511
+ // Lazy resolver: identity plugin may not be loaded, so we fetch it on demand
11512
+ getIdentityService: () => ctx.getService("identity") ?? void 0
11513
+ }));
10681
11514
  server.registerPlugin("/api/v1/plugins", async (app) => pluginRoutes2(app, deps));
10682
11515
  const appConfig = core.configManager.get();
10683
11516
  const workspaceName = appConfig.instanceName ?? "Main";
@@ -10826,7 +11659,7 @@ var init_api_server = __esm({
10826
11659
  });
10827
11660
 
10828
11661
  // src/plugins/sse-adapter/connection-manager.ts
10829
- import { randomBytes as randomBytes4 } from "crypto";
11662
+ import { randomBytes as randomBytes5 } from "crypto";
10830
11663
  var ConnectionManager;
10831
11664
  var init_connection_manager = __esm({
10832
11665
  "src/plugins/sse-adapter/connection-manager.ts"() {
@@ -10835,6 +11668,8 @@ var init_connection_manager = __esm({
10835
11668
  connections = /* @__PURE__ */ new Map();
10836
11669
  // Secondary index: sessionId → Set of connection IDs for O(1) broadcast targeting
10837
11670
  sessionIndex = /* @__PURE__ */ new Map();
11671
+ // Secondary index: userId → Set of connection IDs for user-level event delivery
11672
+ userIndex = /* @__PURE__ */ new Map();
10838
11673
  maxConnectionsPerSession;
10839
11674
  maxTotalConnections;
10840
11675
  constructor(opts) {
@@ -10857,7 +11692,7 @@ var init_connection_manager = __esm({
10857
11692
  if (sessionConns && sessionConns.size >= this.maxConnectionsPerSession) {
10858
11693
  throw new Error("Maximum connections per session reached");
10859
11694
  }
10860
- const id = `conn_${randomBytes4(8).toString("hex")}`;
11695
+ const id = `conn_${randomBytes5(8).toString("hex")}`;
10861
11696
  const connection = { id, sessionId, tokenId, response, connectedAt: /* @__PURE__ */ new Date() };
10862
11697
  this.connections.set(id, connection);
10863
11698
  let sessionConnsSet = this.sessionIndex.get(sessionId);
@@ -10869,7 +11704,66 @@ var init_connection_manager = __esm({
10869
11704
  response.on("close", () => this.removeConnection(id));
10870
11705
  return connection;
10871
11706
  }
10872
- /** Remove a connection from both indexes. Called automatically on client disconnect. */
11707
+ /**
11708
+ * Registers a user-level SSE connection (not tied to a specific session).
11709
+ * Used for notifications and system events delivered to a user.
11710
+ *
11711
+ * @throws if the global connection limit is reached.
11712
+ */
11713
+ addUserConnection(userId, tokenId, response) {
11714
+ if (this.connections.size >= this.maxTotalConnections) {
11715
+ throw new Error("Maximum total connections reached");
11716
+ }
11717
+ const id = `conn_${randomBytes5(8).toString("hex")}`;
11718
+ const connection = {
11719
+ id,
11720
+ sessionId: "",
11721
+ tokenId,
11722
+ userId,
11723
+ response,
11724
+ connectedAt: /* @__PURE__ */ new Date()
11725
+ };
11726
+ this.connections.set(id, connection);
11727
+ let userConns = this.userIndex.get(userId);
11728
+ if (!userConns) {
11729
+ userConns = /* @__PURE__ */ new Set();
11730
+ this.userIndex.set(userId, userConns);
11731
+ }
11732
+ userConns.add(id);
11733
+ response.on("close", () => this.removeConnection(id));
11734
+ return connection;
11735
+ }
11736
+ /**
11737
+ * Writes a serialized SSE event to all connections for a given user.
11738
+ *
11739
+ * Uses the same backpressure strategy as `broadcast`: flag on first overflow,
11740
+ * forcibly close if still backpressured on the next write.
11741
+ */
11742
+ pushToUser(userId, serializedEvent) {
11743
+ const connIds = this.userIndex.get(userId);
11744
+ if (!connIds) return;
11745
+ for (const connId of connIds) {
11746
+ const conn = this.connections.get(connId);
11747
+ if (!conn || conn.response.writableEnded) continue;
11748
+ try {
11749
+ const ok3 = conn.response.write(serializedEvent);
11750
+ if (!ok3) {
11751
+ if (conn.backpressured) {
11752
+ conn.response.end();
11753
+ this.removeConnection(conn.id);
11754
+ } else {
11755
+ conn.backpressured = true;
11756
+ conn.response.once("drain", () => {
11757
+ conn.backpressured = false;
11758
+ });
11759
+ }
11760
+ }
11761
+ } catch {
11762
+ this.removeConnection(conn.id);
11763
+ }
11764
+ }
11765
+ }
11766
+ /** Remove a connection from all indexes. Called automatically on client disconnect. */
10873
11767
  removeConnection(connectionId) {
10874
11768
  const conn = this.connections.get(connectionId);
10875
11769
  if (!conn) return;
@@ -10879,6 +11773,13 @@ var init_connection_manager = __esm({
10879
11773
  sessionConns.delete(connectionId);
10880
11774
  if (sessionConns.size === 0) this.sessionIndex.delete(conn.sessionId);
10881
11775
  }
11776
+ if (conn.userId) {
11777
+ const userConns = this.userIndex.get(conn.userId);
11778
+ if (userConns) {
11779
+ userConns.delete(connectionId);
11780
+ if (userConns.size === 0) this.userIndex.delete(conn.userId);
11781
+ }
11782
+ }
10882
11783
  }
10883
11784
  /** Returns all active connections for a session. */
10884
11785
  getConnectionsBySession(sessionId) {
@@ -10938,6 +11839,7 @@ var init_connection_manager = __esm({
10938
11839
  }
10939
11840
  this.connections.clear();
10940
11841
  this.sessionIndex.clear();
11842
+ this.userIndex.clear();
10941
11843
  }
10942
11844
  };
10943
11845
  }
@@ -11131,6 +12033,22 @@ var init_adapter = __esm({
11131
12033
  this.connectionManager.broadcast(notification.sessionId, serialized);
11132
12034
  }
11133
12035
  }
12036
+ /**
12037
+ * Delivers a push notification to a specific user's SSE connections.
12038
+ *
12039
+ * `platformId` is the userId for the SSE adapter — SSE has no concept of
12040
+ * platform-specific user handles, so we use the internal userId directly.
12041
+ */
12042
+ async sendUserNotification(platformId, message, options) {
12043
+ const serialized = `event: notification:text
12044
+ data: ${JSON.stringify({
12045
+ text: message.text ?? message.summary ?? "",
12046
+ ...options ?? {}
12047
+ })}
12048
+
12049
+ `;
12050
+ this.connectionManager.pushToUser(platformId, serialized);
12051
+ }
11134
12052
  /** SSE has no concept of threads — return sessionId as the threadId */
11135
12053
  async createSessionThread(sessionId, _name) {
11136
12054
  return sessionId;
@@ -11221,8 +12139,14 @@ async function sseRoutes(app, deps) {
11221
12139
  }
11222
12140
  attachments = await resolveAttachments(fileService, sessionId, body.attachments);
11223
12141
  }
11224
- await session.enqueuePrompt(body.prompt, attachments, { sourceAdapterId: "sse" });
11225
- return { ok: true, sessionId, queueDepth: session.queueDepth };
12142
+ const queueDepth = session.queueDepth + 1;
12143
+ const userId = request.auth?.tokenId ?? "api";
12144
+ await deps.core.handleMessageInSession(
12145
+ session,
12146
+ { channelId: "sse", userId, text: body.prompt, attachments },
12147
+ { channelUser: { channelId: "sse", userId } }
12148
+ );
12149
+ return { ok: true, sessionId, queueDepth };
11226
12150
  }
11227
12151
  );
11228
12152
  app.post(
@@ -11300,6 +12224,45 @@ async function sseRoutes(app, deps) {
11300
12224
  total: connections.length
11301
12225
  };
11302
12226
  });
12227
+ app.get("/events", async (request, reply) => {
12228
+ const auth = request.auth;
12229
+ if (!auth?.tokenId) {
12230
+ return reply.status(401).send({ error: "Unauthorized" });
12231
+ }
12232
+ const userId = deps.getUserId?.(auth.tokenId);
12233
+ if (!userId) {
12234
+ return reply.status(403).send({ error: "Identity not set up. Complete /identity/setup first." });
12235
+ }
12236
+ try {
12237
+ deps.connectionManager.addUserConnection(userId, auth.tokenId, reply.raw);
12238
+ } catch (err) {
12239
+ return reply.status(503).send({ error: err.message });
12240
+ }
12241
+ reply.hijack();
12242
+ const raw = reply.raw;
12243
+ raw.writeHead(200, {
12244
+ "Content-Type": "text/event-stream",
12245
+ "Cache-Control": "no-cache",
12246
+ "Connection": "keep-alive",
12247
+ // Disable buffering in Nginx/Cloudflare so events arrive without delay
12248
+ "X-Accel-Buffering": "no"
12249
+ });
12250
+ raw.write(`event: heartbeat
12251
+ data: ${JSON.stringify({ ts: Date.now() })}
12252
+
12253
+ `);
12254
+ const heartbeat = setInterval(() => {
12255
+ if (raw.writableEnded) {
12256
+ clearInterval(heartbeat);
12257
+ return;
12258
+ }
12259
+ raw.write(`event: heartbeat
12260
+ data: ${JSON.stringify({ ts: Date.now() })}
12261
+
12262
+ `);
12263
+ }, 3e4);
12264
+ raw.on("close", () => clearInterval(heartbeat));
12265
+ });
11303
12266
  }
11304
12267
  var init_routes = __esm({
11305
12268
  "src/plugins/sse-adapter/routes.ts"() {
@@ -11353,6 +12316,7 @@ var init_sse_adapter = __esm({
11353
12316
  _connectionManager = connectionManager;
11354
12317
  ctx.registerService("adapter:sse", adapter);
11355
12318
  const commandRegistry = ctx.getService("command-registry");
12319
+ const tokenStore = ctx.getService("token-store");
11356
12320
  ctx.on(BusEvent.SESSION_DELETED, (data) => {
11357
12321
  const { sessionId } = data;
11358
12322
  eventBuffer.cleanup(sessionId);
@@ -11366,7 +12330,8 @@ var init_sse_adapter = __esm({
11366
12330
  core,
11367
12331
  connectionManager,
11368
12332
  eventBuffer,
11369
- commandRegistry: commandRegistry ?? void 0
12333
+ commandRegistry: commandRegistry ?? void 0,
12334
+ getUserId: tokenStore ? (id) => tokenStore.getUserId(id) : void 0
11370
12335
  });
11371
12336
  }, { auth: true });
11372
12337
  ctx.log.info("SSE adapter registered");
@@ -15398,7 +16363,7 @@ var init_commands3 = __esm({
15398
16363
 
15399
16364
  // src/plugins/telegram/permissions.ts
15400
16365
  import { InlineKeyboard as InlineKeyboard11 } from "grammy";
15401
- import { nanoid as nanoid3 } from "nanoid";
16366
+ import { nanoid as nanoid4 } from "nanoid";
15402
16367
  var log26, PermissionHandler;
15403
16368
  var init_permissions = __esm({
15404
16369
  "src/plugins/telegram/permissions.ts"() {
@@ -15425,7 +16390,7 @@ var init_permissions = __esm({
15425
16390
  */
15426
16391
  async sendPermissionRequest(session, request) {
15427
16392
  const threadId = Number(session.threadId);
15428
- const callbackKey = nanoid3(8);
16393
+ const callbackKey = nanoid4(8);
15429
16394
  this.pending.set(callbackKey, {
15430
16395
  sessionId: session.id,
15431
16396
  requestId: request.id,
@@ -17560,10 +18525,19 @@ var init_adapter2 = __esm({
17560
18525
  });
17561
18526
  } catch {
17562
18527
  }
17563
- } else if (response.type === "text" || response.type === "error") {
17564
- const text5 = response.type === "text" ? response.text : `\u274C ${response.message}`;
18528
+ } else if (response.type === "text" || response.type === "error" || response.type === "adaptive") {
18529
+ let text5;
18530
+ let parseMode;
18531
+ if (response.type === "adaptive") {
18532
+ const variant = response.variants?.["telegram"];
18533
+ text5 = variant?.text ?? response.fallback;
18534
+ parseMode = variant?.parse_mode;
18535
+ } else {
18536
+ text5 = response.type === "text" ? response.text : `\u274C ${response.message}`;
18537
+ parseMode = "Markdown";
18538
+ }
17565
18539
  try {
17566
- await ctx.editMessageText(text5, { parse_mode: "Markdown" });
18540
+ await ctx.editMessageText(text5, { ...parseMode && { parse_mode: parseMode } });
17567
18541
  } catch {
17568
18542
  }
17569
18543
  }
@@ -17845,6 +18819,15 @@ OpenACP will automatically retry until this is resolved.`;
17845
18819
  message_thread_id: topicId
17846
18820
  });
17847
18821
  break;
18822
+ case "adaptive": {
18823
+ const variant = response.variants?.["telegram"];
18824
+ const text5 = variant?.text ?? response.fallback;
18825
+ await this.bot.api.sendMessage(chatId, text5, {
18826
+ message_thread_id: topicId,
18827
+ ...variant?.parse_mode && { parse_mode: variant.parse_mode }
18828
+ });
18829
+ break;
18830
+ }
17848
18831
  case "error":
17849
18832
  await this.bot.api.sendMessage(
17850
18833
  chatId,
@@ -17953,12 +18936,25 @@ ${lines.join("\n")}`;
17953
18936
  }
17954
18937
  ctx.replyWithChatAction("typing").catch(() => {
17955
18938
  });
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"));
18939
+ const fromName = [ctx.from.first_name, ctx.from.last_name].filter(Boolean).join(" ") || void 0;
18940
+ this.core.handleMessage(
18941
+ {
18942
+ channelId: "telegram",
18943
+ threadId: String(threadId),
18944
+ userId: String(ctx.from.id),
18945
+ text: forwardText
18946
+ },
18947
+ // Inject structured channel user info into TurnMeta so plugins can identify
18948
+ // the sender by name without adapter-specific fields on IncomingMessage.
18949
+ {
18950
+ channelUser: {
18951
+ channelId: "telegram",
18952
+ userId: String(ctx.from.id),
18953
+ displayName: fromName,
18954
+ username: ctx.from.username
18955
+ }
18956
+ }
18957
+ ).catch((err) => log29.error({ err }, "handleMessage error"));
17962
18958
  });
17963
18959
  this.bot.on("message:photo", async (ctx) => {
17964
18960
  const threadId = ctx.message.message_thread_id;
@@ -18922,6 +19918,7 @@ var init_core_plugins = __esm({
18922
19918
  "src/plugins/core-plugins.ts"() {
18923
19919
  "use strict";
18924
19920
  init_security();
19921
+ init_identity();
18925
19922
  init_file_service2();
18926
19923
  init_context();
18927
19924
  init_speech();
@@ -18933,6 +19930,8 @@ var init_core_plugins = __esm({
18933
19930
  corePlugins = [
18934
19931
  // Service plugins (no adapter dependencies)
18935
19932
  security_default,
19933
+ identity_default,
19934
+ // Must boot after security (blocked users rejected before identity records are created)
18936
19935
  file_service_default,
18937
19936
  context_default,
18938
19937
  speech_default,
@@ -21162,16 +22161,16 @@ var init_prompt_queue = __esm({
21162
22161
  * immediately. Otherwise, it's buffered and the returned promise resolves
21163
22162
  * only after the prompt finishes processing.
21164
22163
  */
21165
- async enqueue(text5, attachments, routing, turnId) {
22164
+ async enqueue(text5, attachments, routing, turnId, meta) {
21166
22165
  if (this.processing) {
21167
22166
  return new Promise((resolve9) => {
21168
- this.queue.push({ text: text5, attachments, routing, turnId, resolve: resolve9 });
22167
+ this.queue.push({ text: text5, attachments, routing, turnId, meta, resolve: resolve9 });
21169
22168
  });
21170
22169
  }
21171
- await this.process(text5, attachments, routing, turnId);
22170
+ await this.process(text5, attachments, routing, turnId, meta);
21172
22171
  }
21173
22172
  /** Run a single prompt through the processor, then drain the next queued item. */
21174
- async process(text5, attachments, routing, turnId) {
22173
+ async process(text5, attachments, routing, turnId, meta) {
21175
22174
  this.processing = true;
21176
22175
  this.abortController = new AbortController();
21177
22176
  const { signal } = this.abortController;
@@ -21181,7 +22180,7 @@ var init_prompt_queue = __esm({
21181
22180
  });
21182
22181
  try {
21183
22182
  await Promise.race([
21184
- this.processor(text5, attachments, routing, turnId),
22183
+ this.processor(text5, attachments, routing, turnId, meta),
21185
22184
  new Promise((_, reject) => {
21186
22185
  signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
21187
22186
  })
@@ -21202,7 +22201,7 @@ var init_prompt_queue = __esm({
21202
22201
  drainNext() {
21203
22202
  const next = this.queue.shift();
21204
22203
  if (next) {
21205
- this.process(next.text, next.attachments, next.routing, next.turnId).then(next.resolve);
22204
+ this.process(next.text, next.attachments, next.routing, next.turnId, next.meta).then(next.resolve);
21206
22205
  }
21207
22206
  }
21208
22207
  /**
@@ -21308,10 +22307,10 @@ var init_permission_gate = __esm({
21308
22307
  });
21309
22308
 
21310
22309
  // src/core/sessions/turn-context.ts
21311
- import { nanoid as nanoid4 } from "nanoid";
22310
+ import { nanoid as nanoid5 } from "nanoid";
21312
22311
  function createTurnContext(sourceAdapterId, responseAdapterId, turnId) {
21313
22312
  return {
21314
- turnId: turnId ?? nanoid4(8),
22313
+ turnId: turnId ?? nanoid5(8),
21315
22314
  sourceAdapterId,
21316
22315
  responseAdapterId
21317
22316
  };
@@ -21339,7 +22338,7 @@ var init_turn_context = __esm({
21339
22338
  });
21340
22339
 
21341
22340
  // src/core/sessions/session.ts
21342
- import { nanoid as nanoid5 } from "nanoid";
22341
+ import { nanoid as nanoid6 } from "nanoid";
21343
22342
  import * as fs41 from "fs";
21344
22343
  var moduleLog, TTS_PROMPT_INSTRUCTION, TTS_BLOCK_REGEX, TTS_MAX_LENGTH, TTS_TIMEOUT_MS, VALID_TRANSITIONS, Session;
21345
22344
  var init_session2 = __esm({
@@ -21419,7 +22418,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
21419
22418
  pendingContext = null;
21420
22419
  constructor(opts) {
21421
22420
  super();
21422
- this.id = opts.id || nanoid5(12);
22421
+ this.id = opts.id || nanoid6(12);
21423
22422
  this.channelId = opts.channelId;
21424
22423
  this.attachedAdapters = [opts.channelId];
21425
22424
  this.agentName = opts.agentName;
@@ -21431,7 +22430,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
21431
22430
  this.log = createSessionLogger(this.id, moduleLog);
21432
22431
  this.log.info({ agentName: this.agentName }, "Session created");
21433
22432
  this.queue = new PromptQueue(
21434
- (text5, attachments, routing, turnId) => this.processPrompt(text5, attachments, routing, turnId),
22433
+ (text5, attachments, routing, turnId, meta) => this.processPrompt(text5, attachments, routing, turnId, meta),
21435
22434
  (err) => {
21436
22435
  this.log.error({ err }, "Prompt execution failed");
21437
22436
  const message = err instanceof Error ? err.message : String(err);
@@ -21530,19 +22529,20 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
21530
22529
  * then adds it to the PromptQueue. Returns a turnId that callers can use to correlate
21531
22530
  * queued/processing events before the prompt actually runs.
21532
22531
  */
21533
- async enqueuePrompt(text5, attachments, routing, externalTurnId) {
21534
- const turnId = externalTurnId ?? nanoid5(8);
22532
+ async enqueuePrompt(text5, attachments, routing, externalTurnId, meta) {
22533
+ const turnId = externalTurnId ?? nanoid6(8);
22534
+ const turnMeta = meta ?? { turnId };
21535
22535
  if (this.middlewareChain) {
21536
- const payload = { text: text5, attachments, sessionId: this.id, sourceAdapterId: routing?.sourceAdapterId };
22536
+ const payload = { text: text5, attachments, sessionId: this.id, sourceAdapterId: routing?.sourceAdapterId, meta: turnMeta };
21537
22537
  const result = await this.middlewareChain.execute(Hook.AGENT_BEFORE_PROMPT, payload, async (p2) => p2);
21538
22538
  if (!result) return turnId;
21539
22539
  text5 = result.text;
21540
22540
  attachments = result.attachments;
21541
22541
  }
21542
- await this.queue.enqueue(text5, attachments, routing, turnId);
22542
+ await this.queue.enqueue(text5, attachments, routing, turnId, turnMeta);
21543
22543
  return turnId;
21544
22544
  }
21545
- async processPrompt(text5, attachments, routing, turnId) {
22545
+ async processPrompt(text5, attachments, routing, turnId, meta) {
21546
22546
  if (this._status === "finished") return;
21547
22547
  this.activeTurnContext = createTurnContext(
21548
22548
  routing?.sourceAdapterId ?? this.channelId,
@@ -21582,6 +22582,13 @@ ${text5}`;
21582
22582
  if (accumulatorListener) {
21583
22583
  this.on(SessionEv.AGENT_EVENT, accumulatorListener);
21584
22584
  }
22585
+ const turnTextBuffer = [];
22586
+ const turnTextListener = (event) => {
22587
+ if (event.type === "text" && typeof event.content === "string") {
22588
+ turnTextBuffer.push(event.content);
22589
+ }
22590
+ };
22591
+ this.on(SessionEv.AGENT_EVENT, turnTextListener);
21585
22592
  const mw = this.middlewareChain;
21586
22593
  const afterEventListener = mw ? (event) => {
21587
22594
  mw.execute(Hook.AGENT_AFTER_EVENT, { sessionId: this.id, event, outgoingMessage: { type: "text", text: "" } }, async (e) => e).catch(() => {
@@ -21591,7 +22598,7 @@ ${text5}`;
21591
22598
  this.agentInstance.on(SessionEv.AGENT_EVENT, afterEventListener);
21592
22599
  }
21593
22600
  if (this.middlewareChain) {
21594
- this.middlewareChain.execute(Hook.TURN_START, { sessionId: this.id, promptText: processed.text, promptNumber: this.promptCount }, async (p2) => p2).catch(() => {
22601
+ this.middlewareChain.execute(Hook.TURN_START, { sessionId: this.id, promptText: processed.text, promptNumber: this.promptCount, turnId: this.activeTurnContext?.turnId ?? turnId ?? "", meta }, async (p2) => p2).catch(() => {
21595
22602
  });
21596
22603
  }
21597
22604
  let stopReason = "end_turn";
@@ -21617,8 +22624,20 @@ ${text5}`;
21617
22624
  if (afterEventListener) {
21618
22625
  this.agentInstance.off(SessionEv.AGENT_EVENT, afterEventListener);
21619
22626
  }
22627
+ this.off(SessionEv.AGENT_EVENT, turnTextListener);
22628
+ const finalTurnId = this.activeTurnContext?.turnId ?? turnId ?? "";
22629
+ if (this.middlewareChain) {
22630
+ this.middlewareChain.execute(Hook.TURN_END, { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart, turnId: finalTurnId, meta }, async (p2) => p2).catch(() => {
22631
+ });
22632
+ }
21620
22633
  if (this.middlewareChain) {
21621
- this.middlewareChain.execute(Hook.TURN_END, { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart }, async (p2) => p2).catch(() => {
22634
+ this.middlewareChain.execute(Hook.AGENT_AFTER_TURN, {
22635
+ sessionId: this.id,
22636
+ turnId: finalTurnId,
22637
+ fullText: turnTextBuffer.join(""),
22638
+ stopReason,
22639
+ meta
22640
+ }, async (p2) => p2).catch(() => {
21622
22641
  });
21623
22642
  }
21624
22643
  this.activeTurnContext = null;
@@ -23315,8 +24334,7 @@ var init_session_factory = __esm({
23315
24334
  const payload = {
23316
24335
  agentName: params.agentName,
23317
24336
  workingDir: params.workingDirectory,
23318
- userId: "",
23319
- // userId is not part of SessionCreateParams — resolved upstream
24337
+ userId: params.userId ?? "",
23320
24338
  channelId: params.channelId,
23321
24339
  threadId: ""
23322
24340
  // threadId is assigned after session creation
@@ -23418,7 +24436,8 @@ var init_session_factory = __esm({
23418
24436
  this.eventBus.emit(BusEvent.SESSION_CREATED, {
23419
24437
  sessionId: session.id,
23420
24438
  agent: session.agentName,
23421
- status: session.status
24439
+ status: session.status,
24440
+ userId: createParams.userId
23422
24441
  });
23423
24442
  }
23424
24443
  return session;
@@ -24663,11 +25682,55 @@ var init_plugin_storage = __esm({
24663
25682
  async list() {
24664
25683
  return Object.keys(this.readKv());
24665
25684
  }
25685
+ async keys(prefix) {
25686
+ const all = Object.keys(this.readKv());
25687
+ return prefix ? all.filter((k) => k.startsWith(prefix)) : all;
25688
+ }
25689
+ async clear() {
25690
+ this.writeChain = this.writeChain.then(() => {
25691
+ this.writeKv({});
25692
+ });
25693
+ return this.writeChain;
25694
+ }
24666
25695
  /** Returns the plugin's data directory, creating it lazily on first access. */
24667
25696
  getDataDir() {
24668
25697
  fs45.mkdirSync(this.dataDir, { recursive: true });
24669
25698
  return this.dataDir;
24670
25699
  }
25700
+ /**
25701
+ * Creates a namespaced storage instance scoped to a session.
25702
+ * Keys are prefixed with `session:{sessionId}:` to isolate session data
25703
+ * from global plugin storage in the same backing file.
25704
+ */
25705
+ forSession(sessionId) {
25706
+ const prefix = `session:${sessionId}:`;
25707
+ return {
25708
+ get: (key) => this.get(`${prefix}${key}`),
25709
+ set: (key, value) => this.set(`${prefix}${key}`, value),
25710
+ delete: (key) => this.delete(`${prefix}${key}`),
25711
+ list: async () => {
25712
+ const all = await this.keys(prefix);
25713
+ return all.map((k) => k.slice(prefix.length));
25714
+ },
25715
+ keys: async (p2) => {
25716
+ const full = p2 ? `${prefix}${p2}` : prefix;
25717
+ const all = await this.keys(full);
25718
+ return all.map((k) => k.slice(prefix.length));
25719
+ },
25720
+ clear: async () => {
25721
+ this.writeChain = this.writeChain.then(() => {
25722
+ const data = this.readKv();
25723
+ for (const key of Object.keys(data)) {
25724
+ if (key.startsWith(prefix)) delete data[key];
25725
+ }
25726
+ this.writeKv(data);
25727
+ });
25728
+ return this.writeChain;
25729
+ },
25730
+ getDataDir: () => this.getDataDir(),
25731
+ forSession: (nestedId) => this.forSession(`${sessionId}:${nestedId}`)
25732
+ };
25733
+ }
24671
25734
  };
24672
25735
  }
24673
25736
  });
@@ -24733,9 +25796,52 @@ function createPluginContext(opts) {
24733
25796
  requirePermission(permissions, "storage:read", "storage.list");
24734
25797
  return storageImpl.list();
24735
25798
  },
25799
+ async keys(prefix) {
25800
+ requirePermission(permissions, "storage:read", "storage.keys");
25801
+ return storageImpl.keys(prefix);
25802
+ },
25803
+ async clear() {
25804
+ requirePermission(permissions, "storage:write", "storage.clear");
25805
+ return storageImpl.clear();
25806
+ },
24736
25807
  getDataDir() {
24737
25808
  requirePermission(permissions, "storage:read", "storage.getDataDir");
24738
25809
  return storageImpl.getDataDir();
25810
+ },
25811
+ forSession(sessionId) {
25812
+ requirePermission(permissions, "storage:read", "storage.forSession");
25813
+ const scoped = storageImpl.forSession(sessionId);
25814
+ return {
25815
+ get: (key) => {
25816
+ requirePermission(permissions, "storage:read", "storage.get");
25817
+ return scoped.get(key);
25818
+ },
25819
+ set: (key, value) => {
25820
+ requirePermission(permissions, "storage:write", "storage.set");
25821
+ return scoped.set(key, value);
25822
+ },
25823
+ delete: (key) => {
25824
+ requirePermission(permissions, "storage:write", "storage.delete");
25825
+ return scoped.delete(key);
25826
+ },
25827
+ list: () => {
25828
+ requirePermission(permissions, "storage:read", "storage.list");
25829
+ return scoped.list();
25830
+ },
25831
+ keys: (prefix) => {
25832
+ requirePermission(permissions, "storage:read", "storage.keys");
25833
+ return scoped.keys(prefix);
25834
+ },
25835
+ clear: () => {
25836
+ requirePermission(permissions, "storage:write", "storage.clear");
25837
+ return scoped.clear();
25838
+ },
25839
+ getDataDir: () => {
25840
+ requirePermission(permissions, "storage:read", "storage.getDataDir");
25841
+ return scoped.getDataDir();
25842
+ },
25843
+ forSession: (nestedId) => storage.forSession(`${sessionId}:${nestedId}`)
25844
+ };
24739
25845
  }
24740
25846
  };
24741
25847
  const ctx = {
@@ -24786,6 +25892,26 @@ function createPluginContext(opts) {
24786
25892
  await router.send(_sessionId, _content);
24787
25893
  }
24788
25894
  },
25895
+ notify(target, message, options) {
25896
+ requirePermission(permissions, "notifications:send", "notify()");
25897
+ const svc = serviceRegistry.get("notifications");
25898
+ if (svc?.notifyUser) {
25899
+ svc.notifyUser(target, message, options).catch(() => {
25900
+ });
25901
+ }
25902
+ },
25903
+ defineHook(_name) {
25904
+ },
25905
+ async emitHook(name, payload) {
25906
+ const qualifiedName = `plugin:${pluginName}:${name}`;
25907
+ return middlewareChain.execute(qualifiedName, payload, (p2) => p2);
25908
+ },
25909
+ async getSessionInfo(sessionId) {
25910
+ requirePermission(permissions, "sessions:read", "getSessionInfo()");
25911
+ const sessionMgr = serviceRegistry.get("session-info");
25912
+ if (!sessionMgr) return void 0;
25913
+ return sessionMgr.getSessionInfo(sessionId);
25914
+ },
24789
25915
  registerMenuItem(item) {
24790
25916
  requirePermission(permissions, "commands:register", "registerMenuItem()");
24791
25917
  const menuRegistry = serviceRegistry.get("menu-registry");
@@ -25643,7 +26769,7 @@ var init_core_items = __esm({
25643
26769
 
25644
26770
  // src/core/core.ts
25645
26771
  import path51 from "path";
25646
- import { nanoid as nanoid6 } from "nanoid";
26772
+ import { nanoid as nanoid7 } from "nanoid";
25647
26773
  var log44, OpenACPCore;
25648
26774
  var init_core = __esm({
25649
26775
  "src/core/core.ts"() {
@@ -25945,7 +27071,7 @@ var init_core = __esm({
25945
27071
  *
25946
27072
  * If no session is found, the user is told to start one with /new.
25947
27073
  */
25948
- async handleMessage(message) {
27074
+ async handleMessage(message, initialMeta) {
25949
27075
  log44.debug(
25950
27076
  {
25951
27077
  channelId: message.channelId,
@@ -25954,10 +27080,12 @@ var init_core = __esm({
25954
27080
  },
25955
27081
  "Incoming message"
25956
27082
  );
27083
+ const turnId = nanoid7(8);
27084
+ const meta = { turnId, ...initialMeta };
25957
27085
  if (this.lifecycleManager?.middlewareChain) {
25958
27086
  const result = await this.lifecycleManager.middlewareChain.execute(
25959
27087
  Hook.MESSAGE_INCOMING,
25960
- message,
27088
+ { ...message, meta },
25961
27089
  async (msg) => msg
25962
27090
  );
25963
27091
  if (!result) return;
@@ -26009,8 +27137,8 @@ ${text5}`;
26009
27137
  }
26010
27138
  const sourceAdapterId = message.routing?.sourceAdapterId ?? message.channelId;
26011
27139
  const routing = sourceAdapterId !== message.routing?.sourceAdapterId ? { ...message.routing, sourceAdapterId } : message.routing;
27140
+ const enrichedMeta = message.meta ?? meta;
26012
27141
  if (sourceAdapterId && sourceAdapterId !== "sse" && sourceAdapterId !== "api") {
26013
- const turnId = nanoid6(8);
26014
27142
  this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
26015
27143
  sessionId: session.id,
26016
27144
  turnId,
@@ -26020,10 +27148,50 @@ ${text5}`;
26020
27148
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
26021
27149
  queueDepth: session.queueDepth
26022
27150
  });
26023
- await session.enqueuePrompt(text5, message.attachments, routing, turnId);
27151
+ await session.enqueuePrompt(text5, message.attachments, routing, turnId, enrichedMeta);
26024
27152
  } else {
26025
- await session.enqueuePrompt(text5, message.attachments, routing);
27153
+ await session.enqueuePrompt(text5, message.attachments, routing, turnId, enrichedMeta);
27154
+ }
27155
+ }
27156
+ /**
27157
+ * Send a message to a known session, running the full message:incoming → agent:beforePrompt
27158
+ * middleware chain (same as handleMessage) but without the threadId-based session lookup.
27159
+ *
27160
+ * Used by channels that already hold a direct session reference (e.g. SSE adapter), where
27161
+ * looking up by channelId+threadId is unreliable (API sessions may have no threadId).
27162
+ *
27163
+ * @param session The target session — caller is responsible for validating its status.
27164
+ * @param message Sender context and message content.
27165
+ * @param initialMeta Optional adapter-specific context to seed the TurnMeta bag
27166
+ * (e.g. channelUser with display name/username).
27167
+ */
27168
+ async handleMessageInSession(session, message, initialMeta) {
27169
+ const turnId = nanoid7(8);
27170
+ const meta = { turnId, ...initialMeta };
27171
+ let text5 = message.text;
27172
+ let { attachments } = message;
27173
+ let enrichedMeta = meta;
27174
+ if (this.lifecycleManager?.middlewareChain) {
27175
+ const payload = {
27176
+ channelId: message.channelId,
27177
+ threadId: session.id,
27178
+ userId: message.userId,
27179
+ text: text5,
27180
+ attachments,
27181
+ meta
27182
+ };
27183
+ const result = await this.lifecycleManager.middlewareChain.execute(
27184
+ Hook.MESSAGE_INCOMING,
27185
+ payload,
27186
+ async (p2) => p2
27187
+ );
27188
+ if (!result) return;
27189
+ text5 = result.text;
27190
+ attachments = result.attachments;
27191
+ enrichedMeta = result.meta ?? meta;
26026
27192
  }
27193
+ const routing = { sourceAdapterId: message.channelId };
27194
+ await session.enqueuePrompt(text5, attachments, routing, turnId, enrichedMeta);
26027
27195
  }
26028
27196
  // --- Unified Session Creation Pipeline ---
26029
27197
  /**
@@ -26584,7 +27752,7 @@ function registerSessionCommands(registry, _core) {
26584
27752
  await assistant.enqueuePrompt(prompt);
26585
27753
  return { type: "delegated" };
26586
27754
  }
26587
- return { type: "text", text: "Usage: /new <agent> <workspace>\nOr use the Assistant topic for guided setup." };
27755
+ return { type: "text", text: "Usage: /new [agent] [workspace]\nOr use the Assistant topic for guided setup." };
26588
27756
  }
26589
27757
  });
26590
27758
  registry.register({
@@ -26656,7 +27824,7 @@ Prompts: ${session.promptCount}` };
26656
27824
  registry.register({
26657
27825
  name: "resume",
26658
27826
  description: "Resume a previous session",
26659
- usage: "<session-number>",
27827
+ usage: "[session-number]",
26660
27828
  category: "system",
26661
27829
  handler: async (args2) => {
26662
27830
  const assistant = core.assistantManager?.get(args2.channelId);
@@ -26670,7 +27838,7 @@ Prompts: ${session.promptCount}` };
26670
27838
  registry.register({
26671
27839
  name: "handoff",
26672
27840
  description: "Hand off session to another agent",
26673
- usage: "<agent-name>",
27841
+ usage: "[agent-name]",
26674
27842
  category: "system",
26675
27843
  handler: async (args2) => {
26676
27844
  if (!args2.sessionId) return { type: "text", text: "Use /handoff inside a session topic." };
@@ -26856,7 +28024,7 @@ function registerAgentCommands(registry, _core) {
26856
28024
  registry.register({
26857
28025
  name: "install",
26858
28026
  description: "Install an agent",
26859
- usage: "<agent-name>",
28027
+ usage: "[agent-name]",
26860
28028
  category: "system",
26861
28029
  handler: async (args2) => {
26862
28030
  const agentName = args2.raw.trim();
@@ -26931,7 +28099,7 @@ function registerAdminCommands(registry, _core) {
26931
28099
  registry.register({
26932
28100
  name: "integrate",
26933
28101
  description: "Set up a new channel integration",
26934
- usage: "<channel>",
28102
+ usage: "[channel]",
26935
28103
  category: "system",
26936
28104
  handler: async (args2) => {
26937
28105
  const channel = args2.raw.trim();
@@ -27334,7 +28502,7 @@ var init_plugin_field_registry = __esm({
27334
28502
 
27335
28503
  // src/core/setup/types.ts
27336
28504
  var ONBOARD_SECTION_OPTIONS, CHANNEL_META;
27337
- var init_types = __esm({
28505
+ var init_types2 = __esm({
27338
28506
  "src/core/setup/types.ts"() {
27339
28507
  "use strict";
27340
28508
  ONBOARD_SECTION_OPTIONS = [
@@ -27843,7 +29011,7 @@ var CHANNEL_PLUGIN_NAME;
27843
29011
  var init_setup_channels = __esm({
27844
29012
  "src/core/setup/setup-channels.ts"() {
27845
29013
  "use strict";
27846
- init_types();
29014
+ init_types2();
27847
29015
  init_helpers2();
27848
29016
  CHANNEL_PLUGIN_NAME = {
27849
29017
  discord: "@openacp/discord-adapter"
@@ -28501,7 +29669,7 @@ async function runReconfigure(configManager, settingsManager) {
28501
29669
  var init_wizard = __esm({
28502
29670
  "src/core/setup/wizard.ts"() {
28503
29671
  "use strict";
28504
- init_types();
29672
+ init_types2();
28505
29673
  init_helpers2();
28506
29674
  init_setup_agents();
28507
29675
  init_setup_run_mode();
@@ -28516,8 +29684,8 @@ var init_wizard = __esm({
28516
29684
  });
28517
29685
 
28518
29686
  // src/core/setup/index.ts
28519
- var setup_exports = {};
28520
- __export(setup_exports, {
29687
+ var setup_exports2 = {};
29688
+ __export(setup_exports2, {
28521
29689
  detectAgents: () => detectAgents,
28522
29690
  printStartBanner: () => printStartBanner,
28523
29691
  runReconfigure: () => runReconfigure,
@@ -28529,7 +29697,7 @@ __export(setup_exports, {
28529
29697
  validateBotToken: () => validateBotToken,
28530
29698
  validateChatId: () => validateChatId
28531
29699
  });
28532
- var init_setup = __esm({
29700
+ var init_setup2 = __esm({
28533
29701
  "src/core/setup/index.ts"() {
28534
29702
  "use strict";
28535
29703
  init_wizard();
@@ -28778,7 +29946,7 @@ async function startServer(opts) {
28778
29946
  const configManager = new ConfigManager(ctx.paths.config);
28779
29947
  const configExists = await configManager.exists();
28780
29948
  if (!configExists) {
28781
- const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
29949
+ const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
28782
29950
  const shouldStart = await runSetup2(configManager, { settingsManager, pluginRegistry });
28783
29951
  if (!shouldStart) process.exit(0);
28784
29952
  }
@@ -28792,7 +29960,7 @@ async function startServer(opts) {
28792
29960
  }
28793
29961
  const isForegroundTTY = !!(process.stdout.isTTY && !process.env.NO_COLOR && config.runMode !== "daemon");
28794
29962
  if (isForegroundTTY) {
28795
- const { printStartBanner: printStartBanner2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
29963
+ const { printStartBanner: printStartBanner2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
28796
29964
  await printStartBanner2();
28797
29965
  }
28798
29966
  let spinner4;
@@ -33307,10 +34475,10 @@ async function cmdOnboard(instanceRoot) {
33307
34475
  const pluginRegistry = new PluginRegistry2(REGISTRY_PATH);
33308
34476
  await pluginRegistry.load();
33309
34477
  if (await cm.exists()) {
33310
- const { runReconfigure: runReconfigure2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
34478
+ const { runReconfigure: runReconfigure2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
33311
34479
  await runReconfigure2(cm, settingsManager);
33312
34480
  } else {
33313
- const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
34481
+ const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
33314
34482
  await runSetup2(cm, { skipRunMode: true, settingsManager, pluginRegistry, instanceRoot: OPENACP_DIR });
33315
34483
  }
33316
34484
  }
@@ -33370,7 +34538,7 @@ async function cmdDefault(command2, instanceRoot) {
33370
34538
  const settingsManager = new SettingsManager2(pluginsDataDir);
33371
34539
  const pluginRegistry = new PluginRegistry2(registryPath);
33372
34540
  await pluginRegistry.load();
33373
- const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
34541
+ const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports2));
33374
34542
  const shouldStart = await runSetup2(cm, { settingsManager, pluginRegistry, instanceRoot: root });
33375
34543
  if (!shouldStart) process.exit(0);
33376
34544
  }