@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
@@ -0,0 +1,351 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { Inject, Injectable } from "@nestjs/common";
11
+ import { sql } from "kysely";
12
+ import { KernelDb } from "../infra/database/kernel-db.js";
13
+ import { TransactionHost } from "../infra/database/transaction-host.js";
14
+ import { Transactional } from "../infra/database/transactional.decorator.js";
15
+ import { BackgroundLaunchLock } from "./launch-lock.js";
16
+ const toRow = (r) => ({
17
+ id: r.id,
18
+ orgId: r.org_id,
19
+ conversationId: r.conversation_id,
20
+ sandboxConversationId: r.sandbox_conversation_id,
21
+ runId: r.run_id,
22
+ callId: r.call_id,
23
+ triggerAccountId: r.trigger_account_id,
24
+ description: r.description,
25
+ command: r.command,
26
+ sid: r.sid,
27
+ outputPath: r.output_path,
28
+ status: r.status,
29
+ lostReason: r.lost_reason,
30
+ exitCode: r.exit_code,
31
+ startedAt: new Date(r.started_at),
32
+ finishedAt: r.finished_at === null ? null : new Date(r.finished_at),
33
+ deadlineAt: new Date(r.deadline_at),
34
+ sandboxCreatedAt: r.sandbox_created_at === null ? null : new Date(r.sandbox_created_at),
35
+ killPending: r.kill_pending,
36
+ readOffset: Number(r.read_offset),
37
+ stoppingAt: r.stopping_at === null ? null : new Date(r.stopping_at),
38
+ });
39
+ /**
40
+ * How long a stop's claim holds: a claim older than this is abandoned (its stopper died), so the
41
+ * poller checks the task again and another stop may claim it. Longer than any stop takes (the
42
+ * lease wait, the SIGTERM grace, the kill and the tail).
43
+ */
44
+ export const STOP_CLAIM_TTL_MS = 5 * 60_000;
45
+ /** A condition: the row is not claimed by a live stop. */
46
+ const unclaimed = sql `(stopping_at IS NULL OR stopping_at < now() - ${STOP_CLAIM_TTL_MS}::integer * interval '1 millisecond')`;
47
+ /**
48
+ * Background Bash tasks (spec §4.1): one `capybara.background_tasks` row per command started with
49
+ * `run_in_background`. The store only reads and writes rows; the launch, the poller and the tools
50
+ * that drive them are other providers.
51
+ *
52
+ * A launch first reserves its row ({@link BackgroundTaskStore.reserve}: `running`, `sid` null),
53
+ * then starts the command and sets `sid` ({@link BackgroundTaskStore.markStarted}), or finishes
54
+ * the row as `failed` ({@link BackgroundTaskStore.failLaunch}). A running row with a null `sid`
55
+ * counts against `maxTasks` but has no process to check or stop: the poller and stops must skip
56
+ * it (the launch that reserved it settles it; one left over by a launch that died is failed by the
57
+ * poller, {@link BackgroundTaskStore.failAbandonedLaunches}).
58
+ */
59
+ let BackgroundTaskStore = class BackgroundTaskStore {
60
+ db;
61
+ txHost;
62
+ launchLock;
63
+ constructor(db, txHost, launchLock) {
64
+ this.db = db;
65
+ this.txHost = txHost;
66
+ this.launchLock = launchLock;
67
+ }
68
+ /**
69
+ * Reserves a launch: under the sandbox's launch lock, counts the running tasks of
70
+ * `row.sandboxConversationId` and inserts `row` as `running` (no `sid`) when fewer than
71
+ * `maxTasks` run. False (nothing written) when the sandbox already runs `maxTasks`.
72
+ */
73
+ async reserve(row, maxTasks) {
74
+ await this.launchLock.lock(row.sandboxConversationId);
75
+ if ((await this.countRunning(row.sandboxConversationId)) >= maxTasks)
76
+ return false;
77
+ await this.insert({ ...row, sid: null, sandboxCreatedAt: null });
78
+ return true;
79
+ }
80
+ /**
81
+ * Records the started command of a reserved row: `started`, or `stopping` when a stop claimed the
82
+ * reservation meanwhile (the launch then kills the command and finishes the row with
83
+ * {@link finishStoppedLaunch}); false when the row is no longer running.
84
+ */
85
+ async markStarted(id, to) {
86
+ const r = await this.db
87
+ .updateTable("background_tasks")
88
+ .set({ sid: to.sid, sandbox_created_at: to.sandboxCreatedAt })
89
+ .where("id", "=", id)
90
+ .where("status", "=", "running")
91
+ .where("sid", "is", null)
92
+ .returning("stopping_at")
93
+ .executeTakeFirst();
94
+ if (!r)
95
+ return false;
96
+ return r.stopping_at === null ? "started" : "stopping";
97
+ }
98
+ /**
99
+ * Finishes a launch whose reservation a stop claimed, once the launch killed what it started:
100
+ * `stopped`. No `background_task_finished` event: the launching call reports it itself.
101
+ */
102
+ async finishStoppedLaunch(id) {
103
+ await this.db
104
+ .updateTable("background_tasks")
105
+ .set({ status: "stopped", finished_at: sql `now()`, stopping_at: null })
106
+ .where("id", "=", id)
107
+ .where("status", "=", "running")
108
+ .execute();
109
+ }
110
+ /**
111
+ * Claims a running task for a stop (`stopping_at`): false when it is no longer running or another
112
+ * live stop claimed it ({@link STOP_CLAIM_TTL_MS}). While claimed the poller leaves it alone.
113
+ */
114
+ async claimStop(id) {
115
+ const r = await this.db
116
+ .updateTable("background_tasks")
117
+ .set({ stopping_at: sql `now()` })
118
+ .where("id", "=", id)
119
+ .where("status", "=", "running")
120
+ .where(unclaimed)
121
+ .executeTakeFirst();
122
+ return Number(r.numUpdatedRows) > 0;
123
+ }
124
+ /** Gives up a stop's claim (the stop failed while the sandbox runs: the task keeps running). */
125
+ async releaseStop(id) {
126
+ await this.db
127
+ .updateTable("background_tasks")
128
+ .set({ stopping_at: null })
129
+ .where("id", "=", id)
130
+ .where("status", "=", "running")
131
+ .execute();
132
+ }
133
+ /**
134
+ * Finishes a reserved row whose launch failed as `failed`, with `reason` in `lost_reason` (and a
135
+ * stop's claim cleared); a row that already has its sid (the command started and is tracked) is
136
+ * left as it is. No `background_task_finished` event: the launching call reports the failure
137
+ * itself.
138
+ */
139
+ async failLaunch(id, reason) {
140
+ await this.db
141
+ .updateTable("background_tasks")
142
+ .set({
143
+ status: "failed",
144
+ finished_at: sql `now()`,
145
+ lost_reason: reason,
146
+ stopping_at: null,
147
+ })
148
+ .where("id", "=", id)
149
+ .where("status", "=", "running")
150
+ // only a reservation that never started: a row with its sid is tracked by the poller
151
+ .where("sid", "is", null)
152
+ .execute();
153
+ }
154
+ /**
155
+ * Finishes as `failed` (`lost_reason` `launch_abandoned`) every reservation that started before
156
+ * `before`, never got its sid and is not claimed by a live stop: the launch that reserved it died
157
+ * (a crashed replica). A stop's claim older than {@link STOP_CLAIM_TTL_MS} does not hold it: a
158
+ * stop only claims a reservation and leaves the rest to the launch, so a dead launch would keep
159
+ * it (and its slot of `maxTasks`) forever. Conditional on `sid` still null, so a launch
160
+ * completing at that moment wins. No `background_task_finished` event: the command never
161
+ * started and the model never saw its id. Returns the rows it finished (their owner and sandbox
162
+ * conversations, for the caller's `changed: "background"` frames).
163
+ */
164
+ async failAbandonedLaunches(before) {
165
+ const r = await this.db
166
+ .updateTable("background_tasks")
167
+ .set({
168
+ status: "failed",
169
+ finished_at: sql `now()`,
170
+ lost_reason: "launch_abandoned",
171
+ stopping_at: null,
172
+ })
173
+ .where("status", "=", "running")
174
+ .where("sid", "is", null)
175
+ .where(unclaimed)
176
+ .where("started_at", "<", before)
177
+ .returning(["id", "conversation_id", "sandbox_conversation_id"])
178
+ .execute();
179
+ return r.map((x) => ({
180
+ id: x.id,
181
+ conversationId: x.conversation_id,
182
+ sandboxConversationId: x.sandbox_conversation_id,
183
+ }));
184
+ }
185
+ async insert(row) {
186
+ await this.db
187
+ .insertInto("background_tasks")
188
+ .values({
189
+ id: row.id,
190
+ org_id: row.orgId,
191
+ conversation_id: row.conversationId,
192
+ sandbox_conversation_id: row.sandboxConversationId,
193
+ run_id: row.runId,
194
+ call_id: row.callId,
195
+ trigger_account_id: row.triggerAccountId,
196
+ description: row.description,
197
+ command: row.command,
198
+ sid: row.sid,
199
+ output_path: row.outputPath,
200
+ started_at: row.startedAt,
201
+ deadline_at: row.deadlineAt,
202
+ sandbox_created_at: row.sandboxCreatedAt,
203
+ })
204
+ .execute();
205
+ }
206
+ async get(id) {
207
+ const r = await this.db
208
+ .selectFrom("background_tasks")
209
+ .selectAll()
210
+ .where("id", "=", id)
211
+ .executeTakeFirst();
212
+ return r === undefined ? undefined : toRow(r);
213
+ }
214
+ /** The running tasks of one sandbox, oldest first. */
215
+ async runningIn(sandboxConversationId) {
216
+ const rows = await this.db
217
+ .selectFrom("background_tasks")
218
+ .selectAll()
219
+ .where("sandbox_conversation_id", "=", sandboxConversationId)
220
+ .where("status", "=", "running")
221
+ .orderBy("started_at", "asc")
222
+ .orderBy("id", "asc")
223
+ .execute();
224
+ return rows.map(toRow);
225
+ }
226
+ /**
227
+ * Every running task that has started (its `sid` is set) and no live stop claimed, across
228
+ * sandboxes, oldest first: what the poller checks. A reservation still launching (`sid` null) is
229
+ * left to its launch, a claimed task to its stop.
230
+ */
231
+ async runningStarted() {
232
+ const rows = await this.db
233
+ .selectFrom("background_tasks")
234
+ .selectAll()
235
+ .where("status", "=", "running")
236
+ .where("sid", "is not", null)
237
+ .where(unclaimed)
238
+ .orderBy("started_at", "asc")
239
+ .orderBy("id", "asc")
240
+ .execute();
241
+ return rows.map(toRow);
242
+ }
243
+ /** The running tasks one conversation owns (not those of its sub-agents), oldest first. */
244
+ async runningOwnedBy(conversationId) {
245
+ const rows = await this.db
246
+ .selectFrom("background_tasks")
247
+ .selectAll()
248
+ .where("conversation_id", "=", conversationId)
249
+ .where("status", "=", "running")
250
+ .orderBy("started_at", "asc")
251
+ .orderBy("id", "asc")
252
+ .execute();
253
+ return rows.map(toRow);
254
+ }
255
+ async countRunning(sandboxConversationId) {
256
+ const r = await this.db
257
+ .selectFrom("background_tasks")
258
+ .select((eb) => eb.fn.countAll().as("n"))
259
+ .where("sandbox_conversation_id", "=", sandboxConversationId)
260
+ .where("status", "=", "running")
261
+ .executeTakeFirstOrThrow();
262
+ return Number(r.n);
263
+ }
264
+ /** The tasks owned by any of the conversations, newest first. */
265
+ async listFor(conversationIds) {
266
+ if (conversationIds.length === 0)
267
+ return [];
268
+ const rows = await this.db
269
+ .selectFrom("background_tasks")
270
+ .selectAll()
271
+ .where("conversation_id", "in", conversationIds)
272
+ .orderBy("started_at", "desc")
273
+ .orderBy("id", "desc")
274
+ .execute();
275
+ return rows.map(toRow);
276
+ }
277
+ /**
278
+ * Conditional `running` to a final status; false when the row was no longer running (another
279
+ * path finished it first), or, with `opts.unclaimed` (the poller), when a live stop claimed it.
280
+ * The caller's transaction also appends the finished event.
281
+ */
282
+ async finish(id, to, opts = {}) {
283
+ let q = this.db
284
+ .updateTable("background_tasks")
285
+ .set({
286
+ status: to.status,
287
+ finished_at: sql `now()`,
288
+ exit_code: to.exitCode ?? null,
289
+ lost_reason: to.lostReason ?? null,
290
+ stopping_at: null,
291
+ ...(to.killPending !== undefined && { kill_pending: to.killPending }),
292
+ })
293
+ .where("id", "=", id)
294
+ .where("status", "=", "running");
295
+ if (opts.unclaimed)
296
+ q = q.where(unclaimed);
297
+ const r = await q.executeTakeFirst();
298
+ return Number(r.numUpdatedRows) > 0;
299
+ }
300
+ /** Moves the read offset forward only (a concurrent reader never moves it back). */
301
+ async advanceOffset(id, to) {
302
+ await this.db
303
+ .updateTable("background_tasks")
304
+ .set({ read_offset: to })
305
+ .where("id", "=", id)
306
+ .where(sql `read_offset < ${to}`)
307
+ .execute();
308
+ }
309
+ /** Finished tasks of a sandbox whose process may still exist. */
310
+ async pendingKills(sandboxConversationId) {
311
+ const rows = await this.db
312
+ .selectFrom("background_tasks")
313
+ .selectAll()
314
+ .where("sandbox_conversation_id", "=", sandboxConversationId)
315
+ .where("kill_pending", "=", true)
316
+ .orderBy("started_at", "asc")
317
+ .execute();
318
+ return rows.map(toRow);
319
+ }
320
+ async clearKill(ids) {
321
+ if (ids.length === 0)
322
+ return;
323
+ await this.db
324
+ .updateTable("background_tasks")
325
+ .set({ kill_pending: false })
326
+ .where("id", "in", ids)
327
+ .execute();
328
+ }
329
+ /** Deletes finished tasks that ended before `before`; a running one is never deleted. */
330
+ async pruneFinished(before) {
331
+ const r = await this.db
332
+ .deleteFrom("background_tasks")
333
+ .where("status", "<>", "running")
334
+ .where("finished_at", "<", before)
335
+ .executeTakeFirst();
336
+ return Number(r.numDeletedRows);
337
+ }
338
+ };
339
+ __decorate([
340
+ Transactional()
341
+ ], BackgroundTaskStore.prototype, "reserve", null);
342
+ __decorate([
343
+ Transactional({ propagation: "mandatory" })
344
+ ], BackgroundTaskStore.prototype, "finish", null);
345
+ BackgroundTaskStore = __decorate([
346
+ Injectable(),
347
+ __param(0, Inject(KernelDb)),
348
+ __param(1, Inject(TransactionHost)),
349
+ __param(2, Inject(BackgroundLaunchLock))
350
+ ], BackgroundTaskStore);
351
+ export { BackgroundTaskStore };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The background task store alone, so `SandboxModule` (which kills lost tasks after a resume) can
3
+ * import it while `BackgroundModule`'s launch and poller import `SandboxModule`, without a cycle.
4
+ */
5
+ export declare class BackgroundStoreModule {
6
+ }
@@ -0,0 +1,22 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Module } from "@nestjs/common";
8
+ import { BackgroundLaunchLock } from "./launch-lock.js";
9
+ import { BackgroundTaskStore } from "./store.js";
10
+ /**
11
+ * The background task store alone, so `SandboxModule` (which kills lost tasks after a resume) can
12
+ * import it while `BackgroundModule`'s launch and poller import `SandboxModule`, without a cycle.
13
+ */
14
+ let BackgroundStoreModule = class BackgroundStoreModule {
15
+ };
16
+ BackgroundStoreModule = __decorate([
17
+ Module({
18
+ providers: [BackgroundTaskStore, BackgroundLaunchLock],
19
+ exports: [BackgroundTaskStore],
20
+ })
21
+ ], BackgroundStoreModule);
22
+ export { BackgroundStoreModule };
@@ -0,0 +1,67 @@
1
+ import { ConversationAccess } from "../conversations/access.js";
2
+ import { EventLog } from "../conversations/event-log.js";
3
+ import { KernelDb } from "../infra/database/kernel-db.js";
4
+ import { SandboxService } from "../sandbox/sandbox.service.js";
5
+ import { BackgroundTaskStopper } from "./stop.js";
6
+ import { type BackgroundTaskStatus, BackgroundTaskStore } from "./store.js";
7
+ /** The most bytes of output one read returns. */
8
+ export declare const OUTPUT_TAIL_MAX_BYTES = 65536;
9
+ /** A task as the API shows it. */
10
+ export interface BackgroundTaskView {
11
+ id: string;
12
+ /** The conversation the task was asked for. */
13
+ conversationId: string;
14
+ /** The child conversation of a sub-agent's task; null for the conversation's own task. */
15
+ subagentId: string | null;
16
+ description: string;
17
+ command: string;
18
+ status: BackgroundTaskStatus;
19
+ exitCode: number | null;
20
+ lostReason: string | null;
21
+ startedAt: string;
22
+ finishedAt: string | null;
23
+ /** True while a stop has claimed the task and it is still running. */
24
+ stopping: boolean;
25
+ }
26
+ export interface BackgroundTaskOutput {
27
+ text: string;
28
+ /** True when `text` is only the end of the output (or, for `event`, the last lines). */
29
+ truncated: boolean;
30
+ /** `live`: read from the running sandbox; `event`: the last lines the finished event kept. */
31
+ source: "live" | "event";
32
+ }
33
+ /**
34
+ * The HTTP side of background tasks (spec 2026-11-05-background-bash-design §5): the tasks of a
35
+ * conversation and of its sub-agents, stopping one, reading its output. A task is reachable
36
+ * through the conversation that owns it or through that conversation's parent, and only by a
37
+ * person who may see the conversation (`ConversationAccess`, the rule of `GET :id`, which is also
38
+ * what `POST :id/abort` checks: the owner).
39
+ */
40
+ export declare class BackgroundTasks {
41
+ private readonly access;
42
+ private readonly store;
43
+ private readonly stopper;
44
+ private readonly sandbox;
45
+ private readonly eventLog;
46
+ private readonly db;
47
+ constructor(access: Pick<ConversationAccess, "isAccessible">, store: Pick<BackgroundTaskStore, "listFor" | "get">, stopper: Pick<BackgroundTaskStopper, "stop">, sandbox: Pick<SandboxService, "available" | "ifRunning">, eventLog: Pick<EventLog, "findByIdem">, db: KernelDb);
48
+ /** The conversation and its sub-agents' conversations; `conversation.not_found` when not visible. */
49
+ private scope;
50
+ private view;
51
+ private owned;
52
+ /** The tasks of the conversation and its sub-agents, newest first. */
53
+ list(orgId: string, conversationId: string, accountId: string): Promise<BackgroundTaskView[]>;
54
+ /**
55
+ * A person's stop. Returns the row after the stop: `running` with `stopping` true when another
56
+ * stop already claimed the task (or the task is a reservation still launching, which its launch
57
+ * then kills); a stop that fails throws. Writes the `background_task.stopped` audit event.
58
+ */
59
+ stop(orgId: string, conversationId: string, accountId: string, taskId: string, actorTokenId?: string): Promise<BackgroundTaskView>;
60
+ /**
61
+ * The end of the task's output, at most `tail` bytes. Read live when the task's sandbox instance
62
+ * runs (never resuming a suspended sandbox); a finished task whose sandbox does not run answers
63
+ * with the last lines its finished event kept. Anything else is
64
+ * `background_task.output_unavailable` (409).
65
+ */
66
+ output(orgId: string, conversationId: string, accountId: string, taskId: string, tail: number): Promise<BackgroundTaskOutput>;
67
+ }
@@ -0,0 +1,186 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { isSandboxGoneError } from "@capybara-hq/plugin-sdk";
11
+ import { Inject, Injectable } from "@nestjs/common";
12
+ import { ConversationAccess } from "../conversations/access.js";
13
+ import { EventLog } from "../conversations/event-log.js";
14
+ import { ApiError } from "../errors.js";
15
+ import { audit } from "../identity/audit.js";
16
+ import { KernelDb } from "../infra/database/kernel-db.js";
17
+ import { SandboxService } from "../sandbox/sandbox.service.js";
18
+ import { UUID_RE } from "../ui/registry.js";
19
+ import { finishedEventKey } from "./finish.js";
20
+ import { TASK_ID_RE, TASKS_DIR } from "./launch.js";
21
+ import { BackgroundTaskStopper } from "./stop.js";
22
+ import { BackgroundTaskStore } from "./store.js";
23
+ /** The most bytes of output one read returns. */
24
+ export const OUTPUT_TAIL_MAX_BYTES = 65_536;
25
+ /** The time limit of reading an output (the exec and the wait for the sandbox's lease). */
26
+ const OUTPUT_READ_TIMEOUT_MS = 30_000;
27
+ const conversationNotFound = () => new ApiError("conversation.not_found", "Conversation not found");
28
+ const taskNotFound = () => new ApiError("background_task.not_found", "Background task not found");
29
+ /**
30
+ * The HTTP side of background tasks (spec 2026-11-05-background-bash-design §5): the tasks of a
31
+ * conversation and of its sub-agents, stopping one, reading its output. A task is reachable
32
+ * through the conversation that owns it or through that conversation's parent, and only by a
33
+ * person who may see the conversation (`ConversationAccess`, the rule of `GET :id`, which is also
34
+ * what `POST :id/abort` checks: the owner).
35
+ */
36
+ let BackgroundTasks = class BackgroundTasks {
37
+ access;
38
+ store;
39
+ stopper;
40
+ sandbox;
41
+ eventLog;
42
+ db;
43
+ constructor(access, store, stopper, sandbox, eventLog, db) {
44
+ this.access = access;
45
+ this.store = store;
46
+ this.stopper = stopper;
47
+ this.sandbox = sandbox;
48
+ this.eventLog = eventLog;
49
+ this.db = db;
50
+ }
51
+ /** The conversation and its sub-agents' conversations; `conversation.not_found` when not visible. */
52
+ async scope(orgId, conversationId, accountId) {
53
+ if (!UUID_RE.test(conversationId) ||
54
+ !(await this.access.isAccessible(orgId, conversationId, accountId))) {
55
+ throw conversationNotFound();
56
+ }
57
+ const children = await this.db
58
+ .selectFrom("conversations")
59
+ .select("id")
60
+ .where("org_id", "=", orgId)
61
+ .where("parent_conversation_id", "=", conversationId)
62
+ .where("kind", "=", "subagent")
63
+ .execute();
64
+ return [conversationId, ...children.map((c) => c.id)];
65
+ }
66
+ view(task, conversationId) {
67
+ return {
68
+ id: task.id,
69
+ conversationId,
70
+ subagentId: task.conversationId === conversationId ? null : task.conversationId,
71
+ description: task.description,
72
+ command: task.command,
73
+ status: task.status,
74
+ exitCode: task.exitCode,
75
+ lostReason: task.lostReason,
76
+ startedAt: task.startedAt.toISOString(),
77
+ finishedAt: task.finishedAt === null ? null : task.finishedAt.toISOString(),
78
+ stopping: task.status === "running" && task.stoppingAt !== null,
79
+ };
80
+ }
81
+ async owned(orgId, conversationId, accountId, taskId) {
82
+ const ids = await this.scope(orgId, conversationId, accountId);
83
+ if (!TASK_ID_RE.test(taskId))
84
+ throw taskNotFound();
85
+ const task = await this.store.get(taskId);
86
+ if (!task || task.orgId !== orgId || !ids.includes(task.conversationId))
87
+ throw taskNotFound();
88
+ return task;
89
+ }
90
+ /** The tasks of the conversation and its sub-agents, newest first. */
91
+ async list(orgId, conversationId, accountId) {
92
+ const ids = await this.scope(orgId, conversationId, accountId);
93
+ return (await this.store.listFor(ids)).map((t) => this.view(t, conversationId));
94
+ }
95
+ /**
96
+ * A person's stop. Returns the row after the stop: `running` with `stopping` true when another
97
+ * stop already claimed the task (or the task is a reservation still launching, which its launch
98
+ * then kills); a stop that fails throws. Writes the `background_task.stopped` audit event.
99
+ */
100
+ async stop(orgId, conversationId, accountId, taskId, actorTokenId) {
101
+ const task = await this.owned(orgId, conversationId, accountId, taskId);
102
+ const after = await this.stopper.stop(task, "person");
103
+ await audit(this.db.db, {
104
+ actorAccountId: accountId,
105
+ ...(actorTokenId !== undefined && { actorTokenId }),
106
+ orgId,
107
+ action: "background_task.stopped",
108
+ target: task.id,
109
+ detail: { taskId: task.id, conversationId: task.conversationId, status: after.status },
110
+ });
111
+ return this.view(after, conversationId);
112
+ }
113
+ /**
114
+ * The end of the task's output, at most `tail` bytes. Read live when the task's sandbox instance
115
+ * runs (never resuming a suspended sandbox); a finished task whose sandbox does not run answers
116
+ * with the last lines its finished event kept. Anything else is
117
+ * `background_task.output_unavailable` (409).
118
+ */
119
+ async output(orgId, conversationId, accountId, taskId, tail) {
120
+ const task = await this.owned(orgId, conversationId, accountId, taskId);
121
+ if (task.sid !== null && task.sandboxCreatedAt !== null && this.sandbox.available) {
122
+ const createdAt = task.sandboxCreatedAt;
123
+ try {
124
+ const r = await this.sandbox.ifRunning(task.sandboxConversationId, { createdAt }, async (p, h, signal) => {
125
+ const res = await p.exec(h, {
126
+ command: tailScript(task.id, tail),
127
+ cwd: "/workspace",
128
+ timeoutMs: OUTPUT_READ_TIMEOUT_MS,
129
+ maxOutputBytes: 2 * OUTPUT_TAIL_MAX_BYTES + 4096,
130
+ signal,
131
+ });
132
+ return res.exitCode === 0 ? parseTail(res.stdout, tail) : null;
133
+ }, AbortSignal.timeout(OUTPUT_READ_TIMEOUT_MS));
134
+ if (r.kind === "ran" && r.value !== null)
135
+ return { ...r.value, source: "live" };
136
+ }
137
+ catch (e) {
138
+ if (!isSandboxGoneError(e))
139
+ throw e;
140
+ }
141
+ }
142
+ if (task.status !== "running") {
143
+ const ev = await this.eventLog.findByIdem(task.conversationId, finishedEventKey(task.id));
144
+ if (ev?.type === "background_task_finished") {
145
+ return { text: ev.payload.lastLines, truncated: true, source: "event" };
146
+ }
147
+ }
148
+ throw new ApiError("background_task.output_unavailable", "The task's output cannot be read now: its sandbox is not running");
149
+ }
150
+ };
151
+ BackgroundTasks = __decorate([
152
+ Injectable(),
153
+ __param(0, Inject(ConversationAccess)),
154
+ __param(1, Inject(BackgroundTaskStore)),
155
+ __param(2, Inject(BackgroundTaskStopper)),
156
+ __param(3, Inject(SandboxService)),
157
+ __param(4, Inject(EventLog)),
158
+ __param(5, Inject(KernelDb))
159
+ ], BackgroundTasks);
160
+ export { BackgroundTasks };
161
+ /** Prints `size <bytes>` and `out <base64 of the last `tail` bytes>`. Relative to `/workspace`. */
162
+ function tailScript(id, tail) {
163
+ if (!TASK_ID_RE.test(id))
164
+ throw new Error(`invalid background task id: ${id}`);
165
+ const f = `${TASKS_DIR}/${id}.out`;
166
+ return [
167
+ `s=$({ wc -c < ${f}; } 2>/dev/null | tr -d ' '); echo "size \${s:-0}"`,
168
+ `printf 'out '; { tail -c ${Math.floor(tail)} ${f} 2>/dev/null | base64 | tr -d '\\n'; }; echo`,
169
+ ].join("\n");
170
+ }
171
+ /** Decodes {@link tailScript}'s output; a cut that splits a UTF-8 character drops its tail bytes. */
172
+ function parseTail(stdout, tail) {
173
+ const size = /^size ([0-9]+)$/m.exec(stdout);
174
+ const out = /^out (\S*)$/m.exec(stdout);
175
+ if (!size || !out)
176
+ return null;
177
+ let bytes = Buffer.from(out[1], "base64");
178
+ const truncated = Number(size[1]) > tail;
179
+ if (truncated) {
180
+ let i = 0;
181
+ while (i < bytes.length && i < 3 && (bytes[i] & 0xc0) === 0x80)
182
+ i++;
183
+ bytes = bytes.subarray(i);
184
+ }
185
+ return { text: bytes.toString("utf8"), truncated };
186
+ }