@capybara-hq/kernel 0.1.0-rc.3 → 0.1.0-rc.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.
Files changed (226) hide show
  1. package/dist/admin/admin.controller.d.ts +9 -4
  2. package/dist/admin/admin.controller.js +13 -4
  3. package/dist/admin/extensions.controller.d.ts +8 -1
  4. package/dist/admin/extensions.controller.js +22 -2
  5. package/dist/agents/agent-lookup.d.ts +3 -0
  6. package/dist/agents/agent-lookup.js +10 -0
  7. package/dist/agents/agent-profile.service.d.ts +29 -0
  8. package/dist/agents/agent-profile.service.js +117 -0
  9. package/dist/agents/agents.controller.d.ts +93 -3
  10. package/dist/agents/agents.controller.js +114 -4
  11. package/dist/agents/agents.module.js +6 -2
  12. package/dist/agents/agents.service.d.ts +13 -5
  13. package/dist/agents/agents.service.js +30 -7
  14. package/dist/agents/definition.d.ts +4 -0
  15. package/dist/agents/definition.js +10 -0
  16. package/dist/agents/instruction-files.d.ts +60 -0
  17. package/dist/agents/instruction-files.js +111 -0
  18. package/dist/agents/models.controller.d.ts +9 -3
  19. package/dist/agents/models.controller.js +19 -4
  20. package/dist/agents/profile.d.ts +96 -0
  21. package/dist/agents/profile.js +51 -0
  22. package/dist/agents/readiness.controller.d.ts +26 -0
  23. package/dist/agents/readiness.controller.js +56 -0
  24. package/dist/agents/templates.js +1 -1
  25. package/dist/background/background.controller.d.ts +38 -0
  26. package/dist/background/background.controller.js +87 -0
  27. package/dist/background/background.module.d.ts +10 -0
  28. package/dist/background/background.module.js +32 -0
  29. package/dist/background/finish.d.ts +43 -0
  30. package/dist/background/finish.js +94 -0
  31. package/dist/background/launch-lock.d.ts +14 -0
  32. package/dist/background/launch-lock.js +39 -0
  33. package/dist/background/launch.d.ts +36 -0
  34. package/dist/background/launch.js +70 -0
  35. package/dist/background/poll.job.d.ts +53 -0
  36. package/dist/background/poll.job.js +182 -0
  37. package/dist/background/render.d.ts +23 -0
  38. package/dist/background/render.js +48 -0
  39. package/dist/background/session.d.ts +37 -0
  40. package/dist/background/session.js +66 -0
  41. package/dist/background/status.d.ts +35 -0
  42. package/dist/background/status.js +57 -0
  43. package/dist/background/stop.d.ts +62 -0
  44. package/dist/background/stop.js +225 -0
  45. package/dist/background/store.d.ts +150 -0
  46. package/dist/background/store.js +351 -0
  47. package/dist/background/store.module.d.ts +6 -0
  48. package/dist/background/store.module.js +22 -0
  49. package/dist/background/tasks.service.d.ts +67 -0
  50. package/dist/background/tasks.service.js +186 -0
  51. package/dist/background/wake-rules.d.ts +57 -0
  52. package/dist/background/wake-rules.js +83 -0
  53. package/dist/background/wake.d.ts +28 -0
  54. package/dist/background/wake.js +57 -0
  55. package/dist/catalog/claude/agents.js +2 -0
  56. package/dist/catalog/marketplaces.service.d.ts +1 -1
  57. package/dist/catalog/plugin-import.js +1 -1
  58. package/dist/catalog/plugin-upgrade.js +1 -1
  59. package/dist/channels/cards.js +1 -1
  60. package/dist/channels/connections.service.d.ts +16 -1
  61. package/dist/channels/connections.service.js +38 -4
  62. package/dist/channels/threads.d.ts +5 -0
  63. package/dist/channels/threads.js +8 -0
  64. package/dist/config/schema.d.ts +18 -0
  65. package/dist/config/schema.js +102 -0
  66. package/dist/conversations/access.js +2 -0
  67. package/dist/conversations/attachments.service.d.ts +30 -0
  68. package/dist/conversations/attachments.service.js +130 -5
  69. package/dist/conversations/conversations.controller.d.ts +36 -5
  70. package/dist/conversations/conversations.controller.js +62 -12
  71. package/dist/conversations/conversations.module.js +3 -0
  72. package/dist/conversations/conversations.service.d.ts +44 -2
  73. package/dist/conversations/conversations.service.js +140 -12
  74. package/dist/conversations/event-log.d.ts +1 -1
  75. package/dist/conversations/event-log.js +10 -1
  76. package/dist/conversations/events.d.ts +58 -0
  77. package/dist/conversations/events.js +57 -2
  78. package/dist/conversations/queue.d.ts +3 -1
  79. package/dist/conversations/queue.js +13 -2
  80. package/dist/conversations/queue.service.js +3 -1
  81. package/dist/conversations/titles.d.ts +7 -0
  82. package/dist/conversations/titles.js +15 -0
  83. package/dist/db/kernel-schema.d.ts +58 -2
  84. package/dist/db/pool.d.ts +5 -0
  85. package/dist/db/pool.js +3 -0
  86. package/dist/errors.d.ts +8 -0
  87. package/dist/errors.js +8 -0
  88. package/dist/evals/run-replay.d.ts +5 -2
  89. package/dist/evals/run-replay.js +30 -7
  90. package/dist/evals/step-replay.js +12 -6
  91. package/dist/evals/trajectory.js +1 -1
  92. package/dist/extensions/admin-ops.js +2 -0
  93. package/dist/extensions/admin-view.d.ts +3 -1
  94. package/dist/extensions/admin-view.js +10 -2
  95. package/dist/extensions/blobs.js +1 -0
  96. package/dist/extensions/config-issues.d.ts +13 -1
  97. package/dist/extensions/config-issues.js +22 -5
  98. package/dist/extensions/config-json-schema.d.ts +43 -7
  99. package/dist/extensions/config-json-schema.js +124 -58
  100. package/dist/extensions/deployment-config-store.d.ts +6 -0
  101. package/dist/extensions/deployment-config.d.ts +50 -3
  102. package/dist/extensions/deployment-config.js +125 -28
  103. package/dist/extensions/display-name.d.ts +22 -0
  104. package/dist/extensions/display-name.js +77 -0
  105. package/dist/extensions/extensions.module.js +6 -0
  106. package/dist/extensions/host.d.ts +26 -1
  107. package/dist/extensions/host.js +82 -9
  108. package/dist/extensions/org-config.controller.d.ts +11 -3
  109. package/dist/extensions/org-config.controller.js +14 -6
  110. package/dist/extensions/org-config.d.ts +6 -0
  111. package/dist/extensions/org-config.js +12 -13
  112. package/dist/extensions/package.d.ts +14 -1
  113. package/dist/extensions/package.js +51 -2
  114. package/dist/extensions/permission-view.d.ts +3 -0
  115. package/dist/extensions/point-defaults.controller.d.ts +34 -0
  116. package/dist/extensions/point-defaults.controller.js +71 -0
  117. package/dist/extensions/point-defaults.d.ts +64 -0
  118. package/dist/extensions/point-defaults.js +206 -0
  119. package/dist/extensions/registry.d.ts +1 -1
  120. package/dist/extensions/registry.js +2 -0
  121. package/dist/http/api.module.js +8 -0
  122. package/dist/http/nest-logger.d.ts +6 -5
  123. package/dist/http/nest-logger.js +24 -10
  124. package/dist/identity/auth.controller.js +9 -5
  125. package/dist/identity/cookies.d.ts +20 -5
  126. package/dist/identity/cookies.js +44 -6
  127. package/dist/identity/me.controller.d.ts +5 -1
  128. package/dist/identity/me.controller.js +12 -3
  129. package/dist/identity/sessions.d.ts +12 -0
  130. package/dist/identity/sessions.js +45 -0
  131. package/dist/index.d.ts +1 -1
  132. package/dist/infra/config/kernel-config.service.js +6 -0
  133. package/dist/infra/database/database.module.js +1 -0
  134. package/dist/infra/events/kernel-events.d.ts +8 -0
  135. package/dist/infra/lifecycle/process-guards.js +4 -0
  136. package/dist/infra/logging/logging.module.d.ts +4 -2
  137. package/dist/infra/logging/logging.module.js +24 -6
  138. package/dist/infra/queue/redis-connection.d.ts +6 -0
  139. package/dist/infra/queue/redis-connection.js +7 -0
  140. package/dist/infra/redis/redis.module.js +6 -2
  141. package/dist/infra/tokens.d.ts +6 -1
  142. package/dist/infra/tokens.js +6 -1
  143. package/dist/log.d.ts +13 -1
  144. package/dist/log.js +93 -17
  145. package/dist/models/purposes.d.ts +23 -0
  146. package/dist/models/purposes.js +31 -10
  147. package/dist/orgs/offboarding.d.ts +3 -1
  148. package/dist/orgs/offboarding.js +26 -16
  149. package/dist/orgs/settings.service.d.ts +5 -0
  150. package/dist/orgs/settings.service.js +13 -2
  151. package/dist/roles/worker.module.js +6 -0
  152. package/dist/runtime/context/build.d.ts +9 -7
  153. package/dist/runtime/context/build.js +27 -19
  154. package/dist/runtime/context/claude-tools.d.ts +2 -1
  155. package/dist/runtime/context/claude-tools.js +5 -4
  156. package/dist/runtime/context/compaction.js +4 -0
  157. package/dist/runtime/langgraph-loop.js +3 -2
  158. package/dist/runtime/loop.d.ts +4 -3
  159. package/dist/runtime/maintenance.d.ts +4 -2
  160. package/dist/runtime/maintenance.js +17 -5
  161. package/dist/runtime/model-call.d.ts +17 -1
  162. package/dist/runtime/model-call.js +3 -2
  163. package/dist/runtime/providers.d.ts +6 -0
  164. package/dist/runtime/providers.js +8 -0
  165. package/dist/runtime/run-queue.d.ts +25 -2
  166. package/dist/runtime/run-queue.js +129 -33
  167. package/dist/runtime/run-worker.js +16 -3
  168. package/dist/runtime/runtime.module.js +8 -3
  169. package/dist/runtime/step-log.d.ts +6 -0
  170. package/dist/runtime/step-log.js +16 -2
  171. package/dist/runtime/subagents/definition.js +3 -1
  172. package/dist/runtime/usage-report.d.ts +2 -1
  173. package/dist/runtime/usage-report.js +2 -1
  174. package/dist/runtime/waits.d.ts +8 -2
  175. package/dist/runtime/waits.js +37 -11
  176. package/dist/sandbox/sandbox.module.js +6 -1
  177. package/dist/sandbox/sandbox.service.d.ts +61 -3
  178. package/dist/sandbox/sandbox.service.js +141 -7
  179. package/dist/tools/approvals.service.js +2 -0
  180. package/dist/tools/builtin-names.d.ts +20 -2
  181. package/dist/tools/builtin-names.js +29 -2
  182. package/dist/tools/builtins/builtin-tools.d.ts +15 -1
  183. package/dist/tools/builtins/builtin-tools.js +34 -2
  184. package/dist/tools/builtins/sandbox/bash.d.ts +1 -1
  185. package/dist/tools/builtins/sandbox/bash.js +230 -3
  186. package/dist/tools/builtins/sandbox/common.d.ts +27 -4
  187. package/dist/tools/builtins/sandbox/common.js +7 -3
  188. package/dist/tools/builtins/sandbox/deliver.js +1 -1
  189. package/dist/tools/builtins/sandbox/index.d.ts +2 -0
  190. package/dist/tools/builtins/sandbox/index.js +6 -0
  191. package/dist/tools/builtins/sandbox/task-output.d.ts +63 -0
  192. package/dist/tools/builtins/sandbox/task-output.js +304 -0
  193. package/dist/tools/builtins/sandbox/task-stop.d.ts +10 -0
  194. package/dist/tools/builtins/sandbox/task-stop.js +112 -0
  195. package/dist/tools/builtins/web/errors.d.ts +8 -0
  196. package/dist/tools/builtins/web/errors.js +53 -0
  197. package/dist/tools/builtins/web/extract.d.ts +62 -0
  198. package/dist/tools/builtins/web/extract.js +267 -0
  199. package/dist/tools/builtins/web/fetch.d.ts +55 -0
  200. package/dist/tools/builtins/web/fetch.js +174 -0
  201. package/dist/tools/builtins/web/index.d.ts +13 -0
  202. package/dist/tools/builtins/web/index.js +12 -0
  203. package/dist/tools/builtins/web/page-cache.d.ts +25 -0
  204. package/dist/tools/builtins/web/page-cache.js +77 -0
  205. package/dist/tools/builtins/web/search.d.ts +25 -0
  206. package/dist/tools/builtins/web/search.js +209 -0
  207. package/dist/tools/builtins/web/url.d.ts +17 -0
  208. package/dist/tools/builtins/web/url.js +43 -0
  209. package/dist/tools/oauth/flow-paths.d.ts +1 -1
  210. package/dist/tools/oauth/flow-paths.js +1 -1
  211. package/dist/tools/pipeline.js +15 -0
  212. package/dist/tools/registry.d.ts +22 -3
  213. package/dist/tools/registry.js +26 -11
  214. package/dist/tools/tools.module.js +12 -0
  215. package/dist/tools/validate.js +1 -1
  216. package/package.json +3 -3
  217. package/src/migrations/0064_account_last_org.sql +7 -0
  218. package/src/migrations/0065_conversation_archive.sql +8 -0
  219. package/src/migrations/0066_extension_display_name.sql +4 -0
  220. package/src/migrations/0067_agent_profiles.sql +20 -0
  221. package/src/migrations/0068_agent_avatar_uploads.sql +7 -0
  222. package/src/migrations/0069_attachments_agent_idx.sql +3 -0
  223. package/src/migrations/0070_background_tasks.sql +37 -0
  224. package/src/migrations/0071_background_tasks_reserve.sql +6 -0
  225. package/src/migrations/0072_background_tasks_stopping.sql +5 -0
  226. package/src/migrations/0073_org_extension_point_defaults.sql +11 -0
@@ -11,6 +11,8 @@ import { randomUUID } from "node:crypto";
11
11
  import { Inject, Injectable } from "@nestjs/common";
12
12
  import { sql } from "kysely";
13
13
  import { AgentLookup, visibleTo } from "../agents/agent-lookup.js";
14
+ import { BackgroundTaskStopper } from "../background/stop.js";
15
+ import { unbindConversation } from "../channels/threads.js";
14
16
  import { ApiError } from "../errors.js";
15
17
  import { KernelDb } from "../infra/database/kernel-db.js";
16
18
  import { TransactionHost } from "../infra/database/transaction-host.js";
@@ -25,6 +27,27 @@ import { AttachmentsService } from "./attachments.service.js";
25
27
  import { EventLog } from "./event-log.js";
26
28
  import { userMessageIdemKey } from "./idem-keys.js";
27
29
  import { NO_SELECTION, planSelection, readSelection } from "./selection.js";
30
+ /** Escapes `\`, `%` and `_` so user text is matched literally by `LIKE … ESCAPE '\'`. */
31
+ export function escapeLike(s) {
32
+ return s.replace(/[\\%_]/g, (ch) => `\\${ch}`);
33
+ }
34
+ /**
35
+ * SQL for the activity of the conversation row aliased/named `ref` (a correlated subquery).
36
+ * A run waiting on its sub-agents (status waiting with `wait_expires_at`, set only for a
37
+ * `subagents` wait) is still working, so it reports `running`; only a wait for a person
38
+ * (approval, input) reports `waiting`.
39
+ */
40
+ function activitySql(ref) {
41
+ const id = sql.ref(`${ref}.id`);
42
+ return sql `(
43
+ SELECT CASE
44
+ WHEN lr.status IN ('queued', 'running') THEN 'running'
45
+ WHEN lr.status = 'waiting' AND lr.wait_expires_at IS NOT NULL THEN 'running'
46
+ WHEN lr.status = 'waiting' THEN 'waiting' END
47
+ FROM capybara.runs lr WHERE lr.conversation_id = ${id}
48
+ ORDER BY lr.created_at DESC, lr.id DESC LIMIT 1
49
+ )`;
50
+ }
28
51
  const notFound = () => new ApiError("conversation.not_found", "Conversation not found");
29
52
  export { userMessageIdemKey } from "./idem-keys.js";
30
53
  const COLUMNS = [
@@ -40,6 +63,7 @@ const COLUMNS = [
40
63
  "updated_at",
41
64
  "parent_conversation_id",
42
65
  "subagent_type",
66
+ "archived_at",
43
67
  ];
44
68
  function toView(r) {
45
69
  return {
@@ -55,6 +79,8 @@ function toView(r) {
55
79
  updatedAt: r.updated_at,
56
80
  parentConversationId: r.parent_conversation_id,
57
81
  subagentType: r.subagent_type,
82
+ archivedAt: r.archived_at,
83
+ activity: r.activity ?? null,
58
84
  };
59
85
  }
60
86
  /** The longest conversation title (characters, after trimming). */
@@ -90,6 +116,7 @@ let ConversationsService = class ConversationsService {
90
116
  content;
91
117
  attachments;
92
118
  invoker;
119
+ background;
93
120
  txHost;
94
121
  constructor(db, eventLog, agents, access,
95
122
  /** Starts and stops the conversation's runs. */
@@ -101,7 +128,9 @@ let ConversationsService = class ConversationsService {
101
128
  /** Binds uploaded files to the conversation in the message's transaction. */
102
129
  attachments,
103
130
  /** Renders a person's skill invocation (phase D). */
104
- invoker, txHost) {
131
+ invoker,
132
+ /** Stops a deleted conversation's background tasks. */
133
+ background, txHost) {
105
134
  this.db = db;
106
135
  this.eventLog = eventLog;
107
136
  this.agents = agents;
@@ -112,6 +141,7 @@ let ConversationsService = class ConversationsService {
112
141
  this.content = content;
113
142
  this.attachments = attachments;
114
143
  this.invoker = invoker;
144
+ this.background = background;
115
145
  this.txHost = txHost;
116
146
  }
117
147
  /**
@@ -139,6 +169,8 @@ let ConversationsService = class ConversationsService {
139
169
  * message's `clientMessageId` (`create_request_id`), so a retried send returns the same
140
170
  * conversation and event with `created: false`. An agent the caller cannot use →
141
171
  * `agent.not_found` and nothing is written. Queues a run afterwards, like `postUserMessage`.
172
+ * `events` are every event the create wrote, in seq order from seq 1 (a `model_switched` before
173
+ * the message when the send switched), so a client can seed the new conversation's log whole.
142
174
  */
143
175
  async createWithMessage(orgId, accountId, agentId, input) {
144
176
  const agent = await this.agents.get(orgId, agentId, accountId);
@@ -149,8 +181,9 @@ let ConversationsService = class ConversationsService {
149
181
  const extra = await this.resolveExtra(orgId, accountId, input);
150
182
  const invoked = await this.prepareSkill(orgId, agentId, undefined, agent.definition, input);
151
183
  const id = randomUUID();
152
- const written = await this.createIn(id, orgId, accountId, agentId, input, next, extra, invoked);
153
- if (!written) {
184
+ const all = await this.createIn(id, orgId, accountId, agentId, input, next, extra, invoked);
185
+ const written = all?.at(-1);
186
+ if (!all || !written) {
154
187
  const raced = await this.findCreated(orgId, accountId, agentId, input.clientMessageId);
155
188
  if (!raced)
156
189
  throw new ApiError("conflict", "The conversation is being created; retry");
@@ -158,7 +191,7 @@ let ConversationsService = class ConversationsService {
158
191
  }
159
192
  await this.eventLog.notify(id, written.seq);
160
193
  await this.runs.ensureRun(id);
161
- return { conversation: await this.rowView(id), event: written, created: true };
194
+ return { conversation: await this.rowView(id), event: written, events: all, created: true };
162
195
  }
163
196
  /** The conversation row and its first message (undefined: a concurrent retry committed first). */
164
197
  // its own transaction: the caller notifies and publishes after the commit
@@ -206,8 +239,10 @@ let ConversationsService = class ConversationsService {
206
239
  const event = await this.eventLog.findByIdem(row.id, userMessageIdemKey(clientMessageId));
207
240
  if (!event)
208
241
  return null;
242
+ // what the first send wrote: the log from its start up to the message
243
+ const events = (await this.eventLog.readAfter(row.id, 0, event.seq)).filter((e) => e.seq <= event.seq);
209
244
  await this.runs.ensureRun(row.id);
210
- return { conversation: toView(row), event, created: false };
245
+ return { conversation: toView(row), event, events, created: false };
211
246
  }
212
247
  async rowView(id) {
213
248
  return toView(await this.db
@@ -231,6 +266,58 @@ let ConversationsService = class ConversationsService {
231
266
  await this.eventLog.publishChanged(conversationId, "title");
232
267
  return toView(row);
233
268
  }
269
+ /**
270
+ * Archives or unarchives a conversation (owner only, as {@link rename}). An archived
271
+ * conversation leaves the default lists but stays readable. Publishes `changed: archived`.
272
+ */
273
+ async setArchived(orgId, conversationId, accountId, archived) {
274
+ await this.writable(orgId, conversationId, accountId);
275
+ const row = await this.db
276
+ .updateTable("conversations")
277
+ .set({ archived_at: archived ? sql `now()` : null })
278
+ .where("id", "=", conversationId)
279
+ .returning(COLUMNS)
280
+ .executeTakeFirstOrThrow();
281
+ await this.eventLog.publishChanged(conversationId, "archived");
282
+ return toView(row);
283
+ }
284
+ /**
285
+ * Soft-deletes a conversation (owner only; a sub-agent's conversation is `conversation.read_only`):
286
+ * stops an active run, hides the conversation everywhere for its owner and drops its channel
287
+ * bindings, then stops every running background task in its sandbox (its sub-agents' too;
288
+ * outside the delete's transaction, a task whose sandbox cannot be reached ends `lost` with
289
+ * `kill_pending`). The tasks are stopped after the conversation is marked deleted, so no finish
290
+ * (the stops' own, or one the poller writes meanwhile) can wake a run on it: `ensureRun` refuses a
291
+ * deleted conversation (the stops' finishes carry `stoppedBy: "deletion"` as well). The event
292
+ * log, runs and trajectory stay (enterprise admins still see them).
293
+ */
294
+ async remove(orgId, conversationId, accountId) {
295
+ await this.writable(orgId, conversationId, accountId);
296
+ try {
297
+ await this.abort(orgId, conversationId, accountId);
298
+ }
299
+ catch (e) {
300
+ if (!(e instanceof ApiError && e.code === "run.not_active"))
301
+ throw e;
302
+ }
303
+ await this.markDeleted(conversationId);
304
+ await this.background.stopForDeletedConversation(conversationId);
305
+ await this.eventLog.publishChanged(conversationId, "deleted");
306
+ }
307
+ // the publish happens after the commit, in the caller
308
+ async markDeleted(conversationId) {
309
+ await this.db
310
+ .updateTable("conversations")
311
+ .set({ deleted_at: sql `now()` })
312
+ .where("id", "=", conversationId)
313
+ .execute();
314
+ // Queued messages must not start a run on a deleted conversation
315
+ await this.db
316
+ .deleteFrom("conversation_queue")
317
+ .where("conversation_id", "=", conversationId)
318
+ .execute();
319
+ await unbindConversation(this.db.db, conversationId);
320
+ }
234
321
  /** 读会话:非所有者、或 agent 已对所有者不可用 → `conversation.not_found`。 */
235
322
  async get(orgId, conversationId, viewerAccountId) {
236
323
  const { kind: _kind, ...row } = await this.readable(orgId, conversationId, viewerAccountId);
@@ -255,6 +342,7 @@ let ConversationsService = class ConversationsService {
255
342
  .where("id", "=", conversationId)
256
343
  .where("org_id", "=", orgId)
257
344
  .where("account_id", "=", viewerAccountId)
345
+ .where("deleted_at", "is", null)
258
346
  // Hidden eval replay conversations (kind 'eval') are not reachable here (only the trajectory
259
347
  // viewer shows them); a sub-agent's conversation is, for its owner (sub-agents spec §6.1)
260
348
  .where("kind", "in", ["chat", "subagent"])
@@ -284,10 +372,13 @@ let ConversationsService = class ConversationsService {
284
372
  let q = this.db
285
373
  .selectFrom("conversations")
286
374
  .select(COLUMNS)
375
+ .select(activitySql("conversations").as("activity"))
287
376
  .where("org_id", "=", orgId)
288
377
  .where("account_id", "=", accountId)
289
378
  // 评测回放的隐藏对话(kind = 'eval')不在「我的对话」里
290
379
  .where("kind", "=", "chat")
380
+ .where("deleted_at", "is", null)
381
+ .where("archived_at", opts.archived === true ? "is not" : "is", null)
291
382
  .where((eb) => eb.exists(eb
292
383
  .selectFrom("agents")
293
384
  .select(sql `1`.as("one"))
@@ -299,6 +390,12 @@ let ConversationsService = class ConversationsService {
299
390
  .limit(opts.limit);
300
391
  if (opts.agentId !== undefined)
301
392
  q = q.where("agent_id", "=", opts.agentId);
393
+ if (opts.q !== undefined) {
394
+ q = q.where(sql `conversations.title ILIKE ${`%${escapeLike(opts.q)}%`} ESCAPE '\\'`);
395
+ }
396
+ if (opts.state !== undefined) {
397
+ q = q.where(sql `${activitySql("conversations")} = ${opts.state}`);
398
+ }
302
399
  if (opts.after !== undefined) {
303
400
  const after = opts.after;
304
401
  q = q.where((eb) => sql `(${eb.ref("updated_at")}, ${eb.ref("id")}) < (
@@ -322,6 +419,11 @@ let ConversationsService = class ConversationsService {
322
419
  .where("agents.org_id", "=", orgId)
323
420
  .where("agents.deleted_at", "is", null)
324
421
  .where(visibleTo(accountId));
422
+ const archivedFilter = opts.archived === true ? sql `c.archived_at IS NOT NULL` : sql `c.archived_at IS NULL`;
423
+ const searchFilter = opts.q === undefined
424
+ ? sql ``
425
+ : sql `AND c.title ILIKE ${`%${escapeLike(opts.q)}%`} ESCAPE '\\'`;
426
+ const stateFilter = opts.state === undefined ? sql `` : sql `AND ${activitySql("c")} = ${opts.state}`;
325
427
  const cursor = opts.after === undefined
326
428
  ? sql ``
327
429
  : sql `WHERE (g.latest_at, g.agent_id) < (
@@ -332,6 +434,7 @@ let ConversationsService = class ConversationsService {
332
434
  SELECT c.agent_id, max(c.updated_at) AS latest_at
333
435
  FROM capybara.conversations c
334
436
  WHERE c.org_id = ${orgId} AND c.account_id = ${accountId} AND c.kind = 'chat'
437
+ AND c.deleted_at IS NULL AND ${archivedFilter} ${searchFilter} ${stateFilter}
335
438
  AND c.agent_id IN ${visibleAgents}
336
439
  GROUP BY c.agent_id
337
440
  ), page AS (
@@ -342,15 +445,18 @@ let ConversationsService = class ConversationsService {
342
445
  )
343
446
  SELECT page.agent_id, page.latest_at, v.definition ->> 'name' AS agent_name,
344
447
  cs.id, cs.org_id, cs.account_id, cs.title, cs.title_source, cs.selection, cs.last_seq,
345
- cs.created_at, cs.updated_at, cs.parent_conversation_id, cs.subagent_type
448
+ cs.created_at, cs.updated_at, cs.parent_conversation_id, cs.subagent_type,
449
+ cs.archived_at, cs.activity
346
450
  FROM page
347
451
  JOIN capybara.agents a ON a.id = page.agent_id
348
452
  JOIN capybara.agent_versions v ON v.agent_id = a.id AND v.version = a.current_version
349
453
  CROSS JOIN LATERAL (
350
454
  SELECT c.id, c.org_id, c.account_id, c.title, c.title_source, c.selection, c.last_seq,
351
- c.created_at, c.updated_at, c.parent_conversation_id, c.subagent_type
455
+ c.created_at, c.updated_at, c.parent_conversation_id, c.subagent_type,
456
+ c.archived_at, ${activitySql("c")} AS activity
352
457
  FROM capybara.conversations c
353
458
  WHERE c.org_id = ${orgId} AND c.account_id = ${accountId} AND c.kind = 'chat'
459
+ AND c.deleted_at IS NULL AND ${archivedFilter} ${searchFilter} ${stateFilter}
354
460
  AND c.agent_id = page.agent_id
355
461
  ORDER BY c.updated_at DESC, c.id DESC
356
462
  LIMIT ${opts.perGroup + 1}
@@ -377,7 +483,14 @@ let ConversationsService = class ConversationsService {
377
483
  }
378
484
  const page = groups.slice(0, opts.limit);
379
485
  const next = groups.length > opts.limit ? (page.at(-1)?.agent.id ?? null) : null;
380
- return { groups: page, next };
486
+ // The archived entry's count ignores `q` and `state`
487
+ const counted = await sql `
488
+ SELECT count(*) AS n FROM capybara.conversations c
489
+ WHERE c.org_id = ${orgId} AND c.account_id = ${accountId} AND c.kind = 'chat'
490
+ AND c.deleted_at IS NULL AND c.archived_at IS NOT NULL
491
+ AND c.agent_id IN ${visibleAgents}
492
+ `.execute(this.db.db);
493
+ return { groups: page, next, archivedCount: Number(counted.rows[0]?.n ?? 0) };
381
494
  }
382
495
  /**
383
496
  * The sub-agents of a conversation, newest first (sub-agents spec §14.3); owner only, as
@@ -544,6 +657,15 @@ let ConversationsService = class ConversationsService {
544
657
  this.userMessage(accountId, input, extra, [], invoked),
545
658
  ]);
546
659
  }
660
+ // The message is written: it brings an archived conversation back into the default lists
661
+ if (conv.archivedAt !== null) {
662
+ await this.db
663
+ .updateTable("conversations")
664
+ .set({ archived_at: null })
665
+ .where("id", "=", conversationId)
666
+ .execute();
667
+ await this.eventLog.publishChanged(conversationId, "archived");
668
+ }
547
669
  await this.runs.ensureRun(conversationId);
548
670
  return event;
549
671
  }
@@ -551,7 +673,8 @@ let ConversationsService = class ConversationsService {
551
673
  // its own transaction: the caller notifies and publishes after the commit
552
674
  async postIn(orgId, conversationId, accountId, input, next, extra, invoked) {
553
675
  const blocks = await this.bindIn(orgId, conversationId, accountId, input.files ?? []);
554
- return this.writeMessage(conversationId, accountId, input, next, extra, blocks, invoked);
676
+ const written = await this.writeMessage(conversationId, accountId, input, next, extra, blocks, invoked);
677
+ return written.at(-1);
555
678
  }
556
679
  /**
557
680
  * The selection a message switches to, or null when it changes nothing. A model equal to the
@@ -591,7 +714,8 @@ let ConversationsService = class ConversationsService {
591
714
  }
592
715
  /**
593
716
  * Writes the message (preceded by its `model_switched` when it switches) inside `t` and updates
594
- * `conversations.selection`; returns the `user_message` event. The caller notifies after commit.
717
+ * `conversations.selection`; returns the events written, in seq order (the `user_message`
718
+ * last). The caller notifies after commit.
595
719
  */
596
720
  async writeMessage(conversationId, accountId, input, next, extra, blocks, invoked) {
597
721
  const events = [];
@@ -616,7 +740,7 @@ let ConversationsService = class ConversationsService {
616
740
  .where("id", "=", conversationId)
617
741
  .execute();
618
742
  }
619
- return written.at(-1);
743
+ return written;
620
744
  }
621
745
  /**
622
746
  * Renders the message's skill invocation (phase D), or undefined when it has none. Resolved
@@ -718,6 +842,9 @@ let ConversationsService = class ConversationsService {
718
842
  __decorate([
719
843
  Transactional({ propagation: "requires_new" })
720
844
  ], ConversationsService.prototype, "createIn", null);
845
+ __decorate([
846
+ Transactional()
847
+ ], ConversationsService.prototype, "markDeleted", null);
721
848
  __decorate([
722
849
  Transactional({ propagation: "requires_new" })
723
850
  ], ConversationsService.prototype, "postIn", null);
@@ -739,6 +866,7 @@ ConversationsService = __decorate([
739
866
  __param(7, Inject(ConnectorContent)),
740
867
  __param(8, Inject(AttachmentsService)),
741
868
  __param(9, Inject(SkillInvoker)),
742
- __param(10, Inject(TransactionHost))
869
+ __param(10, Inject(BackgroundTaskStopper)),
870
+ __param(11, Inject(TransactionHost))
743
871
  ], ConversationsService);
744
872
  export { ConversationsService };
@@ -64,7 +64,7 @@ export type ConversationBusMessage = {
64
64
  * What changed about a conversation outside its event log (UI refresh B): the SSE stream sends it as
65
65
  * `event: changed` with `{ what }`, and the web refetches that part. Never model-visible.
66
66
  */
67
- export declare const CONVERSATION_CHANGES: readonly ["title", "queue", "selection", "files"];
67
+ export declare const CONVERSATION_CHANGES: readonly ["title", "queue", "selection", "files", "archived", "deleted", "background"];
68
68
  export type ConversationChange = (typeof CONVERSATION_CHANGES)[number];
69
69
  /**
70
70
  * 会话事件日志:只追加,是 agent 运行的唯一事实来源。
@@ -41,7 +41,16 @@ export const conversationTopic = (conversationId) => `conv.${conversationId}`;
41
41
  * What changed about a conversation outside its event log (UI refresh B): the SSE stream sends it as
42
42
  * `event: changed` with `{ what }`, and the web refetches that part. Never model-visible.
43
43
  */
44
- export const CONVERSATION_CHANGES = ["title", "queue", "selection", "files"];
44
+ export const CONVERSATION_CHANGES = [
45
+ "title",
46
+ "queue",
47
+ "selection",
48
+ "files",
49
+ "archived",
50
+ "deleted",
51
+ // the conversation's background tasks (or its sub-agents')
52
+ "background",
53
+ ];
45
54
  const notFound = () => new ApiError("conversation.not_found", "Conversation not found");
46
55
  function toEvent(r) {
47
56
  return {
@@ -272,6 +272,11 @@ export declare const ConversationEventSchema: z.ZodDiscriminatedUnion<[z.ZodObje
272
272
  model: z.ZodString;
273
273
  }, z.core.$strip>;
274
274
  triggerAccountId: z.ZodString;
275
+ trigger: z.ZodOptional<z.ZodEnum<{
276
+ background_task: "background_task";
277
+ message: "message";
278
+ }>>;
279
+ triggerSeq: z.ZodOptional<z.ZodNumber>;
275
280
  }, z.core.$strip>;
276
281
  }, z.core.$strip>, z.ZodObject<{
277
282
  type: z.ZodLiteral<"model_call">;
@@ -406,7 +411,9 @@ export declare const ConversationEventSchema: z.ZodDiscriminatedUnion<[z.ZodObje
406
411
  purpose: z.ZodOptional<z.ZodEnum<{
407
412
  compaction: "compaction";
408
413
  title: "title";
414
+ web_fetch: "web_fetch";
409
415
  }>>;
416
+ callId: z.ZodOptional<z.ZodString>;
410
417
  }, z.core.$strip>;
411
418
  }, z.core.$strip>, z.ZodObject<{
412
419
  type: z.ZodLiteral<"reminder">;
@@ -496,6 +503,13 @@ export declare const ConversationEventSchema: z.ZodDiscriminatedUnion<[z.ZodObje
496
503
  }, z.core.$strip>, z.ZodObject<{
497
504
  type: z.ZodLiteral<"approval_requested">;
498
505
  payload: z.ZodObject<{
506
+ requestedFor: z.ZodOptional<z.ZodNullable<z.ZodString>>;
507
+ approverRole: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
508
+ admin: "admin";
509
+ guest: "guest";
510
+ member: "member";
511
+ owner: "owner";
512
+ }>>>;
499
513
  step: z.ZodNumber;
500
514
  callId: z.ZodString;
501
515
  approvalId: z.ZodString;
@@ -659,6 +673,10 @@ export declare const ConversationEventSchema: z.ZodDiscriminatedUnion<[z.ZodObje
659
673
  name: z.ZodString;
660
674
  avatarUrl: z.ZodDefault<z.ZodNullable<z.ZodURL>>;
661
675
  instructions: z.ZodDefault<z.ZodString>;
676
+ instructionFiles: z.ZodOptional<z.ZodArray<z.ZodObject<{
677
+ name: z.ZodString;
678
+ content: z.ZodString;
679
+ }, z.core.$strict>>>;
662
680
  model: z.ZodObject<{
663
681
  provider: z.ZodString;
664
682
  model: z.ZodString;
@@ -754,6 +772,13 @@ export declare const ConversationEventSchema: z.ZodDiscriminatedUnion<[z.ZodObje
754
772
  }, z.core.$strip>, z.ZodObject<{
755
773
  type: z.ZodLiteral<"subagent_waiting">;
756
774
  payload: z.ZodObject<{
775
+ requestedFor: z.ZodOptional<z.ZodNullable<z.ZodString>>;
776
+ approverRole: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
777
+ admin: "admin";
778
+ guest: "guest";
779
+ member: "member";
780
+ owner: "owner";
781
+ }>>>;
757
782
  subagentId: z.ZodString;
758
783
  childRunId: z.ZodString;
759
784
  kind: z.ZodEnum<{
@@ -799,6 +824,29 @@ export declare const ConversationEventSchema: z.ZodDiscriminatedUnion<[z.ZodObje
799
824
  childRunId: z.ZodOptional<z.ZodString>;
800
825
  text: z.ZodString;
801
826
  }, z.core.$strip>;
827
+ }, z.core.$strip>, z.ZodObject<{
828
+ type: z.ZodLiteral<"background_task_finished">;
829
+ payload: z.ZodObject<{
830
+ taskId: z.ZodString;
831
+ description: z.ZodString;
832
+ status: z.ZodEnum<{
833
+ completed: "completed";
834
+ failed: "failed";
835
+ lost: "lost";
836
+ stopped: "stopped";
837
+ timed_out: "timed_out";
838
+ }>;
839
+ exitCode: z.ZodNullable<z.ZodNumber>;
840
+ elapsedMs: z.ZodNumber;
841
+ lastLines: z.ZodString;
842
+ outputPath: z.ZodString;
843
+ reported: z.ZodBoolean;
844
+ lostReason: z.ZodOptional<z.ZodString>;
845
+ stoppedBy: z.ZodOptional<z.ZodEnum<{
846
+ deletion: "deletion";
847
+ person: "person";
848
+ }>>;
849
+ }, z.core.$strip>;
802
850
  }, z.core.$strip>, z.ZodObject<{
803
851
  type: z.ZodLiteral<"compaction_summary">;
804
852
  payload: z.ZodObject<{
@@ -859,6 +907,16 @@ export type ConversationEvent = ConversationEventBody & {
859
907
  runId: string | null;
860
908
  createdAt: Date;
861
909
  };
910
+ /** An event a run absorbs at a step boundary: a person's message or a finished background task. */
911
+ export type AbsorbableEvent = Extract<ConversationEvent, {
912
+ type: "user_message" | "background_task_finished";
913
+ }>;
914
+ /**
915
+ * Whether `e` is absorbed like an interjection (spec 2026-11-05-background-bash-design §2.4): the
916
+ * loop takes it at its next step boundary, `consumed_seq` moves past it, and request building
917
+ * places it where the model call that absorbed it was built.
918
+ */
919
+ export declare const isAbsorbable: (e: ConversationEvent) => e is AbsorbableEvent;
862
920
  /**
863
921
  * What the person typed for a skill invocation (phase D): `/<plugin>:<name> <arguments>` (no
864
922
  * plugin: `/<name>`). Titles and eval case titles use it instead of the rendered skill in
@@ -1,6 +1,7 @@
1
1
  import { DOCUMENT_MEDIA_TYPES, hasControlChars, IMAGE_MEDIA_TYPES, isAttachmentId, isSkillName, MEDIA_NAME_MAX_CHARS, } from "@capybara-hq/plugin-sdk";
2
2
  import { z } from "zod";
3
3
  import { AgentDefinitionSchema, AgentToolRef, ModelRef } from "../agents/definition.js";
4
+ import { ORG_ROLES } from "../orgs/roles.js";
4
5
  import { TOOL_VIEW_DIFF_MAX_LINES, TOOL_VIEW_LINE_MAX_CHARS, TOOL_VIEW_PATH_MAX_CHARS, } from "../tools/view.js";
5
6
  /** 供应商私有数据(`ProviderOpaque`):原样保存、只回放给同一供应商。 */
6
7
  const ProviderOpaqueSchema = z.object({
@@ -17,6 +18,15 @@ const RefMediaSourceSchema = z.object({
17
18
  kind: z.literal("ref"),
18
19
  attachmentId: AttachmentIdSchema,
19
20
  });
21
+ /**
22
+ * Who may decide an approval, as the approval row says (display only, absent in older logs): the
23
+ * assigned account (`requestedFor`), else the escalation role (`approverRole`). Pages word the
24
+ * waiting state with it ("等待 张三 批准") for viewers who cannot decide.
25
+ */
26
+ const ApproverFields = {
27
+ requestedFor: z.string().nullable().optional(),
28
+ approverRole: z.enum(ORG_ROLES).nullable().optional(),
29
+ };
20
30
  /** The longest deny reason (characters): the decision route's limit and `approval_decided.reason`'s. */
21
31
  export const APPROVAL_DENY_REASON_MAX_CHARS = 500;
22
32
  /** 一条 `media_excluded` 事件最多的附件数。 */
@@ -239,6 +249,14 @@ export const ConversationEventSchema = z.discriminatedUnion("type", [
239
249
  agentVersion: z.number().int().positive(),
240
250
  model: ModelRef,
241
251
  triggerAccountId: z.string(),
252
+ /**
253
+ * What started the run (display only): absent or `message` = a person's message (or a
254
+ * sub-agent's task), `background_task` = a finished background task woke the conversation
255
+ * (spec 2026-11-05-background-bash-design §3).
256
+ */
257
+ trigger: z.enum(["message", "background_task"]).optional(),
258
+ /** A wake run's boundary: the seq of the finish that woke it (full-run replay seeds from it). */
259
+ triggerSeq: seqNo.optional(),
242
260
  })),
243
261
  ev("model_call", z.object({
244
262
  step: count,
@@ -326,14 +344,18 @@ export const ConversationEventSchema = z.discriminatedUnion("type", [
326
344
  * Usage of one model call. Absent `purpose` = the step's call (written with its
327
345
  * `assistant_message`); `compaction` = the compaction call before the step (I-1: billed and
328
346
  * reported, but no step consumer may pair it with the step's `model_call`); `title` = the
329
- * conversation title call (UI refresh B: no run, `step` 0, not model-visible, billed and reported).
347
+ * conversation title call (UI refresh B: no run, `step` 0, not model-visible, billed and reported);
348
+ * `web_fetch` = a `WebFetch` extraction call (web tools spec §5: the tool call's run and step,
349
+ * `callId` = the call; not model-visible, billed and reported, counted on a sub-agent's parent).
330
350
  */
331
351
  ev("usage", z.object({
332
352
  step: count,
333
353
  provider: z.string(),
334
354
  model: z.string(),
335
355
  ...ModelUsageShape,
336
- purpose: z.enum(["compaction", "title"]).optional(),
356
+ purpose: z.enum(["compaction", "title", "web_fetch"]).optional(),
357
+ /** The tool call a `web_fetch` usage belongs to. */
358
+ callId: z.string().optional(),
337
359
  })),
338
360
  ev("reminder", z.object({ step: count, text: z.string() })),
339
361
  /** 工具调用开始执行之前写入(续跑时「有开始无结果」的调用记为结果未知,不再执行)。 */
@@ -392,6 +414,7 @@ export const ConversationEventSchema = z.discriminatedUnion("type", [
392
414
  expiresAt: z.string(),
393
415
  /** Which rule asked (UI refresh B; display only, absent in older logs). */
394
416
  policy: z.enum(["agent", "connector", "default", "role", "org", "fixed"]).optional(),
417
+ ...ApproverFields,
395
418
  })),
396
419
  /** 审批已处理(批准 / 拒绝 / 超时 / 取消);`scope` 只在批准时出现,`byAccountId` 为处理人(超时 / 取消为 null)。 */
397
420
  ev("approval_decided", z.object({
@@ -575,6 +598,8 @@ export const ConversationEventSchema = z.discriminatedUnion("type", [
575
598
  questionId: z.string().optional(),
576
599
  toolKey: z.string(),
577
600
  expiresAt: z.string().optional(),
601
+ /** An approval's approver (`kind: "approval"`), as in `approval_requested`. */
602
+ ...ApproverFields,
578
603
  })),
579
604
  /** Parent log: a sub-agent's run ended. Not model-visible (its reply is the call's tool_result). */
580
605
  ev("subagent_finished", z.object({
@@ -609,6 +634,30 @@ export const ConversationEventSchema = z.discriminatedUnion("type", [
609
634
  childRunId: z.string().optional(),
610
635
  text: z.string(),
611
636
  })),
637
+ /**
638
+ * A background Bash task ended (spec 2026-11-05-background-bash-design §2). Model-visible: it is
639
+ * absorbed like an interjection and rendered as a system message (`renderBackgroundFinished`).
640
+ * `reported`: a TaskOutput or TaskStop in the same run already told the model; it is absorbed
641
+ * like any other, renders as one short line and never wakes a run. `lastLines` is the tail of
642
+ * the task's output.
643
+ */
644
+ ev("background_task_finished", z.object({
645
+ taskId: z.string().min(1).max(64),
646
+ description: z.string(),
647
+ status: z.enum(["completed", "failed", "stopped", "timed_out", "lost"]),
648
+ exitCode: z.number().int().nullable(),
649
+ elapsedMs: count,
650
+ lastLines: z.string().max(4000),
651
+ outputPath: z.string(),
652
+ reported: z.boolean(),
653
+ lostReason: z.string().optional(),
654
+ /**
655
+ * The finish never wakes a run, but the next run absorbs it like any other (background
656
+ * Bash §3): `person`, a person stopped the task (the conversation's background section);
657
+ * `deletion`, the conversation was being deleted.
658
+ */
659
+ stoppedBy: z.enum(["person", "deletion"]).optional(),
660
+ })),
612
661
  ev("compaction_summary", z.object({ coversUpToSeq: seqNo, text: z.string() })),
613
662
  ev("compaction_failed", z.object({ attempt: count, reason: z.string() })),
614
663
  ev("run_finished", z.object({
@@ -630,6 +679,12 @@ export const ConversationEventSchema = z.discriminatedUnion("type", [
630
679
  .optional(),
631
680
  })),
632
681
  ]);
682
+ /**
683
+ * Whether `e` is absorbed like an interjection (spec 2026-11-05-background-bash-design §2.4): the
684
+ * loop takes it at its next step boundary, `consumed_seq` moves past it, and request building
685
+ * places it where the model call that absorbed it was built.
686
+ */
687
+ export const isAbsorbable = (e) => e.type === "user_message" || e.type === "background_task_finished";
633
688
  /**
634
689
  * What the person typed for a skill invocation (phase D): `/<plugin>:<name> <arguments>` (no
635
690
  * plugin: `/<name>`). Titles and eval case titles use it instead of the rendered skill in
@@ -125,6 +125,7 @@ export declare function queuedUserMessage(r: QueueRow): AppendEvent;
125
125
  export declare function appendQueuedIn(t: KernelTrx, eventLog: Pick<EventLog, "appendIn">, conversationId: string, r: QueueRow): Promise<{
126
126
  event: ConversationEvent;
127
127
  switched: boolean;
128
+ unarchived: boolean;
128
129
  }>;
129
130
  /**
130
131
  * Why a queued item's skill can no longer be invoked, or null (also null for a plain message).
@@ -145,11 +146,12 @@ export declare const queueFull: () => ApiError;
145
146
  * when the queue is empty. An item whose message is already in the log (a retried enqueue after it
146
147
  * was sent) is dropped and the next one is taken. A head item whose skill can no longer be invoked
147
148
  * stays queued and the queue pauses (`skill_not_invocable`): `{ paused }`. The caller notifies
148
- * after commit.
149
+ * after commit (and publishes `archived` when the send unarchived the conversation).
149
150
  */
150
151
  export declare function dispatchQueuedIn(t: KernelTrx, eventLog: Pick<EventLog, "appendIn">, conversationId: string): Promise<{
151
152
  event: ConversationEvent;
152
153
  switched: boolean;
154
+ unarchived: boolean;
153
155
  } | {
154
156
  paused: QueuePauseReason;
155
157
  } | null>;
@@ -129,7 +129,18 @@ export async function appendQueuedIn(t, eventLog, conversationId, r) {
129
129
  .where("id", "=", conversationId)
130
130
  .execute();
131
131
  }
132
- return { event: written.at(-1), switched: next !== null };
132
+ // As a direct send: the message brings an archived conversation back into the default lists
133
+ const unarchived = await t
134
+ .updateTable("conversations")
135
+ .set({ archived_at: null })
136
+ .where("id", "=", conversationId)
137
+ .where("archived_at", "is not", null)
138
+ .executeTakeFirst();
139
+ return {
140
+ event: written.at(-1),
141
+ switched: next !== null,
142
+ unarchived: Number(unarchived.numUpdatedRows) > 0,
143
+ };
133
144
  }
134
145
  /** The model of the conversation agent's current version (inside `t`). */
135
146
  async function agentModel(t, conversationId) {
@@ -206,7 +217,7 @@ export const queueFull = () => new ApiError("conversation.queue_full", `A conver
206
217
  * when the queue is empty. An item whose message is already in the log (a retried enqueue after it
207
218
  * was sent) is dropped and the next one is taken. A head item whose skill can no longer be invoked
208
219
  * stays queued and the queue pauses (`skill_not_invocable`): `{ paused }`. The caller notifies
209
- * after commit.
220
+ * after commit (and publishes `archived` when the send unarchived the conversation).
210
221
  */
211
222
  export async function dispatchQueuedIn(t, eventLog, conversationId) {
212
223
  for (;;) {
@@ -240,11 +240,13 @@ let ConversationQueue = class ConversationQueue {
240
240
  */
241
241
  async sendNow(orgId, conversationId, accountId, itemId) {
242
242
  await this.conversations.writable(orgId, conversationId, accountId);
243
- const { event, fresh, switched } = await this.sendNowIn(conversationId, itemId);
243
+ const { event, fresh, switched, unarchived } = await this.sendNowIn(conversationId, itemId);
244
244
  if (fresh)
245
245
  await this.eventLog.notify(conversationId, event.seq);
246
246
  if (switched)
247
247
  await this.eventLog.publishChanged(conversationId, "selection");
248
+ if (unarchived)
249
+ await this.eventLog.publishChanged(conversationId, "archived");
248
250
  await this.eventLog.publishChanged(conversationId, "queue");
249
251
  await this.runs.ensureRun(conversationId);
250
252
  return event;