Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 7dcc52d

Browse filesBrowse files
authored
feat: support archive (MoonshotAI#975)
1 parent e79c066 commit 7dcc52d
Copy full SHA for 7dcc52d

19 files changed

+1,183-78Lines changed: 1183 additions & 78 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎CHANGELOG.md‎

Copy file name to clipboardExpand all lines: CHANGELOG.md
+2Lines changed: 2 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Only write entries that are worth mentioning to users.
1111

1212
## Unreleased
1313

14+
- Web: Add session archive feature with auto-archive for sessions older than 15 days
15+
- Web: Add multi-select mode for bulk archive, unarchive, and delete operations
1416
- Web: Update `last_session_id` for work directory when session stream starts
1517
- Web: Fix approval request states not updating when session is interrupted or cancelled
1618
- Web: Add activity status indicator showing agent state (processing, waiting for approval, etc.)
Collapse file

‎docs/en/release-notes/changelog.md‎

Copy file name to clipboardExpand all lines: docs/en/release-notes/changelog.md
+2Lines changed: 2 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This page documents the changes in each Kimi Code CLI release.
1010
- Web: Fix IME composition issue when selecting slash commands
1111
- Web: Fix UI not clearing messages after `/clear`, `/reset`, or `/compact` commands
1212
- Core: Update context token count after compaction completes
13+
- Web: Add session archive feature with auto-archive for sessions older than 15 days
14+
- Web: Add multi-select mode for bulk archive, unarchive, and delete operations
1315

1416
## 1.8.0 (2026-02-05)
1517

Collapse file

‎docs/zh/release-notes/changelog.md‎

Copy file name to clipboardExpand all lines: docs/zh/release-notes/changelog.md
+2Lines changed: 2 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
- Web:修复选择斜杠命令时的输入法组合问题
1111
- Web:修复执行 `/clear``/reset``/compact` 命令后 UI 未清空消息的问题
1212
- Core:压缩完成后更新上下文 token 计数
13+
- Web:添加会话归档功能,自动归档超过 15 天的会话
14+
- Web:添加多选模式,支持批量归档、取消归档和删除操作
1315

1416
## 1.8.0 (2026-02-05)
1517

Collapse file

‎src/kimi_cli/web/api/sessions.py‎

Copy file name to clipboardExpand all lines: src/kimi_cli/web/api/sessions.py
+32-3Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import mimetypes
88
import os
99
import shutil
10+
import time
1011
from datetime import UTC, datetime
1112
from pathlib import Path
1213
from typing import Any, cast
@@ -41,6 +42,7 @@
4142
load_session_by_id,
4243
load_session_metadata,
4344
load_sessions_page,
45+
run_auto_archive,
4446
save_session_metadata,
4547
)
4648
from kimi_cli.wire.jsonrpc import (
@@ -234,16 +236,29 @@ async def list_sessions(
234236
limit: int = 100,
235237
offset: int = 0,
236238
q: str | None = None,
239+
archived: bool | None = None,
237240
) -> list[Session]:
238-
"""List sessions with optional pagination and search."""
241+
"""List sessions with optional pagination and search.
242+
243+
Args:
244+
limit: Maximum number of sessions to return (default 100, max 500).
245+
offset: Number of sessions to skip (default 0).
246+
q: Optional search query to filter by title or work_dir.
247+
archived: Filter by archived status.
248+
- None (default): Only return non-archived sessions.
249+
- True: Only return archived sessions.
250+
"""
239251
if limit <= 0:
240252
limit = 100
241253
if limit > 500:
242254
limit = 500
243255
if offset < 0:
244256
offset = 0
245257

246-
sessions = load_sessions_page(limit=limit, offset=offset, query=q)
258+
# Run auto-archive in background (throttled internally, runs at most once per 5 minutes)
259+
await asyncio.to_thread(run_auto_archive)
260+
261+
sessions = load_sessions_page(limit=limit, offset=offset, query=q, archived=archived)
247262
for session in sessions:
248263
session_process = runner.get_session(session.session_id)
249264
session.is_running = session_process is not None and session_process.is_running
@@ -554,7 +569,7 @@ async def update_session(
554569
request: UpdateSessionRequest,
555570
runner: KimiCLIRunner = Depends(get_runner),
556571
) -> Session:
557-
"""Update a session (e.g., rename title)."""
572+
"""Update a session (e.g., rename title or archive/unarchive)."""
558573
session = get_editable_session(session_id, runner)
559574
session_dir = session.kimi_cli_session.dir
560575

@@ -565,6 +580,20 @@ async def update_session(
565580
if request.title is not None:
566581
metadata = metadata.model_copy(update={"title": request.title})
567582

583+
# Update archived status if provided
584+
if request.archived is not None:
585+
updates: dict[str, bool | float | None] = {"archived": request.archived}
586+
if request.archived:
587+
# User manually archived: set archived_at, reset auto_archive_exempt
588+
updates["archived_at"] = time.time()
589+
updates["auto_archive_exempt"] = False
590+
else:
591+
# User manually unarchived: clear archived_at, set auto_archive_exempt
592+
# This prevents the session from being auto-archived again
593+
updates["archived_at"] = None
594+
updates["auto_archive_exempt"] = True
595+
metadata = metadata.model_copy(update=updates)
596+
568597
# Save metadata
569598
save_session_metadata(session_dir, metadata)
570599

Collapse file

‎src/kimi_cli/web/models.py‎

Copy file name to clipboardExpand all lines: src/kimi_cli/web/models.py
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,14 @@ class Session(BaseModel):
7171
status: SessionStatus | None = Field(default=None, description="Session runtime status")
7272
work_dir: str | None = Field(default=None, description="Working directory for the session")
7373
session_dir: str | None = Field(default=None, description="Session directory path")
74+
archived: bool = Field(default=False, description="Whether the session is archived")
7475

7576

7677
class UpdateSessionRequest(BaseModel):
7778
"""Update session request."""
7879

7980
title: str | None = Field(default=None, min_length=1, max_length=200)
81+
archived: bool | None = Field(default=None, description="Archive or unarchive the session")
8082

8183

8284
class GenerateTitleRequest(BaseModel):
Collapse file

‎src/kimi_cli/web/store/sessions.py‎

Copy file name to clipboardExpand all lines: src/kimi_cli/web/store/sessions.py
+97-1Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
CACHE_TTL = 5.0 # seconds - balance between freshness and performance
3636
SESSION_METADATA_FILENAME = "metadata.json"
3737

38+
# Auto-archive configuration
39+
AUTO_ARCHIVE_DAYS = 15 # Sessions older than this will be auto-archived
40+
3841
_sessions_cache: list[JointSession] | None = None
3942
_cache_timestamp: float = 0.0
4043
_sessions_index_cache: list[SessionIndexEntry] | None = None
@@ -70,6 +73,9 @@ class SessionMetadata(BaseModel):
7073
title_generated: bool = False
7174
title_generate_attempts: int = 0
7275
wire_mtime: float | None = None
76+
archived: bool = False
77+
archived_at: float | None = None
78+
auto_archive_exempt: bool = False # True if user manually unarchived, exempt from auto-archive
7379

7480

7581
@dataclass(slots=True)
@@ -230,6 +236,7 @@ def _build_kimi_session(entry: SessionIndexEntry) -> KimiCLISession:
230236

231237
def _build_joint_session(entry: SessionIndexEntry) -> JointSession:
232238
kimi_session = _build_kimi_session(entry)
239+
archived = entry.metadata.archived if entry.metadata else False
233240
return JointSession(
234241
session_id=entry.session_id,
235242
title=entry.title,
@@ -239,10 +246,33 @@ def _build_joint_session(entry: SessionIndexEntry) -> JointSession:
239246
work_dir=entry.work_dir,
240247
session_dir=str(entry.session_dir),
241248
kimi_cli_session=kimi_session,
249+
archived=archived,
242250
)
243251

244252

253+
def _should_auto_archive(last_updated: datetime, session_metadata: SessionMetadata) -> bool:
254+
"""Check if a session should be auto-archived based on age and exemption status."""
255+
# Already archived, no need to auto-archive
256+
if session_metadata.archived:
257+
return False
258+
259+
# User manually unarchived this session, exempt from auto-archive
260+
if session_metadata.auto_archive_exempt:
261+
return False
262+
263+
# Check if session is older than AUTO_ARCHIVE_DAYS
264+
now = datetime.now(tz=UTC)
265+
age_days = (now - last_updated).days
266+
return age_days >= AUTO_ARCHIVE_DAYS
267+
268+
245269
def _build_sessions_index() -> list[SessionIndexEntry]:
270+
"""Build the sessions index from disk.
271+
272+
Note: This function only reads data and does NOT perform auto-archive writes.
273+
Auto-archive is handled separately by run_auto_archive() to avoid disk writes
274+
during read operations.
275+
"""
246276
metadata = load_metadata()
247277
entries: list[SessionIndexEntry] = []
248278

@@ -277,6 +307,53 @@ def _build_sessions_index() -> list[SessionIndexEntry]:
277307
return entries
278308

279309

310+
# Track when auto-archive was last run to avoid running too frequently
311+
_last_auto_archive_time: float = 0.0
312+
AUTO_ARCHIVE_INTERVAL = 300.0 # Run auto-archive at most once every 5 minutes
313+
314+
315+
def run_auto_archive() -> int:
316+
"""Run auto-archive on old sessions.
317+
318+
This function is designed to be called periodically (e.g., on app startup,
319+
or via a background task) rather than on every read operation.
320+
321+
Returns:
322+
Number of sessions that were auto-archived.
323+
"""
324+
global _last_auto_archive_time
325+
326+
now = time.time()
327+
if now - _last_auto_archive_time < AUTO_ARCHIVE_INTERVAL:
328+
return 0
329+
330+
_last_auto_archive_time = now
331+
archived_count = 0
332+
333+
# Load fresh index (bypass cache to get current state)
334+
entries = _build_sessions_index()
335+
336+
for entry in entries:
337+
if entry.metadata is None:
338+
continue
339+
340+
if _should_auto_archive(entry.last_updated, entry.metadata):
341+
updated_metadata = entry.metadata.model_copy(
342+
update={
343+
"archived": True,
344+
"archived_at": time.time(),
345+
}
346+
)
347+
save_session_metadata(entry.session_dir, updated_metadata)
348+
archived_count += 1
349+
350+
# Invalidate cache if we archived anything
351+
if archived_count > 0:
352+
invalidate_sessions_cache()
353+
354+
return archived_count
355+
356+
280357
def _load_sessions_index_cached() -> list[SessionIndexEntry]:
281358
global _sessions_index_cache, _index_cache_timestamp
282359

@@ -327,10 +404,29 @@ def load_sessions_page(
327404
limit: int = 100,
328405
offset: int = 0,
329406
query: str | None = None,
407+
archived: bool | None = None,
330408
) -> list[JointSession]:
331-
"""Load a paginated list of sessions, optionally filtered by query."""
409+
"""Load a paginated list of sessions, optionally filtered by query and archived status.
410+
411+
Args:
412+
limit: Maximum number of sessions to return.
413+
offset: Number of sessions to skip.
414+
query: Optional search query to filter by title or work_dir.
415+
archived: Filter by archived status.
416+
- None (default): Only return non-archived sessions.
417+
- True: Only return archived sessions.
418+
- False: Only return non-archived sessions.
419+
"""
332420
entries = list(_load_sessions_index_cached())
333421

422+
# Filter by archived status
423+
if archived is None or archived is False:
424+
# Default: only non-archived sessions
425+
entries = [e for e in entries if not (e.metadata and e.metadata.archived)]
426+
else:
427+
# Only archived sessions
428+
entries = [e for e in entries if e.metadata and e.metadata.archived]
429+
334430
if query:
335431
query_text = query.strip().lower()
336432
if query_text:
Collapse file

‎web/openapi.json‎

Copy file name to clipboardExpand all lines: web/openapi.json
+35-3Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
"get": {
115115
"tags": ["sessions"],
116116
"summary": "List all sessions",
117-
"description": "List sessions with optional pagination and search.",
117+
"description": "List sessions with optional pagination and search.\n\nArgs:\n limit: Maximum number of sessions to return (default 100, max 500).\n offset: Number of sessions to skip (default 0).\n q: Optional search query to filter by title or work_dir.\n archived: Filter by archived status.\n - None (default): Only return non-archived sessions.\n - True: Only return archived sessions.",
118118
"operationId": "list_sessions_api_sessions__get",
119119
"parameters": [
120120
{
@@ -137,6 +137,15 @@
137137
"anyOf": [{ "type": "string" }, { "type": "null" }],
138138
"title": "Q"
139139
}
140+
},
141+
{
142+
"name": "archived",
143+
"in": "query",
144+
"required": false,
145+
"schema": {
146+
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
147+
"title": "Archived"
148+
}
140149
}
141150
],
142151
"responses": {
@@ -278,7 +287,7 @@
278287
"patch": {
279288
"tags": ["sessions"],
280289
"summary": "Update session",
281-
"description": "Update a session (e.g., rename title).",
290+
"description": "Update a session (e.g., rename title or archive/unarchive).",
282291
"operationId": "update_session_api_sessions__session_id__patch",
283292
"parameters": [
284293
{
@@ -712,6 +721,11 @@
712721
"work_dir": {
713722
"anyOf": [{ "type": "string" }, { "type": "null" }],
714723
"title": "Work Dir"
724+
},
725+
"create_dir": {
726+
"type": "boolean",
727+
"title": "Create Dir",
728+
"default": false
715729
}
716730
},
717731
"type": "object",
@@ -855,7 +869,14 @@
855869
"properties": {
856870
"app": {
857871
"type": "string",
858-
"enum": ["finder", "cursor", "vscode", "iterm", "terminal"],
872+
"enum": [
873+
"finder",
874+
"cursor",
875+
"vscode",
876+
"iterm",
877+
"terminal",
878+
"antigravity"
879+
],
859880
"title": "App"
860881
},
861882
"path": { "type": "string", "title": "Path" }
@@ -934,6 +955,12 @@
934955
"anyOf": [{ "type": "string" }, { "type": "null" }],
935956
"title": "Session Dir",
936957
"description": "Session directory path"
958+
},
959+
"archived": {
960+
"type": "boolean",
961+
"title": "Archived",
962+
"description": "Whether the session is archived",
963+
"default": false
937964
}
938965
},
939966
"type": "object",
@@ -1081,6 +1108,11 @@
10811108
{ "type": "null" }
10821109
],
10831110
"title": "Title"
1111+
},
1112+
"archived": {
1113+
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
1114+
"title": "Archived",
1115+
"description": "Archive or unarchive the session"
10841116
}
10851117
},
10861118
"type": "object",

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.