@troykelly/openclaw-projects 0.0.25 → 0.0.29

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.
@@ -10,7 +10,7 @@
10
10
  import { ZodError } from 'zod';
11
11
  import { createApiClient } from './api-client.js';
12
12
  import { redactConfig, resolveConfigSecretsSync, resolveNamespaceConfig, validateRawConfig } from './config.js';
13
- import { extractContext, getUserScopeKey } from './context.js';
13
+ import { extractContext, getUserScopeKey, resolveAgentId } from './context.js';
14
14
  import { createOAuthGatewayMethods, registerOAuthGatewayRpcMethods } from './gateway/oauth-rpc-methods.js';
15
15
  import { createGatewayMethods, registerGatewayRpcMethods } from './gateway/rpc-methods.js';
16
16
  import { createAutoCaptureHook, createGraphAwareRecallHook } from './hooks.js';
@@ -1449,7 +1449,7 @@ export async function refreshNamespacesAsync(state) {
1449
1449
  return;
1450
1450
  state.refreshInFlight = true;
1451
1451
  try {
1452
- const response = await state.apiClient.get('/api/namespaces', { user_id: state.user_id, user_email: state.user_email });
1452
+ const response = await state.apiClient.get('/api/namespaces', { user_id: state.agentId, user_email: state.agentEmail });
1453
1453
  if (!response.success) {
1454
1454
  state.logger.warn('Namespace discovery failed, keeping cached list', { error: response.error.message });
1455
1455
  // Do NOT update timestamp on failure — let the next check retry sooner
@@ -1487,18 +1487,20 @@ export async function refreshNamespacesAsync(state) {
1487
1487
  * Create tool execution handlers
1488
1488
  */
1489
1489
  function createToolHandlers(state) {
1490
- const { config, logger, apiClient, user_id, user_email, resolvedNamespace } = state;
1491
- /** Build RequestOptions with user_id and user_email for identity resolution (#1567) */
1492
- const reqOpts = () => ({ user_id, user_email });
1493
- /**
1494
- * Get the effective namespace for a store/create operation.
1495
- * Uses explicit tool param if provided, otherwise falls back to config default.
1496
- */
1490
+ const { config, logger, apiClient } = state;
1491
+ // Issue #1644: Read user_id from mutable state on every call.
1492
+ const getAgentId = () => state.agentId;
1493
+ /** Read user_id from mutable state on every call (Issue #1644) */
1494
+ const reqOpts = () => ({
1495
+ user_id: state.agentId,
1496
+ user_email: state.agentEmail,
1497
+ });
1498
+ /** Read namespace from mutable state on every call (Issue #1644) */
1497
1499
  function getStoreNamespace(params) {
1498
1500
  const ns = params.namespace;
1499
1501
  if (typeof ns === 'string' && ns.length > 0)
1500
1502
  return ns;
1501
- return resolvedNamespace.default;
1503
+ return state.resolvedNamespace.default;
1502
1504
  }
1503
1505
  /**
1504
1506
  * Get the effective namespaces for a query/list operation.
@@ -1514,7 +1516,7 @@ function createToolHandlers(state) {
1514
1516
  if (interval > 0 && !state.hasStaticRecall && Date.now() - state.lastNamespaceRefreshMs > interval) {
1515
1517
  refreshNamespacesAsync(state);
1516
1518
  }
1517
- return resolvedNamespace.recall;
1519
+ return state.resolvedNamespace.recall;
1518
1520
  }
1519
1521
  return {
1520
1522
  async memory_recall(params) {
@@ -1572,7 +1574,7 @@ function createToolHandlers(state) {
1572
1574
  success: true,
1573
1575
  data: {
1574
1576
  content,
1575
- details: { count: memories.length, memories, user_id },
1577
+ details: { count: memories.length, memories, user_id: state.agentId },
1576
1578
  },
1577
1579
  };
1578
1580
  }
@@ -1704,7 +1706,7 @@ function createToolHandlers(state) {
1704
1706
  const queryParams = new URLSearchParams({ item_type: 'project', limit: String(limit) });
1705
1707
  if (status !== 'all')
1706
1708
  queryParams.set('status', status);
1707
- queryParams.set('user_email', user_id); // Issue #1172: scope by user
1709
+ queryParams.set('user_email', state.agentId); // Issue #1172: scope by user
1708
1710
  // Namespace scoping (Issue #1428)
1709
1711
  const projListNs = getRecallNamespaces(params);
1710
1712
  if (projListNs.length > 0)
@@ -1728,7 +1730,7 @@ function createToolHandlers(state) {
1728
1730
  async project_get(params) {
1729
1731
  const { project_id } = params;
1730
1732
  try {
1731
- const response = await apiClient.get(`/api/work-items/${project_id}?user_email=${encodeURIComponent(user_id)}`, reqOpts());
1733
+ const response = await apiClient.get(`/api/work-items/${project_id}?user_email=${encodeURIComponent(state.agentId)}`, reqOpts());
1732
1734
  if (!response.success) {
1733
1735
  return { success: false, error: response.error.message };
1734
1736
  }
@@ -1749,7 +1751,7 @@ function createToolHandlers(state) {
1749
1751
  async project_create(params) {
1750
1752
  const { name, description, status = 'active', } = params;
1751
1753
  try {
1752
- const response = await apiClient.post('/api/work-items', { title: name, description, item_type: 'project', status, user_email: user_id, namespace: getStoreNamespace(params) }, reqOpts());
1754
+ const response = await apiClient.post('/api/work-items', { title: name, description, item_type: 'project', status, user_email: state.agentId, namespace: getStoreNamespace(params) }, reqOpts());
1753
1755
  if (!response.success) {
1754
1756
  return { success: false, error: response.error.message };
1755
1757
  }
@@ -1773,7 +1775,7 @@ function createToolHandlers(state) {
1773
1775
  item_type: 'task',
1774
1776
  limit: String(limit),
1775
1777
  offset: String(offset),
1776
- user_email: user_id, // Issue #1172: scope by user
1778
+ user_email: state.agentId, // Issue #1172: scope by user
1777
1779
  });
1778
1780
  if (project_id)
1779
1781
  queryParams.set('parent_work_item_id', project_id);
@@ -1816,7 +1818,7 @@ function createToolHandlers(state) {
1816
1818
  async todo_create(params) {
1817
1819
  const { title, description, project_id, priority = 'medium', dueDate, } = params;
1818
1820
  try {
1819
- const body = { title, description, item_type: 'task', priority, user_email: user_id, namespace: getStoreNamespace(params) };
1821
+ const body = { title, description, item_type: 'task', priority, user_email: state.agentId, namespace: getStoreNamespace(params) };
1820
1822
  if (project_id)
1821
1823
  body.parent_work_item_id = project_id;
1822
1824
  if (dueDate)
@@ -1841,7 +1843,7 @@ function createToolHandlers(state) {
1841
1843
  async todo_complete(params) {
1842
1844
  const { todoId } = params;
1843
1845
  try {
1844
- const response = await apiClient.patch(`/api/work-items/${todoId}/status?user_email=${encodeURIComponent(user_id)}`, { status: 'completed' }, reqOpts());
1846
+ const response = await apiClient.patch(`/api/work-items/${todoId}/status?user_email=${encodeURIComponent(state.agentId)}`, { status: 'completed' }, reqOpts());
1845
1847
  if (!response.success) {
1846
1848
  return { success: false, error: response.error.message };
1847
1849
  }
@@ -1868,7 +1870,7 @@ function createToolHandlers(state) {
1868
1870
  types: 'work_item',
1869
1871
  limit: String(fetchLimit),
1870
1872
  semantic: 'true',
1871
- user_email: user_id, // Issue #1216: scope results to current user
1873
+ user_email: state.agentId, // Issue #1216: scope results to current user
1872
1874
  });
1873
1875
  // Namespace scoping (Issue #1428)
1874
1876
  const todoSearchNs = getRecallNamespaces(params);
@@ -1929,22 +1931,22 @@ function createToolHandlers(state) {
1929
1931
  }
1930
1932
  },
1931
1933
  async project_search(params) {
1932
- const tool = createProjectSearchTool({ client: apiClient, logger, config, user_id });
1934
+ const tool = createProjectSearchTool({ client: apiClient, logger, config, user_id: getAgentId() });
1933
1935
  return tool.execute(params);
1934
1936
  },
1935
1937
  async context_search(params) {
1936
- const tool = createContextSearchTool({ client: apiClient, logger, config, user_id });
1938
+ const tool = createContextSearchTool({ client: apiClient, logger, config, user_id: getAgentId() });
1937
1939
  return tool.execute(params);
1938
1940
  },
1939
1941
  async contact_search(params) {
1940
1942
  const { query, limit = 10 } = params;
1941
1943
  try {
1942
- const queryParams = new URLSearchParams({ search: query, limit: String(limit), user_email: user_id });
1944
+ const queryParams = new URLSearchParams({ search: query, limit: String(limit), user_email: state.agentId });
1943
1945
  const contactSearchNs = getRecallNamespaces(params);
1944
1946
  if (contactSearchNs.length > 0)
1945
1947
  queryParams.set('namespaces', contactSearchNs.join(','));
1946
1948
  const response = await apiClient.get(`/api/contacts?${queryParams}`, {
1947
- user_id,
1949
+ user_id: state.agentId,
1948
1950
  });
1949
1951
  if (!response.success) {
1950
1952
  return { success: false, error: response.error.message };
@@ -1964,7 +1966,7 @@ function createToolHandlers(state) {
1964
1966
  async contact_get(params) {
1965
1967
  const { contact_id } = params;
1966
1968
  try {
1967
- const response = await apiClient.get(`/api/contacts/${contact_id}?user_email=${encodeURIComponent(user_id)}`, reqOpts());
1969
+ const response = await apiClient.get(`/api/contacts/${contact_id}?user_email=${encodeURIComponent(state.agentId)}`, reqOpts());
1968
1970
  if (!response.success) {
1969
1971
  return { success: false, error: response.error.message };
1970
1972
  }
@@ -1995,7 +1997,7 @@ function createToolHandlers(state) {
1995
1997
  }
1996
1998
  try {
1997
1999
  const body = {
1998
- user_email: user_id,
2000
+ user_email: state.agentId,
1999
2001
  namespace: getStoreNamespace(params),
2000
2002
  notes,
2001
2003
  contact_kind: contact_kind ?? 'person',
@@ -2206,7 +2208,7 @@ function createToolHandlers(state) {
2206
2208
  };
2207
2209
  }
2208
2210
  logger.info('sms_send invoked', {
2209
- user_id,
2211
+ user_id: state.agentId,
2210
2212
  bodyLength: body.length,
2211
2213
  hasIdempotencyKey: !!idempotency_key,
2212
2214
  });
@@ -2214,7 +2216,7 @@ function createToolHandlers(state) {
2214
2216
  const response = await apiClient.post('/api/twilio/sms/send', { to, body, idempotency_key }, reqOpts());
2215
2217
  if (!response.success) {
2216
2218
  logger.error('sms_send API error', {
2217
- user_id,
2219
+ user_id: state.agentId,
2218
2220
  status: response.error.status,
2219
2221
  code: response.error.code,
2220
2222
  });
@@ -2225,7 +2227,7 @@ function createToolHandlers(state) {
2225
2227
  }
2226
2228
  const { message_id, thread_id, status } = response.data;
2227
2229
  logger.debug('sms_send completed', {
2228
- user_id,
2230
+ user_id: state.agentId,
2229
2231
  message_id,
2230
2232
  status,
2231
2233
  });
@@ -2233,13 +2235,13 @@ function createToolHandlers(state) {
2233
2235
  success: true,
2234
2236
  data: {
2235
2237
  content: `SMS sent successfully (ID: ${message_id}, Status: ${status})`,
2236
- details: { message_id, thread_id, status, user_id },
2238
+ details: { message_id, thread_id, status, user_id: state.agentId },
2237
2239
  },
2238
2240
  };
2239
2241
  }
2240
2242
  catch (error) {
2241
2243
  logger.error('sms_send failed', {
2242
- user_id,
2244
+ user_id: state.agentId,
2243
2245
  error: error instanceof Error ? error.message : String(error),
2244
2246
  });
2245
2247
  // Sanitize error message (remove phone numbers for privacy)
@@ -2284,7 +2286,7 @@ function createToolHandlers(state) {
2284
2286
  };
2285
2287
  }
2286
2288
  logger.info('email_send invoked', {
2287
- user_id,
2289
+ user_id: state.agentId,
2288
2290
  subjectLength: subject.length,
2289
2291
  bodyLength: body.length,
2290
2292
  hasHtmlBody: !!html_body,
@@ -2295,7 +2297,7 @@ function createToolHandlers(state) {
2295
2297
  const response = await apiClient.post('/api/postmark/email/send', { to, subject, body, html_body, thread_id, idempotency_key }, reqOpts());
2296
2298
  if (!response.success) {
2297
2299
  logger.error('email_send API error', {
2298
- user_id,
2300
+ user_id: state.agentId,
2299
2301
  status: response.error.status,
2300
2302
  code: response.error.code,
2301
2303
  });
@@ -2306,7 +2308,7 @@ function createToolHandlers(state) {
2306
2308
  }
2307
2309
  const { message_id, thread_id: responseThreadId, status } = response.data;
2308
2310
  logger.debug('email_send completed', {
2309
- user_id,
2311
+ user_id: state.agentId,
2310
2312
  message_id,
2311
2313
  status,
2312
2314
  });
@@ -2314,13 +2316,13 @@ function createToolHandlers(state) {
2314
2316
  success: true,
2315
2317
  data: {
2316
2318
  content: `Email sent successfully (ID: ${message_id}, Status: ${status})`,
2317
- details: { message_id, thread_id: responseThreadId, status, user_id },
2319
+ details: { message_id, thread_id: responseThreadId, status, user_id: state.agentId },
2318
2320
  },
2319
2321
  };
2320
2322
  }
2321
2323
  catch (error) {
2322
2324
  logger.error('email_send failed', {
2323
- user_id,
2325
+ user_id: state.agentId,
2324
2326
  error: error instanceof Error ? error.message : String(error),
2325
2327
  });
2326
2328
  // Sanitize error message (remove email addresses for privacy)
@@ -2344,7 +2346,7 @@ function createToolHandlers(state) {
2344
2346
  };
2345
2347
  }
2346
2348
  logger.info('message_search invoked', {
2347
- user_id,
2349
+ user_id: state.agentId,
2348
2350
  queryLength: query.length,
2349
2351
  channel,
2350
2352
  hasContactId: !!contact_id,
@@ -2370,7 +2372,7 @@ function createToolHandlers(state) {
2370
2372
  const response = await apiClient.get(`/api/search?${queryParams}`, reqOpts());
2371
2373
  if (!response.success) {
2372
2374
  logger.error('message_search API error', {
2373
- user_id,
2375
+ user_id: state.agentId,
2374
2376
  status: response.error.status,
2375
2377
  code: response.error.code,
2376
2378
  });
@@ -2391,7 +2393,7 @@ function createToolHandlers(state) {
2391
2393
  similarity: r.score,
2392
2394
  }));
2393
2395
  logger.debug('message_search completed', {
2394
- user_id,
2396
+ user_id: state.agentId,
2395
2397
  resultCount: messages.length,
2396
2398
  total,
2397
2399
  });
@@ -2406,10 +2408,10 @@ function createToolHandlers(state) {
2406
2408
  promptGuardUrl: config.promptGuardUrl,
2407
2409
  });
2408
2410
  if (detection.detected) {
2409
- const logDecision = injectionLogLimiter.shouldLog(user_id);
2411
+ const logDecision = injectionLogLimiter.shouldLog(state.agentId);
2410
2412
  if (logDecision.log) {
2411
2413
  logger.warn(logDecision.summary ? 'injection detection log summary for previous window' : 'potential prompt injection detected in message_search result', {
2412
- user_id,
2414
+ user_id: state.agentId,
2413
2415
  message_id: m.id,
2414
2416
  patterns: detection.patterns,
2415
2417
  source: detection.source,
@@ -2446,13 +2448,13 @@ function createToolHandlers(state) {
2446
2448
  success: true,
2447
2449
  data: {
2448
2450
  content,
2449
- details: { messages, total, user_id },
2451
+ details: { messages, total, user_id: state.agentId },
2450
2452
  },
2451
2453
  };
2452
2454
  }
2453
2455
  catch (error) {
2454
2456
  logger.error('message_search failed', {
2455
- user_id,
2457
+ user_id: state.agentId,
2456
2458
  error: error instanceof Error ? error.message : String(error),
2457
2459
  });
2458
2460
  return {
@@ -2464,7 +2466,7 @@ function createToolHandlers(state) {
2464
2466
  async thread_list(params) {
2465
2467
  const { channel, contact_id, limit = 20, } = params;
2466
2468
  logger.info('thread_list invoked', {
2467
- user_id,
2469
+ user_id: state.agentId,
2468
2470
  channel,
2469
2471
  hasContactId: !!contact_id,
2470
2472
  limit,
@@ -2483,7 +2485,7 @@ function createToolHandlers(state) {
2483
2485
  const response = await apiClient.get(`/api/search?${queryParams}`, reqOpts());
2484
2486
  if (!response.success) {
2485
2487
  logger.error('thread_list API error', {
2486
- user_id,
2488
+ user_id: state.agentId,
2487
2489
  status: response.error.status,
2488
2490
  code: response.error.code,
2489
2491
  });
@@ -2496,7 +2498,7 @@ function createToolHandlers(state) {
2496
2498
  const results = response.data.results ?? response.data.threads ?? [];
2497
2499
  const total = response.data.total ?? results.length;
2498
2500
  logger.debug('thread_list completed', {
2499
- user_id,
2501
+ user_id: state.agentId,
2500
2502
  threadCount: results.length,
2501
2503
  total,
2502
2504
  });
@@ -2525,13 +2527,13 @@ function createToolHandlers(state) {
2525
2527
  success: true,
2526
2528
  data: {
2527
2529
  content,
2528
- details: { threads: results, total, user_id },
2530
+ details: { threads: results, total, user_id: state.agentId },
2529
2531
  },
2530
2532
  };
2531
2533
  }
2532
2534
  catch (error) {
2533
2535
  logger.error('thread_list failed', {
2534
- user_id,
2536
+ user_id: state.agentId,
2535
2537
  error: error instanceof Error ? error.message : String(error),
2536
2538
  });
2537
2539
  return {
@@ -2550,7 +2552,7 @@ function createToolHandlers(state) {
2550
2552
  };
2551
2553
  }
2552
2554
  logger.info('thread_get invoked', {
2553
- user_id,
2555
+ user_id: state.agentId,
2554
2556
  thread_id,
2555
2557
  message_limit,
2556
2558
  });
@@ -2560,7 +2562,7 @@ function createToolHandlers(state) {
2560
2562
  const response = await apiClient.get(`/api/threads/${thread_id}/history?${queryParams}`, reqOpts());
2561
2563
  if (!response.success) {
2562
2564
  logger.error('thread_get API error', {
2563
- user_id,
2565
+ user_id: state.agentId,
2564
2566
  thread_id,
2565
2567
  status: response.error.status,
2566
2568
  code: response.error.code,
@@ -2572,7 +2574,7 @@ function createToolHandlers(state) {
2572
2574
  }
2573
2575
  const { thread, messages } = response.data;
2574
2576
  logger.debug('thread_get completed', {
2575
- user_id,
2577
+ user_id: state.agentId,
2576
2578
  thread_id,
2577
2579
  message_count: messages.length,
2578
2580
  });
@@ -2589,10 +2591,10 @@ function createToolHandlers(state) {
2589
2591
  promptGuardUrl: config.promptGuardUrl,
2590
2592
  });
2591
2593
  if (detection.detected) {
2592
- const logDecision = injectionLogLimiter.shouldLog(user_id);
2594
+ const logDecision = injectionLogLimiter.shouldLog(state.agentId);
2593
2595
  if (logDecision.log) {
2594
2596
  logger.warn(logDecision.summary ? 'injection detection log summary for previous window' : 'potential prompt injection detected in thread_get result', {
2595
- user_id,
2597
+ user_id: state.agentId,
2596
2598
  thread_id,
2597
2599
  message_id: m.id,
2598
2600
  patterns: detection.patterns,
@@ -2623,13 +2625,13 @@ function createToolHandlers(state) {
2623
2625
  success: true,
2624
2626
  data: {
2625
2627
  content,
2626
- details: { thread, messages, user_id },
2628
+ details: { thread, messages, user_id: state.agentId },
2627
2629
  },
2628
2630
  };
2629
2631
  }
2630
2632
  catch (error) {
2631
2633
  logger.error('thread_get failed', {
2632
- user_id,
2634
+ user_id: state.agentId,
2633
2635
  thread_id,
2634
2636
  error: error instanceof Error ? error.message : String(error),
2635
2637
  });
@@ -2648,7 +2650,7 @@ function createToolHandlers(state) {
2648
2650
  };
2649
2651
  }
2650
2652
  logger.info('relationship_set invoked', {
2651
- user_id,
2653
+ user_id: state.agentId,
2652
2654
  contactALength: contact_a.length,
2653
2655
  contactBLength: contact_b.length,
2654
2656
  relationshipLength: relationship.length,
@@ -2659,7 +2661,7 @@ function createToolHandlers(state) {
2659
2661
  contact_a,
2660
2662
  contact_b,
2661
2663
  relationship_type: relationship,
2662
- user_email: user_id, // Issue #1172: scope by user
2664
+ user_email: state.agentId, // Issue #1172: scope by user
2663
2665
  namespace: getStoreNamespace(params), // Issue #1428
2664
2666
  };
2665
2667
  if (notes) {
@@ -2683,7 +2685,7 @@ function createToolHandlers(state) {
2683
2685
  contact_a: respA,
2684
2686
  contact_b: respB,
2685
2687
  relationship_type,
2686
- user_id,
2688
+ user_id: state.agentId,
2687
2689
  },
2688
2690
  },
2689
2691
  };
@@ -2702,7 +2704,7 @@ function createToolHandlers(state) {
2702
2704
  };
2703
2705
  }
2704
2706
  logger.info('relationship_query invoked', {
2705
- user_id,
2707
+ user_id: state.agentId,
2706
2708
  contactLength: contact.length,
2707
2709
  hasTypeFilter: !!type_filter,
2708
2710
  });
@@ -2715,7 +2717,7 @@ function createToolHandlers(state) {
2715
2717
  }
2716
2718
  else {
2717
2719
  // Search for contact by name (Issue #1172: scope by user_email)
2718
- const searchParams = new URLSearchParams({ search: contact, limit: '1', user_email: user_id });
2720
+ const searchParams = new URLSearchParams({ search: contact, limit: '1', user_email: state.agentId });
2719
2721
  const searchResponse = await apiClient.get(`/api/contacts?${searchParams}`, reqOpts());
2720
2722
  if (!searchResponse.success) {
2721
2723
  return { success: false, error: searchResponse.error.message };
@@ -2727,7 +2729,7 @@ function createToolHandlers(state) {
2727
2729
  contact_id = contacts[0].id;
2728
2730
  }
2729
2731
  // Use graph traversal endpoint which returns related_contacts
2730
- const response = await apiClient.get(`/api/contacts/${contact_id}/relationships?user_email=${encodeURIComponent(user_id)}`, reqOpts());
2732
+ const response = await apiClient.get(`/api/contacts/${contact_id}/relationships?user_email=${encodeURIComponent(state.agentId)}`, reqOpts());
2731
2733
  if (!response.success) {
2732
2734
  if (response.error.code === 'NOT_FOUND') {
2733
2735
  return { success: false, error: 'Contact not found.' };
@@ -2746,7 +2748,7 @@ function createToolHandlers(state) {
2746
2748
  success: true,
2747
2749
  data: {
2748
2750
  content: `No relationships found for ${contact_name}.`,
2749
- details: { contact_id, contact_name, related_contacts: [], user_id },
2751
+ details: { contact_id, contact_name, related_contacts: [], user_id: state.agentId },
2750
2752
  },
2751
2753
  };
2752
2754
  }
@@ -2760,7 +2762,7 @@ function createToolHandlers(state) {
2760
2762
  success: true,
2761
2763
  data: {
2762
2764
  content: lines.join('\n'),
2763
- details: { contact_id, contact_name, related_contacts, user_id },
2765
+ details: { contact_id, contact_name, related_contacts, user_id: state.agentId },
2764
2766
  },
2765
2767
  };
2766
2768
  }
@@ -2785,7 +2787,7 @@ function createToolHandlers(state) {
2785
2787
  };
2786
2788
  }
2787
2789
  logger.info('file_share invoked', {
2788
- user_id,
2790
+ user_id: state.agentId,
2789
2791
  file_id: fileId,
2790
2792
  expires_in: expiresIn,
2791
2793
  max_downloads: maxDownloads,
@@ -2798,7 +2800,7 @@ function createToolHandlers(state) {
2798
2800
  const response = await apiClient.post(`/api/files/${fileId}/share`, body, reqOpts());
2799
2801
  if (!response.success) {
2800
2802
  logger.error('file_share API error', {
2801
- user_id,
2803
+ user_id: state.agentId,
2802
2804
  file_id: fileId,
2803
2805
  status: response.error.status,
2804
2806
  code: response.error.code,
@@ -2810,7 +2812,7 @@ function createToolHandlers(state) {
2810
2812
  }
2811
2813
  const { url, share_token, expires_at, filename, content_type, size_bytes } = response.data;
2812
2814
  logger.debug('file_share completed', {
2813
- user_id,
2815
+ user_id: state.agentId,
2814
2816
  file_id: fileId,
2815
2817
  share_token,
2816
2818
  expires_at,
@@ -2850,14 +2852,14 @@ function createToolHandlers(state) {
2850
2852
  filename,
2851
2853
  content_type,
2852
2854
  size_bytes,
2853
- user_id,
2855
+ user_id: state.agentId,
2854
2856
  },
2855
2857
  },
2856
2858
  };
2857
2859
  }
2858
2860
  catch (error) {
2859
2861
  logger.error('file_share failed', {
2860
- user_id,
2862
+ user_id: state.agentId,
2861
2863
  file_id: fileId,
2862
2864
  error: error instanceof Error ? error.message : String(error),
2863
2865
  });
@@ -2869,8 +2871,12 @@ function createToolHandlers(state) {
2869
2871
  },
2870
2872
  // Skill store tools: delegate to tool modules for Zod validation,
2871
2873
  // credential detection, text sanitization, and error sanitization (Issue #824)
2874
+ // NOTE: These tools capture user_id at creation time. When state.agentId is updated
2875
+ // by hook context (Issue #1644), skill store tools will use the registration-time value.
2876
+ // This is acceptable because skill store operations are scoped by API key, not user_id.
2877
+ // A follow-up issue should refactor tool modules to accept getter functions.
2872
2878
  ...(() => {
2873
- const toolOptions = { client: apiClient, logger, config, user_id };
2879
+ const toolOptions = { client: apiClient, logger, config, user_id: getAgentId() };
2874
2880
  const putTool = createSkillStorePutTool(toolOptions);
2875
2881
  const getTool = createSkillStoreGetTool(toolOptions);
2876
2882
  const listTool = createSkillStoreListTool(toolOptions);
@@ -2890,7 +2896,8 @@ function createToolHandlers(state) {
2890
2896
  })(),
2891
2897
  // Entity link tools: delegate to tool modules (Issue #1220)
2892
2898
  ...(() => {
2893
- const toolOptions = { client: apiClient, logger, config, user_id };
2899
+ const toolOptions = { client: apiClient, logger, config, user_id: getAgentId() };
2900
+ // NOTE: Same caveat as skill store tools above (Issue #1644).
2894
2901
  const setTool = createLinksSetTool(toolOptions);
2895
2902
  const queryTool = createLinksQueryTool(toolOptions);
2896
2903
  const removeTool = createLinksRemoveTool(toolOptions);
@@ -3253,7 +3260,7 @@ export const registerOpenClaw = (api) => {
3253
3260
  // user_setting.email. The email is needed for FK-constrained operations.
3254
3261
  const user_email = context.user?.email;
3255
3262
  // Store plugin state
3256
- const state = { config, logger, apiClient, user_id, user_email, resolvedNamespace, hasStaticRecall, lastNamespaceRefreshMs: 0, refreshInFlight: false };
3263
+ const state = { config, logger, apiClient, agentId: user_id, agentEmail: user_email, resolvedNamespace, hasStaticRecall, lastNamespaceRefreshMs: 0, refreshInFlight: false };
3257
3264
  // Create tool handlers
3258
3265
  const handlers = createToolHandlers(state);
3259
3266
  // Register all 30 tools with correct OpenClaw Gateway execute signature
@@ -3766,7 +3773,7 @@ export const registerOpenClaw = (api) => {
3766
3773
  client: apiClient,
3767
3774
  logger,
3768
3775
  config,
3769
- user_id,
3776
+ getAgentId: () => state.agentId,
3770
3777
  timeoutMs: HOOK_TIMEOUT_MS,
3771
3778
  });
3772
3779
  /**
@@ -3774,7 +3781,29 @@ export const registerOpenClaw = (api) => {
3774
3781
  * performs semantic memory search, and returns { prependContext } to inject
3775
3782
  * relevant memories into the conversation.
3776
3783
  */
3777
- const beforeAgentStartHandler = async (event, _ctx) => {
3784
+ const beforeAgentStartHandler = async (event, ctx) => {
3785
+ // Issue #1655: Detect concurrent session conflict
3786
+ if (state.activeSessionKey && ctx.sessionKey && state.activeSessionKey !== ctx.sessionKey) {
3787
+ logger.warn('Concurrent session detected — agent identity may be stale', {
3788
+ previousSession: state.activeSessionKey,
3789
+ newSession: ctx.sessionKey,
3790
+ previousAgentId: state.agentId,
3791
+ });
3792
+ }
3793
+ state.activeSessionKey = ctx.sessionKey;
3794
+ // Issue #1644: resolve agent ID from hook context and update state
3795
+ const resolvedId = resolveAgentId(ctx, config.agentId, state.agentId);
3796
+ if (resolvedId !== state.agentId) {
3797
+ const previousId = state.agentId;
3798
+ state.agentId = resolvedId;
3799
+ state.resolvedNamespace = resolveNamespaceConfig(config.namespace, resolvedId);
3800
+ logger.info('Agent ID resolved from hook context', {
3801
+ previousId,
3802
+ resolvedId,
3803
+ defaultNamespace: state.resolvedNamespace.default,
3804
+ recallNamespaces: state.resolvedNamespace.recall,
3805
+ });
3806
+ }
3778
3807
  logger.debug('Auto-recall hook triggered', {
3779
3808
  promptLength: event.prompt?.length ?? 0,
3780
3809
  });
@@ -3811,14 +3840,24 @@ export const registerOpenClaw = (api) => {
3811
3840
  client: apiClient,
3812
3841
  logger,
3813
3842
  config,
3814
- user_id,
3843
+ getAgentId: () => state.agentId,
3815
3844
  timeoutMs: HOOK_TIMEOUT_MS * 2, // Allow more time for capture (10s)
3816
3845
  });
3817
3846
  /**
3818
3847
  * agent_end handler: Extracts messages from the completed conversation,
3819
3848
  * filters sensitive content, and posts to the capture API for memory storage.
3820
3849
  */
3821
- const agentEndHandler = async (event, _ctx) => {
3850
+ const agentEndHandler = async (event, ctx) => {
3851
+ // Issue #1644: ensure agent ID is resolved even if before_agent_start didn't fire
3852
+ const resolvedId = resolveAgentId(ctx, config.agentId, state.agentId);
3853
+ if (resolvedId !== state.agentId) {
3854
+ state.agentId = resolvedId;
3855
+ state.resolvedNamespace = resolveNamespaceConfig(config.namespace, resolvedId);
3856
+ logger.info('Agent ID resolved from agent_end context', {
3857
+ resolvedId,
3858
+ defaultNamespace: state.resolvedNamespace.default,
3859
+ });
3860
+ }
3822
3861
  logger.debug('Auto-capture hook triggered', {
3823
3862
  message_count: event.messages?.length ?? 0,
3824
3863
  success: event.success,
@@ -3843,6 +3882,8 @@ export const registerOpenClaw = (api) => {
3843
3882
  error: error instanceof Error ? error.message : String(error),
3844
3883
  });
3845
3884
  }
3885
+ // Issue #1655: Clear session key after agent ends
3886
+ state.activeSessionKey = undefined;
3846
3887
  };
3847
3888
  if (typeof api.on === 'function') {
3848
3889
  // Modern registration: api.on('agent_end', handler)
@@ -3880,7 +3921,7 @@ export const registerOpenClaw = (api) => {
3880
3921
  await autoLinkInboundMessage({
3881
3922
  client: apiClient,
3882
3923
  logger,
3883
- user_id,
3924
+ getAgentId: () => state.agentId,
3884
3925
  message: {
3885
3926
  thread_id: event.thread_id,
3886
3927
  senderEmail: event.senderEmail ?? (event.sender?.includes('@') ? event.sender : undefined),
@@ -3907,14 +3948,14 @@ export const registerOpenClaw = (api) => {
3907
3948
  const gatewayMethods = createGatewayMethods({
3908
3949
  logger,
3909
3950
  apiClient,
3910
- user_id,
3951
+ getAgentId: () => state.agentId,
3911
3952
  });
3912
3953
  registerGatewayRpcMethods(api, gatewayMethods);
3913
3954
  // Register OAuth Gateway RPC methods (Issue #1054)
3914
3955
  const oauthGatewayMethods = createOAuthGatewayMethods({
3915
3956
  logger,
3916
3957
  apiClient,
3917
- user_id,
3958
+ getAgentId: () => state.agentId,
3918
3959
  });
3919
3960
  registerOAuthGatewayRpcMethods(api, oauthGatewayMethods);
3920
3961
  // Register background notification service (Issue #325)
@@ -3935,7 +3976,7 @@ export const registerOpenClaw = (api) => {
3935
3976
  const notificationService = createNotificationService({
3936
3977
  logger,
3937
3978
  apiClient,
3938
- user_id,
3979
+ getAgentId: () => state.agentId,
3939
3980
  events: eventEmitter,
3940
3981
  config: {
3941
3982
  enabled: config.autoRecall, // Only enable if auto-recall is enabled
@@ -3950,7 +3991,7 @@ export const registerOpenClaw = (api) => {
3950
3991
  .description('Show plugin status and statistics')
3951
3992
  .action(async () => {
3952
3993
  try {
3953
- const response = await apiClient.get('/api/health', { user_id, user_email });
3994
+ const response = await apiClient.get('/api/health', { user_id: state.agentId, user_email: state.agentEmail });
3954
3995
  if (response.success) {
3955
3996
  console.log('Plugin Status: Connected');
3956
3997
  }
@@ -3981,10 +4022,15 @@ export const registerOpenClaw = (api) => {
3981
4022
  }
3982
4023
  });
3983
4024
  });
4025
+ // Issue #1644: warn if agent ID is "unknown" at registration (will be resolved from hook context later)
4026
+ if (context.agent.agentId === 'unknown' && !config.agentId) {
4027
+ logger.warn('Agent ID not available at registration time — will resolve from hook context. ' +
4028
+ 'Set config.agentId for explicit override. (Issue #1644)');
4029
+ }
3984
4030
  logger.info('OpenClaw Projects plugin registered', {
3985
4031
  agentId: context.agent.agentId,
3986
4032
  sessionId: context.session.sessionId,
3987
- user_id,
4033
+ user_id: state.agentId,
3988
4034
  toolCount: tools.length,
3989
4035
  config: redactConfig(config),
3990
4036
  });