@capybara-hq/kernel 0.1.0-rc.22 → 0.1.0-rc.24
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/bootstrap.json +25 -25
- package/dist/admin/admin.controller.d.ts +3 -3
- package/dist/admin/admin.controller.js +6 -2
- package/dist/admin/admin.module.d.ts +2 -1
- package/dist/admin/admin.module.js +7 -3
- package/dist/admin/admin.schemas.d.ts +13 -0
- package/dist/admin/outbox-dead.controller.d.ts +23 -0
- package/dist/admin/outbox-dead.controller.js +62 -0
- package/dist/admin/outbox-dead.schemas.d.ts +8 -0
- package/dist/admin/outbox-dead.schemas.js +18 -0
- package/dist/admin/outbox-dead.service.d.ts +66 -0
- package/dist/admin/outbox-dead.service.js +179 -0
- package/dist/admin/overview.service.d.ts +40 -5
- package/dist/admin/overview.service.js +41 -7
- package/dist/agents/from-template.js +1 -1
- package/dist/background/finish.d.ts +5 -2
- package/dist/background/finish.js +14 -3
- package/dist/background/launch.d.ts +44 -5
- package/dist/background/launch.js +92 -9
- package/dist/background/session.d.ts +14 -2
- package/dist/background/session.js +26 -4
- package/dist/background/status.d.ts +1 -1
- package/dist/background/status.js +3 -3
- package/dist/background/stop.d.ts +0 -2
- package/dist/background/stop.js +1 -21
- package/dist/background/store.d.ts +8 -0
- package/dist/background/store.js +24 -0
- package/dist/catalog/admin-catalog.controller.d.ts +21 -2
- package/dist/catalog/admin-catalog.controller.js +45 -6
- package/dist/catalog/admin-catalog.schemas.d.ts +5 -0
- package/dist/catalog/admin-catalog.schemas.js +8 -0
- package/dist/catalog/catalog-content.d.ts +5 -0
- package/dist/catalog/catalog-content.js +1 -0
- package/dist/catalog/catalog.module.d.ts +2 -1
- package/dist/catalog/catalog.module.js +5 -1
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/platform-import-update.d.ts +50 -0
- package/dist/catalog/platform-import-update.js +116 -0
- package/dist/catalog/plugin-import.d.ts +36 -1
- package/dist/catalog/plugin-import.js +23 -1
- package/dist/catalog/plugin-upgrade.d.ts +18 -0
- package/dist/catalog/plugin-upgrade.js +19 -0
- package/dist/catalog/requests.service.js +2 -2
- package/dist/channels/channels.module.d.ts +5 -2
- package/dist/channels/channels.module.js +9 -2
- package/dist/channels/deliver-job.d.ts +25 -0
- package/dist/channels/deliver-job.js +25 -0
- package/dist/channels/inbound.d.ts +1 -1
- package/dist/channels/inbound.js +2 -15
- package/dist/channels/jobs.d.ts +28 -18
- package/dist/channels/jobs.js +63 -26
- package/dist/config/schema.d.ts +3 -6
- package/dist/config/schema.js +20 -27
- package/dist/conversations/agent-tree.d.ts +16 -0
- package/dist/conversations/agent-tree.js +36 -0
- package/dist/conversations/conversations.service.d.ts +44 -14
- package/dist/conversations/conversations.service.js +52 -38
- package/dist/conversations/event-log.d.ts +7 -10
- package/dist/conversations/event-log.js +24 -15
- package/dist/conversations/events.js +2 -1
- package/dist/conversations/queue.service.d.ts +8 -3
- package/dist/conversations/queue.service.js +37 -13
- package/dist/conversations/titles.d.ts +21 -8
- package/dist/conversations/titles.js +42 -15
- package/dist/db/kernel-schema.d.ts +26 -8
- package/dist/devices/devices.service.d.ts +12 -3
- package/dist/devices/devices.service.js +30 -9
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +15 -0
- package/dist/evals/run-replay.js +14 -8
- package/dist/extensions/extensions.module.js +3 -0
- package/dist/extensions/lifecycle.d.ts +6 -1
- package/dist/extensions/lifecycle.js +9 -2
- package/dist/extensions/usage-subscribers.d.ts +43 -0
- package/dist/extensions/usage-subscribers.js +108 -0
- package/dist/http/api.module.js +2 -0
- package/dist/identity/settings.d.ts +53 -3
- package/dist/identity/settings.js +92 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/infra/cli/kernel-cli.module.js +5 -0
- package/dist/infra/config/kernel-config.service.d.ts +7 -0
- package/dist/infra/config/kernel-config.service.js +20 -0
- package/dist/infra/events/kernel-events.d.ts +9 -35
- package/dist/infra/outbox/outbox-relay.module.d.ts +6 -0
- package/dist/infra/outbox/outbox-relay.module.js +18 -0
- package/dist/infra/outbox/outbox.dispatcher.d.ts +103 -0
- package/dist/infra/outbox/outbox.dispatcher.js +310 -0
- package/dist/infra/outbox/outbox.module.d.ts +24 -0
- package/dist/infra/outbox/outbox.module.js +50 -0
- package/dist/infra/outbox/outbox.relay.d.ts +31 -0
- package/dist/infra/outbox/outbox.relay.js +76 -0
- package/dist/infra/outbox/outbox.service.d.ts +51 -0
- package/dist/infra/outbox/outbox.service.js +131 -0
- package/dist/infra/outbox/registry.d.ts +120 -0
- package/dist/infra/outbox/registry.js +108 -0
- package/dist/infra/queue/queue-names.d.ts +8 -0
- package/dist/infra/queue/queue-names.js +16 -0
- package/dist/kernel.module.js +3 -0
- package/dist/local-devices/device-policy.service.d.ts +4 -0
- package/dist/local-devices/device-policy.service.js +61 -3
- package/dist/local-devices/local-device-gateway.d.ts +4 -2
- package/dist/local-devices/local-device-gateway.js +39 -14
- package/dist/local-devices/machines.d.ts +25 -1
- package/dist/local-devices/machines.js +91 -15
- package/dist/local-devices/policy-summary.d.ts +44 -1
- package/dist/local-devices/policy-summary.js +69 -0
- package/dist/models/embeddings.d.ts +9 -9
- package/dist/models/embeddings.js +11 -18
- package/dist/models/models.module.js +3 -0
- package/dist/orgs/orgs.service.js +7 -0
- package/dist/orgs/settings.controller.js +2 -2
- package/dist/orgs/settings.service.d.ts +11 -5
- package/dist/orgs/settings.service.js +28 -14
- package/dist/overrides.d.ts +20 -3
- package/dist/overrides.js +10 -2
- package/dist/platform/exclusions.js +5 -0
- package/dist/platform/ops/admin.ops.js +50 -9
- package/dist/platform/ops/catalog-admin.ops.d.ts +3 -1
- package/dist/platform/ops/catalog-admin.ops.js +32 -4
- package/dist/platform/ops/org-settings.ops.js +3 -3
- package/dist/platform/ops/orgs.ops.js +1 -0
- package/dist/platform/ops/outbox.ops.d.ts +21 -0
- package/dist/platform/ops/outbox.ops.js +125 -0
- package/dist/platform/ops/platform.ops.js +1 -1
- package/dist/platform/ops/schedules.ops.js +40 -1
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +3 -0
- package/dist/platform/route-schemas.js +3 -0
- package/dist/roles/worker.module.js +9 -4
- package/dist/runtime/langgraph-loop.js +13 -5
- package/dist/runtime/limits.d.ts +15 -0
- package/dist/runtime/limits.js +25 -0
- package/dist/runtime/loop.d.ts +8 -3
- package/dist/runtime/loop.js +2 -1
- package/dist/runtime/maintenance.d.ts +4 -2
- package/dist/runtime/maintenance.js +17 -2
- package/dist/runtime/model-call.d.ts +14 -16
- package/dist/runtime/model-call.js +14 -24
- package/dist/runtime/queue-lost-jobs.d.ts +24 -0
- package/dist/runtime/queue-lost-jobs.js +51 -0
- package/dist/runtime/run-control.d.ts +42 -5
- package/dist/runtime/run-control.js +66 -12
- package/dist/runtime/run-finish.d.ts +39 -12
- package/dist/runtime/run-finish.js +97 -14
- package/dist/runtime/run-jobs.d.ts +95 -13
- package/dist/runtime/run-jobs.js +199 -28
- package/dist/runtime/run-orphan-check.job.d.ts +142 -0
- package/dist/runtime/run-orphan-check.job.js +339 -0
- package/dist/runtime/run-processors.d.ts +16 -9
- package/dist/runtime/run-processors.js +1 -1
- package/dist/runtime/run-queue.d.ts +64 -55
- package/dist/runtime/run-queue.js +147 -284
- package/dist/runtime/run-steps.d.ts +1 -2
- package/dist/runtime/run-steps.js +8 -11
- package/dist/runtime/run-worker.d.ts +6 -17
- package/dist/runtime/run-worker.js +32 -34
- package/dist/runtime/runtime.module.d.ts +2 -3
- package/dist/runtime/runtime.module.js +5 -6
- package/dist/runtime/step-log.d.ts +16 -4
- package/dist/runtime/step-log.js +26 -6
- package/dist/runtime/stream.controller.d.ts +2 -4
- package/dist/runtime/stream.controller.js +5 -11
- package/dist/runtime/subagents/child.js +1 -1
- package/dist/runtime/subagents/definition.d.ts +2 -1
- package/dist/runtime/subagents/definition.js +4 -2
- package/dist/runtime/subagents/deliveries.d.ts +6 -3
- package/dist/runtime/subagents/deliveries.js +16 -7
- package/dist/runtime/subagents/dispatch.d.ts +12 -12
- package/dist/runtime/subagents/dispatch.js +13 -38
- package/dist/runtime/subagents/messages.d.ts +5 -1
- package/dist/runtime/subagents/messages.js +11 -3
- package/dist/runtime/subagents/skill-fork.d.ts +3 -3
- package/dist/runtime/subagents/skill-fork.js +5 -5
- package/dist/runtime/subagents/subagents.service.js +1 -1
- package/dist/runtime/subagents/tree-jobs.d.ts +61 -0
- package/dist/runtime/subagents/tree-jobs.js +86 -0
- package/dist/runtime/usage-report-jobs.d.ts +23 -48
- package/dist/runtime/usage-report-jobs.js +67 -119
- package/dist/runtime/usage-report.d.ts +28 -77
- package/dist/runtime/usage-report.js +35 -213
- package/dist/runtime/waits.d.ts +48 -198
- package/dist/runtime/waits.js +103 -805
- package/dist/sandbox/platform-tiers.d.ts +86 -0
- package/dist/sandbox/platform-tiers.js +97 -0
- package/dist/sandbox/sandbox.service.d.ts +18 -5
- package/dist/sandbox/sandbox.service.js +75 -20
- package/dist/sandbox/tiers.d.ts +14 -2
- package/dist/sandbox/tiers.js +8 -0
- package/dist/schedules/calendar.d.ts +134 -0
- package/dist/schedules/calendar.js +410 -0
- package/dist/schedules/fire.d.ts +1 -3
- package/dist/schedules/fire.js +5 -10
- package/dist/schedules/run-tracker.d.ts +0 -5
- package/dist/schedules/run-tracker.js +0 -13
- package/dist/schedules/scan.job.js +15 -20
- package/dist/schedules/schedule-math.d.ts +42 -0
- package/dist/schedules/schedule-math.js +52 -0
- package/dist/schedules/scheduled-tasks.service.d.ts +16 -0
- package/dist/schedules/scheduled-tasks.service.js +53 -0
- package/dist/schedules/schedules.controller.d.ts +5 -1
- package/dist/schedules/schedules.controller.js +15 -1
- package/dist/schedules/schedules.schemas.d.ts +35 -0
- package/dist/schedules/schedules.schemas.js +27 -0
- package/dist/schedules/store.d.ts +5 -0
- package/dist/schedules/store.js +20 -0
- package/dist/tools/approvals.service.d.ts +26 -7
- package/dist/tools/approvals.service.js +88 -10
- package/dist/tools/builtin-names.d.ts +5 -3
- package/dist/tools/builtin-names.js +12 -4
- package/dist/tools/builtins/builtin-tools.d.ts +1 -2
- package/dist/tools/builtins/builtin-tools.js +21 -17
- package/dist/tools/builtins/sandbox/bash.js +9 -3
- package/dist/tools/builtins/sandbox/common.d.ts +18 -1
- package/dist/tools/builtins/sandbox/common.js +14 -1
- package/dist/tools/builtins/sandbox/index.d.ts +2 -1
- package/dist/tools/builtins/sandbox/index.js +4 -1
- package/dist/tools/builtins/sandbox/task-list.d.ts +12 -0
- package/dist/tools/builtins/sandbox/task-list.js +130 -0
- package/dist/tools/builtins/sandbox/task-output.d.ts +8 -1
- package/dist/tools/builtins/sandbox/task-output.js +21 -3
- package/dist/tools/builtins/web/extract.d.ts +2 -4
- package/dist/tools/builtins/web/extract.js +2 -5
- package/dist/tools/pipeline.d.ts +1 -1
- package/dist/tools/pipeline.js +1 -1
- package/dist/tools/questions.service.d.ts +15 -6
- package/dist/tools/questions.service.js +20 -8
- package/dist/tools/registry.d.ts +4 -2
- package/dist/tools/registry.js +4 -3
- package/dist/tools/tools.module.js +4 -3
- package/package.json +3 -3
- package/src/migrations/0118_device_policy_ops.sql +6 -0
- package/src/migrations/0119_outbox.sql +22 -0
- package/src/migrations/0120_drop_usage_report_sweep.sql +7 -0
|
@@ -9,6 +9,8 @@ import { AccountsService } from "../identity/accounts.js";
|
|
|
9
9
|
import { PlatformInboxService } from "../inbox/platform-inbox.service.js";
|
|
10
10
|
import { OrgsService } from "../orgs/orgs.service.js";
|
|
11
11
|
import { ProviderRegistry, type ProviderStatus } from "../runtime/providers.js";
|
|
12
|
+
import { QueueLostJobs } from "../runtime/queue-lost-jobs.js";
|
|
13
|
+
import { OutboxDeadService } from "./outbox-dead.service.js";
|
|
12
14
|
/** The most rows each attention list returns; the rest are counted in `more`. */
|
|
13
15
|
export declare const ATTENTION_LIST_MAX = 20;
|
|
14
16
|
/** The admin overview's numbers (`GET /api/v1/admin/overview`): counts only. */
|
|
@@ -17,7 +19,8 @@ export interface AdminOverviewStats {
|
|
|
17
19
|
accounts: number;
|
|
18
20
|
/**
|
|
19
21
|
* What waits for a super admin: the platform inbox total plus the extensions, plugin updates and
|
|
20
|
-
* model providers of {@link AdminAttention} (each counted in full, not capped)
|
|
22
|
+
* model providers of {@link AdminAttention} (each counted in full, not capped), every dead
|
|
23
|
+
* outbox row and the run queue's lost-jobs item.
|
|
21
24
|
*/
|
|
22
25
|
attentionTotal: number;
|
|
23
26
|
/** An attention source could not be read: `attentionTotal` leaves its items out. */
|
|
@@ -33,7 +36,7 @@ export type AttentionExtensionReason = "config" | "load_failed" | "approval" | "
|
|
|
33
36
|
/** What is wrong with a model provider: its model list failed, or it lists no usable chat model. */
|
|
34
37
|
export type AttentionProviderProblem = "list_failed" | "no_chat_model";
|
|
35
38
|
/** An attention list a source failed to read (it is then empty, the others intact). */
|
|
36
|
-
export type AttentionSource = "extensions" | "updates" | "providers";
|
|
39
|
+
export type AttentionSource = "extensions" | "updates" | "providers" | "outbox" | "queue";
|
|
37
40
|
export interface AttentionExtension {
|
|
38
41
|
name: string;
|
|
39
42
|
displayName: LocalizedName | null;
|
|
@@ -61,6 +64,27 @@ export interface AttentionProvider {
|
|
|
61
64
|
icon: string | null;
|
|
62
65
|
problem: AttentionProviderProblem;
|
|
63
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Background work the kernel could not queue (transactional outbox spec §3.4): how many outbox
|
|
69
|
+
* rows are dead and the oldest one's `dead_at`. One item for all of them; the rows themselves are
|
|
70
|
+
* `GET /api/v1/admin/outbox/dead`.
|
|
71
|
+
*/
|
|
72
|
+
export interface AttentionOutboxDead {
|
|
73
|
+
kind: "outbox_dead";
|
|
74
|
+
count: number;
|
|
75
|
+
oldestAt: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The run queue lost background jobs (transactional outbox spec §8.3): the `run-orphan-check`
|
|
79
|
+
* found queued runs whose job was neither in the queue nor owed, which only Redis losing data
|
|
80
|
+
* explains. Their jobs were added again; the item says Redis durability needs a look, how many
|
|
81
|
+
* jobs were lost and when last (it stays a week after the last loss).
|
|
82
|
+
*/
|
|
83
|
+
export interface AttentionQueueLostJobs {
|
|
84
|
+
kind: "queue_lost_jobs";
|
|
85
|
+
count: number;
|
|
86
|
+
lastAt: string;
|
|
87
|
+
}
|
|
64
88
|
/**
|
|
65
89
|
* `GET /api/v1/admin/overview/attention` (spec §5.2, §9): what needs a super admin besides the
|
|
66
90
|
* inbox, each list at most {@link ATTENTION_LIST_MAX} rows with the rest counted in `more`, and
|
|
@@ -71,6 +95,10 @@ export interface AdminAttention {
|
|
|
71
95
|
extensions: AttentionExtension[];
|
|
72
96
|
updates: AttentionUpdate[];
|
|
73
97
|
providers: AttentionProvider[];
|
|
98
|
+
/** System items: at most one, `outbox_dead`, when some outbox rows are dead. */
|
|
99
|
+
outbox: AttentionOutboxDead[];
|
|
100
|
+
/** System items: at most one, `queue_lost_jobs`, when the run queue lost jobs this week. */
|
|
101
|
+
queue: AttentionQueueLostJobs[];
|
|
74
102
|
more: Record<AttentionSource, number>;
|
|
75
103
|
unavailable: AttentionSource[];
|
|
76
104
|
}
|
|
@@ -81,8 +109,9 @@ export declare function providerProblem(p: ProviderStatus): AttentionProviderPro
|
|
|
81
109
|
/**
|
|
82
110
|
* The admin overview (spec §4.1, §5.2): its numbers and the 待处理 lists, read live from the
|
|
83
111
|
* sources the admin pages read (`ExtensionAdmin`, the catalog, the provider registry's cached
|
|
84
|
-
* lists). Each source is read on its own: a failing one is reported in
|
|
85
|
-
* audited (decision D5) and not cached further (every source is PG
|
|
112
|
+
* lists, the dead outbox rows, the run queue's lost-jobs canary). Each source is read on its own: a failing one is reported in
|
|
113
|
+
* `unavailable`. Reads are not audited (decision D5) and not cached further (every source is PG
|
|
114
|
+
* or already Redis-cached).
|
|
86
115
|
*/
|
|
87
116
|
export declare class AdminOverviewService {
|
|
88
117
|
private readonly extensions;
|
|
@@ -94,10 +123,16 @@ export declare class AdminOverviewService {
|
|
|
94
123
|
private readonly orgs;
|
|
95
124
|
private readonly accounts;
|
|
96
125
|
private readonly marketplaces;
|
|
126
|
+
private readonly outboxDead;
|
|
127
|
+
private readonly lostJobs;
|
|
97
128
|
private readonly log;
|
|
98
|
-
constructor(extensions: ExtensionAdmin, catalog: CatalogService, registry: ProviderRegistry, displayNames: ExtensionDisplayNames, icons: PluginIcons, inbox: PlatformInboxService, orgs: OrgsService, accounts: AccountsService, marketplaces: MarketplacesService, log: Logger);
|
|
129
|
+
constructor(extensions: ExtensionAdmin, catalog: CatalogService, registry: ProviderRegistry, displayNames: ExtensionDisplayNames, icons: PluginIcons, inbox: PlatformInboxService, orgs: OrgsService, accounts: AccountsService, marketplaces: MarketplacesService, outboxDead: OutboxDeadService, lostJobs: Pick<QueueLostJobs, "summary">, log: Logger);
|
|
99
130
|
stats(): Promise<AdminOverviewStats>;
|
|
100
131
|
attention(): Promise<AdminAttention>;
|
|
132
|
+
/** The `queue_lost_jobs` item when the run queue lost jobs this week, else none. */
|
|
133
|
+
private queueRows;
|
|
134
|
+
/** The `outbox_dead` item when some outbox rows are dead, else none. */
|
|
135
|
+
private outboxRows;
|
|
101
136
|
/**
|
|
102
137
|
* The extensions needing a look, but not one pending approval whose plugin waits in the platform
|
|
103
138
|
* inbox (a permission confirmation or an org upload: the inbox already lists and counts it). A
|
|
@@ -18,6 +18,8 @@ import { PlatformInboxService } from "../inbox/platform-inbox.service.js";
|
|
|
18
18
|
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
19
19
|
import { OrgsService } from "../orgs/orgs.service.js";
|
|
20
20
|
import { ProviderRegistry } from "../runtime/providers.js";
|
|
21
|
+
import { QueueLostJobs } from "../runtime/queue-lost-jobs.js";
|
|
22
|
+
import { OutboxDeadService } from "./outbox-dead.service.js";
|
|
21
23
|
/** The most rows each attention list returns; the rest are counted in `more`. */
|
|
22
24
|
export const ATTENTION_LIST_MAX = 20;
|
|
23
25
|
/** The reason an extension is listed, or null when it needs nothing. */
|
|
@@ -53,8 +55,9 @@ export function providerProblem(p) {
|
|
|
53
55
|
/**
|
|
54
56
|
* The admin overview (spec §4.1, §5.2): its numbers and the 待处理 lists, read live from the
|
|
55
57
|
* sources the admin pages read (`ExtensionAdmin`, the catalog, the provider registry's cached
|
|
56
|
-
* lists). Each source is read on its own: a failing one is reported in
|
|
57
|
-
* audited (decision D5) and not cached further (every source is PG
|
|
58
|
+
* lists, the dead outbox rows, the run queue's lost-jobs canary). Each source is read on its own: a failing one is reported in
|
|
59
|
+
* `unavailable`. Reads are not audited (decision D5) and not cached further (every source is PG
|
|
60
|
+
* or already Redis-cached).
|
|
58
61
|
*/
|
|
59
62
|
let AdminOverviewService = class AdminOverviewService {
|
|
60
63
|
extensions;
|
|
@@ -66,8 +69,10 @@ let AdminOverviewService = class AdminOverviewService {
|
|
|
66
69
|
orgs;
|
|
67
70
|
accounts;
|
|
68
71
|
marketplaces;
|
|
72
|
+
outboxDead;
|
|
73
|
+
lostJobs;
|
|
69
74
|
log;
|
|
70
|
-
constructor(extensions, catalog, registry, displayNames, icons, inbox, orgs, accounts, marketplaces, log) {
|
|
75
|
+
constructor(extensions, catalog, registry, displayNames, icons, inbox, orgs, accounts, marketplaces, outboxDead, lostJobs, log) {
|
|
71
76
|
this.extensions = extensions;
|
|
72
77
|
this.catalog = catalog;
|
|
73
78
|
this.registry = registry;
|
|
@@ -77,6 +82,8 @@ let AdminOverviewService = class AdminOverviewService {
|
|
|
77
82
|
this.orgs = orgs;
|
|
78
83
|
this.accounts = accounts;
|
|
79
84
|
this.marketplaces = marketplaces;
|
|
85
|
+
this.outboxDead = outboxDead;
|
|
86
|
+
this.lostJobs = lostJobs;
|
|
80
87
|
this.log = log;
|
|
81
88
|
}
|
|
82
89
|
async stats() {
|
|
@@ -87,10 +94,17 @@ let AdminOverviewService = class AdminOverviewService {
|
|
|
87
94
|
this.attention(),
|
|
88
95
|
]);
|
|
89
96
|
const listed = (s) => attention[s].length + attention.more[s];
|
|
97
|
+
// each dead row waits for a super admin, not the one item that counts them
|
|
98
|
+
const dead = attention.outbox.reduce((n, o) => n + o.count, 0);
|
|
90
99
|
return {
|
|
91
100
|
orgs: orgs.length,
|
|
92
101
|
accounts,
|
|
93
|
-
attentionTotal: inbox +
|
|
102
|
+
attentionTotal: inbox +
|
|
103
|
+
listed("extensions") +
|
|
104
|
+
listed("updates") +
|
|
105
|
+
listed("providers") +
|
|
106
|
+
dead +
|
|
107
|
+
attention.queue.length,
|
|
94
108
|
partial: attention.unavailable.length > 0,
|
|
95
109
|
};
|
|
96
110
|
}
|
|
@@ -98,16 +112,20 @@ let AdminOverviewService = class AdminOverviewService {
|
|
|
98
112
|
// the catalog is read once: the update rows, and the plugins the inbox already lists
|
|
99
113
|
const items = this.catalogItems();
|
|
100
114
|
items.catch(() => { }); // awaited (and reported) by updateRows
|
|
101
|
-
const [extensions, updates, providers] = await Promise.allSettled([
|
|
115
|
+
const [extensions, updates, providers, outbox, queue] = await Promise.allSettled([
|
|
102
116
|
this.extensionRows(items),
|
|
103
117
|
this.updateRows(items),
|
|
104
118
|
this.providerRows(),
|
|
119
|
+
this.outboxRows(),
|
|
120
|
+
this.queueRows(),
|
|
105
121
|
]);
|
|
106
122
|
const out = {
|
|
107
123
|
extensions: [],
|
|
108
124
|
updates: [],
|
|
109
125
|
providers: [],
|
|
110
|
-
|
|
126
|
+
outbox: [],
|
|
127
|
+
queue: [],
|
|
128
|
+
more: { extensions: 0, updates: 0, providers: 0, outbox: 0, queue: 0 },
|
|
111
129
|
unavailable: [],
|
|
112
130
|
};
|
|
113
131
|
/** The rows of a source that answered (capped), or none and its name in `unavailable`. */
|
|
@@ -127,8 +145,22 @@ let AdminOverviewService = class AdminOverviewService {
|
|
|
127
145
|
out.extensions = take("extensions", extensions);
|
|
128
146
|
out.updates = take("updates", updates);
|
|
129
147
|
out.providers = take("providers", providers);
|
|
148
|
+
out.outbox = take("outbox", outbox);
|
|
149
|
+
out.queue = take("queue", queue);
|
|
130
150
|
return out;
|
|
131
151
|
}
|
|
152
|
+
/** The `queue_lost_jobs` item when the run queue lost jobs this week, else none. */
|
|
153
|
+
async queueRows() {
|
|
154
|
+
const lost = await this.lostJobs.summary();
|
|
155
|
+
return lost === null ? [] : [{ kind: "queue_lost_jobs", ...lost }];
|
|
156
|
+
}
|
|
157
|
+
/** The `outbox_dead` item when some outbox rows are dead, else none. */
|
|
158
|
+
async outboxRows() {
|
|
159
|
+
const { count, oldestAt } = await this.outboxDead.summary();
|
|
160
|
+
if (count === 0 || oldestAt === null)
|
|
161
|
+
return [];
|
|
162
|
+
return [{ kind: "outbox_dead", count, oldestAt: oldestAt.toISOString() }];
|
|
163
|
+
}
|
|
132
164
|
/**
|
|
133
165
|
* The extensions needing a look, but not one pending approval whose plugin waits in the platform
|
|
134
166
|
* inbox (a permission confirmation or an org upload: the inbox already lists and counts it). A
|
|
@@ -218,6 +250,8 @@ AdminOverviewService = __decorate([
|
|
|
218
250
|
__param(6, Inject(OrgsService)),
|
|
219
251
|
__param(7, Inject(AccountsService)),
|
|
220
252
|
__param(8, Inject(MarketplacesService)),
|
|
221
|
-
__param(9, Inject(
|
|
253
|
+
__param(9, Inject(OutboxDeadService)),
|
|
254
|
+
__param(10, Inject(QueueLostJobs)),
|
|
255
|
+
__param(11, Inject(KERNEL_LOGGER))
|
|
222
256
|
], AdminOverviewService);
|
|
223
257
|
export { AdminOverviewService };
|
|
@@ -329,7 +329,7 @@ export async function planFromTemplate(db, orgId, row, opts) {
|
|
|
329
329
|
}
|
|
330
330
|
if (describesSubagents)
|
|
331
331
|
listed.add(AGENT_TOOL);
|
|
332
|
-
// `TaskOutput` / `TaskStop` follow `Bash`
|
|
332
|
+
// `TaskOutput` / `TaskStop` / `TaskList` follow `Bash`
|
|
333
333
|
for (const name of AGENT_BUILTIN_NAMES)
|
|
334
334
|
if (!listed.has(name))
|
|
335
335
|
offBuiltin(name);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventLog } from "../conversations/event-log.js";
|
|
2
2
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
3
3
|
import { KernelEvents } from "../infra/events/kernel-events.js";
|
|
4
|
+
import { OutboxService } from "../infra/outbox/outbox.service.js";
|
|
4
5
|
import { type BackgroundTaskRow, BackgroundTaskStore } from "./store.js";
|
|
5
6
|
/** Where a task ends (as {@link BackgroundTaskStore.finish} takes it). */
|
|
6
7
|
export type BackgroundFinish = Parameters<BackgroundTaskStore["finish"]>[1];
|
|
@@ -19,7 +20,8 @@ export declare const finishedEventKey: (taskId: string) => string;
|
|
|
19
20
|
* task (two pollers, a poll and a stop) exactly one writes it. After the commit: the log's notify,
|
|
20
21
|
* the `changed: "background"` frame (to the owner and, for a sub-agent's task, the conversation
|
|
21
22
|
* that owns the sandbox, whose panel lists it too) and the `background_task.finished` domain event
|
|
22
|
-
* (`BackgroundWake` wakes an idle conversation; it lives in the runtime, above this module).
|
|
23
|
+
* (`BackgroundWake` wakes an idle conversation; it lives in the runtime, above this module). A
|
|
24
|
+
* finish that may wake the conversation also owes the ensure-run backstop in the transaction.
|
|
23
25
|
*
|
|
24
26
|
* Holds no lock and runs no sandbox command: the caller reads `lastLines` before.
|
|
25
27
|
*/
|
|
@@ -28,7 +30,8 @@ export declare class BackgroundTaskFinisher {
|
|
|
28
30
|
private readonly eventLog;
|
|
29
31
|
private readonly events;
|
|
30
32
|
readonly txHost: TransactionHost;
|
|
31
|
-
|
|
33
|
+
private readonly outbox;
|
|
34
|
+
constructor(store: Pick<BackgroundTaskStore, "finish">, eventLog: EventLog, events: KernelEvents, txHost: TransactionHost, outbox: Pick<OutboxService, "add">);
|
|
32
35
|
/**
|
|
33
36
|
* Finishes `task` as `to` with its last output lines (cut to their last 4 000 characters);
|
|
34
37
|
* `reported` when a tool call already told the model in its run. False (nothing written) when the
|
|
@@ -13,6 +13,8 @@ import { afterCommit } from "../infra/database/after-commit.js";
|
|
|
13
13
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
14
14
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
15
15
|
import { KernelEvents } from "../infra/events/kernel-events.js";
|
|
16
|
+
import { OutboxService } from "../infra/outbox/outbox.service.js";
|
|
17
|
+
import { oweEnsureRun } from "../runtime/run-jobs.js";
|
|
16
18
|
import { taskOutputFile } from "./launch.js";
|
|
17
19
|
import { LAST_LINES_MAX_CHARS } from "./session.js";
|
|
18
20
|
import { BackgroundTaskStore } from "./store.js";
|
|
@@ -25,7 +27,8 @@ export const finishedEventKey = (taskId) => `background_task_finished:${taskId}`
|
|
|
25
27
|
* task (two pollers, a poll and a stop) exactly one writes it. After the commit: the log's notify,
|
|
26
28
|
* the `changed: "background"` frame (to the owner and, for a sub-agent's task, the conversation
|
|
27
29
|
* that owns the sandbox, whose panel lists it too) and the `background_task.finished` domain event
|
|
28
|
-
* (`BackgroundWake` wakes an idle conversation; it lives in the runtime, above this module).
|
|
30
|
+
* (`BackgroundWake` wakes an idle conversation; it lives in the runtime, above this module). A
|
|
31
|
+
* finish that may wake the conversation also owes the ensure-run backstop in the transaction.
|
|
29
32
|
*
|
|
30
33
|
* Holds no lock and runs no sandbox command: the caller reads `lastLines` before.
|
|
31
34
|
*/
|
|
@@ -34,11 +37,13 @@ let BackgroundTaskFinisher = class BackgroundTaskFinisher {
|
|
|
34
37
|
eventLog;
|
|
35
38
|
events;
|
|
36
39
|
txHost;
|
|
37
|
-
|
|
40
|
+
outbox;
|
|
41
|
+
constructor(store, eventLog, events, txHost, outbox) {
|
|
38
42
|
this.store = store;
|
|
39
43
|
this.eventLog = eventLog;
|
|
40
44
|
this.events = events;
|
|
41
45
|
this.txHost = txHost;
|
|
46
|
+
this.outbox = outbox;
|
|
42
47
|
}
|
|
43
48
|
/**
|
|
44
49
|
* Finishes `task` as `to` with its last output lines (cut to their last 4 000 characters);
|
|
@@ -71,6 +76,11 @@ let BackgroundTaskFinisher = class BackgroundTaskFinisher {
|
|
|
71
76
|
},
|
|
72
77
|
},
|
|
73
78
|
]);
|
|
79
|
+
// a finish that may wake the conversation owes the ensure-run backstop with its event: the
|
|
80
|
+
// wake after the commit (`BackgroundWake`) may never happen (transactional outbox spec §6)
|
|
81
|
+
if (event && !reported && opts.stoppedBy === undefined) {
|
|
82
|
+
await oweEnsureRun(this.outbox, task.conversationId, event.seq, task.orgId);
|
|
83
|
+
}
|
|
74
84
|
const conversations = [...new Set([task.conversationId, task.sandboxConversationId])];
|
|
75
85
|
afterCommit(async () => {
|
|
76
86
|
if (event)
|
|
@@ -90,6 +100,7 @@ BackgroundTaskFinisher = __decorate([
|
|
|
90
100
|
__param(0, Inject(BackgroundTaskStore)),
|
|
91
101
|
__param(1, Inject(EventLog)),
|
|
92
102
|
__param(2, Inject(KernelEvents)),
|
|
93
|
-
__param(3, Inject(TransactionHost))
|
|
103
|
+
__param(3, Inject(TransactionHost)),
|
|
104
|
+
__param(4, Inject(OutboxService))
|
|
94
105
|
], BackgroundTaskFinisher);
|
|
95
106
|
export { BackgroundTaskFinisher };
|
|
@@ -28,14 +28,53 @@ export declare const taskSidPath: (id: string) => string;
|
|
|
28
28
|
export declare function newTaskId(): string;
|
|
29
29
|
/** A task id ({@link newTaskId}). */
|
|
30
30
|
export declare const TASK_ID_RE: RegExp;
|
|
31
|
+
/**
|
|
32
|
+
* A shell fragment that sets how a task's processes are found and signalled by its sid, defining
|
|
33
|
+
* `tk_alive <sid>` (status 0 while the task has a process) and `tk_kill <SIGNAL> <sid>` (0: sent,
|
|
34
|
+
* 1: no process left, anything else: a failure), and `pg` to the mode:
|
|
35
|
+
* - `-s`, the session, where `pgrep` has `-s` (Linux procps and busybox, the sandbox-fake shims:
|
|
36
|
+
* `pgrep -s 0`, the own session, exits 0 or 1): `pgrep -s` and `pkill -s`;
|
|
37
|
+
* - `-g`, the process group, elsewhere (macOS). Its `pgrep` has no `-s`, and inside the OS sandbox
|
|
38
|
+
* (Seatbelt) a command can neither list processes nor signal those of another command. So the
|
|
39
|
+
* task's process group (its id is the sid) is watched by its launcher ({@link GROUP_LAUNCHER}),
|
|
40
|
+
* which signals it on request: `tk_kill` appends the signal to `.capybara/tasks/g<sid>.ctl`, and
|
|
41
|
+
* `tk_alive` asks `kill(0, -sid)`, where "not permitted" (another command's group) still means
|
|
42
|
+
* the group exists. A group signal is one system call, so it misses no process forked meanwhile.
|
|
43
|
+
* Relative to the workspace root. Every script that asks for or signals a task's processes starts
|
|
44
|
+
* with it; the launch picks its launcher with the same test.
|
|
45
|
+
*/
|
|
46
|
+
export declare const SESSION_MODE_SCRIPT: string;
|
|
47
|
+
/**
|
|
48
|
+
* The process group launcher (macOS: {@link SESSION_MODE_SCRIPT}'s `-g`), a perl program run as
|
|
49
|
+
* `perl -e <this> <seconds> <task path>` from the workspace root. It makes a session of its own
|
|
50
|
+
* (leaving the launch exec's process group), starts `bash <task>.sh` as the leader of a new process
|
|
51
|
+
* group (the task; its id, the sid, goes to `<task>.sid` through a rename) and watches it every
|
|
52
|
+
* 0.1 s, outside the group:
|
|
53
|
+
* - the command exits: its status as the shell gives it (`128 + signal` when killed) goes to
|
|
54
|
+
* `<task>.exit`, and what it left running in the group is SIGKILLed (the task ends when its
|
|
55
|
+
* command exits, as the poll does on Linux);
|
|
56
|
+
* - the time limit passes: 137 goes to `<task>.exit`, then the group is SIGKILLed (as
|
|
57
|
+
* `timeout -s KILL`);
|
|
58
|
+
* - a signal is requested in `g<sid>.ctl` ({@link SESSION_MODE_SCRIPT}'s `tk_kill`, read through
|
|
59
|
+
* a rename): the group gets it, and no exit file is written any more (a stop, as on Linux).
|
|
60
|
+
* It exits once the group is empty. Contains no single quote (it goes between single quotes).
|
|
61
|
+
*/
|
|
62
|
+
export declare const GROUP_LAUNCHER: string;
|
|
31
63
|
/**
|
|
32
64
|
* The shell of the launch exec: writes `command` (base64 in the script, so nothing is quoted by
|
|
33
65
|
* hand) to `.capybara/tasks/<id>.sh` and starts it with bash (the shell a foreground `Bash` call
|
|
34
|
-
* runs its command in)
|
|
35
|
-
* status written to `<id>.exit` and its output to
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
66
|
+
* runs its command in), detached from the exec under a hard time limit of `seconds` that SIGKILLs
|
|
67
|
+
* every process of the task, with its exit status written to `<id>.exit` and its output to
|
|
68
|
+
* `<id>.out`; the task's sid goes to `<id>.sid` and is printed. Where `pgrep` has `-s`
|
|
69
|
+
* ({@link SESSION_MODE_SCRIPT}) the command runs in a session of its own under
|
|
70
|
+
* `setsid nohup sh -c 'timeout -s KILL <seconds> …'` and the sid is `$!`, the pid `setsid` started;
|
|
71
|
+
* elsewhere (macOS: no `setsid`, no `timeout`, no `pgrep -s`) {@link GROUP_LAUNCHER} runs it in a
|
|
72
|
+
* process group of its own, watched by the launcher, and writes the sid (the group id) itself,
|
|
73
|
+
* which the launch waits for (up to 10 s). The script starts with `BACKGROUND_LAUNCH_LINE`, so a
|
|
74
|
+
* local device, which kills what any other exec leaves running, leaves the task alone. An
|
|
75
|
+
* environment without one of the needed programs fails with a readable error on stderr and exit
|
|
76
|
+
* code 127, before anything starts. `id` must be a task id and `seconds` a positive integer (both
|
|
77
|
+
* go into the script unquoted).
|
|
39
78
|
*/
|
|
40
79
|
export declare function launchScript(id: string, command: string, seconds: number): string;
|
|
41
80
|
/** The model-visible result of a launch (spec §2.1); `workspace`: the instance's workspace root. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { DEFAULT_SANDBOX_ROOTS } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { BACKGROUND_LAUNCH_LINE, DEFAULT_SANDBOX_ROOTS } from "@capybara-hq/plugin-sdk";
|
|
3
3
|
/**
|
|
4
4
|
* Launching a background Bash task (spec 2026-11-05-background-bash-design §4.2): the task id, the
|
|
5
5
|
* one short exec that starts the command detached from the exec, and the model-visible result.
|
|
@@ -34,18 +34,89 @@ export function newTaskId() {
|
|
|
34
34
|
out += BASE32[b & 31];
|
|
35
35
|
return out;
|
|
36
36
|
}
|
|
37
|
-
/** The programs
|
|
38
|
-
const
|
|
37
|
+
/** The programs a session launch and the later polls and stops need (Linux, sandbox-fake). */
|
|
38
|
+
const REQUIRED_SESSION = ["bash", "setsid", "nohup", "timeout", "pgrep", "pkill", "base64"];
|
|
39
|
+
/** The programs a process group launch and the later polls and stops need (macOS). */
|
|
40
|
+
const REQUIRED_GROUP = ["bash", "perl", "nohup", "base64"];
|
|
39
41
|
/** A task id ({@link newTaskId}). */
|
|
40
42
|
export const TASK_ID_RE = /^bt_[a-z2-7]{6}$/;
|
|
43
|
+
/**
|
|
44
|
+
* A shell fragment that sets how a task's processes are found and signalled by its sid, defining
|
|
45
|
+
* `tk_alive <sid>` (status 0 while the task has a process) and `tk_kill <SIGNAL> <sid>` (0: sent,
|
|
46
|
+
* 1: no process left, anything else: a failure), and `pg` to the mode:
|
|
47
|
+
* - `-s`, the session, where `pgrep` has `-s` (Linux procps and busybox, the sandbox-fake shims:
|
|
48
|
+
* `pgrep -s 0`, the own session, exits 0 or 1): `pgrep -s` and `pkill -s`;
|
|
49
|
+
* - `-g`, the process group, elsewhere (macOS). Its `pgrep` has no `-s`, and inside the OS sandbox
|
|
50
|
+
* (Seatbelt) a command can neither list processes nor signal those of another command. So the
|
|
51
|
+
* task's process group (its id is the sid) is watched by its launcher ({@link GROUP_LAUNCHER}),
|
|
52
|
+
* which signals it on request: `tk_kill` appends the signal to `.capybara/tasks/g<sid>.ctl`, and
|
|
53
|
+
* `tk_alive` asks `kill(0, -sid)`, where "not permitted" (another command's group) still means
|
|
54
|
+
* the group exists. A group signal is one system call, so it misses no process forked meanwhile.
|
|
55
|
+
* Relative to the workspace root. Every script that asks for or signals a task's processes starts
|
|
56
|
+
* with it; the launch picks its launcher with the same test.
|
|
57
|
+
*/
|
|
58
|
+
export const SESSION_MODE_SCRIPT = [
|
|
59
|
+
'if pgrep -s 0 >/dev/null 2>&1 || [ "$?" -eq 1 ]; then pg=-s',
|
|
60
|
+
'tk_alive() { pgrep -s "$1" >/dev/null 2>&1; }',
|
|
61
|
+
'tk_kill() { pkill -"$1" -s "$2"; }',
|
|
62
|
+
"else pg=-g",
|
|
63
|
+
`tk_alive() { perl -e 'exit((kill(0, -$ARGV[0]) || $!{EPERM}) ? 0 : 1)' "$1"; }`,
|
|
64
|
+
`tk_kill() { tk_alive "$2" || return 1; echo "$1" >> ${TASKS_DIR}/g"$2".ctl || return 2; }`,
|
|
65
|
+
"fi",
|
|
66
|
+
].join("\n");
|
|
67
|
+
/**
|
|
68
|
+
* The process group launcher (macOS: {@link SESSION_MODE_SCRIPT}'s `-g`), a perl program run as
|
|
69
|
+
* `perl -e <this> <seconds> <task path>` from the workspace root. It makes a session of its own
|
|
70
|
+
* (leaving the launch exec's process group), starts `bash <task>.sh` as the leader of a new process
|
|
71
|
+
* group (the task; its id, the sid, goes to `<task>.sid` through a rename) and watches it every
|
|
72
|
+
* 0.1 s, outside the group:
|
|
73
|
+
* - the command exits: its status as the shell gives it (`128 + signal` when killed) goes to
|
|
74
|
+
* `<task>.exit`, and what it left running in the group is SIGKILLed (the task ends when its
|
|
75
|
+
* command exits, as the poll does on Linux);
|
|
76
|
+
* - the time limit passes: 137 goes to `<task>.exit`, then the group is SIGKILLed (as
|
|
77
|
+
* `timeout -s KILL`);
|
|
78
|
+
* - a signal is requested in `g<sid>.ctl` ({@link SESSION_MODE_SCRIPT}'s `tk_kill`, read through
|
|
79
|
+
* a rename): the group gets it, and no exit file is written any more (a stop, as on Linux).
|
|
80
|
+
* It exits once the group is empty. Contains no single quote (it goes between single quotes).
|
|
81
|
+
*/
|
|
82
|
+
export const GROUP_LAUNCHER = [
|
|
83
|
+
"use POSIX;",
|
|
84
|
+
"my ($n, $t) = @ARGV;",
|
|
85
|
+
"my ($d) = $t =~ m{^(.*)/};",
|
|
86
|
+
'sub put { my ($f, $v) = @_; open(my $h, ">", "$f.tmp") or return; print $h "$v\\n"; close($h); rename("$f.tmp", $f) }',
|
|
87
|
+
'POSIX::setsid() or die "setsid: $!\\n";',
|
|
88
|
+
"my $c = fork();",
|
|
89
|
+
'defined $c or die "fork: $!\\n";',
|
|
90
|
+
'if ($c == 0) { setpgid(0, 0); exec("bash", "$t.sh") or POSIX::_exit(127) }',
|
|
91
|
+
"setpgid($c, $c);",
|
|
92
|
+
'put("$t.sid", $c);',
|
|
93
|
+
'my $ctl = "$d/g$c.ctl";',
|
|
94
|
+
"my $end = time() + $n;",
|
|
95
|
+
"my ($code, $done);",
|
|
96
|
+
'sub fin { put("$t.exit", $_[0]) unless $done; $done = 1 }',
|
|
97
|
+
"while (1) {",
|
|
98
|
+
' if (rename($ctl, "$ctl.$$")) { open(my $h, "<", "$ctl.$$"); my @s = grep { /^[A-Z0-9]+$/ } map { s/\\s+//gr } <$h>; close($h); unlink("$ctl.$$"); $done = 1; kill($_, -$c) for @s }',
|
|
99
|
+
' if (!defined $code && waitpid($c, WNOHANG) == $c) { $code = ($? & 127) ? 128 + ($? & 127) : $? >> 8; fin($code); kill("KILL", -$c) }',
|
|
100
|
+
' if (!defined $code && time() >= $end) { fin(137); kill("KILL", -$c) }',
|
|
101
|
+
" last if defined $code && !kill(0, -$c);",
|
|
102
|
+
" select(undef, undef, undef, 0.1);",
|
|
103
|
+
"}",
|
|
104
|
+
].join(" ");
|
|
41
105
|
/**
|
|
42
106
|
* The shell of the launch exec: writes `command` (base64 in the script, so nothing is quoted by
|
|
43
107
|
* hand) to `.capybara/tasks/<id>.sh` and starts it with bash (the shell a foreground `Bash` call
|
|
44
|
-
* runs its command in)
|
|
45
|
-
* status written to `<id>.exit` and its output to
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
108
|
+
* runs its command in), detached from the exec under a hard time limit of `seconds` that SIGKILLs
|
|
109
|
+
* every process of the task, with its exit status written to `<id>.exit` and its output to
|
|
110
|
+
* `<id>.out`; the task's sid goes to `<id>.sid` and is printed. Where `pgrep` has `-s`
|
|
111
|
+
* ({@link SESSION_MODE_SCRIPT}) the command runs in a session of its own under
|
|
112
|
+
* `setsid nohup sh -c 'timeout -s KILL <seconds> …'` and the sid is `$!`, the pid `setsid` started;
|
|
113
|
+
* elsewhere (macOS: no `setsid`, no `timeout`, no `pgrep -s`) {@link GROUP_LAUNCHER} runs it in a
|
|
114
|
+
* process group of its own, watched by the launcher, and writes the sid (the group id) itself,
|
|
115
|
+
* which the launch waits for (up to 10 s). The script starts with `BACKGROUND_LAUNCH_LINE`, so a
|
|
116
|
+
* local device, which kills what any other exec leaves running, leaves the task alone. An
|
|
117
|
+
* environment without one of the needed programs fails with a readable error on stderr and exit
|
|
118
|
+
* code 127, before anything starts. `id` must be a task id and `seconds` a positive integer (both
|
|
119
|
+
* go into the script unquoted).
|
|
49
120
|
*/
|
|
50
121
|
export function launchScript(id, command, seconds) {
|
|
51
122
|
if (!TASK_ID_RE.test(id))
|
|
@@ -55,15 +126,27 @@ export function launchScript(id, command, seconds) {
|
|
|
55
126
|
}
|
|
56
127
|
const b64 = Buffer.from(command, "utf8").toString("base64");
|
|
57
128
|
const t = `${TASKS_DIR}/${id}`;
|
|
129
|
+
const need = (list) => `for c in ${list.join(" ")}; do command -v "$c" >/dev/null 2>&1 || { echo "Background tasks need ${list.join(", ")} in this environment; $c is missing." >&2; exit 127; }; done`;
|
|
58
130
|
return [
|
|
131
|
+
// first: a local device kills what an exec leaves running, except a launch's detached task
|
|
132
|
+
BACKGROUND_LAUNCH_LINE,
|
|
59
133
|
"set -e",
|
|
60
|
-
|
|
134
|
+
SESSION_MODE_SCRIPT,
|
|
135
|
+
`if [ "$pg" = -s ]; then ${need(REQUIRED_SESSION)}; else ${need(REQUIRED_GROUP)}; fi`,
|
|
61
136
|
`mkdir -p ${TASKS_DIR}`,
|
|
62
137
|
`printf '%s' '${b64}' | base64 -d > ${t}.sh`,
|
|
138
|
+
`if [ "$pg" = -s ]; then`,
|
|
63
139
|
`setsid nohup sh -c 'timeout -s KILL ${seconds} bash ${t}.sh; echo $? > ${t}.exit' > ${t}.out 2>&1 < /dev/null &`,
|
|
64
140
|
// the session id goes to a file first: a launch that ends before its output comes back
|
|
65
141
|
// (aborted, timed out) still leaves the id of what it started
|
|
66
142
|
`echo $! > ${t}.sid`,
|
|
143
|
+
"else",
|
|
144
|
+
`rm -f ${t}.sid`,
|
|
145
|
+
`nohup perl -e '${GROUP_LAUNCHER}' ${seconds} ${t} > ${t}.out 2>&1 < /dev/null &`,
|
|
146
|
+
// the launcher writes the group id once the group exists
|
|
147
|
+
`i=0; while [ ! -s ${t}.sid ] && [ "$i" -lt 200 ]; do sleep 0.05; i=$((i+1)); done`,
|
|
148
|
+
`[ -s ${t}.sid ] || { echo "The background task did not start: no process group id after 10 s." >&2; exit 1; }`,
|
|
149
|
+
"fi",
|
|
67
150
|
`cat ${t}.sid`,
|
|
68
151
|
].join("\n");
|
|
69
152
|
}
|
|
@@ -4,7 +4,8 @@ import type { HeldProvider, TaggedProvider } from "../sandbox/sandbox.service.js
|
|
|
4
4
|
* A background task's session inside the sandbox (spec 2026-11-05-background-bash-design §4.2):
|
|
5
5
|
* every process of a task is in the session `setsid` started, so liveness is `pgrep -s <sid>` and
|
|
6
6
|
* stopping is `pkill -s <sid>` (never `kill -0` or `/proc`, which disagree between sandbox-fake and
|
|
7
|
-
* Linux).
|
|
7
|
+
* Linux). macOS has no `setsid` and its `pgrep` / `pkill` no `-s`: there the task is the process
|
|
8
|
+
* group the launch made (`launchScript`), signalled with `kill -- -<sid>` ({@link SESSION_MODE_SCRIPT}).
|
|
8
9
|
*/
|
|
9
10
|
/**
|
|
10
11
|
* `p` with every `exec` carrying `actor` (`SandboxService.ifRunning` tags execs but leaves the
|
|
@@ -27,7 +28,8 @@ export declare function assertTaskRef(id: string, sid: string): void;
|
|
|
27
28
|
* session is empty (pkill exits 1: no process left): a process may fork between pkill's listing
|
|
28
29
|
* and its signals, and the child would live on. Sets `ks` to 0 when the session was confirmed
|
|
29
30
|
* empty, else to pkill's failing status (3 when it was still not empty after 10 tries). Uses only
|
|
30
|
-
* the variables `ks`, `kc` and `ki`. `sid` must be
|
|
31
|
+
* the variables and functions of {@link SESSION_MODE_SCRIPT} (it starts with it), `ks`, `kc` and `ki`. `sid` must be
|
|
32
|
+
* a session id.
|
|
31
33
|
*/
|
|
32
34
|
export declare function killSessionScript(sid: string): string;
|
|
33
35
|
/**
|
|
@@ -37,6 +39,16 @@ export declare function killSessionScript(sid: string): string;
|
|
|
37
39
|
* empty; false when it could not be (an error is never thrown).
|
|
38
40
|
*/
|
|
39
41
|
export declare function killSession(p: Pick<HeldProvider, "exec">, h: SandboxHandle, sid: string): Promise<boolean>;
|
|
42
|
+
/** How long a stop waits after SIGTERM before SIGKILL (spec §2.3). */
|
|
43
|
+
export declare const STOP_GRACE_MS = 5000;
|
|
44
|
+
/** The time limit of the SIGTERM exec: the grace period plus some room. */
|
|
45
|
+
export declare const TERM_EXEC_TIMEOUT_MS: number;
|
|
46
|
+
/**
|
|
47
|
+
* Exit code 0 with `exited <code>` when the task had already finished (its exit file exists),
|
|
48
|
+
* else sends SIGTERM to the session and waits up to {@link STOP_GRACE_MS} for it to empty, then
|
|
49
|
+
* prints `term`. Relative to the workspace root.
|
|
50
|
+
*/
|
|
51
|
+
export declare function termScript(id: string, sid: string): string;
|
|
40
52
|
/** The longest `lastLines` of a finished task (spec §2.4). */
|
|
41
53
|
export declare const LAST_LINES_MAX_CHARS = 4000;
|
|
42
54
|
/**
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { rootsOf } from "@capybara-hq/plugin-sdk";
|
|
2
|
-
import { TASK_ID_RE, TASKS_DIR } from "./launch.js";
|
|
2
|
+
import { SESSION_MODE_SCRIPT, TASK_ID_RE, TASKS_DIR } from "./launch.js";
|
|
3
3
|
/**
|
|
4
4
|
* A background task's session inside the sandbox (spec 2026-11-05-background-bash-design §4.2):
|
|
5
5
|
* every process of a task is in the session `setsid` started, so liveness is `pgrep -s <sid>` and
|
|
6
6
|
* stopping is `pkill -s <sid>` (never `kill -0` or `/proc`, which disagree between sandbox-fake and
|
|
7
|
-
* Linux).
|
|
7
|
+
* Linux). macOS has no `setsid` and its `pgrep` / `pkill` no `-s`: there the task is the process
|
|
8
|
+
* group the launch made (`launchScript`), signalled with `kill -- -<sid>` ({@link SESSION_MODE_SCRIPT}).
|
|
8
9
|
*/
|
|
9
10
|
/**
|
|
10
11
|
* `p` with every `exec` carrying `actor` (`SandboxService.ifRunning` tags execs but leaves the
|
|
@@ -35,12 +36,13 @@ export function assertTaskRef(id, sid) {
|
|
|
35
36
|
* session is empty (pkill exits 1: no process left): a process may fork between pkill's listing
|
|
36
37
|
* and its signals, and the child would live on. Sets `ks` to 0 when the session was confirmed
|
|
37
38
|
* empty, else to pkill's failing status (3 when it was still not empty after 10 tries). Uses only
|
|
38
|
-
* the variables `ks`, `kc` and `ki`. `sid` must be
|
|
39
|
+
* the variables and functions of {@link SESSION_MODE_SCRIPT} (it starts with it), `ks`, `kc` and `ki`. `sid` must be
|
|
40
|
+
* a session id.
|
|
39
41
|
*/
|
|
40
42
|
export function killSessionScript(sid) {
|
|
41
43
|
if (!SID_RE.test(sid))
|
|
42
44
|
throw new Error(`invalid background task session: ${JSON.stringify(sid)}`);
|
|
43
|
-
return
|
|
45
|
+
return `${SESSION_MODE_SCRIPT}\nks=3; for ki in 1 2 3 4 5 6 7 8 9 10; do tk_kill KILL ${sid}; kc=$?; if [ "$kc" -eq 1 ]; then ks=0; break; fi; if [ "$kc" -ne 0 ]; then ks=$kc; break; fi; sleep 0.2; done`;
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
46
48
|
* Kills every process of the session `sid` ({@link killSessionScript}) in an exec of its own. Any
|
|
@@ -62,6 +64,26 @@ export async function killSession(p, h, sid) {
|
|
|
62
64
|
.catch(() => undefined);
|
|
63
65
|
return r?.exitCode === 0;
|
|
64
66
|
}
|
|
67
|
+
/** How long a stop waits after SIGTERM before SIGKILL (spec §2.3). */
|
|
68
|
+
export const STOP_GRACE_MS = 5000;
|
|
69
|
+
/** The time limit of the SIGTERM exec: the grace period plus some room. */
|
|
70
|
+
export const TERM_EXEC_TIMEOUT_MS = STOP_GRACE_MS + 5000;
|
|
71
|
+
/**
|
|
72
|
+
* Exit code 0 with `exited <code>` when the task had already finished (its exit file exists),
|
|
73
|
+
* else sends SIGTERM to the session and waits up to {@link STOP_GRACE_MS} for it to empty, then
|
|
74
|
+
* prints `term`. Relative to the workspace root.
|
|
75
|
+
*/
|
|
76
|
+
export function termScript(id, sid) {
|
|
77
|
+
const f = `${TASKS_DIR}/${id}`;
|
|
78
|
+
const tries = Math.ceil(STOP_GRACE_MS / 200);
|
|
79
|
+
return [
|
|
80
|
+
`if [ -s ${f}.exit ]; then echo "exited $(cat ${f}.exit)"; exit 0; fi`,
|
|
81
|
+
SESSION_MODE_SCRIPT,
|
|
82
|
+
`tk_kill TERM ${sid} >/dev/null 2>&1`,
|
|
83
|
+
`i=0; while [ "$i" -lt ${tries} ] && tk_alive ${sid}; do sleep 0.2; i=$((i+1)); done`,
|
|
84
|
+
"echo term",
|
|
85
|
+
].join("\n");
|
|
86
|
+
}
|
|
65
87
|
/** The most bytes of a task's output end a tail reads (the event keeps at most 4 000 characters). */
|
|
66
88
|
const TAIL_BYTES = 12_000;
|
|
67
89
|
/** The longest `lastLines` of a finished task (spec §2.4). */
|
|
@@ -5,7 +5,7 @@ export interface TaskReport {
|
|
|
5
5
|
/** The exit file's code; null while there is none. */
|
|
6
6
|
exitCode: number | null;
|
|
7
7
|
/**
|
|
8
|
-
* Whether the session still has a process: `pgrep -s <sid
|
|
8
|
+
* Whether the session still has a process: `pgrep -s <sid>` (`-g` on macOS), or, for a task whose command
|
|
9
9
|
* exited, whether its leftover processes could not be confirmed killed.
|
|
10
10
|
*/
|
|
11
11
|
alive: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TASK_ID_RE, TASKS_DIR } from "./launch.js";
|
|
1
|
+
import { SESSION_MODE_SCRIPT, TASK_ID_RE, TASKS_DIR } from "./launch.js";
|
|
2
2
|
import { decodeTail, killSessionScript, SID_RE, tailCommand } from "./session.js";
|
|
3
3
|
import { exitedAs } from "./stop.js";
|
|
4
4
|
/**
|
|
@@ -17,7 +17,7 @@ export function pollScript(tasks) {
|
|
|
17
17
|
}
|
|
18
18
|
const f = `${TASKS_DIR}/${id}`;
|
|
19
19
|
return [
|
|
20
|
-
`if
|
|
20
|
+
`if tk_alive ${sid}; then a=1; else a=0; fi`,
|
|
21
21
|
`if [ -s ${f}.exit ]; then c=$(tr -dc '0-9-' < ${f}.exit); else c=-; fi`,
|
|
22
22
|
`[ -n "$c" ] || c=-`,
|
|
23
23
|
`if [ "$c" != - ] && [ "$a" = 1 ]; then ${killSessionScript(sid)}; [ "$ks" = 0 ] && a=0; fi`,
|
|
@@ -25,7 +25,7 @@ export function pollScript(tasks) {
|
|
|
25
25
|
`echo "${id} $c $a \${t:--}"`,
|
|
26
26
|
].join("\n");
|
|
27
27
|
});
|
|
28
|
-
return lines.join("\n");
|
|
28
|
+
return [SESSION_MODE_SCRIPT, ...lines].join("\n");
|
|
29
29
|
}
|
|
30
30
|
/** Parses {@link pollScript}'s output; a task missing from it is absent from the map. */
|
|
31
31
|
export function parsePollOutput(stdout) {
|
|
@@ -5,8 +5,6 @@ import { type BackgroundFinish, BackgroundTaskFinisher } from "./finish.js";
|
|
|
5
5
|
import { type BackgroundTaskRow, BackgroundTaskStore } from "./store.js";
|
|
6
6
|
/** Why a task is stopped. */
|
|
7
7
|
export type StopReason = "tool" | "person" | "subagent_end" | "conversation_deleted";
|
|
8
|
-
/** How long a stop waits after SIGTERM before SIGKILL (spec §2.3). */
|
|
9
|
-
export declare const STOP_GRACE_MS = 5000;
|
|
10
8
|
/** How a task that exited by itself ends (spec §4.3): exit 137 past its deadline timed out. */
|
|
11
9
|
export declare function exitedAs(task: BackgroundTaskRow, code: number, now: Date): BackgroundFinish;
|
|
12
10
|
/** How a task whose sandbox is not running ends (`ifRunning`'s `not_running` status). */
|