openbot 0.4.0 → 0.4.3

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.
Files changed (50) hide show
  1. package/dist/app/cli.js +1 -1
  2. package/dist/app/config.js +10 -0
  3. package/dist/app/server.js +200 -3
  4. package/dist/harness/index.js +18 -0
  5. package/dist/plugins/approval/index.js +35 -20
  6. package/dist/plugins/bash/index.js +195 -0
  7. package/dist/plugins/delegation/index.js +6 -2
  8. package/dist/plugins/openbot/context.js +54 -9
  9. package/dist/plugins/openbot/history.js +47 -1
  10. package/dist/plugins/openbot/index.js +43 -3
  11. package/dist/plugins/openbot/runtime.js +91 -27
  12. package/dist/plugins/openbot/system-prompt.js +21 -1
  13. package/dist/plugins/plugin-manager/index.js +87 -3
  14. package/dist/plugins/shell/index.js +2 -1
  15. package/dist/plugins/storage/files.js +67 -0
  16. package/dist/plugins/storage/index.js +184 -7
  17. package/dist/plugins/storage/service.js +215 -59
  18. package/dist/plugins/ui/index.js +109 -150
  19. package/dist/services/abort.js +43 -0
  20. package/dist/services/plugins/registry.js +5 -3
  21. package/dist/services/plugins/service.js +66 -11
  22. package/docs/agents.md +5 -8
  23. package/docs/architecture.md +1 -1
  24. package/docs/plugins.md +28 -7
  25. package/docs/templates/AGENT.example.md +4 -4
  26. package/package.json +7 -7
  27. package/src/app/cli.ts +1 -1
  28. package/src/app/config.ts +13 -0
  29. package/src/app/server.ts +235 -3
  30. package/src/app/types.ts +284 -14
  31. package/src/harness/index.ts +21 -0
  32. package/src/plugins/approval/index.ts +37 -20
  33. package/src/plugins/bash/index.ts +232 -0
  34. package/src/plugins/delegation/index.ts +7 -2
  35. package/src/plugins/openbot/context.ts +58 -9
  36. package/src/plugins/openbot/history.ts +52 -1
  37. package/src/plugins/openbot/index.ts +45 -3
  38. package/src/plugins/openbot/runtime.ts +121 -27
  39. package/src/plugins/openbot/system-prompt.ts +21 -1
  40. package/src/plugins/plugin-manager/index.ts +105 -3
  41. package/src/plugins/storage/files.ts +81 -0
  42. package/src/plugins/storage/index.ts +198 -8
  43. package/src/plugins/storage/service.ts +282 -59
  44. package/src/plugins/ui/index.ts +123 -0
  45. package/src/services/abort.ts +46 -0
  46. package/src/services/plugins/domain.ts +34 -1
  47. package/src/services/plugins/registry.ts +5 -3
  48. package/src/services/plugins/service.ts +136 -45
  49. package/src/services/plugins/types.ts +5 -1
  50. package/src/plugins/shell/index.ts +0 -123
@@ -1,5 +1,5 @@
1
1
  import { ORCHESTRATOR_AGENT_ID, STATE_AGENT_ID } from '../../app/agent-ids.js';
2
- import { DEFAULT_PLUGINS_DIR, DEFAULT_AGENTS_DIR, DEFAULT_BASE_DIR, DEFAULT_CHANNELS_DIR, loadConfig, resolvePath, VARIABLES_FILE, } from '../../app/config.js';
2
+ import { DEFAULT_PLUGINS_DIR, DEFAULT_AGENTS_DIR, DEFAULT_BASE_DIR, DEFAULT_CHANNELS_DIR, getDefaultChannelCwd, loadConfig, resolvePath, VARIABLES_FILE, } from '../../app/config.js';
3
3
  import fs from 'node:fs/promises';
4
4
  import { readFileSync } from 'node:fs';
5
5
  import path from 'node:path';
@@ -7,10 +7,10 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
7
7
  import crypto from 'node:crypto';
8
8
  import matter from 'gray-matter';
9
9
  import { openbotPlugin } from '../openbot/index.js';
10
- import { OPENBOT_SYSTEM_PROMPT } from '../openbot/system-prompt.js';
11
10
  import { listBuiltInPlugins, parsePluginModule } from '../../services/plugins/registry.js';
12
11
  import { processService } from '../../services/process.js';
13
12
  import { memoryService } from '../memory/service.js';
13
+ import { guessMimeType, resolveChannelFile, statChannelFile, } from './files.js';
14
14
  const resolveBaseDir = () => {
15
15
  const config = loadConfig();
16
16
  return resolvePath(config.baseDir || DEFAULT_BASE_DIR);
@@ -80,14 +80,17 @@ const getConversationDir = (channelId, threadId) => {
80
80
  /** Built-in orchestrator agent id. Not creatable as a normal disk agent. */
81
81
  const SYSTEM_AGENT_ID = ORCHESTRATOR_AGENT_ID;
82
82
  const SYSTEM_DEFAULT_PLUGINS = [
83
- { id: 'openbot', config: { model: 'openai/gpt-5.4-mini' } },
84
- { id: 'shell' },
85
- { id: 'approval' },
86
- { id: 'memory' },
87
- { id: 'delegation' },
88
- { id: 'storage' },
83
+ {
84
+ id: 'openbot',
85
+ config: {
86
+ model: 'openai/gpt-5.4-mini',
87
+ approval: {
88
+ actions: ['action:bash', 'action:create_channel', 'action:delete_channel'],
89
+ },
90
+ },
91
+ },
89
92
  ];
90
- /** No `openbot` / `shell` — storage-side effects and infra plugins only. */
93
+ /** No `openbot` / `bash` — storage-side effects and infra plugins only. */
91
94
  const STATE_DEFAULT_PLUGINS = [
92
95
  { id: 'storage' },
93
96
  { id: 'plugin-manager' },
@@ -99,7 +102,7 @@ function getSystemAgentDetails(overrides) {
99
102
  name: 'OpenBot',
100
103
  image: getBundledSystemAgentImage(),
101
104
  description: 'First-party orchestration agent for OpenBot.',
102
- instructions: OPENBOT_SYSTEM_PROMPT,
105
+ instructions: '',
103
106
  plugins: SYSTEM_DEFAULT_PLUGINS.map((ref) => ref.id),
104
107
  pluginRefs: SYSTEM_DEFAULT_PLUGINS,
105
108
  createdAt: new Date(),
@@ -179,6 +182,22 @@ const assertAgentIdFormat = (agentId) => {
179
182
  };
180
183
  const getAgentsRootDir = () => path.join(resolveBaseDir(), DEFAULT_AGENTS_DIR);
181
184
  const getLastReadFilePath = () => path.join(resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR), '_meta', 'last-read.json');
185
+ /** Sentinel key for channel-root events (no threadId). */
186
+ export const ROOT_THREAD_KEY = '__root__';
187
+ const readLastReadMap = async () => {
188
+ const raw = await readJsonFile(getLastReadFilePath(), {});
189
+ const map = {};
190
+ for (const [channelId, value] of Object.entries(raw)) {
191
+ if (typeof value === 'string') {
192
+ // Migrate old format
193
+ map[channelId] = { [ROOT_THREAD_KEY]: value };
194
+ }
195
+ else if (value && typeof value === 'object') {
196
+ map[channelId] = value;
197
+ }
198
+ }
199
+ return map;
200
+ };
182
201
  const THREAD_TITLE_MAX_LENGTH = 80;
183
202
  const buildThreadTitleFromEvent = (event) => {
184
203
  let rawContent = '';
@@ -197,14 +216,25 @@ const buildThreadTitleFromEvent = (event) => {
197
216
  };
198
217
  const readJsonFile = async (filePath, fallback) => {
199
218
  try {
200
- return JSON.parse(await fs.readFile(filePath, 'utf-8'));
219
+ const content = (await fs.readFile(filePath, 'utf-8')).trim();
220
+ if (!content)
221
+ return fallback;
222
+ return JSON.parse(content);
201
223
  }
202
224
  catch (e) {
203
225
  if (e?.code === 'ENOENT')
204
226
  return fallback;
227
+ if (e instanceof SyntaxError)
228
+ return fallback;
205
229
  throw e;
206
230
  }
207
231
  };
232
+ const writeJsonFileAtomically = async (filePath, data) => {
233
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
234
+ const tmp = `${filePath}.tmp`;
235
+ await fs.writeFile(tmp, JSON.stringify(data, null, 2), 'utf-8');
236
+ await fs.rename(tmp, filePath);
237
+ };
208
238
  const toVariablesRecord = (raw) => {
209
239
  if (!raw || typeof raw !== 'object') {
210
240
  return {};
@@ -352,15 +382,19 @@ const parsePluginRefs = (raw) => {
352
382
  };
353
383
  const serializePluginRefs = (refs) => refs.map((ref) => (ref.config ? { id: ref.id, config: ref.config } : { id: ref.id }));
354
384
  export const storageService = {
355
- getLastReadByChannel: async () => {
356
- return readJsonFile(getLastReadFilePath(), {});
385
+ getLastReadMap: async () => {
386
+ return readLastReadMap();
357
387
  },
358
- setLastReadForChannel: async ({ channelId, lastReadEventId, }) => {
388
+ setLastRead: async ({ channelId, threadId, lastReadEventId, }) => {
359
389
  const p = getLastReadFilePath();
360
390
  await fs.mkdir(path.dirname(p), { recursive: true });
361
- const map = await readJsonFile(p, {});
362
- map[channelId] = lastReadEventId;
363
- await fs.writeFile(p, JSON.stringify(map, null, 2), 'utf-8');
391
+ const map = await readLastReadMap();
392
+ if (!map[channelId])
393
+ map[channelId] = {};
394
+ map[channelId][threadId || ROOT_THREAD_KEY] = lastReadEventId;
395
+ const tmp = `${p}.tmp`;
396
+ await fs.writeFile(tmp, JSON.stringify(map, null, 2), 'utf-8');
397
+ await fs.rename(tmp, p);
364
398
  },
365
399
  getChannels: async () => {
366
400
  const channelsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR);
@@ -371,16 +405,16 @@ export const storageService = {
371
405
  await fs.mkdir(channelsDir, { recursive: true });
372
406
  }
373
407
  const channelNames = (await fs.readdir(channelsDir)).filter((name) => !name.startsWith('.') && name !== '_meta');
374
- const lastReadByChannel = await storageService.getLastReadByChannel();
408
+ const lastReadMap = await storageService.getLastReadMap();
375
409
  const channels = await Promise.all(channelNames.map(async (name) => {
376
410
  const channelDir = getConversationDir(name);
411
+ const stats = await fs.stat(channelDir);
377
412
  const statePath = path.join(channelDir, 'state.json');
378
413
  let cwd;
379
414
  let displayName = name;
380
415
  let participants = [];
381
416
  try {
382
- const stateContent = await fs.readFile(statePath, 'utf-8');
383
- const parsed = JSON.parse(stateContent);
417
+ const parsed = await readJsonFile(statePath, {});
384
418
  const fields = readChannelStateFileFields(parsed);
385
419
  cwd = fields.cwd;
386
420
  displayName = fields.name ?? name;
@@ -395,30 +429,30 @@ export const storageService = {
395
429
  description: '',
396
430
  cwd,
397
431
  participants,
398
- createdAt: new Date(),
399
- updatedAt: new Date(),
432
+ createdAt: stats.birthtime,
433
+ updatedAt: stats.mtime,
400
434
  };
401
- const rid = lastReadByChannel[name];
402
- try {
403
- const events = await storageService.getEvents({ channelId: name });
404
- const latestId = events[events.length - 1]?.id;
405
- channel.hasUnseenMessages = !!(latestId && latestId !== rid);
406
- }
407
- catch {
408
- channel.hasUnseenMessages = false;
409
- }
435
+ const channelLastRead = lastReadMap[name] || {};
410
436
  try {
437
+ // Check root unread
438
+ const rootEvents = await storageService.getEvents({ channelId: name });
439
+ const rootLatestId = rootEvents[rootEvents.length - 1]?.id;
440
+ const rootUnseen = !!(rootLatestId && rootLatestId !== channelLastRead[ROOT_THREAD_KEY]);
441
+ // Check threads unread
411
442
  const allThreads = await storageService.getThreads({ channelId: name });
412
443
  channel.recentThreads = allThreads
413
444
  .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
414
445
  .slice(0, 5);
446
+ const threadsUnseen = allThreads.some(t => t.hasUnseenMessages);
447
+ channel.hasUnseenMessages = rootUnseen || threadsUnseen;
415
448
  }
416
449
  catch {
450
+ channel.hasUnseenMessages = false;
417
451
  channel.recentThreads = [];
418
452
  }
419
453
  return channel;
420
454
  }));
421
- return channels;
455
+ return channels.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
422
456
  },
423
457
  createChannel: async ({ channelId, spec, initialState, cwd, }) => {
424
458
  const normalizedChannelId = channelId.trim();
@@ -441,13 +475,52 @@ export const storageService = {
441
475
  const finalState = {
442
476
  ...(initialState || {}),
443
477
  };
444
- if (cwd) {
445
- finalState.cwd = cwd;
446
- }
478
+ const rawCwd = (typeof cwd === 'string' && cwd.trim()) ||
479
+ (typeof finalState.cwd === 'string' && finalState.cwd.trim()) ||
480
+ getDefaultChannelCwd(normalizedChannelId);
481
+ const resolvedCwd = resolvePath(rawCwd);
482
+ finalState.cwd = resolvedCwd;
483
+ await fs.mkdir(resolvedCwd, { recursive: true });
447
484
  await fs.mkdir(channelDir, { recursive: true });
448
485
  await fs.writeFile(specPath, spec?.trim() ||
449
- `# ${normalizedChannelId}\n\nDefine the goals and rules for this channel here.\n`);
450
- await fs.writeFile(statePath, JSON.stringify(finalState, null, 2));
486
+ `# ${normalizedChannelId}\n\n`);
487
+ await writeJsonFileAtomically(statePath, finalState);
488
+ },
489
+ deleteChannel: async ({ channelId }) => {
490
+ const normalizedChannelId = channelId.trim();
491
+ if (!normalizedChannelId) {
492
+ throw new Error('channelId is required');
493
+ }
494
+ if (normalizedChannelId === '_meta') {
495
+ throw new Error('Cannot delete reserved channel path');
496
+ }
497
+ const channelDir = getConversationDir(normalizedChannelId);
498
+ try {
499
+ await fs.access(channelDir);
500
+ }
501
+ catch (error) {
502
+ if (error?.code === 'ENOENT') {
503
+ const err = new Error(`Channel "${normalizedChannelId}" does not exist.`);
504
+ err.code = 'CHANNEL_NOT_FOUND';
505
+ throw err;
506
+ }
507
+ throw error;
508
+ }
509
+ await fs.rm(channelDir, { recursive: true, force: true });
510
+ try {
511
+ const lastReadPath = getLastReadFilePath();
512
+ const map = await readJsonFile(lastReadPath, {});
513
+ if (normalizedChannelId in map) {
514
+ delete map[normalizedChannelId];
515
+ await fs.mkdir(path.dirname(lastReadPath), { recursive: true });
516
+ const tmp = `${lastReadPath}.tmp`;
517
+ await fs.writeFile(tmp, JSON.stringify(map, null, 2), 'utf-8');
518
+ await fs.rename(tmp, lastReadPath);
519
+ }
520
+ }
521
+ catch {
522
+ // ignore last-read cleanup failures
523
+ }
451
524
  },
452
525
  createThread: async ({ channelId, threadId, threadTitle, initialState, }) => {
453
526
  const normalizedChannelId = channelId.trim();
@@ -472,8 +545,7 @@ export const storageService = {
472
545
  if (threadTitle?.trim()) {
473
546
  baseState.name = threadTitle.trim();
474
547
  }
475
- await fs.mkdir(threadDir, { recursive: true });
476
- await fs.writeFile(statePath, JSON.stringify(baseState, null, 2));
548
+ await writeJsonFileAtomically(statePath, baseState);
477
549
  },
478
550
  getThreads: async ({ channelId }) => {
479
551
  const threadsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR + '/' + channelId + '/threads');
@@ -483,6 +555,8 @@ export const storageService = {
483
555
  catch {
484
556
  return [];
485
557
  }
558
+ const lastReadMap = await storageService.getLastReadMap();
559
+ const channelLastRead = lastReadMap[channelId] || {};
486
560
  const threadNames = (await fs.readdir(threadsDir)).filter((name) => !name.startsWith('.'));
487
561
  const threads = await Promise.all(threadNames.map(async (name) => {
488
562
  const threadPath = path.join(threadsDir, name);
@@ -490,8 +564,7 @@ export const storageService = {
490
564
  const threadStatePath = path.join(threadPath, 'state.json');
491
565
  let threadDisplayName = name;
492
566
  try {
493
- const threadStateRaw = await fs.readFile(threadStatePath, 'utf-8');
494
- const threadState = JSON.parse(threadStateRaw);
567
+ const threadState = await readJsonFile(threadStatePath, {});
495
568
  const threadName = typeof threadState.name === 'string' ? threadState.name.trim() : '';
496
569
  if (threadName) {
497
570
  threadDisplayName = threadName;
@@ -502,12 +575,22 @@ export const storageService = {
502
575
  console.error(`Failed to read thread state for channel ${channelId} thread ${name}`, error);
503
576
  }
504
577
  }
578
+ let hasUnseen = false;
579
+ try {
580
+ const events = await storageService.getEvents({ channelId, threadId: name });
581
+ const latestId = events[events.length - 1]?.id;
582
+ hasUnseen = !!(latestId && latestId !== channelLastRead[name]);
583
+ }
584
+ catch {
585
+ // ignore
586
+ }
505
587
  return {
506
588
  id: name,
507
589
  name: threadDisplayName,
508
590
  channelId,
509
591
  createdAt: stats.birthtime,
510
592
  updatedAt: stats.mtime,
593
+ hasUnseenMessages: hasUnseen,
511
594
  };
512
595
  }));
513
596
  return threads;
@@ -517,8 +600,7 @@ export const storageService = {
517
600
  const statePath = `${threadDir}/state.json`;
518
601
  let state = {};
519
602
  try {
520
- const stateContent = await fs.readFile(statePath, 'utf-8');
521
- state = JSON.parse(stateContent);
603
+ state = await readJsonFile(statePath, {});
522
604
  }
523
605
  catch (error) {
524
606
  if (error?.code !== 'ENOENT') {
@@ -550,8 +632,7 @@ export const storageService = {
550
632
  }
551
633
  let state = {};
552
634
  try {
553
- const stateContent = await fs.readFile(statePath, 'utf-8');
554
- state = JSON.parse(stateContent);
635
+ state = await readJsonFile(statePath, {});
555
636
  }
556
637
  catch (error) {
557
638
  if (error?.code !== 'ENOENT') {
@@ -582,8 +663,7 @@ export const storageService = {
582
663
  ...currentState,
583
664
  ...patch,
584
665
  };
585
- await fs.mkdir(channelDir, { recursive: true });
586
- await fs.writeFile(statePath, JSON.stringify(newState, null, 2));
666
+ await writeJsonFileAtomically(statePath, newState);
587
667
  }
588
668
  catch (error) {
589
669
  console.error(`Failed to patch channel state for channel ${channelId}`, error);
@@ -600,8 +680,7 @@ export const storageService = {
600
680
  ...currentState,
601
681
  ...patch,
602
682
  };
603
- await fs.mkdir(threadDir, { recursive: true });
604
- await fs.writeFile(statePath, JSON.stringify(newState, null, 2));
683
+ await writeJsonFileAtomically(statePath, newState);
605
684
  }
606
685
  catch (error) {
607
686
  console.error(`Failed to patch thread state for channel ${channelId} thread ${threadId}`, error);
@@ -1074,11 +1153,9 @@ export const storageService = {
1074
1153
  if (!baseCwd) {
1075
1154
  throw new Error('Channel has no CWD configured');
1076
1155
  }
1077
- const resolvedBase = path.resolve(baseCwd);
1078
- const targetDir = path.resolve(resolvedBase, subPath);
1079
- if (!targetDir.startsWith(resolvedBase)) {
1080
- throw new Error('Access denied: directory escape');
1081
- }
1156
+ const targetDir = subPath
1157
+ ? resolveChannelFile(baseCwd, subPath)
1158
+ : resolvePath(baseCwd);
1082
1159
  const entries = await fs.readdir(targetDir, { withFileTypes: true });
1083
1160
  return entries
1084
1161
  .filter((e) => !e.name.startsWith('.'))
@@ -1093,13 +1170,88 @@ export const storageService = {
1093
1170
  if (!baseCwd) {
1094
1171
  throw new Error('Channel has no CWD configured');
1095
1172
  }
1096
- const resolvedBase = path.resolve(baseCwd);
1097
- const targetFile = path.resolve(resolvedBase, filePath);
1098
- if (!targetFile.startsWith(resolvedBase)) {
1099
- throw new Error('Access denied: directory escape');
1100
- }
1173
+ const targetFile = resolveChannelFile(baseCwd, filePath);
1101
1174
  return fs.readFile(targetFile, 'utf-8');
1102
1175
  },
1176
+ readChannelFile: async ({ channelId, path: filePath, encoding = 'utf8', }) => {
1177
+ const details = await storageService.getChannelDetails({ channelId });
1178
+ const baseCwd = details.cwd;
1179
+ if (!baseCwd) {
1180
+ throw new Error('Channel has no CWD configured');
1181
+ }
1182
+ const targetFile = resolveChannelFile(baseCwd, filePath);
1183
+ const buf = await fs.readFile(targetFile);
1184
+ const content = encoding === 'base64' ? buf.toString('base64') : buf.toString('utf-8');
1185
+ return {
1186
+ content,
1187
+ mimeType: guessMimeType(targetFile),
1188
+ size: buf.length,
1189
+ };
1190
+ },
1191
+ writeChannelFile: async ({ channelId, path: filePath, content, encoding = 'utf8', overwrite = false, }) => {
1192
+ const details = await storageService.getChannelDetails({ channelId });
1193
+ const baseCwd = details.cwd;
1194
+ if (!baseCwd) {
1195
+ throw new Error('Channel has no CWD configured');
1196
+ }
1197
+ const abs = resolveChannelFile(baseCwd, filePath);
1198
+ await fs.mkdir(path.dirname(abs), { recursive: true });
1199
+ if (!overwrite) {
1200
+ try {
1201
+ await fs.access(abs);
1202
+ throw new Error('File already exists');
1203
+ }
1204
+ catch (error) {
1205
+ const code = error?.code;
1206
+ if (code !== 'ENOENT') {
1207
+ throw error;
1208
+ }
1209
+ }
1210
+ }
1211
+ const buf = encoding === 'base64' ? Buffer.from(content, 'base64') : Buffer.from(content, 'utf8');
1212
+ await fs.writeFile(abs, buf);
1213
+ return {
1214
+ path: filePath,
1215
+ size: buf.length,
1216
+ mimeType: guessMimeType(abs),
1217
+ };
1218
+ },
1219
+ uploadChannelFile: async ({ channelId, path: filePath, body, overwrite = false, }) => {
1220
+ const details = await storageService.getChannelDetails({ channelId });
1221
+ const baseCwd = details.cwd;
1222
+ if (!baseCwd) {
1223
+ throw new Error('Channel has no CWD configured');
1224
+ }
1225
+ const abs = resolveChannelFile(baseCwd, filePath);
1226
+ await fs.mkdir(path.dirname(abs), { recursive: true });
1227
+ if (!overwrite) {
1228
+ try {
1229
+ await fs.access(abs);
1230
+ throw new Error('File already exists');
1231
+ }
1232
+ catch (error) {
1233
+ const code = error?.code;
1234
+ if (code !== 'ENOENT') {
1235
+ throw error;
1236
+ }
1237
+ }
1238
+ }
1239
+ await fs.writeFile(abs, body);
1240
+ return {
1241
+ path: filePath,
1242
+ size: body.length,
1243
+ mimeType: guessMimeType(abs),
1244
+ };
1245
+ },
1246
+ getChannelFileStat: async ({ channelId, path: filePath, }) => {
1247
+ const details = await storageService.getChannelDetails({ channelId });
1248
+ const baseCwd = details.cwd;
1249
+ if (!baseCwd) {
1250
+ throw new Error('Channel has no CWD configured');
1251
+ }
1252
+ const { abs, size } = await statChannelFile(baseCwd, filePath);
1253
+ return { abs, size, mimeType: guessMimeType(abs) };
1254
+ },
1103
1255
  appendMemory: memoryService.appendMemory,
1104
1256
  listMemories: memoryService.listMemories,
1105
1257
  deleteMemory: memoryService.deleteMemory,
@@ -1135,12 +1287,16 @@ export const storageService = {
1135
1287
  console.warn(`[storage] Failed to load thread details for channel ${channelId} thread: ${threadId}`, error);
1136
1288
  }
1137
1289
  }
1290
+ const threadState = threadDetails?.state || {};
1138
1291
  return {
1139
1292
  runId,
1140
1293
  agentId,
1141
1294
  channelId,
1142
1295
  threadId,
1143
1296
  triggerEvent: event,
1297
+ pendingToolCallIds: Array.isArray(threadState.pendingToolCallIds)
1298
+ ? threadState.pendingToolCallIds
1299
+ : undefined,
1144
1300
  agentDetails: {
1145
1301
  id: agentDetails.id,
1146
1302
  name: agentDetails.name,