a2acalling 0.6.48 → 0.6.50

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,6 +1,7 @@
1
1
  const state = {
2
2
  settings: null,
3
3
  dashboardStatus: null,
4
+ autoUpdate: null,
4
5
  callbookDevices: [],
5
6
  contacts: [],
6
7
  selectedContactId: null,
@@ -63,6 +64,20 @@ function esc(text) {
63
64
  .replaceAll("'", ''');
64
65
  }
65
66
 
67
+ function formatUpdaterState(stateValue) {
68
+ const state = String(stateValue || '').trim() || 'unknown';
69
+ return state.replaceAll('_', ' ');
70
+ }
71
+
72
+ function updaterPillClass(stateValue) {
73
+ const state = String(stateValue || '').trim();
74
+ if (state === 'failed') return 'err';
75
+ if (state === 'waiting_for_safe_restart' || state === 'checking' || state === 'downloading' || state === 'applying' || state === 'restarting') {
76
+ return 'warn';
77
+ }
78
+ return 'ok';
79
+ }
80
+
66
81
  async function copyText(value) {
67
82
  const text = String(value || '');
68
83
  if (!text) return false;
@@ -284,7 +299,7 @@ function bindContactsActions() {
284
299
  if (!urlEl || !serverNameEl) return;
285
300
  if (mineEl && !mineEl.checked) return;
286
301
  if (serverNameEl.value.trim()) return;
287
- const match = String(urlEl.value || '').trim().match(/^(?:a2a|oclaw):\\/\\/([^/]+)\\//);
302
+ const match = String(urlEl.value || '').trim().match(/^(?:a2a|oclaw):\/\/([^/]+)\//);
288
303
  if (match && match[1]) {
289
304
  serverNameEl.value = match[1];
290
305
  }
@@ -1035,14 +1050,59 @@ function renderCallbookStatus() {
1035
1050
  `;
1036
1051
  }
1037
1052
 
1053
+ function renderAutoUpdateStatus() {
1054
+ const el = document.getElementById('auto-update-status');
1055
+ const toggleBtn = document.getElementById('auto-update-toggle');
1056
+ if (!el) return;
1057
+
1058
+ const au = state.autoUpdate;
1059
+ if (!au) {
1060
+ el.textContent = 'Loading…';
1061
+ if (toggleBtn) toggleBtn.disabled = true;
1062
+ return;
1063
+ }
1064
+
1065
+ const stateText = formatUpdaterState(au.state);
1066
+ const pillClass = updaterPillClass(au.state);
1067
+ const enabled = Boolean(au.enabled);
1068
+ const intervalSec = Number.isFinite(au.interval_ms) ? Math.floor(au.interval_ms / 1000) : null;
1069
+
1070
+ el.innerHTML = `
1071
+ <div><strong>Status:</strong> <span class="status-pill ${pillClass}">${esc(stateText)}</span></div>
1072
+ <div><strong>Enabled:</strong> ${enabled ? 'yes' : 'no'}</div>
1073
+ <div><strong>Current version:</strong> <span class="mono">${esc(au.current_version || '-')}</span></div>
1074
+ <div><strong>Latest version:</strong> <span class="mono">${esc(au.latest_version || '-')}</span></div>
1075
+ <div><strong>Target version:</strong> <span class="mono">${esc(au.target_version || '-')}</span></div>
1076
+ <div><strong>Active calls:</strong> ${esc(String(au.active_calls || 0))}</div>
1077
+ <div><strong>Interval:</strong> ${intervalSec === null ? '-' : `${intervalSec}s`}</div>
1078
+ <div><strong>Last checked:</strong> ${esc(fmtDate(au.last_checked_at))}</div>
1079
+ <div><strong>Last success:</strong> ${esc(fmtDate(au.last_success_at))}</div>
1080
+ ${au.defer_reason ? `<div><strong>Deferred:</strong> ${esc(au.defer_reason)}</div>` : ''}
1081
+ ${au.last_error ? `<div><strong>Error:</strong> <span class="mono">${esc(au.last_error)}</span></div>` : ''}
1082
+ `;
1083
+
1084
+ if (toggleBtn) {
1085
+ toggleBtn.disabled = false;
1086
+ toggleBtn.textContent = enabled ? 'Disable auto-update' : 'Enable auto-update';
1087
+ }
1088
+ }
1089
+
1038
1090
  async function loadDashboardStatus(refreshIp = false) {
1039
1091
  const payload = await request(`/status${refreshIp ? '?refresh_ip=true' : ''}`);
1040
1092
  state.dashboardStatus = payload;
1093
+ state.autoUpdate = payload.auto_update || state.autoUpdate;
1041
1094
  renderCallbookStatus();
1095
+ renderAutoUpdateStatus();
1042
1096
  renderContacts();
1043
1097
  renderContactDetail();
1044
1098
  }
1045
1099
 
1100
+ async function loadAutoUpdateStatus() {
1101
+ const payload = await request('/update/status');
1102
+ state.autoUpdate = payload.auto_update || null;
1103
+ renderAutoUpdateStatus();
1104
+ }
1105
+
1046
1106
  function renderCallbookDevices() {
1047
1107
  const tbody = document.querySelector('#callbook-devices-table tbody');
1048
1108
  if (!tbody) return;
@@ -1154,6 +1214,47 @@ function bindCallbookActions() {
1154
1214
  });
1155
1215
  }
1156
1216
 
1217
+ function bindAutoUpdateActions() {
1218
+ document.getElementById('auto-update-refresh')?.addEventListener('click', () => {
1219
+ loadAutoUpdateStatus().catch(err => showNotice(err.message));
1220
+ });
1221
+
1222
+ document.getElementById('auto-update-check')?.addEventListener('click', async () => {
1223
+ try {
1224
+ await request('/update/check', { method: 'POST', body: JSON.stringify({}) });
1225
+ await loadAutoUpdateStatus();
1226
+ showNotice('Update check complete');
1227
+ } catch (err) {
1228
+ showNotice(err.message);
1229
+ }
1230
+ });
1231
+
1232
+ document.getElementById('auto-update-now')?.addEventListener('click', async () => {
1233
+ try {
1234
+ await request('/update/now', { method: 'POST', body: JSON.stringify({}) });
1235
+ await loadAutoUpdateStatus();
1236
+ showNotice('Update triggered');
1237
+ } catch (err) {
1238
+ showNotice(err.message);
1239
+ }
1240
+ });
1241
+
1242
+ document.getElementById('auto-update-toggle')?.addEventListener('click', async () => {
1243
+ const au = state.autoUpdate || {};
1244
+ const nextEnabled = !Boolean(au.enabled);
1245
+ try {
1246
+ await request('/update/config', {
1247
+ method: 'PUT',
1248
+ body: JSON.stringify({ enabled: nextEnabled })
1249
+ });
1250
+ await loadAutoUpdateStatus();
1251
+ showNotice(nextEnabled ? 'Auto-update enabled' : 'Auto-update disabled');
1252
+ } catch (err) {
1253
+ showNotice(err.message);
1254
+ }
1255
+ });
1256
+ }
1257
+
1157
1258
  function renderInvites() {
1158
1259
  const tbody = document.querySelector('#invites-table tbody');
1159
1260
  tbody.innerHTML = '';
@@ -1247,6 +1348,7 @@ async function bootstrap() {
1247
1348
  bindContactsActions();
1248
1349
  bindSettingsActions();
1249
1350
  bindCallbookActions();
1351
+ bindAutoUpdateActions();
1250
1352
  bindInviteActions();
1251
1353
  bindRefreshButtons();
1252
1354
 
@@ -1254,6 +1356,7 @@ async function bootstrap() {
1254
1356
  await Promise.all([
1255
1357
  loadSettings(),
1256
1358
  loadDashboardStatus(),
1359
+ loadAutoUpdateStatus(),
1257
1360
  loadCallbookDevices(),
1258
1361
  loadContacts(),
1259
1362
  loadCalls(),
@@ -1262,6 +1365,10 @@ async function bootstrap() {
1262
1365
  loadLogs()
1263
1366
  ]);
1264
1367
  showNotice('Dashboard loaded');
1368
+
1369
+ setInterval(() => {
1370
+ loadAutoUpdateStatus().catch(() => {});
1371
+ }, 10000);
1265
1372
  } catch (err) {
1266
1373
  showNotice(err.message);
1267
1374
  }
@@ -171,6 +171,15 @@
171
171
  <h3>Remote Callbook</h3>
172
172
  <div id="callbook-status" class="card"></div>
173
173
 
174
+ <h3>Auto Update</h3>
175
+ <div id="auto-update-status" class="card">Loading…</div>
176
+ <div class="row">
177
+ <button id="auto-update-refresh" type="button">Refresh</button>
178
+ <button id="auto-update-check" type="button">Check now</button>
179
+ <button id="auto-update-now" type="button">Update now</button>
180
+ <button id="auto-update-toggle" type="button">Disable auto-update</button>
181
+ </div>
182
+
174
183
  <form id="callbook-provision-form" class="card">
175
184
  <div class="row">
176
185
  <button type="submit">Create Install Link (24h)</button>
@@ -202,6 +202,33 @@ tr[data-selected="1"] td {
202
202
  display: none;
203
203
  }
204
204
 
205
+ .status-pill {
206
+ display: inline-block;
207
+ padding: 0.15rem 0.45rem;
208
+ border-radius: 999px;
209
+ border: 1px solid var(--line);
210
+ font-size: 0.78rem;
211
+ font-weight: 600;
212
+ }
213
+
214
+ .status-pill.ok {
215
+ background: #ecfdf3;
216
+ border-color: #9bd8b8;
217
+ color: #125934;
218
+ }
219
+
220
+ .status-pill.warn {
221
+ background: #fff7e8;
222
+ border-color: #f1d08e;
223
+ color: #8a5a00;
224
+ }
225
+
226
+ .status-pill.err {
227
+ background: #fff0f1;
228
+ border-color: #efb1b6;
229
+ color: #8c1d26;
230
+ }
231
+
205
232
  @media (max-width: 720px) {
206
233
  nav {
207
234
  overflow-x: auto;
@@ -9,6 +9,7 @@
9
9
 
10
10
  const { execSync, spawn } = require('child_process');
11
11
  const { createLogger } = require('./logger');
12
+ const { HARD_FALLBACK_TURN_TIMEOUT_MS } = require('./turn-timeout');
12
13
 
13
14
  const logger = createLogger({ component: 'a2a.claude-subagent' });
14
15
 
@@ -216,7 +217,7 @@ function parseSubagentResponse(resultText) {
216
217
  * @param {number} timeoutMs - Timeout in milliseconds
217
218
  * @returns {Promise<{ stdout: string, stderr: string }>}
218
219
  */
219
- function spawnClaude(args, timeoutMs = 180000) {
220
+ function spawnClaude(args, timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS) {
220
221
  return new Promise((resolve, reject) => {
221
222
  const proc = spawn('claude', args, {
222
223
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -302,7 +303,7 @@ function extractResultFromJson(stdout) {
302
303
  * @param {Array} options.activeThreads - Active conversation threads
303
304
  * @param {Array} options.candidateCollaborations - Candidate collaboration ideas
304
305
  * @param {boolean} options.closeSignal - Whether close has been signaled
305
- * @param {number} [options.timeoutMs=180000] - Timeout in milliseconds
306
+ * @param {number} [options.timeoutMs=300000] - Timeout in milliseconds
306
307
  * @returns {Promise<{ message: string, statePatch: object|null, flags: array, sessionId: string }>}
307
308
  */
308
309
  async function runClaudeTurn(options) {
@@ -317,7 +318,7 @@ async function runClaudeTurn(options) {
317
318
  activeThreads = [],
318
319
  candidateCollaborations = [],
319
320
  closeSignal = false,
320
- timeoutMs = 180000
321
+ timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS
321
322
  } = options;
322
323
 
323
324
  const turnPrompt = buildTurnPrompt({
@@ -396,10 +397,10 @@ async function runClaudeTurn(options) {
396
397
  *
397
398
  * @param {string} sessionId - Session ID to resume
398
399
  * @param {string} reason - Why the conversation is ending
399
- * @param {number} [timeoutMs=120000] - Timeout in milliseconds
400
+ * @param {number} [timeoutMs=300000] - Timeout in milliseconds
400
401
  * @returns {Promise<{ summary: string, ownerSummary: string, actionItems: array, flags: array }>}
401
402
  */
402
- async function runClaudeSummary(sessionId, reason, timeoutMs = 120000) {
403
+ async function runClaudeSummary(sessionId, reason, timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS) {
403
404
  if (!sessionId) {
404
405
  throw new Error('Cannot summarize without a session ID');
405
406
  }
package/src/lib/config.js CHANGED
@@ -224,6 +224,7 @@ const DEFAULT_CONFIG = {
224
224
  perHour: 100,
225
225
  perDay: 1000
226
226
  },
227
+ turnTimeoutMs: 300000, // default Claude turn timeout
227
228
  maxPendingRequests: 5 // max connection requests per hour
228
229
  },
229
230
 
@@ -233,6 +234,14 @@ const DEFAULT_CONFIG = {
233
234
  description: '',
234
235
  hostname: ''
235
236
  },
237
+
238
+ // Auto-updater
239
+ auto_update: {
240
+ enabled: true,
241
+ intervalMs: 60 * 60 * 1000,
242
+ allowMajor: false,
243
+ lastGoodVersion: null
244
+ },
236
245
 
237
246
  // Timestamps
238
247
  createdAt: null,
@@ -381,6 +390,39 @@ class A2AConfig {
381
390
  return this.config;
382
391
  }
383
392
 
393
+ getAutoUpdate() {
394
+ const current = (this.config && typeof this.config.auto_update === 'object' && this.config.auto_update)
395
+ ? this.config.auto_update
396
+ : {};
397
+ return {
398
+ enabled: current.enabled !== false,
399
+ intervalMs: Number.isFinite(current.intervalMs) && current.intervalMs > 0
400
+ ? current.intervalMs
401
+ : DEFAULT_CONFIG.auto_update.intervalMs,
402
+ allowMajor: Boolean(current.allowMajor),
403
+ lastGoodVersion: current.lastGoodVersion || null
404
+ };
405
+ }
406
+
407
+ setAutoUpdate(patch = {}) {
408
+ const current = this.getAutoUpdate();
409
+ const next = { ...current };
410
+ if (patch.enabled !== undefined) next.enabled = Boolean(patch.enabled);
411
+ if (patch.intervalMs !== undefined) {
412
+ const parsed = Number.parseInt(String(patch.intervalMs), 10);
413
+ if (Number.isFinite(parsed) && parsed > 0) {
414
+ next.intervalMs = parsed;
415
+ }
416
+ }
417
+ if (patch.allowMajor !== undefined) next.allowMajor = Boolean(patch.allowMajor);
418
+ if (patch.lastGoodVersion !== undefined) {
419
+ next.lastGoodVersion = patch.lastGoodVersion ? String(patch.lastGoodVersion).trim() : null;
420
+ }
421
+ this.config.auto_update = next;
422
+ this._save();
423
+ return next;
424
+ }
425
+
384
426
  // Export for sharing
385
427
  export() {
386
428
  return {
@@ -21,6 +21,8 @@ const {
21
21
  } = require('./prompt-template');
22
22
  const { getTopicsForTier, formatTopicsForPrompt, loadManifest } = require('./disclosure');
23
23
  const { createLogger } = require('./logger');
24
+ const { buildUnifiedSummaryPrompt } = require('./summary-prompt');
25
+ const { resolveTokenTimeoutMs, resolveTurnTimeoutMs } = require('./turn-timeout');
24
26
 
25
27
  const logger = createLogger({ component: 'a2a.conversation-driver' });
26
28
 
@@ -129,9 +131,16 @@ class ConversationDriver {
129
131
  this.summarizer = options.summarizer || null;
130
132
  this.ownerContext = options.ownerContext || {};
131
133
  this.claudeMode = options.runtime?.mode === 'claude';
132
- this.claudeTimeoutMs = options.claudeTimeoutMs || 180000;
133
134
 
134
- const clientTimeout = this.claudeMode ? 200000 : 65000;
135
+ const tokenTimeoutMs = options.tokenTimeoutMs
136
+ || options.claudeTimeoutMs
137
+ || resolveTokenTimeoutMs(options.token);
138
+ const configTimeoutMs = options.configTurnTimeoutMs;
139
+ this.claudeTimeoutMs = resolveTurnTimeoutMs({ tokenTimeoutMs, configTimeoutMs });
140
+
141
+ const clientTimeout = this.claudeMode
142
+ ? Math.max(this.claudeTimeoutMs + 20000, 200000)
143
+ : 65000;
135
144
  this.client = new A2AClient({ caller: this.caller, timeout: clientTimeout });
136
145
  }
137
146
 
@@ -143,37 +152,75 @@ class ConversationDriver {
143
152
  _buildSummarizer() {
144
153
  const runtime = this.runtime;
145
154
  const agentContext = this.agentContext;
155
+ const caller = this.caller;
156
+ const tier = this.tier;
146
157
 
147
158
  return async (messages, ownerContext) => {
148
159
  if (!messages || messages.length === 0) {
149
160
  return { summary: null };
150
161
  }
151
162
 
152
- // Build the summary prompt (same structure as server.js generateSummary)
153
- const messageText = messages.map(m => {
154
- const role = m.direction === 'inbound' ? '[Them]' : '[You]';
155
- return `${role}: ${m.content}`;
156
- }).join('\n');
157
-
158
- const prompt = `Summarize this A2A call for the owner. Write from the owner's perspective.
163
+ // Build transcript in unified format
164
+ const transcript = messages.map(m => ({
165
+ direction: m.direction,
166
+ content: m.content
167
+ }));
159
168
 
160
- You initiated this call.
161
-
162
- Conversation:
163
- ${messageText}
169
+ // Load disclosure manifest for the tier
170
+ let disclosure = null;
171
+ try {
172
+ const tierTopics = getTopicsForTier(tier);
173
+ if (tierTopics) {
174
+ disclosure = {
175
+ topics: tierTopics.topics || [],
176
+ objectives: tierTopics.objectives || [],
177
+ doNotDiscuss: tierTopics.do_not_discuss || [],
178
+ neverDisclose: tierTopics.never_disclose || []
179
+ };
180
+ }
181
+ } catch (e) {
182
+ // Disclosure is optional — continue without it
183
+ }
164
184
 
165
- Structure your summary with these sections:
185
+ // Look up collaboration state from convStore if available
186
+ let collaborationState = null;
187
+ if (this.convStore) {
188
+ try {
189
+ const dbState = this.convStore.loadCollabState(this.lastConversationId || '');
190
+ if (dbState) {
191
+ collaborationState = {
192
+ phase: dbState.phase,
193
+ overlapScore: dbState.overlapScore,
194
+ turnCount: dbState.turnCount,
195
+ activeThreads: dbState.activeThreads,
196
+ candidateCollaborations: dbState.candidateCollaborations,
197
+ closeSignal: dbState.closeSignal
198
+ };
199
+ }
200
+ } catch (e) {
201
+ // Best effort
202
+ }
203
+ }
166
204
 
167
- **Who:** Who you called, who they represent, key facts about them.
168
- **Key Discoveries:** What was learned about the other side — capabilities, interests, blind spots.
169
- **Collaboration Potential:** Rate HIGH/MEDIUM/LOW. List specific opportunities identified.
170
- **What We Learned vs Shared:** Brief information exchange audit — what did we get, what did we give.
171
- **Recommended Follow-Up:**
172
- - [ ] Actionable item 1
173
- - [ ] Actionable item 2
174
- **Assessment:** One-sentence strategic value judgment.
205
+ // Map ownerContext into unified format
206
+ const unifiedOwnerContext = {
207
+ agentName: agentContext.name,
208
+ ownerName: agentContext.owner,
209
+ goals: ownerContext?.goals || []
210
+ };
175
211
 
176
- Be concise but specific. No filler.`;
212
+ const prompt = buildUnifiedSummaryPrompt({
213
+ transcript,
214
+ callerInfo: {
215
+ name: caller.name || null,
216
+ owner: caller.owner || null,
217
+ context: caller.context || null
218
+ },
219
+ conversationObjective: 'You initiated this call.',
220
+ disclosure,
221
+ collaborationState,
222
+ ownerContext: unifiedOwnerContext
223
+ });
177
224
 
178
225
  // Try runtime.summarize if available (OpenClaw path)
179
226
  if (typeof runtime.summarize === 'function') {
@@ -182,7 +229,8 @@ Be concise but specific. No filler.`;
182
229
  sessionId: `summary-${Date.now()}`,
183
230
  prompt,
184
231
  messages,
185
- callerInfo: { name: agentContext.name, owner: agentContext.owner }
232
+ callerInfo: { name: agentContext.name, owner: agentContext.owner },
233
+ timeoutMs: this.claudeMode ? this.claudeTimeoutMs : 35000
186
234
  });
187
235
  } catch (err) {
188
236
  logger.warn('Runtime summarizer failed, using default', {
@@ -505,6 +553,8 @@ Be concise but specific. No filler.`;
505
553
  }
506
554
 
507
555
  // Conclude locally with summarizer
556
+ // Store conversationId so _buildSummarizer can look up collab state
557
+ this.lastConversationId = conversationId;
508
558
  let summary = null;
509
559
  if (this.convStore) {
510
560
  try {
@@ -7,6 +7,7 @@
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { createLogger } = require('./logger');
10
+ const { buildUnifiedSummaryPrompt } = require('./summary-prompt');
10
11
 
11
12
  const logger = createLogger({ component: 'a2a.openclaw-integration' });
12
13
 
@@ -89,73 +90,28 @@ function loadOwnerContext(workspaceDir = process.cwd(), options = {}) {
89
90
  * Track the exchange balance AND surface partnership opportunities.
90
91
  */
91
92
  function buildSummaryPrompt(messages, ownerContext, callerInfo = {}) {
92
- const messageText = messages.map(m => {
93
- const role = m.direction === 'inbound' ? `[Caller${callerInfo.name ? ` - ${callerInfo.name}` : ''}]` : '[You]';
94
- return `${role}: ${m.content}`;
95
- }).join('\n\n');
96
-
97
- const goalsSection = ownerContext.goals?.length ? `### Current Goals\n- ${ownerContext.goals.join('\n- ')}` : '';
98
- const interestsSection = ownerContext.interests?.length ? `### Interests\n- ${ownerContext.interests.join('\n- ')}` : '';
99
-
100
- return `You just finished an A2A agent-to-agent call. Analyze it strategically for your owner.
101
-
102
- ## Philosophy
103
- A2A is cooperative AND adversarial. Each agent maximizes value for their own owner — but the best outcomes are mutual wins. Your job:
104
-
105
- 1. **Track the exchange** — what did we get vs give?
106
- 2. **Find mutual value** — what can BOTH parties gain?
107
- 3. **Surface alignment** — does this connect to owner's goals?
108
- 4. **Advise strategically** — protect interests while building relationships
109
-
110
- ## Your Owner's Context
111
- ${ownerContext.user ? `### From USER.md\n${ownerContext.user.slice(0, 2000)}` : ''}
112
-
113
- ${goalsSection}
114
-
115
- ${interestsSection}
116
-
117
- ## The Conversation
118
- ${messageText}
119
-
120
- ## Caller Context
121
- ${callerInfo.name ? `Name: ${callerInfo.name}` : 'Unknown caller'}
122
- ${callerInfo.context ? `Context: ${callerInfo.context}` : ''}
123
-
124
- ## Your Task
125
- Analyze as a strategic advisor. Return JSON:
126
-
127
- {
128
- "who": "Who called, who they represent, key facts about them",
129
-
130
- "keyDiscoveries": ["What was learned about the other side — capabilities, interests, blind spots"],
131
-
132
- "collaborationPotential": {
133
- "rating": "HIGH | MEDIUM | LOW",
134
- "opportunities": ["specific opportunities identified"]
135
- },
136
-
137
- "exchange": {
138
- "weGot": ["info, commitments, or value we extracted"],
139
- "weGave": ["info, compute, or commitments we provided"],
140
- "balance": "favorable | even | unfavorable"
141
- },
142
-
143
- "recommendedFollowUp": ["actionable items with specifics"],
144
-
145
- "assessment": "One-sentence strategic value judgment",
146
-
147
- "trust": {
148
- "assessment": "appropriate | too_high | too_low",
149
- "recommendation": "maintain | increase | decrease | revoke",
150
- "pattern": "What's their angle? Genuine partner or extractive?"
151
- },
152
-
153
- "ownerBrief": "2-3 sentences: the strategic takeaway for your owner"
154
- }
155
-
156
- Think like a strategic advisor: protect your owner's interests AND find mutual wins.
93
+ // Build transcript in unified format
94
+ const transcript = messages.map(m => ({
95
+ direction: m.direction,
96
+ content: m.content
97
+ }));
98
+
99
+ // Map ownerContext (which has .user, .goals, .interests from USER.md) into unified format
100
+ const unifiedOwnerContext = {
101
+ agentName: null,
102
+ ownerName: null,
103
+ goals: ownerContext?.goals || []
104
+ };
157
105
 
158
- JSON:`;
106
+ return buildUnifiedSummaryPrompt({
107
+ transcript,
108
+ callerInfo: {
109
+ name: callerInfo.name || null,
110
+ owner: callerInfo.owner || null,
111
+ context: callerInfo.context || null
112
+ },
113
+ ownerContext: unifiedOwnerContext
114
+ });
159
115
  }
160
116
 
161
117
  /**
@@ -14,6 +14,7 @@ const { execSync, spawnSync } = require('child_process');
14
14
  const { createLogger } = require('./logger');
15
15
  const { runClaudeTurn: invokeClaudeTurn, buildSubagentSystemPrompt, runClaudeSummary } = require('./claude-subagent');
16
16
  const { getTopicsForTier, formatTopicsForPrompt, loadManifest } = require('./disclosure');
17
+ const { HARD_FALLBACK_TURN_TIMEOUT_MS } = require('./turn-timeout');
17
18
 
18
19
  function commandExists(command) {
19
20
  try {
@@ -208,7 +209,7 @@ function createRuntimeAdapter(options = {}) {
208
209
  activeThreads: context?.activeThreads || [],
209
210
  candidateCollaborations: context?.candidateCollaborations || [],
210
211
  closeSignal: context?.closeSignal || false,
211
- timeoutMs: timeoutMs || 180000
212
+ timeoutMs: timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
212
213
  });
213
214
 
214
215
  // Store session ID from first turn for subsequent --resume
@@ -379,7 +380,7 @@ function createRuntimeAdapter(options = {}) {
379
380
  }
380
381
  }
381
382
 
382
- async function summarize({ sessionId, prompt, messages, callerInfo, traceId, conversationId }) {
383
+ async function summarize({ sessionId, prompt, messages, callerInfo, traceId, conversationId, timeoutMs }) {
383
384
  const effectiveTraceId = traceId || callerInfo?.trace_id || callerInfo?.traceId;
384
385
  const requestId = callerInfo?.request_id || callerInfo?.requestId;
385
386
  const effectiveConversationId = conversationId || callerInfo?.conversation_id || callerInfo?.conversationId;
@@ -388,7 +389,11 @@ function createRuntimeAdapter(options = {}) {
388
389
  if (modeInfo.mode === 'claude') {
389
390
  const session = claudeSessions.get(sessionId);
390
391
  if (session?.claudeSessionId) {
391
- const result = await runClaudeSummary(session.claudeSessionId, 'conversation ended');
392
+ const result = await runClaudeSummary(
393
+ session.claudeSessionId,
394
+ 'conversation ended',
395
+ timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
396
+ );
392
397
  if (result && result.summary) {
393
398
  return result;
394
399
  }