a2acalling 0.6.51 → 0.6.52

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.
@@ -1,5 +1,4 @@
1
1
  use std::sync::atomic::{AtomicBool, AtomicU16, Ordering};
2
- use std::sync::Arc;
3
2
  use std::time::Duration;
4
3
  use tauri::{Emitter, Manager};
5
4
 
@@ -21,8 +20,8 @@ pub fn set_connected(port: u16) {
21
20
 
22
21
  /// Start background health check loop — emits "server-status" events
23
22
  pub fn start_health_monitor(app: tauri::AppHandle) {
24
- let handle = Arc::new(app);
25
- tokio::spawn(async move {
23
+ tauri::async_runtime::spawn(async move {
24
+ let handle = app;
26
25
  loop {
27
26
  tokio::time::sleep(Duration::from_secs(3)).await;
28
27
 
@@ -75,7 +75,7 @@ fn process_dashboard_event(app: &tauri::AppHandle, raw: &str) {
75
75
 
76
76
  /// Connect to server-driven dashboard SSE and map events to native notifications.
77
77
  pub fn start_event_stream_listener(app: tauri::AppHandle) {
78
- tokio::spawn(async move {
78
+ tauri::async_runtime::spawn(async move {
79
79
  // Wait for initial discovery attempt.
80
80
  tokio::time::sleep(Duration::from_secs(2)).await;
81
81
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.51",
3
+ "version": "0.6.52",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -4,7 +4,14 @@
4
4
  * Spawns `claude` CLI processes for real LLM-powered A2A conversations
5
5
  * as an alternative to OpenClaw for A2A conversations.
6
6
  *
7
- * Uses `claude -p` (print mode) with `--resume` for multi-turn context continuity.
7
+ * Design decision (A2A-29):
8
+ * We intentionally run Claude turns in stateless one-shot mode instead of `--resume`.
9
+ * In production we observed intermittent hangs during nested Claude startup/restore.
10
+ * Stateless calls cost more tokens but are operationally safer under load.
11
+ *
12
+ * Permissioning is still enforced:
13
+ * `--allowedTools` is derived per request from token capabilities + allowed topics.
14
+ * We do not hardcode one universal allowlist anymore.
8
15
  */
9
16
 
10
17
  const { execSync, spawn } = require('child_process');
@@ -14,6 +21,8 @@ const { HARD_FALLBACK_TURN_TIMEOUT_MS } = require('./turn-timeout');
14
21
  const logger = createLogger({ component: 'a2a.claude-subagent' });
15
22
 
16
23
  const A2A_RESPONSE_REGEX = /<a2a_response>\s*([\s\S]*?)\s*<\/a2a_response>/i;
24
+ const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-5-20250929';
25
+ const LEGACY_DEFAULT_TOOLS = ['Bash(readonly)', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
17
26
 
18
27
  /**
19
28
  * Check if `claude` CLI is available in PATH.
@@ -289,11 +298,144 @@ function extractResultFromJson(stdout) {
289
298
  }
290
299
  }
291
300
 
301
+ function normalizePermissionList(values) {
302
+ if (!Array.isArray(values)) return [];
303
+ return values
304
+ .map(v => String(v || '').trim().toLowerCase())
305
+ .filter(Boolean);
306
+ }
307
+
308
+ function hasPermissionMatch(values, key) {
309
+ if (!key) return false;
310
+ return values.some(value => value === key || value.startsWith(`${key}.`));
311
+ }
312
+
313
+ /**
314
+ * Resolve Claude tool allowlist from token-derived permissions.
315
+ *
316
+ * Notes:
317
+ * - We preserve legacy behavior when no permission context is provided, because
318
+ * outbound CLI flows may run without token metadata.
319
+ * - When permissions are present, we derive tools deterministically so runtime
320
+ * allowlists remain variable and auditable per token.
321
+ */
322
+ function resolveClaudeAllowedTools({ capabilities = [], allowedTopics = [] } = {}) {
323
+ const normalizedCaps = normalizePermissionList(capabilities);
324
+ const normalizedTopics = normalizePermissionList(allowedTopics);
325
+ const hasPermissionContext = normalizedCaps.length > 0 || normalizedTopics.length > 0;
326
+
327
+ if (!hasPermissionContext) {
328
+ return [...LEGACY_DEFAULT_TOOLS];
329
+ }
330
+
331
+ const hasContextRead = hasPermissionMatch(normalizedCaps, 'context-read')
332
+ || normalizedTopics.includes('chat');
333
+ const hasSearch = hasPermissionMatch(normalizedCaps, 'search')
334
+ || normalizedTopics.includes('search');
335
+ const hasToolsRead = hasPermissionMatch(normalizedCaps, 'tools')
336
+ || hasPermissionMatch(normalizedCaps, 'tools-read')
337
+ || normalizedTopics.includes('tools');
338
+ const hasToolsWrite = hasPermissionMatch(normalizedCaps, 'tools-write')
339
+ || hasPermissionMatch(normalizedCaps, 'tools.write')
340
+ || normalizedTopics.includes('tools-write')
341
+ || normalizedTopics.includes('tools.write');
342
+
343
+ const tools = [];
344
+
345
+ // Keep read-only introspection available for context-aware tiers.
346
+ if (hasContextRead || hasSearch || hasToolsRead || hasToolsWrite) {
347
+ tools.push('Read', 'Grep', 'Glob');
348
+ }
349
+
350
+ // Web tools are explicitly tied to search-style permissions.
351
+ if (hasSearch) {
352
+ tools.push('WebSearch', 'WebFetch');
353
+ }
354
+
355
+ // Shell access is gated behind tool permissions, with explicit writable opt-in.
356
+ if (hasToolsWrite) {
357
+ tools.unshift('Bash');
358
+ } else if (hasToolsRead) {
359
+ tools.unshift('Bash(readonly)');
360
+ }
361
+
362
+ // Fail closed to read-only file inspection if metadata is custom/unknown.
363
+ if (tools.length === 0) {
364
+ return ['Read', 'Grep', 'Glob'];
365
+ }
366
+
367
+ return tools;
368
+ }
369
+
370
+ function buildClaudeToolArg(allowedTools) {
371
+ return Array.isArray(allowedTools) ? allowedTools.join(' ').trim() : '';
372
+ }
373
+
374
+ function parseSummaryPayload(resultText) {
375
+ const text = String(resultText || '').trim();
376
+ if (!text) return null;
377
+
378
+ // Backwards-compatible: older prompts wrapped JSON in <a2a_response>.
379
+ const tagged = text.match(A2A_RESPONSE_REGEX);
380
+ if (tagged && tagged[1]) {
381
+ try {
382
+ return JSON.parse(tagged[1].trim());
383
+ } catch (err) {
384
+ logger.warn('Failed to parse tagged summary JSON', {
385
+ event: 'subagent_summary_tag_parse_failed',
386
+ error: err
387
+ });
388
+ }
389
+ }
390
+
391
+ // Preferred path for unified summary prompt: direct JSON object.
392
+ try {
393
+ const parsed = JSON.parse(text);
394
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
395
+ return parsed;
396
+ }
397
+ } catch (err) {
398
+ logger.debug('Summary result is not direct JSON; falling back to plain text summary', {
399
+ event: 'subagent_summary_raw_fallback',
400
+ data: { output_length: text.length }
401
+ });
402
+ }
403
+
404
+ return null;
405
+ }
406
+
407
+ function summarizeFromPayload(payload, fallbackText) {
408
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
409
+ return null;
410
+ }
411
+
412
+ // Native A2A summary payload shape.
413
+ if (typeof payload.summary === 'string' || typeof payload.ownerSummary === 'string') {
414
+ return {
415
+ summary: payload.summary || payload.message || fallbackText || '',
416
+ ownerSummary: payload.ownerSummary || payload.summary || payload.message || fallbackText || '',
417
+ actionItems: Array.isArray(payload.actionItems) ? payload.actionItems : [],
418
+ flags: Array.isArray(payload.flags) ? payload.flags : []
419
+ };
420
+ }
421
+
422
+ // Unified summary schema shape (headline/assessment/nextSteps).
423
+ if (typeof payload.headline === 'string') {
424
+ return {
425
+ summary: payload.headline,
426
+ ownerSummary: typeof payload.assessment === 'string' ? payload.assessment : payload.headline,
427
+ actionItems: Array.isArray(payload.nextSteps) ? payload.nextSteps : [],
428
+ flags: []
429
+ };
430
+ }
431
+
432
+ return null;
433
+ }
434
+
292
435
  /**
293
436
  * Run a single turn of the Claude subagent.
294
437
  *
295
438
  * @param {Object} options
296
- * @param {string} options.sessionId - Conversation session ID (used for --resume on turn 2+)
297
439
  * @param {string} options.systemPrompt - System prompt (used on turn 1 only)
298
440
  * @param {string} options.turnMessage - The inbound message from the remote agent
299
441
  * @param {number} options.turn - Current turn number (1-based)
@@ -303,12 +445,14 @@ function extractResultFromJson(stdout) {
303
445
  * @param {Array} options.activeThreads - Active conversation threads
304
446
  * @param {Array} options.candidateCollaborations - Candidate collaboration ideas
305
447
  * @param {boolean} options.closeSignal - Whether close has been signaled
448
+ * @param {Array<string>} [options.capabilities] - Token capabilities (permission source of truth)
449
+ * @param {Array<string>} [options.allowedTopics] - Token allowed topics (permission source of truth)
450
+ * @param {function} [options.spawnFn] - Injectable process runner for tests
306
451
  * @param {number} [options.timeoutMs=300000] - Timeout in milliseconds
307
- * @returns {Promise<{ message: string, statePatch: object|null, flags: array, sessionId: string }>}
452
+ * @returns {Promise<{ message: string, statePatch: object|null, flags: array }>}
308
453
  */
309
454
  async function runClaudeTurn(options) {
310
455
  const {
311
- sessionId,
312
456
  systemPrompt,
313
457
  turnMessage,
314
458
  turn = 1,
@@ -318,6 +462,9 @@ async function runClaudeTurn(options) {
318
462
  activeThreads = [],
319
463
  candidateCollaborations = [],
320
464
  closeSignal = false,
465
+ capabilities = [],
466
+ allowedTopics = [],
467
+ spawnFn = spawnClaude,
321
468
  timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS
322
469
  } = options;
323
470
 
@@ -333,29 +480,21 @@ async function runClaudeTurn(options) {
333
480
  });
334
481
 
335
482
  const startAt = Date.now();
336
- const allowedTools = 'Bash(readonly) Read Grep Glob WebSearch WebFetch';
337
-
338
- let args;
339
- if (turn === 1 || !sessionId) {
340
- // First turn: create new session
341
- args = [
342
- '-p',
343
- '--output-format', 'json',
344
- '--system-prompt', systemPrompt,
345
- '--allowedTools', allowedTools,
346
- '--model', 'claude-sonnet-4-5-20250929',
347
- turnPrompt
348
- ];
349
- } else {
350
- // Subsequent turns: resume existing session
351
- args = [
352
- '-p',
353
- '--output-format', 'json',
354
- '--resume', sessionId,
355
- '--allowedTools', allowedTools,
356
- turnPrompt
357
- ];
483
+ const allowedTools = resolveClaudeAllowedTools({ capabilities, allowedTopics });
484
+ const allowedToolsArg = buildClaudeToolArg(allowedTools);
485
+ const args = [
486
+ '-p',
487
+ '--output-format', 'json',
488
+ '--system-prompt', systemPrompt,
489
+ '--model', DEFAULT_CLAUDE_MODEL
490
+ ];
491
+
492
+ // We always provide --allowedTools explicitly so token permissioning stays
493
+ // enforced in Claude mode even after moving to stateless turns.
494
+ if (allowedToolsArg) {
495
+ args.push('--allowedTools', allowedToolsArg);
358
496
  }
497
+ args.push(turnPrompt);
359
498
 
360
499
  logger.debug('Spawning Claude subagent turn', {
361
500
  event: 'subagent_turn_start',
@@ -363,13 +502,14 @@ async function runClaudeTurn(options) {
363
502
  turn,
364
503
  max_turns: maxTurns,
365
504
  phase,
366
- is_resume: turn > 1 && Boolean(sessionId),
505
+ is_stateless: true,
506
+ allowed_tools: allowedTools,
367
507
  timeout_ms: timeoutMs
368
508
  }
369
509
  });
370
510
 
371
- const { stdout } = await spawnClaude(args, timeoutMs);
372
- const { result, sessionId: newSessionId } = extractResultFromJson(stdout);
511
+ const { stdout } = await spawnFn(args, timeoutMs);
512
+ const { result } = extractResultFromJson(stdout);
373
513
  const parsed = parseSubagentResponse(result);
374
514
 
375
515
  logger.debug('Claude subagent turn completed', {
@@ -379,91 +519,87 @@ async function runClaudeTurn(options) {
379
519
  duration_ms: Date.now() - startAt,
380
520
  message_length: parsed.message.length,
381
521
  has_state_patch: Boolean(parsed.statePatch),
382
- flag_count: parsed.flags.length,
383
- session_id: newSessionId || sessionId
522
+ flag_count: parsed.flags.length
384
523
  }
385
524
  });
386
525
 
387
526
  return {
388
527
  message: parsed.message,
389
528
  statePatch: parsed.statePatch,
390
- flags: parsed.flags,
391
- sessionId: newSessionId || sessionId
529
+ flags: parsed.flags
392
530
  };
393
531
  }
394
532
 
395
533
  /**
396
- * Run a summary turn using the Claude subagent session.
534
+ * Run a summary turn in stateless Claude mode.
397
535
  *
398
- * @param {string} sessionId - Session ID to resume
399
- * @param {string} reason - Why the conversation is ending
536
+ * @param {Object} options
537
+ * @param {string} options.prompt - Unified summary prompt
538
+ * @param {string} [options.reason] - Why the conversation is ending
539
+ * @param {Array<string>} [options.capabilities] - Token capabilities for summary turn tooling
540
+ * @param {Array<string>} [options.allowedTopics] - Token allowed topics for summary turn tooling
541
+ * @param {function} [options.spawnFn] - Injectable process runner for tests
400
542
  * @param {number} [timeoutMs=300000] - Timeout in milliseconds
401
543
  * @returns {Promise<{ summary: string, ownerSummary: string, actionItems: array, flags: array }>}
402
544
  */
403
- async function runClaudeSummary(sessionId, reason, timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS) {
404
- if (!sessionId) {
405
- throw new Error('Cannot summarize without a session ID');
406
- }
407
-
408
- const summaryPrompt = `The conversation is ending. Reason: ${reason || 'max turns reached'}.
545
+ async function runClaudeSummary(options = {}) {
546
+ const {
547
+ prompt,
548
+ reason,
549
+ capabilities = [],
550
+ allowedTopics = [],
551
+ spawnFn = spawnClaude,
552
+ timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS
553
+ } = options;
409
554
 
410
- Provide a structured summary. Respond with ONLY a JSON block:
555
+ const summaryPrompt = String(prompt || '').trim();
556
+ if (!summaryPrompt) {
557
+ throw new Error('Cannot summarize without a prompt');
558
+ }
411
559
 
412
- <a2a_response>
413
- {
414
- "message": "Brief 1-2 sentence summary of the conversation.",
415
- "statePatch": {"phase": "close", "closeSignal": true},
416
- "flags": [],
417
- "summary": "Detailed summary for the conversation record.",
418
- "ownerSummary": "Summary written for the owner highlighting key findings and opportunities.",
419
- "actionItems": ["Specific follow-up item 1", "Specific follow-up item 2"]
420
- }
421
- </a2a_response>`;
560
+ const allowedTools = resolveClaudeAllowedTools({ capabilities, allowedTopics });
561
+ const allowedToolsArg = buildClaudeToolArg(allowedTools);
422
562
 
423
563
  const args = [
424
564
  '-p',
425
565
  '--output-format', 'json',
426
- '--resume', sessionId,
427
- summaryPrompt
566
+ '--model', DEFAULT_CLAUDE_MODEL,
428
567
  ];
429
568
 
569
+ if (allowedToolsArg) {
570
+ args.push('--allowedTools', allowedToolsArg);
571
+ }
572
+ args.push(
573
+ '--append-system-prompt',
574
+ `Conversation summary mode. Reason: ${reason || 'conversation ended'}. Return only structured summary JSON.`,
575
+ summaryPrompt
576
+ );
577
+
430
578
  const startAt = Date.now();
431
579
 
432
580
  logger.debug('Spawning Claude summary', {
433
581
  event: 'subagent_summary_start',
434
- data: { session_id: sessionId, reason }
582
+ data: {
583
+ reason: reason || 'conversation ended',
584
+ allowed_tools: allowedTools
585
+ }
435
586
  });
436
587
 
437
- const { stdout } = await spawnClaude(args, timeoutMs);
588
+ const { stdout } = await spawnFn(args, timeoutMs);
438
589
  const { result } = extractResultFromJson(stdout);
439
-
440
- // Try to extract structured summary from <a2a_response>
441
- const match = result.match(A2A_RESPONSE_REGEX);
442
- if (match) {
443
- try {
444
- const parsed = JSON.parse(match[1].trim());
445
- logger.debug('Claude summary completed', {
446
- event: 'subagent_summary_complete',
447
- data: {
448
- session_id: sessionId,
449
- duration_ms: Date.now() - startAt,
450
- has_summary: Boolean(parsed.summary),
451
- action_item_count: Array.isArray(parsed.actionItems) ? parsed.actionItems.length : 0
452
- }
453
- });
454
-
455
- return {
456
- summary: parsed.summary || parsed.message || result.replace(A2A_RESPONSE_REGEX, '').trim(),
457
- ownerSummary: parsed.ownerSummary || parsed.summary || parsed.message || '',
458
- actionItems: Array.isArray(parsed.actionItems) ? parsed.actionItems : [],
459
- flags: Array.isArray(parsed.flags) ? parsed.flags : []
460
- };
461
- } catch (err) {
462
- logger.warn('Failed to parse summary JSON', {
463
- event: 'subagent_summary_parse_failed',
464
- error: err
465
- });
466
- }
590
+ const summaryPayload = parseSummaryPayload(result);
591
+ const parsedSummary = summarizeFromPayload(summaryPayload, result.trim());
592
+
593
+ if (parsedSummary) {
594
+ logger.debug('Claude summary completed', {
595
+ event: 'subagent_summary_complete',
596
+ data: {
597
+ duration_ms: Date.now() - startAt,
598
+ has_summary: Boolean(parsedSummary.summary),
599
+ action_item_count: parsedSummary.actionItems.length
600
+ }
601
+ });
602
+ return parsedSummary;
467
603
  }
468
604
 
469
605
  // Fallback: use raw text as summary
@@ -480,6 +616,7 @@ module.exports = {
480
616
  isClaudeAvailable,
481
617
  buildSubagentSystemPrompt,
482
618
  buildTurnPrompt,
619
+ resolveClaudeAllowedTools,
483
620
  runClaudeTurn,
484
621
  runClaudeSummary,
485
622
  parseSubagentResponse
@@ -128,6 +128,10 @@ class ConversationDriver {
128
128
  this.maxTurns = options.maxTurns || 30;
129
129
  this.onTurn = options.onTurn || null;
130
130
  this.tier = options.tier || 'public';
131
+ // Optional permission envelope for runtimes (primarily Claude mode).
132
+ // If provided by caller, this keeps tool allowlists variable per token/profile.
133
+ this.capabilities = Array.isArray(options.capabilities) ? options.capabilities : [];
134
+ this.allowedTopics = Array.isArray(options.allowedTopics) ? options.allowedTopics : [];
131
135
  this.summarizer = options.summarizer || null;
132
136
  this.ownerContext = options.ownerContext || {};
133
137
  this.claudeMode = options.runtime?.mode === 'claude';
@@ -407,7 +411,10 @@ class ConversationDriver {
407
411
  tier: this.tier,
408
412
  ownerName: this.agentContext.owner,
409
413
  agentName: this.agentContext.name,
410
- roleContext: 'You initiated this call.'
414
+ roleContext: 'You initiated this call.',
415
+ capabilities: this.capabilities,
416
+ allowedTopics: this.allowedTopics,
417
+ allowed_topics: this.allowedTopics
411
418
  };
412
419
  if (this.claudeMode) {
413
420
  contextPayload.turnCount = turn + 1;
@@ -149,7 +149,9 @@ function createRuntimeAdapter(options = {}) {
149
149
  }
150
150
  });
151
151
 
152
- // Claude subagent session tracking
152
+ // Claude state tracking.
153
+ // Design decision (A2A-29): we keep per-conversation state for prompt/metadata
154
+ // continuity, but Claude execution itself is stateless (no `--resume`).
153
155
  const claudeSessions = new Map();
154
156
 
155
157
  async function runClaudeTurnAdapter({ sessionId, message, caller, context, timeoutMs }) {
@@ -180,7 +182,18 @@ function createRuntimeAdapter(options = {}) {
180
182
  roleContext: context?.roleContext || ''
181
183
  });
182
184
 
183
- session = { claudeSessionId: null, systemPrompt, turnCount: 0, lastMeta: null };
185
+ session = {
186
+ systemPrompt,
187
+ turnCount: 0,
188
+ lastMeta: null,
189
+ // Keep a permission snapshot so summary runs with the same policy envelope.
190
+ permissionSnapshot: {
191
+ capabilities: Array.isArray(context?.capabilities) ? context.capabilities : [],
192
+ allowedTopics: Array.isArray(context?.allowedTopics || context?.allowed_topics)
193
+ ? (context?.allowedTopics || context?.allowed_topics)
194
+ : []
195
+ }
196
+ };
184
197
  claudeSessions.set(sessionId, session);
185
198
  }
186
199
 
@@ -199,7 +212,6 @@ function createRuntimeAdapter(options = {}) {
199
212
  });
200
213
 
201
214
  const result = await invokeClaudeTurn({
202
- sessionId: session.claudeSessionId,
203
215
  systemPrompt: session.systemPrompt,
204
216
  turnMessage: message,
205
217
  turn: session.turnCount,
@@ -209,12 +221,21 @@ function createRuntimeAdapter(options = {}) {
209
221
  activeThreads: context?.activeThreads || [],
210
222
  candidateCollaborations: context?.candidateCollaborations || [],
211
223
  closeSignal: context?.closeSignal || false,
224
+ capabilities: Array.isArray(context?.capabilities)
225
+ ? context.capabilities
226
+ : (session.permissionSnapshot?.capabilities || []),
227
+ allowedTopics: Array.isArray(context?.allowedTopics || context?.allowed_topics)
228
+ ? (context?.allowedTopics || context?.allowed_topics)
229
+ : (session.permissionSnapshot?.allowedTopics || []),
212
230
  timeoutMs: timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
213
231
  });
214
232
 
215
- // Store session ID from first turn for subsequent --resume
216
- if (result.sessionId) {
217
- session.claudeSessionId = result.sessionId;
233
+ // Update permission snapshot if the caller supplied explicit context this turn.
234
+ if (Array.isArray(context?.capabilities)) {
235
+ session.permissionSnapshot.capabilities = context.capabilities;
236
+ }
237
+ if (Array.isArray(context?.allowedTopics || context?.allowed_topics)) {
238
+ session.permissionSnapshot.allowedTopics = context?.allowedTopics || context?.allowed_topics;
218
239
  }
219
240
 
220
241
  // Store flags/state for retrieval via getLastTurnMeta
@@ -385,20 +406,28 @@ function createRuntimeAdapter(options = {}) {
385
406
  const requestId = callerInfo?.request_id || callerInfo?.requestId;
386
407
  const effectiveConversationId = conversationId || callerInfo?.conversation_id || callerInfo?.conversationId;
387
408
 
388
- // Claude mode: use the subagent session for summarization
409
+ // Claude mode: stateless summary invocation (no session restore dependency).
389
410
  if (modeInfo.mode === 'claude') {
390
411
  const session = claudeSessions.get(sessionId);
391
- if (session?.claudeSessionId) {
392
- const result = await runClaudeSummary(
393
- session.claudeSessionId,
394
- 'conversation ended',
395
- timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
396
- );
397
- if (result && result.summary) {
398
- return result;
399
- }
412
+ const capabilities = session?.permissionSnapshot?.capabilities
413
+ || callerInfo?.capabilities
414
+ || [];
415
+ const allowedTopics = session?.permissionSnapshot?.allowedTopics
416
+ || callerInfo?.allowedTopics
417
+ || callerInfo?.allowed_topics
418
+ || [];
419
+
420
+ const result = await runClaudeSummary({
421
+ prompt,
422
+ reason: 'conversation ended',
423
+ capabilities,
424
+ allowedTopics,
425
+ timeoutMs: timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
426
+ });
427
+ if (result && result.summary) {
428
+ return result;
400
429
  }
401
- throw new Error('Claude summary session not available or returned empty result');
430
+ throw new Error('Claude summary returned empty result');
402
431
  }
403
432
 
404
433
  if (modeInfo.mode !== 'openclaw') {
package/src/routes/a2a.js CHANGED
@@ -390,6 +390,9 @@ function createRoutes(options = {}) {
390
390
  if (monitor) {
391
391
  monitor.trackActivity(a2aContext.conversation_id, {
392
392
  ...sanitizedCaller,
393
+ tier: validation.tier,
394
+ capabilities: validation.capabilities,
395
+ allowed_topics: validation.allowed_topics,
393
396
  trace_id: traceId,
394
397
  request_id: requestId
395
398
  });
package/src/server.js CHANGED
@@ -621,7 +621,9 @@ async function callAgent(message, a2aContext) {
621
621
  conversationId,
622
622
  tier: tierInfo,
623
623
  ownerName: agentContext.owner,
624
+ capabilities: Array.isArray(a2aContext.capabilities) ? a2aContext.capabilities : [],
624
625
  allowedTopics: a2aContext.allowed_topics || [],
626
+ allowed_topics: a2aContext.allowed_topics || [],
625
627
  timeoutMs: runtime.mode === 'claude' ? claudeTurnTimeoutMs : 65000,
626
628
  traceId,
627
629
  requestId