@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.
- package/dist/admin/admin.controller.d.ts +9 -4
- package/dist/admin/admin.controller.js +13 -4
- package/dist/admin/extensions.controller.d.ts +8 -1
- package/dist/admin/extensions.controller.js +22 -2
- package/dist/agents/agent-lookup.d.ts +3 -0
- package/dist/agents/agent-lookup.js +10 -0
- package/dist/agents/agent-profile.service.d.ts +29 -0
- package/dist/agents/agent-profile.service.js +117 -0
- package/dist/agents/agents.controller.d.ts +93 -3
- package/dist/agents/agents.controller.js +114 -4
- package/dist/agents/agents.module.js +6 -2
- package/dist/agents/agents.service.d.ts +13 -5
- package/dist/agents/agents.service.js +30 -7
- package/dist/agents/definition.d.ts +4 -0
- package/dist/agents/definition.js +10 -0
- package/dist/agents/instruction-files.d.ts +60 -0
- package/dist/agents/instruction-files.js +111 -0
- package/dist/agents/models.controller.d.ts +9 -3
- package/dist/agents/models.controller.js +19 -4
- package/dist/agents/profile.d.ts +96 -0
- package/dist/agents/profile.js +51 -0
- package/dist/agents/readiness.controller.d.ts +26 -0
- package/dist/agents/readiness.controller.js +56 -0
- package/dist/agents/templates.js +1 -1
- package/dist/background/background.controller.d.ts +38 -0
- package/dist/background/background.controller.js +87 -0
- package/dist/background/background.module.d.ts +10 -0
- package/dist/background/background.module.js +32 -0
- package/dist/background/finish.d.ts +43 -0
- package/dist/background/finish.js +94 -0
- package/dist/background/launch-lock.d.ts +14 -0
- package/dist/background/launch-lock.js +39 -0
- package/dist/background/launch.d.ts +36 -0
- package/dist/background/launch.js +70 -0
- package/dist/background/poll.job.d.ts +53 -0
- package/dist/background/poll.job.js +182 -0
- package/dist/background/render.d.ts +23 -0
- package/dist/background/render.js +48 -0
- package/dist/background/session.d.ts +37 -0
- package/dist/background/session.js +66 -0
- package/dist/background/status.d.ts +35 -0
- package/dist/background/status.js +57 -0
- package/dist/background/stop.d.ts +62 -0
- package/dist/background/stop.js +225 -0
- package/dist/background/store.d.ts +150 -0
- package/dist/background/store.js +351 -0
- package/dist/background/store.module.d.ts +6 -0
- package/dist/background/store.module.js +22 -0
- package/dist/background/tasks.service.d.ts +67 -0
- package/dist/background/tasks.service.js +186 -0
- package/dist/background/wake-rules.d.ts +57 -0
- package/dist/background/wake-rules.js +83 -0
- package/dist/background/wake.d.ts +28 -0
- package/dist/background/wake.js +57 -0
- package/dist/catalog/claude/agents.js +2 -0
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/plugin-import.js +1 -1
- package/dist/catalog/plugin-upgrade.js +1 -1
- package/dist/channels/cards.js +1 -1
- package/dist/channels/connections.service.d.ts +16 -1
- package/dist/channels/connections.service.js +38 -4
- package/dist/channels/threads.d.ts +5 -0
- package/dist/channels/threads.js +8 -0
- package/dist/config/schema.d.ts +18 -0
- package/dist/config/schema.js +102 -0
- package/dist/conversations/access.js +2 -0
- package/dist/conversations/attachments.service.d.ts +30 -0
- package/dist/conversations/attachments.service.js +130 -5
- package/dist/conversations/conversations.controller.d.ts +36 -5
- package/dist/conversations/conversations.controller.js +62 -12
- package/dist/conversations/conversations.module.js +3 -0
- package/dist/conversations/conversations.service.d.ts +44 -2
- package/dist/conversations/conversations.service.js +140 -12
- package/dist/conversations/event-log.d.ts +1 -1
- package/dist/conversations/event-log.js +10 -1
- package/dist/conversations/events.d.ts +58 -0
- package/dist/conversations/events.js +57 -2
- package/dist/conversations/queue.d.ts +3 -1
- package/dist/conversations/queue.js +13 -2
- package/dist/conversations/queue.service.js +3 -1
- package/dist/conversations/titles.d.ts +7 -0
- package/dist/conversations/titles.js +15 -0
- package/dist/db/kernel-schema.d.ts +58 -2
- package/dist/db/pool.d.ts +5 -0
- package/dist/db/pool.js +3 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +8 -0
- package/dist/evals/run-replay.d.ts +5 -2
- package/dist/evals/run-replay.js +30 -7
- package/dist/evals/step-replay.js +12 -6
- package/dist/evals/trajectory.js +1 -1
- package/dist/extensions/admin-ops.js +2 -0
- package/dist/extensions/admin-view.d.ts +3 -1
- package/dist/extensions/admin-view.js +10 -2
- package/dist/extensions/blobs.js +1 -0
- package/dist/extensions/config-issues.d.ts +13 -1
- package/dist/extensions/config-issues.js +22 -5
- package/dist/extensions/config-json-schema.d.ts +43 -7
- package/dist/extensions/config-json-schema.js +124 -58
- package/dist/extensions/deployment-config-store.d.ts +6 -0
- package/dist/extensions/deployment-config.d.ts +50 -3
- package/dist/extensions/deployment-config.js +125 -28
- package/dist/extensions/display-name.d.ts +22 -0
- package/dist/extensions/display-name.js +77 -0
- package/dist/extensions/extensions.module.js +6 -0
- package/dist/extensions/host.d.ts +26 -1
- package/dist/extensions/host.js +82 -9
- package/dist/extensions/org-config.controller.d.ts +11 -3
- package/dist/extensions/org-config.controller.js +14 -6
- package/dist/extensions/org-config.d.ts +6 -0
- package/dist/extensions/org-config.js +12 -13
- package/dist/extensions/package.d.ts +14 -1
- package/dist/extensions/package.js +51 -2
- package/dist/extensions/permission-view.d.ts +3 -0
- package/dist/extensions/point-defaults.controller.d.ts +34 -0
- package/dist/extensions/point-defaults.controller.js +71 -0
- package/dist/extensions/point-defaults.d.ts +64 -0
- package/dist/extensions/point-defaults.js +206 -0
- package/dist/extensions/registry.d.ts +1 -1
- package/dist/extensions/registry.js +2 -0
- package/dist/http/api.module.js +8 -0
- package/dist/http/nest-logger.d.ts +6 -5
- package/dist/http/nest-logger.js +24 -10
- package/dist/identity/auth.controller.js +9 -5
- package/dist/identity/cookies.d.ts +20 -5
- package/dist/identity/cookies.js +44 -6
- package/dist/identity/me.controller.d.ts +5 -1
- package/dist/identity/me.controller.js +12 -3
- package/dist/identity/sessions.d.ts +12 -0
- package/dist/identity/sessions.js +45 -0
- package/dist/index.d.ts +1 -1
- package/dist/infra/config/kernel-config.service.js +6 -0
- package/dist/infra/database/database.module.js +1 -0
- package/dist/infra/events/kernel-events.d.ts +8 -0
- package/dist/infra/lifecycle/process-guards.js +4 -0
- package/dist/infra/logging/logging.module.d.ts +4 -2
- package/dist/infra/logging/logging.module.js +24 -6
- package/dist/infra/queue/redis-connection.d.ts +6 -0
- package/dist/infra/queue/redis-connection.js +7 -0
- package/dist/infra/redis/redis.module.js +6 -2
- package/dist/infra/tokens.d.ts +6 -1
- package/dist/infra/tokens.js +6 -1
- package/dist/log.d.ts +13 -1
- package/dist/log.js +93 -17
- package/dist/models/purposes.d.ts +23 -0
- package/dist/models/purposes.js +31 -10
- package/dist/orgs/offboarding.d.ts +3 -1
- package/dist/orgs/offboarding.js +26 -16
- package/dist/orgs/settings.service.d.ts +5 -0
- package/dist/orgs/settings.service.js +13 -2
- package/dist/roles/worker.module.js +6 -0
- package/dist/runtime/context/build.d.ts +9 -7
- package/dist/runtime/context/build.js +27 -19
- package/dist/runtime/context/claude-tools.d.ts +2 -1
- package/dist/runtime/context/claude-tools.js +5 -4
- package/dist/runtime/context/compaction.js +4 -0
- package/dist/runtime/langgraph-loop.js +3 -2
- package/dist/runtime/loop.d.ts +4 -3
- package/dist/runtime/maintenance.d.ts +4 -2
- package/dist/runtime/maintenance.js +17 -5
- package/dist/runtime/model-call.d.ts +17 -1
- package/dist/runtime/model-call.js +3 -2
- package/dist/runtime/providers.d.ts +6 -0
- package/dist/runtime/providers.js +8 -0
- package/dist/runtime/run-queue.d.ts +25 -2
- package/dist/runtime/run-queue.js +129 -33
- package/dist/runtime/run-worker.js +16 -3
- package/dist/runtime/runtime.module.js +8 -3
- package/dist/runtime/step-log.d.ts +6 -0
- package/dist/runtime/step-log.js +16 -2
- package/dist/runtime/subagents/definition.js +3 -1
- package/dist/runtime/usage-report.d.ts +2 -1
- package/dist/runtime/usage-report.js +2 -1
- package/dist/runtime/waits.d.ts +8 -2
- package/dist/runtime/waits.js +37 -11
- package/dist/sandbox/sandbox.module.js +6 -1
- package/dist/sandbox/sandbox.service.d.ts +61 -3
- package/dist/sandbox/sandbox.service.js +141 -7
- package/dist/tools/approvals.service.js +2 -0
- package/dist/tools/builtin-names.d.ts +20 -2
- package/dist/tools/builtin-names.js +29 -2
- package/dist/tools/builtins/builtin-tools.d.ts +15 -1
- package/dist/tools/builtins/builtin-tools.js +34 -2
- package/dist/tools/builtins/sandbox/bash.d.ts +1 -1
- package/dist/tools/builtins/sandbox/bash.js +230 -3
- package/dist/tools/builtins/sandbox/common.d.ts +27 -4
- package/dist/tools/builtins/sandbox/common.js +7 -3
- package/dist/tools/builtins/sandbox/deliver.js +1 -1
- package/dist/tools/builtins/sandbox/index.d.ts +2 -0
- package/dist/tools/builtins/sandbox/index.js +6 -0
- package/dist/tools/builtins/sandbox/task-output.d.ts +63 -0
- package/dist/tools/builtins/sandbox/task-output.js +304 -0
- package/dist/tools/builtins/sandbox/task-stop.d.ts +10 -0
- package/dist/tools/builtins/sandbox/task-stop.js +112 -0
- package/dist/tools/builtins/web/errors.d.ts +8 -0
- package/dist/tools/builtins/web/errors.js +53 -0
- package/dist/tools/builtins/web/extract.d.ts +62 -0
- package/dist/tools/builtins/web/extract.js +267 -0
- package/dist/tools/builtins/web/fetch.d.ts +55 -0
- package/dist/tools/builtins/web/fetch.js +174 -0
- package/dist/tools/builtins/web/index.d.ts +13 -0
- package/dist/tools/builtins/web/index.js +12 -0
- package/dist/tools/builtins/web/page-cache.d.ts +25 -0
- package/dist/tools/builtins/web/page-cache.js +77 -0
- package/dist/tools/builtins/web/search.d.ts +25 -0
- package/dist/tools/builtins/web/search.js +209 -0
- package/dist/tools/builtins/web/url.d.ts +17 -0
- package/dist/tools/builtins/web/url.js +43 -0
- package/dist/tools/oauth/flow-paths.d.ts +1 -1
- package/dist/tools/oauth/flow-paths.js +1 -1
- package/dist/tools/pipeline.js +15 -0
- package/dist/tools/registry.d.ts +22 -3
- package/dist/tools/registry.js +26 -11
- package/dist/tools/tools.module.js +12 -0
- package/dist/tools/validate.js +1 -1
- package/package.json +3 -3
- package/src/migrations/0064_account_last_org.sql +7 -0
- package/src/migrations/0065_conversation_archive.sql +8 -0
- package/src/migrations/0066_extension_display_name.sql +4 -0
- package/src/migrations/0067_agent_profiles.sql +20 -0
- package/src/migrations/0068_agent_avatar_uploads.sql +7 -0
- package/src/migrations/0069_attachments_agent_idx.sql +3 -0
- package/src/migrations/0070_background_tasks.sql +37 -0
- package/src/migrations/0071_background_tasks_reserve.sql +6 -0
- package/src/migrations/0072_background_tasks_stopping.sql +5 -0
- 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,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
|
+
}
|