Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d26ba00
feat(kap-server): add page-number mode and total to GET /api/v2/sessions
liruifengv Aug 15, 2026
bd51811
feat(kap-server): add meta.updated_before filter to GET /api/v2/sessions
liruifengv Aug 15, 2026
2855ee5
feat(kap-server): add POST /api/v2/sessions:archive and :restore batc…
liruifengv Aug 15, 2026
6ae1b2f
docs(server-api): document v2 sessions page mode, total, updated_befo…
liruifengv Aug 15, 2026
5614588
fix(kap-server): deep-import workspace lifecycle symbols in the v2 se…
liruifengv Aug 17, 2026
fb0a45d
fix(kap-server): inline the live-handler lookup in the batch route
liruifengv Aug 17, 2026
10bd0cd
fix(kap-server): drive the batch hot path through getLiveSessionById
liruifengv Aug 17, 2026
0273106
refactor(kap-server): move the batch live/cold split into agent-core-v2
liruifengv Aug 17, 2026
d860352
fix(agent-core-v2): import sessionLookup relatively from coldSessionA…
liruifengv Aug 17, 2026
34b0ebf
Merge remote-tracking branch 'origin/main' into feat/v2-sessions-admin
liruifengv Aug 17, 2026
88215ed
fix(agent-core-v2): migrate the batch hot path to ISessionManager
liruifengv Aug 17, 2026
191204c
feat(kap-server): add the id,archived item projection to GET /api/v2/…
liruifengv Aug 17, 2026
19c63c5
Merge remote-tracking branch 'origin/main' into feat/v2-sessions-admin
liruifengv Aug 17, 2026
53a9e3f
fix(agent-core-v2): serialize the batch cold write against in-flight …
liruifengv Aug 17, 2026
0db9570
Merge remote-tracking branch 'origin/main' into feat/v2-sessions-admin
liruifengv Aug 17, 2026
63a8130
fix(agent-core-v2): publish SessionArchived as an Event2 class in col…
liruifengv Aug 17, 2026
46db11d
fix(agent-core-v2): serialize batch archive/restore with session life…
liruifengv Aug 17, 2026
62f2e93
fix(agent-core-v2): serialize session delete with the lifecycle chain
liruifengv Aug 17, 2026
7c7d896
fix(agent-core-v2): mirror the persisted metadata on cold archive, no…
liruifengv Aug 17, 2026
c28156a
docs(agent-core-v2): bring sessionManager comments and new tests to p…
liruifengv Aug 17, 2026
6d0ea97
fix(agent-core-v2): normalize legacy session metadata before the cold…
liruifengv Aug 17, 2026
e825bc2
fix(kap-server): serialize the v1 single-session archive with the lif…
liruifengv Aug 17, 2026
fa04d35
chore: drop changesets for internal-only protocol work
liruifengv Aug 17, 2026
568e47d
fix(agent-core-v2): encode cold-archived metadata for v1 readers
liruifengv Aug 17, 2026
aa05630
fix(agent-core-v2): serialize fork and createChild with the source se…
liruifengv Aug 17, 2026
46d88de
refactor(agent-core-v2): chain every session lifecycle method and han…
liruifengv Aug 18, 2026
2c0e315
fix(agent-core-v2): propagate failed resumes to the next settle
liruifengv Aug 18, 2026
165fdf3
fix(agent-core-v2): roll back the unannounced handle when a resume fa…
liruifengv Aug 18, 2026
b0c43b6
fix(agent-core-v2): read and migrate the legacy session-meta location…
liruifengv Aug 18, 2026
b7a19b6
Merge remote-tracking branch 'origin/main' into feat/v2-sessions-admin
liruifengv Aug 18, 2026
f18d154
fix(agent-core-v2): serialize explicit-id session creation with the l…
liruifengv Aug 18, 2026
b079259
style(kap-server): strip comments from the session routes per the no-…
liruifengv Aug 18, 2026
be79e0e
fix(agent-core-v2): serialize explicit fork and child target ids on t…
liruifengv Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cold-session-batch-archive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/agent-core-v2": patch
Comment thread
liruifengv marked this conversation as resolved.
Outdated
---

Add a cold-session archive/restore path that patches the persisted metadata document, mirrors the flipped summary into the session-index read model, and republishes the archived bus event without materializing the session, backing the new `POST /api/v2/sessions:archive` / `:restore` batch endpoints (per-item results; live sessions still run the full lifecycle).
30 changes: 28 additions & 2 deletions docs/en/reference/server-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Error codes are grouped by band:
List endpoints come in two styles:

- **Cursor style**: `before_id` / `after_id` (mutually exclusive) plus `page_size` (1–100), responding with `{ items, has_more }`. Used by the session list, message list, transcript, and others.
- **`page_token`**: an opaque token (bound to a fingerprint of the query conditions), used by `POST /api/v1/search` and `GET /api/v2/sessions`. Changing any query condition mid-pagination invalidates the token: v2 returns `40922`, search returns `40001`.
- **`page_token`**: an opaque token (bound to a fingerprint of the query conditions), used by `POST /api/v1/search` and `GET /api/v2/sessions`. Changing any query condition mid-pagination invalidates the token: v2 returns `40922`, search returns `40001`. `GET /api/v2/sessions` also offers a stateless `page` page-number mode as an alternative.

## REST endpoints

Expand Down Expand Up @@ -245,6 +245,8 @@ In-session file operations go through `POST /api/v1/sessions/{session_id}/fs:{ac
| `POST /api/v1/search` | Cross-session full-text search; `mode` is `terms` (default) or `literal` (exact substring); `page_token` pagination |
| `GET /api/v1/connections` | List live WebSocket connections |
| `GET /api/v2/sessions` | Next-generation session list, see below |
| `POST /api/v2/sessions:archive` | Batch-archive sessions, see below |
| `POST /api/v2/sessions:restore` | Batch-restore archived sessions, see below |
| `/api/v1/debug/*` | Reflection debug RPC; mounted only with `--debug-endpoints` on loopback, not a stable protocol |

### `GET /api/v2/sessions`
Expand All @@ -256,13 +258,37 @@ A next-generation session query for list views — filtering, sorting, and field
| `workspace.id` | Filter by workspace; repeatable |
| `activity.status` | Filter by activity status: `running` / `approval` / `question` / `failed` / `idle`; repeatable |
| `meta.updated_after` | Only sessions updated after this time (epoch milliseconds) |
| `meta.updated_before` | Only sessions updated before this time (epoch milliseconds) |
| `meta.archived` | `true` / `false` (default) / `all` |
| `sort` | `meta.updated_at_desc` (default) / `meta.updated_at_asc` / `meta.created_at_desc` |
| `include` | Comma-separated extra field groups; currently only `git` (branch and PR info, deduplicated per directory and cached for 60 seconds) |
| `page_size` | 1–100, default 50 |
| `page_token` | Pagination token from the previous page |
| `page` | Stateless 1-based page number; mutually exclusive with `page_token` (`40001` when combined) |

Every response item carries the `workspace`, `meta`, and `activity` groups, plus `git` when `include=git`. The page token binds the first page's query conditions; changing them mid-pagination returns `40922`.
Every response item carries the `workspace`, `meta`, and `activity` groups, plus `git` when `include=git`. Every page additionally carries `total`, the size of the filtered set. The page token binds the first page's query conditions; changing them mid-pagination returns `40922`. `page` mode is a stateless alternative for jumping to arbitrary pages: every request is an independent snapshot, no token is minted, and `next_page_token` is always `null`.

### `POST /api/v2/sessions:archive` and `POST /api/v2/sessions:restore`

Batch archive/restore for session-management views. The body is `{ "ids": ["session_..."] }` — non-empty, at most 5000 unique ids (duplicates collapse). Live sessions go through the full lifecycle; cold sessions are patched on disk without being loaded.

Only a body validation failure fails the whole request (`40001`). Otherwise the response is per-item: `data.results` keeps the input order with `{ id, ok }` or `{ id, ok: false, error }` (an unknown id reports `40401` in its own item), plus `succeeded` / `failed` counts.

```json
{
"code": 0,
"msg": "success",
"data": {
"results": [
{ "id": "session_a", "ok": true },
{ "id": "session_b", "ok": false, "error": { "code": 40401, "message": "session session_b does not exist" } }
],
"succeeded": 1,
"failed": 1
},
"request_id": "req_..."
}
```

## WebSocket protocol

Expand Down
30 changes: 28 additions & 2 deletions docs/zh/reference/server-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ HTTP 状态码几乎总是 200,业务结果以 `code` 为准。例外情况:
列表端点有两种分页风格:

- **游标式**:`before_id` / `after_id`(互斥)加 `page_size`(1–100),响应为 `{ items, has_more }`。用于会话列表、消息列表、转录等。
- **`page_token`**:不透明令牌(内部绑定了查询条件指纹),用于 `POST /api/v1/search` 与 `GET /api/v2/sessions`。翻页途中改变任何查询条件会使令牌失效:v2 返回 `40922`,search 返回 `40001`。
- **`page_token`**:不透明令牌(内部绑定了查询条件指纹),用于 `POST /api/v1/search` 与 `GET /api/v2/sessions`。翻页途中改变任何查询条件会使令牌失效:v2 返回 `40922`,search 返回 `40001`。`GET /api/v2/sessions` 另提供无状态的 `page` 页码模式作为替代。

## REST 端点

Expand Down Expand Up @@ -245,6 +245,8 @@ PTY 终端接口,仅 loopback 绑定时挂载。
| `POST /api/v1/search` | 跨会话全文搜索,`mode` 为 `terms`(默认)或 `literal`(精确子串),`page_token` 分页 |
| `GET /api/v1/connections` | 列出当前在线的 WebSocket 连接 |
| `GET /api/v2/sessions` | 新一代会话列表,见下节 |
| `POST /api/v2/sessions:archive` | 批量归档会话,见下节 |
| `POST /api/v2/sessions:restore` | 批量恢复已归档会话,见下节 |
| `/api/v1/debug/*` | 反射式调试 RPC,仅 `--debug-endpoints` 且 loopback 时挂载,不属于稳定协议 |

### `GET /api/v2/sessions`
Expand All @@ -256,13 +258,37 @@ PTY 终端接口,仅 loopback 绑定时挂载。
| `workspace.id` | 按工作区过滤,可重复 |
| `activity.status` | 按活动状态过滤:`running` / `approval` / `question` / `failed` / `idle`,可重复 |
| `meta.updated_after` | 只看该时间(epoch 毫秒)之后更新过的会话 |
| `meta.updated_before` | 只看该时间(epoch 毫秒)之前更新过的会话 |
| `meta.archived` | `true` / `false`(默认)/ `all` |
| `sort` | `meta.updated_at_desc`(默认)/ `meta.updated_at_asc` / `meta.created_at_desc` |
| `include` | 逗号分隔的附加字段组;目前支持 `git`(分支与 PR 信息,按目录去重并缓存 60 秒) |
| `page_size` | 1–100,默认 50 |
| `page_token` | 上一页返回的翻页令牌 |
| `page` | 无状态的 1 起始页码;与 `page_token` 互斥(同传返回 `40001`) |

响应每项固定包含 `workspace`、`meta`、`activity` 三组,`include=git` 时附加 `git` 组。翻页令牌绑定首页查询条件,中途改条件返回 `40922`。
响应每项固定包含 `workspace`、`meta`、`activity` 三组,`include=git` 时附加 `git` 组。每页额外携带 `total`,即过滤后的集合大小。翻页令牌绑定首页查询条件,中途改条件返回 `40922`。`page` 模式是跳页用的无状态替代:每次请求都是独立快照,不签发令牌,`next_page_token` 恒为 `null`。

### `POST /api/v2/sessions:archive` 与 `POST /api/v2/sessions:restore`

面向会话管理页的批量归档/恢复。请求体为 `{ "ids": ["session_..."] }`——非空、去重后不超过 5000 条。仍在线的会话走完整生命周期;未加载的冷会话直接改写磁盘上的元数据,不会被加载。

只有请求体校验失败才会让整个请求失败(`40001`);其余情况按条返回:`data.results` 保持输入顺序,每项为 `{ id, ok }` 或 `{ id, ok: false, error }`(不存在的 id 在自身条目里报 `40401`),并附 `succeeded` / `failed` 计数。

```json
{
"code": 0,
"msg": "success",
"data": {
"results": [
{ "id": "session_a", "ok": true },
{ "id": "session_b", "ok": false, "error": { "code": 40401, "message": "session session_b does not exist" } }
],
"succeeded": 1,
"failed": 1
},
"request_id": "req_..."
}
```

## WebSocket 协议

Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ export * from '#/app/workspaceLifecycle/sessionLookup';
export * from '#/workspace/workspaceContext/workspaceContext';
export * from '#/workspace/sessionLifecycle/sessionLifecycle';
export * from '#/workspace/sessionLifecycle/sessionLifecycleService';
export * from '#/workspace/sessionLifecycle/coldSessionArchive';
export * from '#/workspace/sessionLifecycle/internal/addressing';
export * from '#/session/externalHooks/externalHooks';
export * from '#/session/externalHooks/externalHooksService';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* `sessionLifecycle` domain — cold-session archive/restore without session
* materialization.
*
* Writes the archived flag straight into the persisted metadata document
* (`state.json` under the handler-chain scope derived through
* `internal/addressing` from the `bootstrap` sessions scope) via the
* `storage` access-pattern store, mirrors the flipped summary into the
* `sessionIndex` mirror queue (drained by the caller, never per item), and
Comment thread
liruifengv marked this conversation as resolved.
Outdated
* publishes the same `event.session.archived` bus event the live
* `ISessionLifecycleService.archive` emits through `event` — restore
* publishes nothing, matching the live `restore` (which only flips the
* flag through `ISessionMetadata`). `updatedAt` is preserved verbatim,
* mirroring `setArchived`'s `touchUpdatedAt: false` semantics, and every
* other persisted field survives the read-modify-write untouched. Call
* only for a session with no live handle in any workspace handler — a
* live session must go through the full lifecycle so its agents drain
* and its scope tears down; the direct write deliberately races a
* concurrent resume unsynchronized (the read model heals by
* reconciliation). Existence reads from `ISessionIndex`: an unknown id
* and an index entry whose document is unreadable both report
* `not_found`.
*/

import type { ServicesAccessor } from '#/_base/di/instantiation';
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
import { IEventService } from '#/app/event/event';
import { ISessionIndex, ISessionIndexMirror } from '#/app/sessionIndex/sessionIndex';
import { IAtomicDocumentStore } from '#/persistence/interface/atomicDocumentStore';
import type { SessionMeta } from '#/session/sessionMetadata/sessionMetadata';

import { sessionScopeOf, workspacePersistenceScope } from './internal/addressing';

export type ColdSessionArchiveOutcome = 'updated' | 'not_found';

export async function setColdSessionArchived(
accessor: ServicesAccessor,
sessionId: string,
archived: boolean,
): Promise<ColdSessionArchiveOutcome> {
const summary = await accessor.get(ISessionIndex).get(sessionId);
if (summary === undefined) return 'not_found';
const docs = accessor.get(IAtomicDocumentStore);
const metaScope = sessionScopeOf(
workspacePersistenceScope(
accessor.get(IBootstrapService).scope('sessions'),
summary.workspaceId,
),
sessionId,
);
let persisted: SessionMeta | undefined;
try {
persisted = await docs.get<SessionMeta>(metaScope, 'state.json');
} catch {
persisted = undefined;
Comment thread
liruifengv marked this conversation as resolved.
Outdated
}
if (persisted === undefined) return 'not_found';
const archivedAt = archived ? Date.now() : undefined;
await docs.set(metaScope, 'state.json', { ...persisted, archived, archivedAt });
Comment thread
liruifengv marked this conversation as resolved.
Outdated
accessor.get(ISessionIndexMirror).record({ ...summary, archived, archivedAt });
Comment thread
liruifengv marked this conversation as resolved.
Outdated
if (archived) {
accessor
.get(IEventService)
.publish({ type: 'event.session.archived', payload: { sessionId } });
}
return 'updated';
}
Loading
Loading