@magclaw/cli-core 0.1.34 → 0.1.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magclaw/cli-core",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Shared local MagClaw CLI implementation used by daemon and computer packages.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -61,6 +61,79 @@ function now() {
61
61
  return new Date().toISOString();
62
62
  }
63
63
 
64
+ function claudeStreamEvents(raw) {
65
+ if (!raw || typeof raw !== 'object') return [];
66
+ const event = raw;
67
+ const output = [];
68
+ if (event.type === 'system' && event.subtype === 'init') {
69
+ output.push({
70
+ type: 'system',
71
+ sessionId: event.session_id || event.sessionId || '',
72
+ model: event.model || '',
73
+ cwd: event.cwd || '',
74
+ });
75
+ return output;
76
+ }
77
+ const content = Array.isArray(event.message?.content) ? event.message.content : [];
78
+ if (event.type === 'assistant') {
79
+ for (const block of content) {
80
+ if (block?.type === 'text' && typeof block.text === 'string' && block.text) {
81
+ output.push({ type: 'text', delta: block.text });
82
+ } else if (block?.type === 'thinking' && typeof block.thinking === 'string' && block.thinking) {
83
+ output.push({ type: 'thinking', delta: block.thinking });
84
+ } else if (block?.type === 'tool_use' && block.id && block.name) {
85
+ output.push({ type: 'tool_use', id: block.id, name: block.name, input: block.input });
86
+ }
87
+ }
88
+ return output;
89
+ }
90
+ if (event.type === 'user') {
91
+ for (const block of content) {
92
+ if (block?.type === 'tool_result' && block.tool_use_id) {
93
+ output.push({
94
+ type: 'tool_result',
95
+ id: block.tool_use_id,
96
+ output: typeof block.content === 'string' ? block.content : JSON.stringify(block.content),
97
+ isError: block.is_error === true,
98
+ });
99
+ }
100
+ }
101
+ return output;
102
+ }
103
+ if (event.type === 'result') {
104
+ if (event.usage) {
105
+ output.push({
106
+ type: 'usage',
107
+ inputTokens: event.usage.input_tokens,
108
+ outputTokens: event.usage.output_tokens,
109
+ costUsd: event.total_cost_usd,
110
+ });
111
+ }
112
+ output.push({ type: 'done', sessionId: event.session_id || event.sessionId || '' });
113
+ }
114
+ return output;
115
+ }
116
+
117
+ function claudeToolActivityDetail(event) {
118
+ if (!event || typeof event !== 'object') return 'Claude Code activity';
119
+ if (event.type === 'tool_use') return `Claude Code using ${event.name || 'tool'}`;
120
+ if (event.type === 'tool_result') return event.isError ? 'Claude Code tool returned an error' : 'Claude Code tool completed';
121
+ if (event.type === 'thinking') return 'Claude Code thinking';
122
+ if (event.type === 'usage') return `Claude Code usage input=${event.inputTokens || 0} output=${event.outputTokens || 0}`;
123
+ return 'Claude Code activity';
124
+ }
125
+
126
+ function codexStderrRuntimeError(text = '') {
127
+ const detail = String(text || '').trim();
128
+ if (!detail) return '';
129
+ const lower = detail.toLowerCase();
130
+ if (lower.includes('responses_websocket') && lower.includes('error')) return detail.slice(0, 2000);
131
+ if (lower.includes('failed to connect to websocket') && lower.includes('/v1/responses')) return detail.slice(0, 2000);
132
+ if (lower.includes('authentication') && lower.includes('openai')) return detail.slice(0, 2000);
133
+ if (lower.includes('not logged in') || lower.includes('login is required')) return detail.slice(0, 2000);
134
+ return '';
135
+ }
136
+
64
137
  function packageInfoFromSpec(packageSpec = '') {
65
138
  const match = String(packageSpec || '').trim().match(/^(@magclaw\/(?:daemon|computer))(?:@(.+))?$/);
66
139
  return {
@@ -1721,6 +1794,60 @@ function contextSnippet(value, limit = 240) {
1721
1794
  return text.length >= limit ? `${text.slice(0, Math.max(0, limit - 3)).trim()}...` : text;
1722
1795
  }
1723
1796
 
1797
+ function contextImageMimeFromName(value = '') {
1798
+ const name = String(value || '').toLowerCase().split(/[?#]/)[0];
1799
+ if (name.endsWith('.png')) return 'image/png';
1800
+ if (name.endsWith('.jpg') || name.endsWith('.jpeg')) return 'image/jpeg';
1801
+ if (name.endsWith('.webp')) return 'image/webp';
1802
+ if (name.endsWith('.gif')) return 'image/gif';
1803
+ if (name.endsWith('.svg')) return 'image/svg+xml';
1804
+ return '';
1805
+ }
1806
+
1807
+ function contextDataImageUrl(value = '') {
1808
+ const text = String(value || '').trim();
1809
+ return /^data:image\//i.test(text) ? text : '';
1810
+ }
1811
+
1812
+ function contextImageType(reference = {}) {
1813
+ const explicit = String(reference.type || reference.mime || reference.mimeType || '').toLowerCase();
1814
+ if (explicit.startsWith('image/')) return explicit;
1815
+ const data = contextDataImageUrl(reference.dataUrl || reference.url || reference.downloadUrl);
1816
+ if (data) return data.match(/^data:([^;,]+)[;,]/i)?.[1]?.toLowerCase() || 'image';
1817
+ return contextImageMimeFromName(reference.name || reference.filename || reference.url || reference.downloadUrl || reference.path || reference.description);
1818
+ }
1819
+
1820
+ function isContextImageReference(reference = {}) {
1821
+ return contextImageType(reference).startsWith('image/');
1822
+ }
1823
+
1824
+ function remoteImageUrl(value = '', serverUrl = '', fallbackPath = '') {
1825
+ const raw = String(value || '').trim();
1826
+ if (contextDataImageUrl(raw)) return raw;
1827
+ const base = String(serverUrl || DEFAULT_SERVER_URL).replace(/\/+$/, '');
1828
+ const candidate = raw || String(fallbackPath || '').trim();
1829
+ if (!candidate) return '';
1830
+ if (candidate.startsWith('/')) {
1831
+ try {
1832
+ return new URL(candidate, base).toString();
1833
+ } catch {
1834
+ return '';
1835
+ }
1836
+ }
1837
+ if (/^https?:\/\//i.test(candidate)) {
1838
+ try {
1839
+ const parsed = new URL(candidate);
1840
+ if (['0.0.0.0', '127.0.0.1', 'localhost', '::1'].includes(parsed.hostname) && base) {
1841
+ return new URL(`${parsed.pathname}${parsed.search}${parsed.hash}`, base).toString();
1842
+ }
1843
+ return parsed.toString();
1844
+ } catch {
1845
+ return '';
1846
+ }
1847
+ }
1848
+ return '';
1849
+ }
1850
+
1724
1851
  function contextActorName(pack, id) {
1725
1852
  const value = String(id || '').trim();
1726
1853
  if (!value) return 'unknown';
@@ -1767,6 +1894,50 @@ function renderContextTasks(pack) {
1767
1894
  }).join('\n');
1768
1895
  }
1769
1896
 
1897
+ function renderContextAttachments(pack) {
1898
+ const attachments = contextArray(pack?.attachments);
1899
+ if (!attachments.length) return '- (none)';
1900
+ return attachments.map((item) => {
1901
+ const details = [
1902
+ item.id ? `id=${item.id}` : '',
1903
+ item.messageId ? `from msg=${item.messageId}` : '',
1904
+ item.source ? `source=${item.source}` : '',
1905
+ item.path ? `path=${item.path}` : '',
1906
+ item.url ? `url=${item.url}` : '',
1907
+ item.id ? `tool=read_attachment(attachmentId="${item.id}")` : '',
1908
+ ].filter(Boolean).join(', ');
1909
+ return `- ${item.name || item.filename || item.id || 'attachment'} ${item.type || item.mime || 'file'} ${Number(item.bytes || item.sizeBytes || 0)} bytes${details ? ` (${details})` : ''}`;
1910
+ }).join('\n');
1911
+ }
1912
+
1913
+ function renderContextTargetAgentAvatar(pack) {
1914
+ const avatar = pack?.targetAgent?.avatar;
1915
+ if (!avatar || avatar.kind === 'none') return '';
1916
+ const description = avatar.description ? ` (${avatar.description})` : '';
1917
+ if (avatar.visualInput !== false && isContextImageReference(avatar)) {
1918
+ return `- Your profile avatar: image supplied as visual input${description}. Use it when the user asks what your avatar shows.`;
1919
+ }
1920
+ return `- Your profile avatar: ${avatar.description || 'configured'}, but no visual input is available.`;
1921
+ }
1922
+
1923
+ function contextParticipantAvatarVisualInputs(pack, targetAgentId = '') {
1924
+ return compactContextParticipants(pack, targetAgentId).selected.filter((item) => (
1925
+ item.id !== targetAgentId
1926
+ && item.type === 'agent'
1927
+ && item.avatar
1928
+ && item.avatar.kind !== 'none'
1929
+ && item.avatar.visualInput !== false
1930
+ && isContextImageReference(item.avatar)
1931
+ ));
1932
+ }
1933
+
1934
+ function renderContextParticipantAvatarInputs(pack, targetAgentId = '') {
1935
+ const visible = contextParticipantAvatarVisualInputs(pack, targetAgentId);
1936
+ if (!visible.length) return '';
1937
+ const names = visible.map((item) => `@${item.name || item.id}`).join(', ');
1938
+ return `- Participant avatar visual inputs: ${names}. Use these when comparing an uploaded image to another Agent avatar; call read_agent_avatar if the relevant Agent is omitted.`;
1939
+ }
1940
+
1770
1941
  function renderContextEventMembers(pack, event = {}) {
1771
1942
  const ids = [
1772
1943
  ...contextArray(event.memberIds),
@@ -1889,6 +2060,8 @@ function renderRemoteAgentContextPack(pack, targetAgentId = '') {
1889
2060
  `- Space: ${pack.space?.type || 'space'} (${pack.space?.visibility || 'public'}${pack.space?.defaultChannel ? ', default workspace channel' : ''})`,
1890
2061
  pack.space?.description ? `- Channel description: ${contextSnippet(pack.space.description, 180)}` : '',
1891
2062
  `- Participants shown: ${participants.selected.map((item) => renderContextParticipant(item, targetAgentId)).join(', ') || '(none)'}`,
2063
+ renderContextTargetAgentAvatar(pack),
2064
+ renderContextParticipantAvatarInputs(pack, targetAgentId),
1892
2065
  participants.omitted ? `- Participants omitted: ${participants.omitted}. Use list_agents/read_agent_profile or search_agent_memory when a broader roster or specialties matter.` : '',
1893
2066
  pack.space?.type === 'channel' && !pack.space?.defaultChannel ? renderContextSuggestedMembers(pack) : '',
1894
2067
  '',
@@ -1917,9 +2090,12 @@ function renderRemoteAgentContextPack(pack, targetAgentId = '') {
1917
2090
  'Relevant tasks:',
1918
2091
  renderContextTasks(pack),
1919
2092
  '',
2093
+ 'Visible attachment metadata and original-file tools:',
2094
+ renderContextAttachments(pack),
2095
+ '',
1920
2096
  renderContextPeerMemory(pack),
1921
2097
  '',
1922
- 'Progressive context tools: list_agents, read_agent_profile, read_history, search_messages, search_agent_memory, read_agent_memory, read_agent_file, and list_tasks are available through MagClaw MCP.',
2098
+ 'Progressive context tools: list_agents, read_agent_profile, read_agent_avatar, read_history, search_messages, list_attachments, read_attachment, search_agent_memory, read_agent_memory, read_agent_file, and list_tasks are available through MagClaw MCP.',
1923
2099
  'For "who can we bring in" or agent suitability questions, use the server member list above first; call list_agents without a target for the server-wide agent roster, because target filters to the current channel.',
1924
2100
  'For agent capability or specialty questions, use peer memory first; if memory is empty or weak, search_messages/read_history for earlier user role assignments before saying the fact is unknown.',
1925
2101
  'Use this compact snapshot first. Call the tools only when the answer depends on omitted participants, deeper history, memory, or task details.',
@@ -2020,11 +2196,14 @@ function canonicalMagClawToolName(name) {
2020
2196
  'send_message',
2021
2197
  'read_history',
2022
2198
  'search_messages',
2199
+ 'list_attachments',
2200
+ 'read_attachment',
2023
2201
  'search_agent_memory',
2024
2202
  'read_agent_memory',
2025
2203
  'read_agent_file',
2026
2204
  'list_agents',
2027
2205
  'read_agent_profile',
2206
+ 'read_agent_avatar',
2028
2207
  'write_memory',
2029
2208
  'list_tasks',
2030
2209
  'create_tasks',
@@ -2508,11 +2687,14 @@ function daemonSkillTools() {
2508
2687
  'send_message',
2509
2688
  'read_history',
2510
2689
  'search_messages',
2690
+ 'list_attachments',
2691
+ 'read_attachment',
2511
2692
  'search_agent_memory',
2512
2693
  'read_agent_memory',
2513
2694
  'read_agent_file',
2514
2695
  'list_agents',
2515
2696
  'read_agent_profile',
2697
+ 'read_agent_avatar',
2516
2698
  'write_memory',
2517
2699
  'list_tasks',
2518
2700
  'create_tasks',
@@ -2808,9 +2990,12 @@ class CodexAgentSession {
2808
2990
  this.completedToolCallIds = new Set();
2809
2991
  this.activeTurnToolSignatures = new Set();
2810
2992
  this.activeTurnUsedSendMessage = false;
2993
+ this.activeTurnSawResponseDelta = false;
2994
+ this.activeTurnDeltaItemIds = new Set();
2811
2995
  this.codexMessageQueue = Promise.resolve();
2812
2996
  this.streamActivityTimer = null;
2813
2997
  this.pendingStreamActivity = null;
2998
+ this.lastRuntimeError = '';
2814
2999
  this.trajectoryCoalesceMs = envInteger(this.env, 'MAGCLAW_DAEMON_TRAJECTORY_COALESCE_MS', DEFAULT_TRAJECTORY_COALESCE_MS, { min: 0, max: 5_000 });
2815
3000
  }
2816
3001
 
@@ -2960,6 +3145,38 @@ class CodexAgentSession {
2960
3145
  if (payload) this.send(payload);
2961
3146
  }
2962
3147
 
3148
+ async reportRuntimeError(errorText, rawText = '') {
3149
+ const error = String(errorText || rawText || 'Codex runtime error.').trim().slice(0, 2000);
3150
+ if (!error) return;
3151
+ if (this.status === 'error' && this.lastRuntimeError === error) return;
3152
+ this.lastRuntimeError = error;
3153
+ const activity = {
3154
+ source: 'codex-stderr',
3155
+ error,
3156
+ text: String(rawText || error).trim().slice(0, 2000),
3157
+ at: now(),
3158
+ };
3159
+ this.send({
3160
+ type: 'agent:error',
3161
+ commandId: this.activeDeliveryId || undefined,
3162
+ deliveryId: this.activeDeliveryId || null,
3163
+ agentId: this.agent.id,
3164
+ error,
3165
+ });
3166
+ if (this.activeDeliveryId) {
3167
+ await this.markDelivery(this.activeDeliveryId, 'failed', {
3168
+ agentId: this.agent.id,
3169
+ sessionKey: this.sessionKey || null,
3170
+ messageId: this.lastSourceMessage?.id || null,
3171
+ workItemId: this.lastSourceMessage?.workItemId || null,
3172
+ error,
3173
+ }).catch((markError) => {
3174
+ logWarning('daemon', `Failed to mark delivery ${this.activeDeliveryId} failed after Codex runtime error: ${markError.message}`);
3175
+ });
3176
+ }
3177
+ this.sendStatus('error', activity);
3178
+ }
3179
+
2963
3180
  async requestMagClawTool(pathname, { method = 'GET', query = {}, body = null } = {}) {
2964
3181
  const url = new URL(`${this.serverUrl.replace(/\/+$/, '')}${pathname}`);
2965
3182
  for (const [key, value] of Object.entries(query || {})) {
@@ -2998,6 +3215,92 @@ class CodexAgentSession {
2998
3215
  }
2999
3216
  }
3000
3217
 
3218
+ async readAttachmentImageInput(reference = {}) {
3219
+ const attachmentId = String(reference.id || reference.attachmentId || reference.attachment_id || '').trim();
3220
+ if (!attachmentId) return null;
3221
+ try {
3222
+ const data = await this.requestMagClawTool('/api/agent-tools/attachments/read', {
3223
+ query: {
3224
+ agentId: this.agent.id,
3225
+ attachmentId,
3226
+ maxBytes: 8 * 1024 * 1024,
3227
+ },
3228
+ });
3229
+ const dataUrl = contextDataImageUrl(data?.dataUrl);
3230
+ if (!dataUrl || data?.file?.truncated) return null;
3231
+ return {
3232
+ key: `attachment:${attachmentId}`,
3233
+ input: { type: 'image', url: dataUrl },
3234
+ };
3235
+ } catch (error) {
3236
+ logWarning('attachments', `Could not read image attachment ${attachmentId} for agent ${this.agent.id}: ${error.message}`);
3237
+ return null;
3238
+ }
3239
+ }
3240
+
3241
+ async imageInputFromContextReference(reference = {}, { preferReadAttachment = false } = {}) {
3242
+ if (!isContextImageReference(reference)) return null;
3243
+ if (preferReadAttachment) {
3244
+ const resolved = await this.readAttachmentImageInput(reference);
3245
+ if (resolved) return resolved;
3246
+ }
3247
+ const dataUrl = contextDataImageUrl(reference.dataUrl || reference.url || reference.downloadUrl);
3248
+ if (dataUrl) {
3249
+ return {
3250
+ key: `url:${dataUrl}`,
3251
+ input: { type: 'image', url: dataUrl },
3252
+ };
3253
+ }
3254
+ const url = remoteImageUrl(reference.url || reference.downloadUrl || '', this.serverUrl, reference.description);
3255
+ if (url) {
3256
+ return {
3257
+ key: `url:${url}`,
3258
+ input: { type: 'image', url },
3259
+ };
3260
+ }
3261
+ const filePath = String(reference.path || '').trim();
3262
+ if (filePath && existsSync(filePath)) {
3263
+ return {
3264
+ key: `path:${filePath}`,
3265
+ input: { type: 'localImage', path: filePath },
3266
+ };
3267
+ }
3268
+ return null;
3269
+ }
3270
+
3271
+ async imageInputsForDelivery(message = {}) {
3272
+ const inputs = [];
3273
+ const seen = new Set();
3274
+ const pack = message?.contextPack || {};
3275
+ const attachments = contextArray(pack.attachments);
3276
+ for (const attachment of attachments) {
3277
+ const resolved = await this.imageInputFromContextReference(attachment, { preferReadAttachment: true });
3278
+ if (!resolved || seen.has(resolved.key)) continue;
3279
+ seen.add(resolved.key);
3280
+ inputs.push(resolved.input);
3281
+ }
3282
+ const avatar = pack?.targetAgent?.avatar || null;
3283
+ if (avatar?.visualInput !== false) {
3284
+ const resolved = await this.imageInputFromContextReference(avatar);
3285
+ if (resolved && !seen.has(resolved.key)) {
3286
+ seen.add(resolved.key);
3287
+ inputs.push(resolved.input);
3288
+ }
3289
+ }
3290
+ const targetAgentId = String(pack?.targetAgentId || pack?.targetAgent?.id || this.agent.id || '');
3291
+ for (const participant of contextArray(pack.participants)) {
3292
+ if (participant?.type !== 'agent') continue;
3293
+ if (targetAgentId && String(participant.id || '') === targetAgentId) continue;
3294
+ const participantAvatar = participant.avatar || null;
3295
+ if (!participantAvatar || participantAvatar.visualInput === false || !isContextImageReference(participantAvatar)) continue;
3296
+ const resolved = await this.imageInputFromContextReference(participantAvatar);
3297
+ if (!resolved || seen.has(resolved.key)) continue;
3298
+ seen.add(resolved.key);
3299
+ inputs.push(resolved.input);
3300
+ }
3301
+ return inputs;
3302
+ }
3303
+
3001
3304
  async executeMagClawTool(name, rawArgs = {}) {
3002
3305
  const args = { ...rawArgs, agentId: rawArgs.agentId || this.agent.id };
3003
3306
  switch (name) {
@@ -3035,6 +3338,25 @@ class CodexAgentSession {
3035
3338
  limit: args.limit,
3036
3339
  },
3037
3340
  });
3341
+ case 'list_attachments':
3342
+ return this.requestMagClawTool('/api/agent-tools/attachments', {
3343
+ query: {
3344
+ agentId: args.agentId,
3345
+ target: args.target || args.channel,
3346
+ workItemId: args.workItemId || args.work_item_id,
3347
+ messageId: args.messageId || args.message_id,
3348
+ limit: args.limit,
3349
+ },
3350
+ });
3351
+ case 'read_attachment':
3352
+ return this.requestMagClawTool('/api/agent-tools/attachments/read', {
3353
+ query: {
3354
+ agentId: args.agentId,
3355
+ attachmentId: args.attachmentId || args.attachment_id || args.id,
3356
+ maxBytes: args.maxBytes || args.max_bytes,
3357
+ format: args.format,
3358
+ },
3359
+ });
3038
3360
  case 'search_agent_memory':
3039
3361
  return this.requestMagClawTool('/api/agent-tools/memory/search', {
3040
3362
  query: {
@@ -3076,6 +3398,14 @@ class CodexAgentSession {
3076
3398
  targetAgentId: args.targetAgentId || args.targetAgent,
3077
3399
  },
3078
3400
  });
3401
+ case 'read_agent_avatar':
3402
+ return this.requestMagClawTool('/api/agent-tools/agents/avatar/read', {
3403
+ query: {
3404
+ agentId: args.agentId,
3405
+ targetAgentId: args.targetAgentId || args.targetAgent,
3406
+ maxBytes: args.maxBytes || args.max_bytes,
3407
+ },
3408
+ });
3079
3409
  case 'write_memory':
3080
3410
  {
3081
3411
  const local = await writeDaemonLocalMemory(this.agentDir(), this.agent, args);
@@ -3161,6 +3491,18 @@ class CodexAgentSession {
3161
3491
  }
3162
3492
  }
3163
3493
 
3494
+ appendCompletedAgentText(text = '', { hadDelta = false } = {}) {
3495
+ const value = String(text || '');
3496
+ if (!value) return;
3497
+ if (hadDelta) {
3498
+ if (this.responseBuffer.endsWith(value) || this.responseBuffer.includes(value)) return;
3499
+ if (value.startsWith(this.responseBuffer)) this.responseBuffer = value;
3500
+ else this.responseBuffer += value;
3501
+ return;
3502
+ }
3503
+ if (!this.responseBuffer.includes(value)) this.responseBuffer += value;
3504
+ }
3505
+
3164
3506
  async executeCodexToolItem(item = {}, requestId = null, params = {}) {
3165
3507
  const callId = codexToolCallId(item) || String(params.callId || params.call_id || '');
3166
3508
  const name = canonicalMagClawToolName(codexToolName(item));
@@ -3257,10 +3599,18 @@ class CodexAgentSession {
3257
3599
  this.child.stderr.on('data', (chunk) => {
3258
3600
  const text = chunk.toString().trim();
3259
3601
  if (!text) return;
3602
+ const runtimeError = codexStderrRuntimeError(text);
3603
+ if (runtimeError) {
3604
+ this.reportRuntimeError(runtimeError, text).catch((error) => {
3605
+ logWarning('daemon', `Failed to report Codex runtime error for ${this.agent.id}: ${error.message}`);
3606
+ });
3607
+ return;
3608
+ }
3260
3609
  this.send({
3261
3610
  type: 'agent:activity',
3262
3611
  agentId: this.agent.id,
3263
3612
  status: this.status || 'working',
3613
+ deliveryId: this.activeDeliveryId || null,
3264
3614
  activity: { source: 'codex-stderr', text: text.slice(0, 2000), at: now() },
3265
3615
  });
3266
3616
  });
@@ -3301,19 +3651,23 @@ class CodexAgentSession {
3301
3651
  this.pendingPrompts.push({ prompt, message, workItem, deliveryId });
3302
3652
  return;
3303
3653
  }
3304
- this.startTurn(prompt, message, workItem, deliveryId);
3654
+ await this.startTurn(prompt, message, workItem, deliveryId);
3305
3655
  }
3306
3656
 
3307
- startTurn(prompt, message = {}, workItem = null, deliveryId = '') {
3657
+ async startTurn(prompt, message = {}, workItem = null, deliveryId = '') {
3308
3658
  if (!this.threadId) return false;
3309
3659
  this.activeDeliveryId = deliveryId || '';
3310
3660
  this.activeTurnToolSignatures = new Set();
3311
3661
  this.activeTurnUsedSendMessage = false;
3662
+ this.activeTurnSawResponseDelta = false;
3663
+ this.activeTurnDeltaItemIds = new Set();
3664
+ this.lastRuntimeError = '';
3312
3665
  const model = this.agent.model || undefined;
3313
3666
  const effort = this.agent.reasoningEffort || undefined;
3667
+ const imageInputs = await this.imageInputsForDelivery(message);
3314
3668
  const params = {
3315
3669
  threadId: this.threadId,
3316
- input: [{ type: 'text', text: prompt }],
3670
+ input: [{ type: 'text', text: prompt }, ...imageInputs],
3317
3671
  approvalPolicy: codexApprovalPolicy(this.env),
3318
3672
  ...(model ? { model } : {}),
3319
3673
  ...(effort ? { effort } : {}),
@@ -3382,7 +3736,7 @@ class CodexAgentSession {
3382
3736
  this.send({ type: 'agent:session', agentId: this.agent.id, status: 'idle', sessionId: this.threadId, sessionKey: this.sessionKey || null });
3383
3737
  this.sendStatus('idle', { source: '@magclaw/daemon', detail: 'Codex session ready', at: now() });
3384
3738
  const queued = this.pendingPrompts.splice(0);
3385
- for (const item of queued) this.startTurn(item.prompt, item.message, item.workItem, item.deliveryId);
3739
+ for (const item of queued) await this.startTurn(item.prompt, item.message, item.workItem, item.deliveryId);
3386
3740
  } else if (pending?.method === 'turn/start' || pending?.method === 'turn/steer') {
3387
3741
  this.activeTurnId = message.result?.turn?.id || message.result?.turnId || this.activeTurnId;
3388
3742
  }
@@ -3418,6 +3772,9 @@ class CodexAgentSession {
3418
3772
  }
3419
3773
  if (method === 'item/agentMessage/delta' || method === 'response/output_text/delta') {
3420
3774
  this.responseBuffer += String(params.delta || params.text || '');
3775
+ const itemId = String(params.itemId || params.item_id || params.item?.id || '');
3776
+ if (itemId) this.activeTurnDeltaItemIds.add(itemId);
3777
+ this.activeTurnSawResponseDelta = true;
3421
3778
  this.queueCodexStreamActivity();
3422
3779
  return;
3423
3780
  }
@@ -3425,7 +3782,10 @@ class CodexAgentSession {
3425
3782
  const item = params.item || {};
3426
3783
  if (await this.executeCodexToolItem(item, null, params)) return;
3427
3784
  const text = item?.text || item?.message || params.text || '';
3428
- if (text) this.responseBuffer += String(text);
3785
+ const itemId = String(item?.id || item?.itemId || item?.item_id || '');
3786
+ this.appendCompletedAgentText(text, {
3787
+ hadDelta: Boolean((itemId && this.activeTurnDeltaItemIds.has(itemId)) || this.activeTurnSawResponseDelta),
3788
+ });
3429
3789
  return;
3430
3790
  }
3431
3791
  if (method === 'turn/completed' || method === 'turn/failed') {
@@ -3457,6 +3817,8 @@ class CodexAgentSession {
3457
3817
  this.responseBuffer = '';
3458
3818
  this.activeTurnId = '';
3459
3819
  this.activeTurnUsedSendMessage = false;
3820
+ this.activeTurnSawResponseDelta = false;
3821
+ this.activeTurnDeltaItemIds.clear();
3460
3822
  this.sendStatus(method === 'turn/completed' ? 'idle' : 'error', {
3461
3823
  source: '@magclaw/daemon',
3462
3824
  detail: method === 'turn/completed' ? 'Turn completed' : 'Turn failed',
@@ -3495,6 +3857,11 @@ class ClaudeAgentSession {
3495
3857
  this.status = 'offline';
3496
3858
  this.started = false;
3497
3859
  this.activeDeliveryId = '';
3860
+ this.responseBuffer = '';
3861
+ this.lastSourceMessage = null;
3862
+ this.pendingMessageDelta = null;
3863
+ this.messageDeltaTimer = null;
3864
+ this.trajectoryCoalesceMs = envInteger(this.env, 'MAGCLAW_DAEMON_TRAJECTORY_COALESCE_MS', DEFAULT_TRAJECTORY_COALESCE_MS, { min: 0, max: 5_000 });
3498
3865
  }
3499
3866
 
3500
3867
  agentDir() {
@@ -3558,6 +3925,87 @@ class ClaudeAgentSession {
3558
3925
  });
3559
3926
  }
3560
3927
 
3928
+ queueMessageDelta(delta = '') {
3929
+ const body = this.responseBuffer.trim();
3930
+ if (!body) return;
3931
+ this.pendingMessageDelta = {
3932
+ type: 'agent:message_delta',
3933
+ agentId: this.agent.id,
3934
+ deliveryId: this.activeDeliveryId || null,
3935
+ payload: {
3936
+ body,
3937
+ delta: String(delta || ''),
3938
+ message: this.lastSourceMessage || null,
3939
+ sourceMessage: this.lastSourceMessage || null,
3940
+ spaceType: this.lastSourceMessage?.spaceType || 'channel',
3941
+ spaceId: this.lastSourceMessage?.spaceId || 'chan_all',
3942
+ parentMessageId: this.lastSourceMessage?.parentMessageId || null,
3943
+ idempotencyKey: this.activeDeliveryId || null,
3944
+ },
3945
+ };
3946
+ if (this.trajectoryCoalesceMs <= 0) {
3947
+ this.flushMessageDelta();
3948
+ return;
3949
+ }
3950
+ if (this.messageDeltaTimer) return;
3951
+ this.messageDeltaTimer = setTimeout(() => {
3952
+ this.messageDeltaTimer = null;
3953
+ this.flushMessageDelta();
3954
+ }, this.trajectoryCoalesceMs);
3955
+ this.messageDeltaTimer.unref?.();
3956
+ }
3957
+
3958
+ flushMessageDelta() {
3959
+ if (this.messageDeltaTimer) {
3960
+ clearTimeout(this.messageDeltaTimer);
3961
+ this.messageDeltaTimer = null;
3962
+ }
3963
+ const payload = this.pendingMessageDelta;
3964
+ this.pendingMessageDelta = null;
3965
+ if (payload) this.send(payload);
3966
+ }
3967
+
3968
+ handleClaudeStreamEvent(event) {
3969
+ if (event.type === 'system') {
3970
+ if (event.sessionId) {
3971
+ this.send({
3972
+ type: 'agent:session',
3973
+ agentId: this.agent.id,
3974
+ status: this.status,
3975
+ sessionId: event.sessionId,
3976
+ sessionKey: null,
3977
+ });
3978
+ }
3979
+ return;
3980
+ }
3981
+ if (event.type === 'text') {
3982
+ this.responseBuffer += String(event.delta || '');
3983
+ this.queueMessageDelta(event.delta || '');
3984
+ this.send({
3985
+ type: 'agent:activity',
3986
+ agentId: this.agent.id,
3987
+ status: 'working',
3988
+ deliveryId: this.activeDeliveryId || null,
3989
+ activity: { source: 'claude-code-stream', chars: this.responseBuffer.length, at: now() },
3990
+ });
3991
+ return;
3992
+ }
3993
+ if (event.type === 'thinking' || event.type === 'tool_use' || event.type === 'tool_result' || event.type === 'usage') {
3994
+ this.send({
3995
+ type: 'agent:activity',
3996
+ agentId: this.agent.id,
3997
+ status: event.type === 'thinking' ? 'thinking' : 'working',
3998
+ deliveryId: this.activeDeliveryId || null,
3999
+ activity: {
4000
+ source: 'claude-code-stream',
4001
+ phase: event.type,
4002
+ detail: claudeToolActivityDetail(event),
4003
+ at: now(),
4004
+ },
4005
+ });
4006
+ }
4007
+ }
4008
+
3561
4009
  async start() {
3562
4010
  if (this.started) return;
3563
4011
  await this.prepare();
@@ -3568,11 +4016,12 @@ class ClaudeAgentSession {
3568
4016
  async deliver(message = {}, workItem = null, deliveryId = '') {
3569
4017
  await this.start();
3570
4018
  this.activeDeliveryId = deliveryId || '';
4019
+ this.responseBuffer = '';
4020
+ this.lastSourceMessage = message || null;
3571
4021
  const prompt = deliveryPrompt(this.agent, message, workItem);
3572
4022
  const claudeCommand = this.env.CLAUDE_PATH || 'claude';
3573
- const args = ['--print'];
4023
+ const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose'];
3574
4024
  if (this.agent.model) args.push('--model', String(this.agent.model));
3575
- args.push(prompt);
3576
4025
  const timeoutMs = Number(this.env.MAGCLAW_DAEMON_RUNTIME_TIMEOUT_MS || 10 * 60 * 1000);
3577
4026
  if (this.activeDeliveryId) {
3578
4027
  await this.markDelivery(this.activeDeliveryId, 'started', {
@@ -3583,31 +4032,33 @@ class ClaudeAgentSession {
3583
4032
  }
3584
4033
  this.sendStatus('thinking', { source: 'claude-code', detail: 'Claude Code turn started', at: now() });
3585
4034
  await new Promise((resolve) => {
3586
- let stdout = '';
3587
4035
  let stderr = '';
4036
+ let stdoutBuffer = '';
3588
4037
  let settled = false;
4038
+ const finalMessageFrame = (body) => ({
4039
+ type: 'agent:message',
4040
+ agentId: this.agent.id,
4041
+ deliveryId: this.activeDeliveryId || null,
4042
+ payload: {
4043
+ body,
4044
+ message,
4045
+ sourceMessage: message,
4046
+ spaceType: message.spaceType || 'channel',
4047
+ spaceId: message.spaceId || 'chan_all',
4048
+ parentMessageId: message.parentMessageId || null,
4049
+ idempotencyKey: this.activeDeliveryId || null,
4050
+ },
4051
+ });
3589
4052
  const finish = (status, detail) => {
3590
4053
  if (settled) return;
3591
4054
  settled = true;
3592
4055
  clearTimeout(timer);
4056
+ this.flushMessageDelta();
3593
4057
  this.child = null;
4058
+ const body = this.responseBuffer.trim();
3594
4059
  if (status === 'idle') {
3595
- const body = stdout.trim();
3596
4060
  if (body) {
3597
- const frame = {
3598
- type: 'agent:message',
3599
- agentId: this.agent.id,
3600
- deliveryId: this.activeDeliveryId || null,
3601
- payload: {
3602
- body,
3603
- message,
3604
- sourceMessage: message,
3605
- spaceType: message.spaceType || 'channel',
3606
- spaceId: message.spaceId || 'chan_all',
3607
- parentMessageId: message.parentMessageId || null,
3608
- idempotencyKey: this.activeDeliveryId || null,
3609
- },
3610
- };
4061
+ const frame = finalMessageFrame(body);
3611
4062
  this.send(frame);
3612
4063
  this.markDelivery(this.activeDeliveryId, 'completed', { resultFrame: frame }).catch(() => {});
3613
4064
  }
@@ -3615,8 +4066,10 @@ class ClaudeAgentSession {
3615
4066
  this.sendStatus('idle', { source: 'claude-code', detail: detail || 'Claude Code turn completed', at: now() });
3616
4067
  } else {
3617
4068
  const error = detail || stderr.trim() || 'Claude Code failed.';
4069
+ const frame = body ? finalMessageFrame(body) : null;
4070
+ if (frame) this.send(frame);
3618
4071
  this.send({ type: 'agent:error', commandId: this.activeDeliveryId || undefined, deliveryId: this.activeDeliveryId || null, agentId: this.agent.id, error });
3619
- this.markDelivery(this.activeDeliveryId, 'failed', { error }).catch(() => {});
4072
+ this.markDelivery(this.activeDeliveryId, 'failed', { error, ...(frame ? { resultFrame: frame } : {}) }).catch(() => {});
3620
4073
  this.sendStatus('error', { source: 'claude-code', error, at: now() });
3621
4074
  }
3622
4075
  this.activeDeliveryId = '';
@@ -3639,19 +4092,40 @@ class ClaudeAgentSession {
3639
4092
  finish('error', 'Claude Code session timed out.');
3640
4093
  }, timeoutMs);
3641
4094
  this.child.stdout.on('data', (chunk) => {
3642
- stdout += chunk.toString();
3643
- this.send({
3644
- type: 'agent:activity',
3645
- agentId: this.agent.id,
3646
- status: 'working',
3647
- activity: { source: 'claude-code', chars: stdout.length, at: now() },
3648
- });
4095
+ stdoutBuffer += chunk.toString();
4096
+ const lines = stdoutBuffer.split(/\r?\n/);
4097
+ stdoutBuffer = lines.pop() || '';
4098
+ for (const line of lines) {
4099
+ if (!line.trim()) continue;
4100
+ try {
4101
+ const raw = JSON.parse(line);
4102
+ for (const event of claudeStreamEvents(raw)) this.handleClaudeStreamEvent(event);
4103
+ } catch (error) {
4104
+ stderr += `${line}\n`;
4105
+ this.send({
4106
+ type: 'agent:activity',
4107
+ agentId: this.agent.id,
4108
+ status: 'working',
4109
+ deliveryId: this.activeDeliveryId || null,
4110
+ activity: { source: 'claude-code-stream', error: `Invalid stream JSON: ${error.message}`, at: now() },
4111
+ });
4112
+ }
4113
+ }
3649
4114
  });
3650
4115
  this.child.stderr.on('data', (chunk) => {
3651
4116
  stderr += chunk.toString();
3652
4117
  });
3653
4118
  this.child.on('error', (error) => finish('error', error.message));
3654
4119
  this.child.on('close', (code, signal) => {
4120
+ if (stdoutBuffer.trim()) {
4121
+ try {
4122
+ const raw = JSON.parse(stdoutBuffer.trim());
4123
+ for (const event of claudeStreamEvents(raw)) this.handleClaudeStreamEvent(event);
4124
+ } catch (error) {
4125
+ stderr += `${stdoutBuffer}\n`;
4126
+ }
4127
+ stdoutBuffer = '';
4128
+ }
3655
4129
  if (code === 0) finish('idle');
3656
4130
  else finish('error', stderr.trim() || `Claude Code exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`);
3657
4131
  });
@@ -3660,6 +4134,7 @@ class ClaudeAgentSession {
3660
4134
 
3661
4135
  stop() {
3662
4136
  this.status = 'stopping';
4137
+ this.flushMessageDelta();
3663
4138
  if (this.child) this.child.kill('SIGTERM');
3664
4139
  this.started = false;
3665
4140
  }
@@ -4600,7 +5075,11 @@ class MagClawDaemon {
4600
5075
  }
4601
5076
  const sessionContext = {
4602
5077
  sessionKey: message.payload?.sessionKey || message.sessionKey || '',
4603
- workspaceId: message.workspaceId || message.payload?.workspaceId || '',
5078
+ workspaceId: message.payload?.message?.workspaceId
5079
+ || message.payload?.workItem?.workspaceId
5080
+ || message.payload?.workspaceId
5081
+ || message.workspaceId
5082
+ || '',
4604
5083
  message: message.payload?.message || {},
4605
5084
  };
4606
5085
  const sessionKey = sessionContext.sessionKey || daemonConversationLaneKey({
package/src/mcp-bridge.js CHANGED
@@ -246,6 +246,36 @@ const tools = [
246
246
  targetAgent: { type: 'string' },
247
247
  }),
248
248
  },
249
+ {
250
+ name: 'read_agent_avatar',
251
+ description: 'Read a MagClaw agent avatar image for visual comparison with uploaded attachments.',
252
+ inputSchema: schema({
253
+ targetAgentId: { type: 'string' },
254
+ targetAgent: { type: 'string' },
255
+ maxBytes: { type: 'number' },
256
+ }),
257
+ },
258
+ {
259
+ name: 'list_attachments',
260
+ description: 'List MagClaw attachment metadata visible to this agent.',
261
+ inputSchema: schema({
262
+ target: { type: 'string' },
263
+ channel: { type: 'string' },
264
+ workItemId: { type: 'string' },
265
+ messageId: { type: 'string' },
266
+ limit: { type: 'number' },
267
+ }),
268
+ },
269
+ {
270
+ name: 'read_attachment',
271
+ description: 'Read an uploaded MagClaw attachment original file. Image attachments are returned as MCP image content when possible.',
272
+ inputSchema: schema({
273
+ attachmentId: { type: 'string' },
274
+ id: { type: 'string' },
275
+ maxBytes: { type: 'number' },
276
+ format: { type: 'string' },
277
+ }),
278
+ },
249
279
  {
250
280
  name: 'write_memory',
251
281
  description: 'Record a concise durable memory for this agent.',
@@ -358,8 +388,20 @@ function sendError(id, code, message, data = null) {
358
388
  process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, error: { code, message, data } })}\n`);
359
389
  }
360
390
 
361
- function textResult(text) {
362
- return { content: [{ type: 'text', text: String(text || '') }] };
391
+ function imageResultContent(value = {}) {
392
+ const type = String(value?.avatar?.type || value?.file?.type || value?.attachment?.type || '').toLowerCase();
393
+ if (!type.startsWith('image/')) return null;
394
+ if (value?.file?.truncated || value?.avatar?.truncated) return null;
395
+ const data = String(value?.contentBase64 || '').trim();
396
+ if (!data) return null;
397
+ return { type: 'image', data, mimeType: type };
398
+ }
399
+
400
+ function contentResult(value) {
401
+ const content = [{ type: 'text', text: jsonText(value) }];
402
+ const image = imageResultContent(value);
403
+ if (image) content.push(image);
404
+ return { content };
363
405
  }
364
406
 
365
407
  function jsonText(value) {
@@ -511,6 +553,33 @@ async function callTool(name, rawArgs = {}) {
511
553
  targetAgentId: args.targetAgentId || args.targetAgent,
512
554
  },
513
555
  });
556
+ case 'read_agent_avatar':
557
+ return request('/api/agent-tools/agents/avatar/read', {
558
+ query: {
559
+ agentId: args.agentId,
560
+ targetAgentId: args.targetAgentId || args.targetAgent,
561
+ maxBytes: args.maxBytes || args.max_bytes,
562
+ },
563
+ });
564
+ case 'list_attachments':
565
+ return request('/api/agent-tools/attachments', {
566
+ query: {
567
+ agentId: args.agentId,
568
+ target: args.target || args.channel,
569
+ workItemId: args.workItemId || args.work_item_id,
570
+ messageId: args.messageId || args.message_id,
571
+ limit: args.limit,
572
+ },
573
+ });
574
+ case 'read_attachment':
575
+ return request('/api/agent-tools/attachments/read', {
576
+ query: {
577
+ agentId: args.agentId,
578
+ attachmentId: args.attachmentId || args.attachment_id || args.id,
579
+ maxBytes: args.maxBytes || args.max_bytes,
580
+ format: args.format,
581
+ },
582
+ });
514
583
  case 'list_tasks':
515
584
  return request('/api/agent-tools/tasks', {
516
585
  query: {
@@ -587,7 +656,7 @@ async function handle(message) {
587
656
  }
588
657
  if (message.method === 'tools/call') {
589
658
  const result = await callTool(message.params?.name, message.params?.arguments || {});
590
- send(id, textResult(jsonText(result)));
659
+ send(id, contentResult(result));
591
660
  return;
592
661
  }
593
662
  if (message.method === 'notifications/initialized' || message.method === 'initialized') return;