ds-bot 0.1.2 → 0.1.4

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.
@@ -40,7 +40,7 @@ export function install(rt) {
40
40
  ...bot, sessionId: rt.chatOf(bot.id), parts: (segments ?? []).map(segment => segment.sessionId), ...(kickoff ? { waiting: true } : {}),
41
41
  })),
42
42
  // `parts`: the members' hidden group Sessions and their earlier segments, for usage.
43
- rooms: Object.values(state.rooms).map(({ log, seen, botRounds, sessions, segments, ...room }) => {
43
+ rooms: Object.values(state.rooms).map(({ log, seen, botRounds, lateRounds, sessions, segments, ...room }) => {
44
44
  const parts = [...Object.values(sessions ?? {}), ...Object.values(segments ?? {}).flat().map(segment => segment.sessionId)]
45
45
  const last = (log ?? []).filter(line => line.kind === undefined).at(-1)
46
46
  return { ...room, parts, ...(last ? { preview: last.who === 'User' ? last.text : `${last.who}:${last.text}` } : {}) }
@@ -51,6 +51,7 @@ export function install(rt) {
51
51
  agents: Object.values(state.agents),
52
52
  // Key names and who may use them; values never leave the host.
53
53
  secrets: rt.secretsView(),
54
+ capabilities: rt.capabilityLibraryView(),
54
55
  secretRequests: state.secretRequests ?? {},
55
56
  colors: [...COLORS, ...EXTRA_COLORS],
56
57
  models: rt.cachedCatalog(),
@@ -67,11 +68,19 @@ export function install(rt) {
67
68
  })
68
69
  // What changes without a new revision, sent with every state answer. `memoryNews`:
69
70
  // per Bot, its latest memory change; `memoryReviews`: memory reviews waiting or
70
- // running.
71
- const live = () => ({ activity: rt.activityView(), memoryNews: rt.memoryNews(), memoryReviews: rt.memoryReviews(), connectors: rt.connectorsView() })
71
+ // running; `work`: per Bot, the turn it runs and its tool; per group, the members whose
72
+ // turn there runs; and the approvals waiting.
73
+ const live = () => ({
74
+ activity: rt.activityView(),
75
+ work: { bots: rt.workView(), rooms: rt.roomWorkView(), approvals: rt.approvalsView() },
76
+ memoryNews: rt.memoryNews(),
77
+ memoryReviews: rt.memoryReviews(),
78
+ connectors: rt.connectorsView(),
79
+ capabilityStatuses: rt.capabilityStatusesView(),
80
+ })
72
81
  // What a host that only reads the team may still answer.
73
82
  // Updates change the dsh profile, not the team, so a reading host may run them too.
74
- const READS = new Set(['state', 'models', 'efforts', 'earlier', 'room-progress', 'exchange', 'usage', 'schedules', 'memory-view', 'browser-wait', 'browser-result', 'diagnostics', 'diagnostics-log', 'diagnostics-clear', 'update-check', 'update-install', 'dsh-update-install', 'repo-stars'])
83
+ const READS = new Set(['state', 'models', 'efforts', 'earlier', 'room-progress', 'exchange', 'usage', 'schedules', 'memory-view', 'browser-wait', 'browser-result', 'diagnostics', 'diagnostics-log', 'diagnostics-clear', 'update-check', 'update-install', 'dsh-update-install', 'repo-stars', 'approval-answer'])
75
84
 
76
85
  function setPrefs(body) {
77
86
  if (body.theme !== undefined) {
@@ -196,6 +205,11 @@ export function install(rt) {
196
205
  await rt.load()
197
206
  await rt.deleteRoom(body.id)
198
207
  return ok(view())
208
+ case 'approval-answer':
209
+ rt.answerApproval(body.id, body.choice)
210
+ return ok(true)
211
+ case 'stop-member-work':
212
+ return ok(await rt.stopMemberWork(String(body.roomId ?? ''), String(body.botId ?? '')))
199
213
  case 'dismiss-question':
200
214
  await rt.load()
201
215
  if (state.questions[body.sessionId] !== undefined) {
@@ -215,6 +229,12 @@ export function install(rt) {
215
229
  case 'connector-retry':
216
230
  await rt.load()
217
231
  return ok(await rt.connectorEndpoint(endpoint, body))
232
+ case 'capability-save':
233
+ case 'capability-delete':
234
+ case 'capability-approve':
235
+ case 'bot-capability':
236
+ await rt.load()
237
+ return ok(await rt.capabilityEndpoint(endpoint, body))
218
238
  case 'schedules':
219
239
  case 'schedule-create':
220
240
  case 'schedule-delete':
@@ -237,6 +257,7 @@ export function install(rt) {
237
257
  // Claim the question before queueing so a second answer is refused.
238
258
  delete state.questions[sessionId]
239
259
  try {
260
+ await rt.ensureCapabilities(sessionId)
240
261
  const agent = await rt.resolveAgent(sessionId)
241
262
  agent.followup(createUserMessage({ content: [{ type: 'text', text: reply.text }], source: reply.source }))
242
263
  await ctx.sessions.flush(agent.session)
@@ -254,6 +275,7 @@ export function install(rt) {
254
275
  // A Bot id or an earlier part of its own chat means the current part.
255
276
  const past = rt.pastOwners.get(body.sessionId)
256
277
  const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId) : past && past.roomId === undefined ? rt.chatOf(past.botId) : body.sessionId
278
+ await rt.ensureCapabilities(sessionId)
257
279
  await ctx.sessionController.prompt({
258
280
  requestId: randomUUID(),
259
281
  sessionId,
@@ -26,6 +26,9 @@ export const ROOM_REPLY_TIMEOUT_MS = 240_000
26
26
  export const SOURCE_KIND = 'bot'
27
27
  // An answer from a question card, in the shape DSH's own late question replies use.
28
28
  export const REPLY_KIND = 'user-question-reply'
29
+ // DSH's snapshot of a referenced conversation, logged right after the user's message
30
+ // that cites it.
31
+ export const REFERENCE_KIND = 'session-reference'
29
32
  // `everyone`: every member answers a plain message in turn, admin first. `admin`: a
30
33
  // plain message goes to the admin, who calls others with @mentions. New groups start
31
34
  // in `admin` mode, because every extra speaker re-reads its whole context; a group
@@ -42,13 +45,21 @@ export const AGENT_SESSION_TITLE_MAX = 80
42
45
  // Every tool this plugin registers for Bots. `read_browser` only exists in the
43
46
  // Desktop profile, so a restriction filters this list down to registered names.
44
47
  export const TEAM_TOOLS = [
45
- 'list_bots', 'message_bot', 'create_bot', 'update_bot', 'list_models',
48
+ 'list_bots', 'message_bot', 'stop_bot', 'create_bot', 'update_bot', 'list_models',
46
49
  'create_group', 'update_group', 'delete_group', 'post_to_group', 'ask_user',
47
50
  'request_secret', 'list_secrets', 'group_relay', 'read_own_chat',
48
51
  'read_group_chat', 'remember', 'forget', 'recall', 'read_browser',
52
+ 'list_capabilities', 'save_capability', 'delete_capability',
53
+ 'attach_capability', 'detach_capability',
49
54
  ]
50
55
  // Rounds Bots may start in one group by posting, before the user speaks there again.
51
56
  export const ROOM_BOT_ROUNDS = 3
57
+ // Answers a group gives to what members post after the round moved on (their finished
58
+ // work, mostly), before the user speaks there again.
59
+ export const ROOM_LATE_ROUNDS = 10
60
+ // A member still at work this long after its first tool call lets the round go on; it
61
+ // posts its reply when the work is done.
62
+ export const ROOM_WORK_GRACE_MS = 15_000
52
63
 
53
64
  export const OWN_CHAT_LINE_MAX = 400
54
65
  // The message an anchor points at is shown whole (up to this), so a long draft takes
@@ -12,8 +12,13 @@ export function install(rt) {
12
12
  return resolved.agent
13
13
  }
14
14
 
15
- const headerFor = (role, fromName, roomName, audience, note = 'new message') => {
15
+ const headerFor = (role, fromName, roomName, audience, note = 'new message', deferred = false, { resumed = false, retried = false } = {}) => {
16
+ if (deferred) return `[Message from ${fromName} · you read it during your last task; answer it now]`
17
+ if (retried && role !== 'room') return '[Your last reply wrote a tool call as text, so nothing ran]'
18
+ if (resumed && role === 'resume') return '[DeepSeek Harness restarted while you were working]'
19
+ if (resumed && role !== 'room') return `[Message from ${fromName} · DeepSeek Harness restarted while you were working on it]`
16
20
  if (role === 'reply') return `[Reply from ${fromName}]`
21
+ if (role === 'stopped') return `[Stopped by ${fromName}]`
17
22
  if (role === 'room') return `[Group chat "${roomName}" · ${note}]`
18
23
  if (role === 'report') return `[Group chat "${roomName}" · replies to your post]`
19
24
  if (role === 'kickoff') return '[Bot team setup]'
@@ -22,8 +27,18 @@ export function install(rt) {
22
27
  return `[Message from ${fromName}]`
23
28
  }
24
29
 
30
+ // Roles recorded as team exchanges.
31
+ const EXCHANGE_ROLES = new Set(['request', 'reply', 'stopped'])
32
+
25
33
  // `sessionId` defaults to the recipient's own chat; group turns name its group Session.
26
- async function deliver({ fromId, toId, sessionId, text, role, hop = 0, replyTo, roomId, audience, note, exchangeId = randomUUID() }) {
34
+ // `deferred`: a request the Bot already read mid-turn, handed back to answer in a
35
+ // turn of its own; its exchange was recorded when it first arrived. `resumed` and
36
+ // `retried` carry on a turn that a restart cut off or that wrote its tool call as
37
+ // text (resume.js, turns.js), and `resent` is a message a shutdown cancelled; their
38
+ // exchange is recorded already too. `askedBy` is the member whose @mention called a
39
+ // group turn (groups.js). `fromRoomId`: the group whose Session the sender wrote
40
+ // from; the reply goes back to that Session (turns.js).
41
+ async function deliver({ fromId, toId, sessionId, text, role, hop = 0, replyTo, roomId, audience, note, exchangeId = randomUUID(), steer = false, deferred = false, resumed = false, retried = false, resent = false, askedBy, fromRoomId }) {
27
42
  const from = rt.botOf(fromId)
28
43
  const to = rt.botOf(toId)
29
44
  if (to === undefined) throw new Error(`Unknown Bot ${toId}`)
@@ -41,16 +56,27 @@ export function install(rt) {
41
56
  ...(replyTo ? { replyTo } : {}),
42
57
  ...(roomId ? { roomId } : {}),
43
58
  ...(audience === 'user' ? { audience } : {}),
59
+ ...(deferred ? { deferred: true } : {}),
60
+ ...(resumed ? { resumed: true } : {}),
61
+ ...(retried ? { retried: true } : {}),
62
+ ...(askedBy ? { askedBy } : {}),
63
+ ...(fromRoomId ? { senderRoomId: fromRoomId } : {}),
44
64
  }
45
- if (role === 'request' || role === 'reply') {
65
+ if (EXCHANGE_ROLES.has(role) && !deferred && !resumed && !retried && !resent) {
46
66
  state.exchanges.push({ id: exchangeId, from: fromId, to: toId, text, role, time: Date.now(), ...(replyTo ? { replyTo } : {}), ...(audience === 'user' ? { audience } : {}) })
47
- void rt.save()
67
+ void rt.save().catch(() => {})
48
68
  }
49
- const agent = await resolveAgent(sessionId ?? rt.chatOf(toId))
50
- agent.followup(createUserMessage({
51
- content: [{ type: 'text', text: `${headerFor(role, fromName, room?.name, audience, note)}\n${text}` }],
69
+ const targetSessionId = sessionId ?? rt.chatOf(toId)
70
+ await rt.ensureCapabilities(targetSessionId)
71
+ const agent = await resolveAgent(targetSessionId)
72
+ // A steered message cuts into a running turn at its next step; a follow-up
73
+ // queues for the next turn (see turns.js for how each is routed).
74
+ const message = createUserMessage({
75
+ content: [{ type: 'text', text: `${headerFor(role, fromName, room?.name, audience, note, deferred, { resumed, retried })}\n${text}` }],
52
76
  source,
53
- }))
77
+ })
78
+ if (steer) agent.steer(message)
79
+ else agent.followup(message)
54
80
  await ctx.sessions.flush(agent.session)
55
81
  return exchangeId
56
82
  }
@@ -16,7 +16,7 @@ const LOG_MAX_BYTES = 1 << 20
16
16
  const STACK_LINES = 8
17
17
  const TEXT_MAX = 2000
18
18
  export const REPORT_ENTRIES = 20
19
- export const ISSUES_URL = 'https://github.com/FeiZhuLulu/DeepSeek-Bot/issues/new'
19
+ export const ISSUES_URL = 'https://github.com/Fei2411/DeepSeek-Bot/issues/new'
20
20
 
21
21
  const quote = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
22
22
 
@@ -144,7 +144,7 @@ export function install(rt) {
144
144
  }).catch((error) => {
145
145
  if (fileFailed) return
146
146
  fileFailed = true
147
- console.error('[ds-bot] diagnostics log not written:', error?.code ?? error?.message ?? error)
147
+ console.error('[ds-bot] diagnostics log not written:', error?.code ?? clean(error?.message ?? String(error)))
148
148
  })
149
149
  }
150
150
 
@@ -164,13 +164,15 @@ export function install(rt) {
164
164
  }
165
165
 
166
166
  const warn = (template, ...args) => {
167
- ctx.logger.warn(template, ...args)
168
- console.error(`[ds-bot] ${template}`, ...args.map(arg => arg?.stack ?? arg))
169
167
  const error = args.find(arg => arg instanceof Error)
168
+ const line = clean(format(template, ...args.map(arg => (arg instanceof Error ? errorText(arg) : arg))))
169
+ const frames = typeof error?.stack === 'string' ? error.stack.split('\n').filter(item => /^\s+at /.test(item)).slice(0, STACK_LINES).map(item => item.trim()) : []
170
+ ctx.logger.warn('%s', line)
171
+ console.error(`[ds-bot] ${line}${frames.length > 0 ? `\n${clean(frames.join('\n'))}` : ''}`)
170
172
  note({
171
173
  level: 'warn',
172
174
  source: 'host',
173
- text: format(template, ...args.map(arg => (arg instanceof Error ? errorText(arg) : arg))).replace(/^ds-bot: /, ''),
175
+ text: line.replace(/^ds-bot: /, ''),
174
176
  code: typeof error?.code === 'string' ? error.code : undefined,
175
177
  stack: error?.stack,
176
178
  })