Add app-server thread/delete API (#25018)

## Why

Clients can archive and unarchive threads today, but there is no
app-server API for permanently removing a thread. Deletion also needs to
cover the full session tree: deleting a main thread should remove
spawned subagent threads and the related local metadata instead of
leaving orphaned rollout files, goals, or subagent state behind.

## What

- Adds the v2 `thread/delete` request and `thread/deleted` notification,
with the response shape kept consistent with `thread/archive`.
- Implements local hard delete for active and archived rollout files.
- Deletes the requested thread's state DB row as the commit point, then
best-effort cleans associated state including spawned descendants,
goals, spawn edges, logs, dynamic tools, and agent job assignments.
- Updates app-server API docs and generated protocol schema/TypeScript
fixtures.
This commit is contained in:
Eric Traut
2026-06-10 11:22:12 -07:00
committed by GitHub
parent a1a8807e9d
commit a19d43a40a
38 changed files with 1464 additions and 88 deletions
@@ -3203,6 +3203,17 @@
],
"type": "object"
},
"ThreadDeleteParams": {
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"type": "object"
},
"ThreadForkParams": {
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using a non-empty path, the thread_id param will be ignored. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
"properties": {
@@ -4514,6 +4525,30 @@
"title": "Thread/archiveRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/delete"
],
"title": "Thread/deleteRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadDeleteParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/deleteRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -3523,6 +3523,17 @@
],
"type": "object"
},
"ThreadDeletedNotification": {
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"type": "object"
},
"ThreadGoal": {
"properties": {
"createdAt": {
@@ -5447,6 +5458,26 @@
"title": "Thread/archivedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/deleted"
],
"title": "Thread/deletedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadDeletedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/deletedNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -324,6 +324,30 @@
"title": "Thread/archiveRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"thread/delete"
],
"title": "Thread/deleteRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadDeleteParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/deleteRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -4061,6 +4085,26 @@
"title": "Thread/archivedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/deleted"
],
"title": "Thread/deletedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadDeletedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/deletedNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -16053,6 +16097,37 @@
"title": "ThreadCompactStartResponse",
"type": "object"
},
"ThreadDeleteParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadDeleteParams",
"type": "object"
},
"ThreadDeleteResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ThreadDeleteResponse",
"type": "object"
},
"ThreadDeletedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadDeletedNotification",
"type": "object"
},
"ThreadForkParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using a non-empty path, the thread_id param will be ignored. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
@@ -1263,6 +1263,30 @@
"title": "Thread/archiveRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/delete"
],
"title": "Thread/deleteRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadDeleteParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/deleteRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -11726,6 +11750,26 @@
"title": "Thread/archivedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"thread/deleted"
],
"title": "Thread/deletedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadDeletedNotification"
}
},
"required": [
"method",
"params"
],
"title": "Thread/deletedNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -13870,6 +13914,37 @@
"title": "ThreadCompactStartResponse",
"type": "object"
},
"ThreadDeleteParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadDeleteParams",
"type": "object"
},
"ThreadDeleteResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ThreadDeleteResponse",
"type": "object"
},
"ThreadDeletedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadDeletedNotification",
"type": "object"
},
"ThreadForkParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using a non-empty path, the thread_id param will be ignored. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadDeleteParams",
"type": "object"
}
@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ThreadDeleteResponse",
"type": "object"
}
@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadDeletedNotification",
"type": "object"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadDeleteParams = { threadId: string, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadDeleteResponse = Record<string, never>;
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadDeletedNotification = { threadId: string, };
@@ -374,6 +374,9 @@ export type { ThreadArchivedNotification } from "./ThreadArchivedNotification";
export type { ThreadClosedNotification } from "./ThreadClosedNotification";
export type { ThreadCompactStartParams } from "./ThreadCompactStartParams";
export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse";
export type { ThreadDeleteParams } from "./ThreadDeleteParams";
export type { ThreadDeleteResponse } from "./ThreadDeleteResponse";
export type { ThreadDeletedNotification } from "./ThreadDeletedNotification";
export type { ThreadForkParams } from "./ThreadForkParams";
export type { ThreadForkResponse } from "./ThreadForkResponse";
export type { ThreadGoal } from "./ThreadGoal";
@@ -480,6 +480,11 @@ client_request_definitions! {
serialization: thread_id(params.thread_id),
response: v2::ThreadArchiveResponse,
},
ThreadDelete => "thread/delete" {
params: v2::ThreadDeleteParams,
serialization: thread_id(params.thread_id),
response: v2::ThreadDeleteResponse,
},
ThreadUnsubscribe => "thread/unsubscribe" {
params: v2::ThreadUnsubscribeParams,
serialization: thread_id(params.thread_id),
@@ -1534,6 +1539,7 @@ server_notification_definitions! {
ThreadStarted => "thread/started" (v2::ThreadStartedNotification),
ThreadStatusChanged => "thread/status/changed" (v2::ThreadStatusChangedNotification),
ThreadArchived => "thread/archived" (v2::ThreadArchivedNotification),
ThreadDeleted => "thread/deleted" (v2::ThreadDeletedNotification),
ThreadUnarchived => "thread/unarchived" (v2::ThreadUnarchivedNotification),
ThreadClosed => "thread/closed" (v2::ThreadClosedNotification),
SkillsChanged => "skills/changed" (v2::SkillsChangedNotification),
@@ -594,6 +594,18 @@ pub struct ThreadArchiveParams {
#[ts(export_to = "v2/")]
pub struct ThreadArchiveResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadDeleteParams {
pub thread_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadDeleteResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -1381,6 +1393,13 @@ pub struct ThreadArchivedNotification {
pub thread_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadDeletedNotification {
pub thread_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]