Deliver durable resource lifecycle events to plugins without polling (#16203)

## Thinking Path

> - Paperclip manages AI agents and their work.
> - Plugins keep external resources aligned with Paperclip projects and
agents.
> - The durable resource journal stores changes, but its plugin
interface requires polling.
> - Project deletion has no journal entry, so consumers must scan for
missing projects.
> - This PR delivers committed journal events through a native plugin
callback and records deletion.
> - Plugins can recover unfinished work without periodic change scans.

## Linked Issues or Issue Description

Related: #15343 introduced the durable inbox. #15631 added required
agent lifecycle participation. This change keeps both contracts and adds
automatic delivery for the resource inbox.

**What happened?**

Plugins must schedule reads of the durable inbox. A deleted project has
no durable deletion event. Consumers must scan saved bindings to detect
its removal.

**Expected behavior**

Core should deliver committed resource events and replay unfinished
work. Empty queues should have no polling timer. Project removal should
leave a durable event.

**Steps to reproduce**

1. Install a plugin that consumes project lifecycle events.
2. Create or update a project. The plugin must call `listLifecycle` to
discover it.
3. Delete the project. The journal has no deletion entry.

## What Changed

- Add `resourceLifecycle` to the manifest and `onResourceLifecycle` to
the SDK. Require `events.subscribe` and a configured, enabled company
scope.
- Reuse transaction-aware work signals and the delivery coordinator.
Wake after commit, on startup and plugin activation, and after
configuration changes.
- Acknowledge only a matching successful result. Retry unfinished or
interrupted work. Retain deferred events without an idle retry timer.
- Wake again after successful worker configuration application, and
retain failed scans through idle drain (both Apex findings fixed).
- Keep ordering per resource and deliver past failed or deferred
resources. Keep the manual inbox methods for existing consumers.
- Record project deletion in the removal transaction. Extend the journal
constraint with a generated migration.

## Verification

- Apex follow-up: 129 focused worker, delivery, coordinator, and
application tests passed; server typecheck passed. Regression coverage
verifies configuration-application races and failed scans across idle
drain.

- Focused lifecycle, transaction coordinator, manifest, and SDK dispatch
suites: 105 tests passed, including single-connection saves, redacted
failure diagnostics, wakeups retained during idle drain, and per-event
retry deadlines.
- Complete SDK suite plus delivery database tests: 128 tests passed.
- Shared, database, and SDK typechecks passed. Direct server `tsc
--noEmit` passed against rebuilt SDK declarations.
- Full local `pnpm test:run` finished with 19,491 passed, 148 skipped,
and 6 failed tests across 5 files (plus two suite setup errors). This
run overlapped branch updates and is not final-head evidence: failures
included missing Cargo, local Git default-branch assumptions, and stale
in-process lifecycle modules. Final Apex-fix commit `75d028788` passed
all CI gates, including the full test shards, and received Apex 5/5 with
no unresolved comments.
- `pnpm -r typecheck` and `pnpm build` were attempted. Both stopped
because Cargo is unavailable locally. CI supplies the Rust toolchain.
- Local installation used the repository's documented lockfile fallback
because master has dependencies missing from its lockfile. No dependency
changes are included here.

## Risks

- Delivery is at least once. Plugins must re-read current state and make
external effects idempotent.
- Signals are process-local, like the other delivery queues. Startup
recovers committed rows. Independent database writers or multiple active
server processes require an explicit ownership and wake protocol.
- Apply migration `0340_open_warlock.sql` before running code that
records project deletion. Earlier deletions cannot be reconstructed.
- Automatic delivery selects plugins with company configuration rows.
`deferred` waits for a later startup, configuration change, or other
work notification.
- This does not replace required agent lifecycle participation or
establish that external mounts are unused.

## Model Used

OpenAI Codex (GPT-6), with tool-assisted code editing, code inspection,
and test execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip authored and GitHub committed 2026-10-13 12:00:30 -07:00
1 parent 5a6d0d08a7
commit db65a0863e
27 files changed
+1252 -145

No files matched your search

@@ -0,0 +1,2 @@
ALTER TABLE "resource_lifecycle_events" DROP CONSTRAINT "resource_lifecycle_events_action_check";--> statement-breakpoint
ALTER TABLE "resource_lifecycle_events" ADD CONSTRAINT "resource_lifecycle_events_action_check" CHECK ("resource_lifecycle_events"."action" = 'create' OR ("resource_lifecycle_events"."resource_type" = 'project' AND "resource_lifecycle_events"."action" IN ('update', 'archive', 'delete')) OR ("resource_lifecycle_events"."resource_type" = 'agent' AND "resource_lifecycle_events"."action" IN ('pause', 'resume', 'terminate')));
@@ -1,6 +1,6 @@
{
"id": "18c156f2-8344-4282-98b7-f702f0035c81",
"prevId": "39d82f2c-8fd5-4606-84df-f51880dbd6a7",
"id": "b32fa655-79c7-4793-a021-22258b6c369c",
"prevId": "045fe4f7-1636-4e1b-b304-11b6128fefcf",
"version": "7",
"dialect": "postgresql",
"tables": {
@@ -9858,6 +9858,12 @@
"notNull": true,
"default": false
},
"execution_defaults": {
"name": "execution_defaults",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"metadata": {
"name": "metadata",
"type": "jsonb",
@@ -10098,6 +10104,12 @@
"notNull": true,
"default": "''"
},
"execution_defaults": {
"name": "execution_defaults",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"allow_direct_messages": {
"name": "allow_direct_messages",
"type": "boolean",
@@ -16312,6 +16324,18 @@
"primaryKey": false,
"notNull": false
},
"owner_agent_id": {
"name": "owner_agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"owner_plugin_id": {
"name": "owner_plugin_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"user_secret_definition_id": {
"name": "user_secret_definition_id",
"type": "uuid",
@@ -16540,6 +16564,40 @@
"method": "btree",
"with": {}
},
"company_secrets_instance_owner_key_uq": {
"name": "company_secrets_instance_owner_key_uq",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "owner_agent_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "owner_plugin_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"where": "\"company_secrets\".\"scope\" = 'instance' and \"company_secrets\".\"deleted_at\" is null",
"concurrently": false,
"method": "btree",
"with": {}
},
"company_secrets_provider_config_idx": {
"name": "company_secrets_provider_config_idx",
"columns": [
@@ -16642,6 +16700,19 @@
"onDelete": "no action",
"onUpdate": "no action"
},
"company_secrets_owner_plugin_id_plugins_id_fk": {
"name": "company_secrets_owner_plugin_id_plugins_id_fk",
"tableFrom": "company_secrets",
"tableTo": "plugins",
"columnsFrom": [
"owner_plugin_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"company_secrets_user_secret_definition_id_user_secret_definitions_id_fk": {
"name": "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk",
"tableFrom": "company_secrets",
@@ -16680,6 +16751,21 @@
],
"onDelete": "set null",
"onUpdate": "no action"
},
"company_secrets_company_id_owner_agent_id_agents_company_id_id_fk": {
"name": "company_secrets_company_id_owner_agent_id_agents_company_id_id_fk",
"tableFrom": "company_secrets",
"tableTo": "agents",
"columnsFrom": [
"company_id",
"owner_agent_id"
],
"columnsTo": [
"company_id",
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
@@ -16688,7 +16774,7 @@
"checkConstraints": {
"company_secrets_scope_shape_check": {
"name": "company_secrets_scope_shape_check",
"value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n )"
"value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n and \"company_secrets\".\"owner_agent_id\" is null and \"company_secrets\".\"owner_plugin_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n and \"company_secrets\".\"owner_agent_id\" is null and \"company_secrets\".\"owner_plugin_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'instance'\n and \"company_secrets\".\"owner_user_id\" is null and \"company_secrets\".\"user_secret_definition_id\" is null\n and \"company_secrets\".\"owner_agent_id\" is not null and \"company_secrets\".\"owner_plugin_id\" is not null\n )"
}
},
"isRLSEnabled": false
@@ -26581,6 +26667,476 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.execution_workspace_repositories": {
"name": "execution_workspace_repositories",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"execution_workspace_id": {
"name": "execution_workspace_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"requested_by_issue_id": {
"name": "requested_by_issue_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"repository_identity": {
"name": "repository_identity",
"type": "text",
"primaryKey": false,
"notNull": true
},
"catalog_repository_id": {
"name": "catalog_repository_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"repo_url": {
"name": "repo_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"relative_path": {
"name": "relative_path",
"type": "text",
"primaryKey": false,
"notNull": true
},
"requested_ref": {
"name": "requested_ref",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'HEAD'"
},
"pinned_commit": {
"name": "pinned_commit",
"type": "text",
"primaryKey": false,
"notNull": false
},
"branch_name": {
"name": "branch_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"request_key": {
"name": "request_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"request_keys": {
"name": "request_keys",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'[]'::jsonb"
},
"state": {
"name": "state",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"failure_code": {
"name": "failure_code",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"execution_workspace_repositories_identity_idx": {
"name": "execution_workspace_repositories_identity_idx",
"columns": [
{
"expression": "execution_workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "repository_identity",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"execution_workspace_repositories_path_idx": {
"name": "execution_workspace_repositories_path_idx",
"columns": [
{
"expression": "execution_workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "relative_path",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"execution_workspace_repositories_request_idx": {
"name": "execution_workspace_repositories_request_idx",
"columns": [
{
"expression": "execution_workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "request_key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"execution_workspace_repositories_company_workspace_idx": {
"name": "execution_workspace_repositories_company_workspace_idx",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "execution_workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"execution_workspace_repositories_company_id_companies_id_fk": {
"name": "execution_workspace_repositories_company_id_companies_id_fk",
"tableFrom": "execution_workspace_repositories",
"tableTo": "companies",
"columnsFrom": [
"company_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"execution_workspace_repositories_execution_workspace_id_execution_workspaces_id_fk": {
"name": "execution_workspace_repositories_execution_workspace_id_execution_workspaces_id_fk",
"tableFrom": "execution_workspace_repositories",
"tableTo": "execution_workspaces",
"columnsFrom": [
"execution_workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"execution_workspace_repositories_requested_by_issue_id_issues_id_fk": {
"name": "execution_workspace_repositories_requested_by_issue_id_issues_id_fk",
"tableFrom": "execution_workspace_repositories",
"tableTo": "issues",
"columnsFrom": [
"requested_by_issue_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.execution_workspace_repository_access": {
"name": "execution_workspace_repository_access",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"root_key": {
"name": "root_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"canonical_cwd": {
"name": "canonical_cwd",
"type": "text",
"primaryKey": false,
"notNull": true
},
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true
},
"scope_id": {
"name": "scope_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"issue_id": {
"name": "issue_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"agent_id": {
"name": "agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"responsible_user_id": {
"name": "responsible_user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"execution_workspace_repository_access_scope_idx": {
"name": "execution_workspace_repository_access_scope_idx",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "root_key",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "kind",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "scope_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"execution_workspace_repository_access_company_id_companies_id_fk": {
"name": "execution_workspace_repository_access_company_id_companies_id_fk",
"tableFrom": "execution_workspace_repository_access",
"tableTo": "companies",
"columnsFrom": [
"company_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.execution_workspace_repository_roots": {
"name": "execution_workspace_repository_roots",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"canonical_cwd": {
"name": "canonical_cwd",
"type": "text",
"primaryKey": false,
"notNull": true
},
"root_key": {
"name": "root_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"mode": {
"name": "mode",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"execution_workspace_repository_roots_company_root_idx": {
"name": "execution_workspace_repository_roots_company_root_idx",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "root_key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"execution_workspace_repository_roots_company_id_companies_id_fk": {
"name": "execution_workspace_repository_roots_company_id_companies_id_fk",
"tableFrom": "execution_workspace_repository_roots",
"tableTo": "companies",
"columnsFrom": [
"company_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.execution_workspace_runtime_leases": {
"name": "execution_workspace_runtime_leases",
"schema": "",
@@ -26836,7 +27392,7 @@
"name": "project_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
"notNull": false
},
"project_workspace_id": {
"name": "project_workspace_id",
@@ -27114,7 +27670,7 @@
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onDelete": "restrict",
"onUpdate": "no action"
},
"execution_workspaces_project_workspace_id_project_workspaces_id_fk": {
@@ -37902,6 +38458,25 @@
"primaryKey": false,
"notNull": false
},
"workspace_binding_revision": {
"name": "workspace_binding_revision",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"workspace_selection": {
"name": "workspace_selection",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workspace_pending_selection": {
"name": "workspace_pending_selection",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"execution_workspace_preference": {
"name": "execution_workspace_preference",
"type": "text",
@@ -45625,7 +46200,7 @@
},
"resource_lifecycle_events_action_check": {
"name": "resource_lifecycle_events_action_check",
"value": "\"resource_lifecycle_events\".\"action\" = 'create' OR (\"resource_lifecycle_events\".\"resource_type\" = 'project' AND \"resource_lifecycle_events\".\"action\" IN ('update', 'archive')) OR (\"resource_lifecycle_events\".\"resource_type\" = 'agent' AND \"resource_lifecycle_events\".\"action\" IN ('pause', 'resume', 'terminate'))"
"value": "\"resource_lifecycle_events\".\"action\" = 'create' OR (\"resource_lifecycle_events\".\"resource_type\" = 'project' AND \"resource_lifecycle_events\".\"action\" IN ('update', 'archive', 'delete')) OR (\"resource_lifecycle_events\".\"resource_type\" = 'agent' AND \"resource_lifecycle_events\".\"action\" IN ('pause', 'resume', 'terminate'))"
}
},
"isRLSEnabled": false
+7
View File
@@ -2367,6 +2367,13 @@
"when": 1791903526575,
"tag": "0339_luxuriant_iron_monger",
"breakpoints": true
},
{
"idx": 340,
"version": "7",
"when": 1791911743153,
"tag": "0340_open_warlock",
"breakpoints": true
}
]
}
@@ -8,12 +8,12 @@ export const resourceLifecycleEvents = pgTable("resource_lifecycle_events", {
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
resourceType: text("resource_type").$type<"agent" | "project">().notNull(),
resourceId: uuid("resource_id").notNull(),
action: text("action").$type<"create" | "update" | "archive" | "pause" | "resume" | "terminate">().notNull(),
action: text("action").$type<"create" | "update" | "archive" | "delete" | "pause" | "resume" | "terminate">().notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
resourceIdx: uniqueIndex("resource_lifecycle_events_creation_idx").on(table.companyId, table.resourceType, table.resourceId).where(sql`${table.action} = 'create'`),
companyIdx: index("resource_lifecycle_events_company_idx").on(table.companyId, table.id),
resourceOrderIdx: index("resource_lifecycle_events_resource_order_idx").on(table.companyId, table.resourceType, table.resourceId, table.id),
resourceTypeCheck: check("resource_lifecycle_events_resource_type_check", sql`${table.resourceType} IN ('agent', 'project')`),
actionCheck: check("resource_lifecycle_events_action_check", sql`${table.action} = 'create' OR (${table.resourceType} = 'project' AND ${table.action} IN ('update', 'archive')) OR (${table.resourceType} = 'agent' AND ${table.action} IN ('pause', 'resume', 'terminate'))`),
actionCheck: check("resource_lifecycle_events_action_check", sql`${table.action} = 'create' OR (${table.resourceType} = 'project' AND ${table.action} IN ('update', 'archive', 'delete')) OR (${table.resourceType} = 'agent' AND ${table.action} IN ('pause', 'resume', 'terminate'))`),
}));