klaude-code 1.2.6__py3-none-any.whl → 1.8.0__py3-none-any.whl
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.
- klaude_code/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
klaude_code/session/session.py
CHANGED
|
@@ -1,13 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import time
|
|
3
5
|
import uuid
|
|
4
6
|
from collections.abc import Iterable, Sequence
|
|
5
7
|
from pathlib import Path
|
|
6
|
-
from typing import
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, PrivateAttr, ValidationError
|
|
11
|
+
|
|
12
|
+
from klaude_code.protocol import events, llm_param, model, tools
|
|
13
|
+
from klaude_code.session.store import JsonlSessionStore, ProjectPaths, build_meta_snapshot
|
|
14
|
+
|
|
15
|
+
_DEFAULT_STORES: dict[str, JsonlSessionStore] = {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _project_key_from_cwd() -> str:
|
|
19
|
+
return str(Path.cwd()).strip("/").replace("/", "-")
|
|
7
20
|
|
|
8
|
-
from pydantic import BaseModel, Field
|
|
9
21
|
|
|
10
|
-
|
|
22
|
+
def _read_json_dict(path: Path) -> dict[str, Any] | None:
|
|
23
|
+
try:
|
|
24
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
25
|
+
except (json.JSONDecodeError, OSError):
|
|
26
|
+
return None
|
|
27
|
+
if not isinstance(raw, dict):
|
|
28
|
+
return None
|
|
29
|
+
return cast(dict[str, Any], raw)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_default_store() -> JsonlSessionStore:
|
|
33
|
+
project_key = _project_key_from_cwd()
|
|
34
|
+
store = _DEFAULT_STORES.get(project_key)
|
|
35
|
+
if store is None:
|
|
36
|
+
store = JsonlSessionStore(project_key=project_key)
|
|
37
|
+
_DEFAULT_STORES[project_key] = store
|
|
38
|
+
return store
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def close_default_store() -> None:
|
|
42
|
+
stores = list(_DEFAULT_STORES.values())
|
|
43
|
+
_DEFAULT_STORES.clear()
|
|
44
|
+
for store in stores:
|
|
45
|
+
await store.aclose()
|
|
11
46
|
|
|
12
47
|
|
|
13
48
|
class Session(BaseModel):
|
|
@@ -15,264 +50,272 @@ class Session(BaseModel):
|
|
|
15
50
|
work_dir: Path
|
|
16
51
|
conversation_history: list[model.ConversationItem] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
17
52
|
sub_agent_state: model.SubAgentState | None = None
|
|
18
|
-
|
|
19
|
-
file_tracker: dict[str, float] = Field(default_factory=dict)
|
|
20
|
-
# Todo list for the session
|
|
53
|
+
file_tracker: dict[str, model.FileStatus] = Field(default_factory=dict)
|
|
21
54
|
todos: list[model.TodoItem] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
22
|
-
# Messages count, redundant state for performance optimization to avoid reading entire jsonl file
|
|
23
|
-
messages_count: int = Field(default=0)
|
|
24
|
-
# Model name used for this session
|
|
25
|
-
# Used in list method SessionMetaBrief
|
|
26
55
|
model_name: str | None = None
|
|
27
|
-
|
|
56
|
+
|
|
57
|
+
model_config_name: str | None = None
|
|
58
|
+
model_thinking: llm_param.Thinking | None = None
|
|
28
59
|
created_at: float = Field(default_factory=lambda: time.time())
|
|
29
60
|
updated_at: float = Field(default_factory=lambda: time.time())
|
|
30
|
-
|
|
31
|
-
# Reminder flags
|
|
32
|
-
loaded_memory: list[str] = Field(default_factory=list)
|
|
33
61
|
need_todo_empty_cooldown_counter: int = Field(exclude=True, default=0)
|
|
34
62
|
need_todo_not_used_cooldown_counter: int = Field(exclude=True, default=0)
|
|
35
63
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
_messages_count_cache: int | None = PrivateAttr(default=None)
|
|
65
|
+
_user_messages_cache: list[str] | None = PrivateAttr(default=None)
|
|
66
|
+
_store: JsonlSessionStore = PrivateAttr(default_factory=get_default_store)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def messages_count(self) -> int:
|
|
70
|
+
"""Count of user, assistant messages, and tool calls in conversation history."""
|
|
71
|
+
if self._messages_count_cache is None:
|
|
72
|
+
self._messages_count_cache = sum(
|
|
73
|
+
1
|
|
74
|
+
for it in self.conversation_history
|
|
75
|
+
if isinstance(it, (model.UserMessageItem, model.AssistantMessageItem, model.ToolCallItem))
|
|
76
|
+
)
|
|
77
|
+
return self._messages_count_cache
|
|
78
|
+
|
|
79
|
+
def _invalidate_messages_count_cache(self) -> None:
|
|
80
|
+
self._messages_count_cache = None
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def user_messages(self) -> list[str]:
|
|
84
|
+
"""All user message contents in this session.
|
|
85
|
+
|
|
86
|
+
This is used for session selection UI and search, and is also persisted
|
|
87
|
+
in meta.json to avoid scanning events.jsonl for every session.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
if self._user_messages_cache is None:
|
|
91
|
+
self._user_messages_cache = [
|
|
92
|
+
it.content for it in self.conversation_history if isinstance(it, model.UserMessageItem) and it.content
|
|
93
|
+
]
|
|
94
|
+
return self._user_messages_cache
|
|
56
95
|
|
|
57
96
|
@staticmethod
|
|
58
97
|
def _project_key() -> str:
|
|
59
|
-
|
|
60
|
-
return str(Path.cwd()).strip("/").replace("/", "-")
|
|
98
|
+
return _project_key_from_cwd()
|
|
61
99
|
|
|
62
100
|
@classmethod
|
|
63
|
-
def
|
|
64
|
-
return
|
|
101
|
+
def paths(cls) -> ProjectPaths:
|
|
102
|
+
return get_default_store().paths
|
|
65
103
|
|
|
66
104
|
@classmethod
|
|
67
|
-
def
|
|
68
|
-
|
|
105
|
+
def exists(cls, id: str) -> bool:
|
|
106
|
+
"""Return True if a persisted session exists for the current project."""
|
|
69
107
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return cls._base_dir() / "messages"
|
|
108
|
+
paths = cls.paths()
|
|
109
|
+
return paths.meta_file(id).exists() or paths.events_file(id).exists()
|
|
73
110
|
|
|
74
111
|
@classmethod
|
|
75
|
-
def
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
prefix = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(self.created_at))
|
|
80
|
-
return self._sessions_dir() / f"{prefix}-{self.id}.json"
|
|
81
|
-
|
|
82
|
-
def _messages_file(self) -> Path:
|
|
83
|
-
prefix = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(self.created_at))
|
|
84
|
-
return self._messages_dir() / f"{prefix}-{self.id}.jsonl"
|
|
112
|
+
def create(cls, id: str | None = None, *, work_dir: Path | None = None) -> Session:
|
|
113
|
+
session = Session(id=id or uuid.uuid4().hex, work_dir=work_dir or Path.cwd())
|
|
114
|
+
session._store = get_default_store()
|
|
115
|
+
return session
|
|
85
116
|
|
|
86
117
|
@classmethod
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
118
|
+
def load_meta(cls, id: str) -> Session:
|
|
119
|
+
store = get_default_store()
|
|
120
|
+
raw = store.load_meta(id)
|
|
121
|
+
if raw is None:
|
|
122
|
+
session = Session(id=id, work_dir=Path.cwd())
|
|
123
|
+
session._store = store
|
|
124
|
+
return session
|
|
125
|
+
|
|
126
|
+
work_dir_str = raw.get("work_dir")
|
|
127
|
+
if not isinstance(work_dir_str, str) or not work_dir_str:
|
|
128
|
+
work_dir_str = str(Path.cwd())
|
|
129
|
+
|
|
130
|
+
sub_agent_state_raw = raw.get("sub_agent_state")
|
|
131
|
+
sub_agent_state = (
|
|
132
|
+
model.SubAgentState.model_validate(sub_agent_state_raw) if isinstance(sub_agent_state_raw, dict) else None
|
|
94
133
|
)
|
|
95
|
-
if not session_candidates:
|
|
96
|
-
# No existing session; create a new one
|
|
97
|
-
return Session(id=id, work_dir=Path.cwd())
|
|
98
|
-
session_path = session_candidates[0]
|
|
99
134
|
|
|
100
|
-
|
|
135
|
+
file_tracker_raw = raw.get("file_tracker")
|
|
136
|
+
file_tracker: dict[str, model.FileStatus] = {}
|
|
137
|
+
if isinstance(file_tracker_raw, dict):
|
|
138
|
+
for k, v in cast(dict[object, object], file_tracker_raw).items():
|
|
139
|
+
if isinstance(k, str) and isinstance(v, dict):
|
|
140
|
+
try:
|
|
141
|
+
file_tracker[k] = model.FileStatus.model_validate(v)
|
|
142
|
+
except ValidationError:
|
|
143
|
+
continue
|
|
101
144
|
|
|
102
|
-
|
|
103
|
-
|
|
145
|
+
todos_raw = raw.get("todos")
|
|
146
|
+
todos: list[model.TodoItem] = []
|
|
147
|
+
if isinstance(todos_raw, list):
|
|
148
|
+
for todo_raw in cast(list[object], todos_raw):
|
|
149
|
+
if not isinstance(todo_raw, dict):
|
|
150
|
+
continue
|
|
151
|
+
try:
|
|
152
|
+
todos.append(model.TodoItem.model_validate(todo_raw))
|
|
153
|
+
except ValidationError:
|
|
154
|
+
continue
|
|
104
155
|
|
|
105
|
-
sub_agent_state_raw = raw.get("sub_agent_state")
|
|
106
|
-
sub_agent_state = model.SubAgentState(**sub_agent_state_raw) if sub_agent_state_raw else None
|
|
107
|
-
file_tracker = dict(raw.get("file_tracker", {}))
|
|
108
|
-
todos: list[model.TodoItem] = [model.TodoItem(**item) for item in raw.get("todos", [])]
|
|
109
|
-
loaded_memory = list(raw.get("loaded_memory", []))
|
|
110
156
|
created_at = float(raw.get("created_at", time.time()))
|
|
111
157
|
updated_at = float(raw.get("updated_at", created_at))
|
|
112
|
-
|
|
113
|
-
|
|
158
|
+
model_name = raw.get("model_name") if isinstance(raw.get("model_name"), str) else None
|
|
159
|
+
model_config_name = raw.get("model_config_name") if isinstance(raw.get("model_config_name"), str) else None
|
|
114
160
|
|
|
115
|
-
|
|
161
|
+
model_thinking_raw = raw.get("model_thinking")
|
|
162
|
+
model_thinking = (
|
|
163
|
+
llm_param.Thinking.model_validate(model_thinking_raw) if isinstance(model_thinking_raw, dict) else None
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
session = Session(
|
|
116
167
|
id=id,
|
|
117
168
|
work_dir=Path(work_dir_str),
|
|
118
169
|
sub_agent_state=sub_agent_state,
|
|
119
170
|
file_tracker=file_tracker,
|
|
120
171
|
todos=todos,
|
|
121
|
-
loaded_memory=loaded_memory,
|
|
122
172
|
created_at=created_at,
|
|
123
173
|
updated_at=updated_at,
|
|
124
|
-
messages_count=messages_count,
|
|
125
174
|
model_name=model_name,
|
|
175
|
+
model_config_name=model_config_name,
|
|
176
|
+
model_thinking=model_thinking,
|
|
126
177
|
)
|
|
178
|
+
session._store = store
|
|
179
|
+
return session
|
|
127
180
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
)
|
|
136
|
-
if msg_candidates:
|
|
137
|
-
messages_path = msg_candidates[0]
|
|
138
|
-
history: list[model.ConversationItem] = []
|
|
139
|
-
for line in messages_path.read_text().splitlines():
|
|
140
|
-
line = line.strip()
|
|
141
|
-
if not line:
|
|
142
|
-
continue
|
|
143
|
-
try:
|
|
144
|
-
obj = json.loads(line)
|
|
145
|
-
t = obj.get("type")
|
|
146
|
-
data = obj.get("data", {})
|
|
147
|
-
cls_type = cls._TypeMap.get(t or "")
|
|
148
|
-
if cls_type is None:
|
|
149
|
-
continue
|
|
150
|
-
item = cls_type(**data)
|
|
151
|
-
# pyright: ignore[reportAssignmentType]
|
|
152
|
-
history.append(item) # type: ignore[arg-type]
|
|
153
|
-
except Exception:
|
|
154
|
-
# Best-effort load; skip malformed lines
|
|
155
|
-
continue
|
|
156
|
-
sess.conversation_history = history
|
|
157
|
-
# Update messages count based on loaded history (only UserMessageItem and AssistantMessageItem)
|
|
158
|
-
sess.messages_count = sum(
|
|
159
|
-
1 for it in history if isinstance(it, (model.UserMessageItem, model.AssistantMessageItem))
|
|
160
|
-
)
|
|
181
|
+
@classmethod
|
|
182
|
+
def load(cls, id: str) -> Session:
|
|
183
|
+
store = get_default_store()
|
|
184
|
+
session = cls.load_meta(id)
|
|
185
|
+
session._store = store
|
|
186
|
+
session.conversation_history = store.load_history(id)
|
|
187
|
+
return session
|
|
161
188
|
|
|
162
|
-
|
|
189
|
+
def append_history(self, items: Sequence[model.ConversationItem]) -> None:
|
|
190
|
+
if not items:
|
|
191
|
+
return
|
|
163
192
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
193
|
+
self.conversation_history.extend(items)
|
|
194
|
+
self._invalidate_messages_count_cache()
|
|
195
|
+
|
|
196
|
+
new_user_messages = [
|
|
197
|
+
it.content for it in items if isinstance(it, model.UserMessageItem) and it.content
|
|
198
|
+
]
|
|
199
|
+
if new_user_messages:
|
|
200
|
+
if self._user_messages_cache is None:
|
|
201
|
+
# Build from full history once to ensure correctness when resuming older sessions.
|
|
202
|
+
self._user_messages_cache = [
|
|
203
|
+
it.content for it in self.conversation_history if isinstance(it, model.UserMessageItem) and it.content
|
|
204
|
+
]
|
|
205
|
+
else:
|
|
206
|
+
self._user_messages_cache.extend(new_user_messages)
|
|
170
207
|
|
|
171
|
-
# Persist session metadata (excluding conversation history)
|
|
172
|
-
# Update timestamps
|
|
173
208
|
if self.created_at <= 0:
|
|
174
209
|
self.created_at = time.time()
|
|
175
210
|
self.updated_at = time.time()
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
211
|
+
|
|
212
|
+
meta = build_meta_snapshot(
|
|
213
|
+
session_id=self.id,
|
|
214
|
+
work_dir=self.work_dir,
|
|
215
|
+
sub_agent_state=self.sub_agent_state,
|
|
216
|
+
file_tracker=self.file_tracker,
|
|
217
|
+
todos=list(self.todos),
|
|
218
|
+
user_messages=self.user_messages,
|
|
219
|
+
created_at=self.created_at,
|
|
220
|
+
updated_at=self.updated_at,
|
|
221
|
+
messages_count=self.messages_count,
|
|
222
|
+
model_name=self.model_name,
|
|
223
|
+
model_config_name=self.model_config_name,
|
|
224
|
+
model_thinking=self.model_thinking,
|
|
225
|
+
)
|
|
226
|
+
self._store.append_and_flush(session_id=self.id, items=items, meta=meta)
|
|
227
|
+
|
|
228
|
+
def fork(self, *, new_id: str | None = None, until_index: int | None = None) -> Session:
|
|
229
|
+
"""Create a new session as a fork of the current session.
|
|
230
|
+
|
|
231
|
+
The forked session copies metadata and conversation history, but does not
|
|
232
|
+
modify the current session.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
new_id: Optional ID for the forked session.
|
|
236
|
+
until_index: If provided, only copy conversation history up to (but not including) this index.
|
|
237
|
+
If None, copy all history.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
forked = Session.create(id=new_id, work_dir=self.work_dir)
|
|
241
|
+
|
|
242
|
+
forked.sub_agent_state = None
|
|
243
|
+
forked.model_name = self.model_name
|
|
244
|
+
forked.model_config_name = self.model_config_name
|
|
245
|
+
forked.model_thinking = self.model_thinking.model_copy(deep=True) if self.model_thinking is not None else None
|
|
246
|
+
forked.file_tracker = {k: v.model_copy(deep=True) for k, v in self.file_tracker.items()}
|
|
247
|
+
forked.todos = [todo.model_copy(deep=True) for todo in self.todos]
|
|
248
|
+
|
|
249
|
+
history_to_copy = (
|
|
250
|
+
self.conversation_history[:until_index] if until_index is not None else self.conversation_history
|
|
196
251
|
)
|
|
252
|
+
items = [it.model_copy(deep=True) for it in history_to_copy]
|
|
253
|
+
if items:
|
|
254
|
+
forked.append_history(items)
|
|
255
|
+
|
|
256
|
+
return forked
|
|
197
257
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
messages_dir.mkdir(parents=True, exist_ok=True)
|
|
201
|
-
mpath = self._messages_file()
|
|
202
|
-
|
|
203
|
-
with mpath.open("a", encoding="utf-8") as f:
|
|
204
|
-
for it in items:
|
|
205
|
-
# Serialize with explicit type tag for reliable load
|
|
206
|
-
t = it.__class__.__name__
|
|
207
|
-
data = it.model_dump(mode="json")
|
|
208
|
-
f.write(json.dumps({"type": t, "data": data}, ensure_ascii=False))
|
|
209
|
-
f.write("\n")
|
|
210
|
-
# Refresh metadata timestamp after history change
|
|
211
|
-
self.save()
|
|
258
|
+
async def wait_for_flush(self) -> None:
|
|
259
|
+
await self._store.wait_for_flush(self.id)
|
|
212
260
|
|
|
213
261
|
@classmethod
|
|
214
262
|
def most_recent_session_id(cls) -> str | None:
|
|
215
|
-
|
|
216
|
-
if not sessions_dir.exists():
|
|
217
|
-
return None
|
|
263
|
+
store = get_default_store()
|
|
218
264
|
latest_id: str | None = None
|
|
219
265
|
latest_ts: float = -1.0
|
|
220
|
-
for
|
|
266
|
+
for meta_path in store.iter_meta_files():
|
|
267
|
+
data = _read_json_dict(meta_path)
|
|
268
|
+
if data is None:
|
|
269
|
+
continue
|
|
270
|
+
if data.get("sub_agent_state") is not None:
|
|
271
|
+
continue
|
|
272
|
+
sid = str(data.get("id", meta_path.parent.name))
|
|
221
273
|
try:
|
|
222
|
-
data = json.loads(p.read_text())
|
|
223
|
-
# Filter out sub-agent sessions
|
|
224
|
-
if data.get("sub_agent_state", None) is not None:
|
|
225
|
-
continue
|
|
226
|
-
sid = str(data.get("id", p.stem))
|
|
227
274
|
ts = float(data.get("updated_at", 0.0))
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
except Exception:
|
|
234
|
-
continue
|
|
275
|
+
except (TypeError, ValueError):
|
|
276
|
+
ts = meta_path.stat().st_mtime
|
|
277
|
+
if ts > latest_ts:
|
|
278
|
+
latest_ts = ts
|
|
279
|
+
latest_id = sid
|
|
235
280
|
return latest_id
|
|
236
281
|
|
|
237
282
|
def need_turn_start(self, prev_item: model.ConversationItem | None, item: model.ConversationItem) -> bool:
|
|
238
|
-
# Emit TurnStartEvent when a new turn starts to show an empty line in replay history
|
|
239
283
|
if not isinstance(
|
|
240
284
|
item,
|
|
241
|
-
model.
|
|
285
|
+
model.ReasoningTextItem | model.AssistantMessageItem | model.ToolCallItem,
|
|
242
286
|
):
|
|
243
287
|
return False
|
|
244
288
|
if prev_item is None:
|
|
245
289
|
return True
|
|
246
|
-
|
|
247
|
-
prev_item,
|
|
248
|
-
model.UserMessageItem | model.ToolResultItem | model.DeveloperMessageItem,
|
|
249
|
-
):
|
|
250
|
-
return True
|
|
251
|
-
return False
|
|
290
|
+
return isinstance(prev_item, model.UserMessageItem | model.ToolResultItem | model.DeveloperMessageItem)
|
|
252
291
|
|
|
253
292
|
def get_history_item(self) -> Iterable[events.HistoryItemEvent]:
|
|
293
|
+
seen_sub_agent_sessions: set[str] = set()
|
|
254
294
|
prev_item: model.ConversationItem | None = None
|
|
295
|
+
last_assistant_content: str = ""
|
|
296
|
+
report_back_result: str | None = None
|
|
297
|
+
yield events.TaskStartEvent(session_id=self.id, sub_agent_state=self.sub_agent_state)
|
|
255
298
|
for it in self.conversation_history:
|
|
256
299
|
if self.need_turn_start(prev_item, it):
|
|
257
|
-
yield events.TurnStartEvent(
|
|
258
|
-
session_id=self.id,
|
|
259
|
-
)
|
|
300
|
+
yield events.TurnStartEvent(session_id=self.id)
|
|
260
301
|
match it:
|
|
261
302
|
case model.AssistantMessageItem() as am:
|
|
262
303
|
content = am.content or ""
|
|
304
|
+
last_assistant_content = content
|
|
263
305
|
yield events.AssistantMessageEvent(
|
|
264
306
|
content=content,
|
|
265
307
|
response_id=am.response_id,
|
|
266
308
|
session_id=self.id,
|
|
267
309
|
)
|
|
268
310
|
case model.ToolCallItem() as tc:
|
|
311
|
+
if tc.name == tools.REPORT_BACK:
|
|
312
|
+
report_back_result = tc.arguments
|
|
269
313
|
yield events.ToolCallEvent(
|
|
270
314
|
tool_call_id=tc.call_id,
|
|
271
315
|
tool_name=tc.name,
|
|
272
316
|
arguments=tc.arguments,
|
|
273
317
|
response_id=tc.response_id,
|
|
274
318
|
session_id=self.id,
|
|
275
|
-
is_replay=True,
|
|
276
319
|
)
|
|
277
320
|
case model.ToolResultItem() as tr:
|
|
278
321
|
yield events.ToolResultEvent(
|
|
@@ -282,129 +325,121 @@ class Session(BaseModel):
|
|
|
282
325
|
ui_extra=tr.ui_extra,
|
|
283
326
|
session_id=self.id,
|
|
284
327
|
status=tr.status,
|
|
285
|
-
|
|
328
|
+
task_metadata=tr.task_metadata,
|
|
286
329
|
)
|
|
287
|
-
|
|
330
|
+
yield from self._iter_sub_agent_history(tr, seen_sub_agent_sessions)
|
|
288
331
|
case model.UserMessageItem() as um:
|
|
289
|
-
yield events.UserMessageEvent(
|
|
290
|
-
content=um.content or "",
|
|
291
|
-
session_id=self.id,
|
|
292
|
-
)
|
|
332
|
+
yield events.UserMessageEvent(content=um.content or "", session_id=self.id, images=um.images)
|
|
293
333
|
case model.ReasoningTextItem() as ri:
|
|
294
|
-
yield events.ThinkingEvent(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
)
|
|
298
|
-
case model.ResponseMetadataItem() as mt:
|
|
299
|
-
yield events.ResponseMetadataEvent(
|
|
300
|
-
session_id=self.id,
|
|
301
|
-
metadata=mt,
|
|
302
|
-
)
|
|
334
|
+
yield events.ThinkingEvent(content=ri.content, session_id=self.id)
|
|
335
|
+
case model.TaskMetadataItem() as mt:
|
|
336
|
+
yield events.TaskMetadataEvent(session_id=self.id, metadata=mt)
|
|
303
337
|
case model.InterruptItem():
|
|
304
|
-
yield events.InterruptEvent(
|
|
305
|
-
session_id=self.id,
|
|
306
|
-
)
|
|
338
|
+
yield events.InterruptEvent(session_id=self.id)
|
|
307
339
|
case model.DeveloperMessageItem() as dm:
|
|
308
|
-
yield events.DeveloperMessageEvent(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
)
|
|
340
|
+
yield events.DeveloperMessageEvent(session_id=self.id, item=dm)
|
|
341
|
+
case model.StreamErrorItem() as se:
|
|
342
|
+
yield events.ErrorEvent(error_message=se.error, can_retry=False, session_id=self.id)
|
|
312
343
|
case _:
|
|
313
344
|
continue
|
|
314
345
|
prev_item = it
|
|
315
346
|
|
|
347
|
+
has_structured_output = report_back_result is not None
|
|
348
|
+
task_result = report_back_result if has_structured_output else last_assistant_content
|
|
349
|
+
yield events.TaskFinishEvent(
|
|
350
|
+
session_id=self.id, task_result=task_result, has_structured_output=has_structured_output
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def _iter_sub_agent_history(
|
|
354
|
+
self, tool_result: model.ToolResultItem, seen_sub_agent_sessions: set[str]
|
|
355
|
+
) -> Iterable[events.HistoryItemEvent]:
|
|
356
|
+
ui_extra = tool_result.ui_extra
|
|
357
|
+
if not isinstance(ui_extra, model.SessionIdUIExtra):
|
|
358
|
+
return
|
|
359
|
+
session_id = ui_extra.session_id
|
|
360
|
+
if not session_id or session_id == self.id:
|
|
361
|
+
return
|
|
362
|
+
if session_id in seen_sub_agent_sessions:
|
|
363
|
+
return
|
|
364
|
+
seen_sub_agent_sessions.add(session_id)
|
|
365
|
+
try:
|
|
366
|
+
sub_session = Session.load(session_id)
|
|
367
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
368
|
+
return
|
|
369
|
+
yield from sub_session.get_history_item()
|
|
370
|
+
|
|
316
371
|
class SessionMetaBrief(BaseModel):
|
|
317
372
|
id: str
|
|
318
373
|
created_at: float
|
|
319
374
|
updated_at: float
|
|
320
375
|
work_dir: str
|
|
321
376
|
path: str
|
|
322
|
-
|
|
323
|
-
messages_count: int = -1
|
|
377
|
+
user_messages: list[str] = []
|
|
378
|
+
messages_count: int = -1
|
|
324
379
|
model_name: str | None = None
|
|
325
380
|
|
|
326
381
|
@classmethod
|
|
327
382
|
def list_sessions(cls) -> list[SessionMetaBrief]:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
Returns a list of dicts with keys: id, created_at, updated_at, work_dir, path.
|
|
331
|
-
Sorted by updated_at descending.
|
|
332
|
-
"""
|
|
333
|
-
sessions_dir = cls._sessions_dir()
|
|
334
|
-
if not sessions_dir.exists():
|
|
335
|
-
return []
|
|
336
|
-
|
|
337
|
-
def _get_first_user_message(session_id: str, created_at: float) -> str | None:
|
|
338
|
-
"""Get the first user message from the session's jsonl file."""
|
|
339
|
-
messages_dir = cls._messages_dir()
|
|
340
|
-
if not messages_dir.exists():
|
|
341
|
-
return None
|
|
342
|
-
|
|
343
|
-
# Find the messages file for this session
|
|
344
|
-
prefix = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(created_at))
|
|
345
|
-
msg_file = messages_dir / f"{prefix}-{session_id}.jsonl"
|
|
346
|
-
|
|
347
|
-
if not msg_file.exists():
|
|
348
|
-
# Try to find by pattern if exact file doesn't exist
|
|
349
|
-
msg_candidates = sorted(
|
|
350
|
-
messages_dir.glob(f"*-{session_id}.jsonl"),
|
|
351
|
-
key=lambda p: p.stat().st_mtime,
|
|
352
|
-
reverse=True,
|
|
353
|
-
)
|
|
354
|
-
if not msg_candidates:
|
|
355
|
-
return None
|
|
356
|
-
msg_file = msg_candidates[0]
|
|
383
|
+
store = get_default_store()
|
|
357
384
|
|
|
385
|
+
def _get_user_messages(session_id: str) -> list[str]:
|
|
386
|
+
events_path = store.paths.events_file(session_id)
|
|
387
|
+
if not events_path.exists():
|
|
388
|
+
return []
|
|
389
|
+
messages: list[str] = []
|
|
358
390
|
try:
|
|
359
|
-
for line in
|
|
360
|
-
|
|
361
|
-
if not
|
|
391
|
+
for line in events_path.read_text(encoding="utf-8").splitlines():
|
|
392
|
+
obj_raw = json.loads(line)
|
|
393
|
+
if not isinstance(obj_raw, dict):
|
|
362
394
|
continue
|
|
363
|
-
obj =
|
|
364
|
-
if obj.get("type")
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
395
|
+
obj = cast(dict[str, Any], obj_raw)
|
|
396
|
+
if obj.get("type") != "UserMessageItem":
|
|
397
|
+
continue
|
|
398
|
+
data_raw = obj.get("data")
|
|
399
|
+
if not isinstance(data_raw, dict):
|
|
400
|
+
continue
|
|
401
|
+
data = cast(dict[str, Any], data_raw)
|
|
402
|
+
content = data.get("content")
|
|
403
|
+
if isinstance(content, str):
|
|
404
|
+
messages.append(content)
|
|
405
|
+
except (OSError, json.JSONDecodeError):
|
|
406
|
+
pass
|
|
407
|
+
return messages
|
|
408
|
+
|
|
409
|
+
def _maybe_backfill_user_messages(*, meta_path: Path, meta: dict[str, Any], user_messages: list[str]) -> None:
|
|
410
|
+
if isinstance(meta.get("user_messages"), list):
|
|
411
|
+
return
|
|
412
|
+
meta["user_messages"] = user_messages
|
|
413
|
+
try:
|
|
414
|
+
tmp_path = meta_path.with_suffix(".json.tmp")
|
|
415
|
+
tmp_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
416
|
+
tmp_path.replace(meta_path)
|
|
417
|
+
except OSError:
|
|
418
|
+
return
|
|
384
419
|
|
|
385
420
|
items: list[Session.SessionMetaBrief] = []
|
|
386
|
-
for
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
except Exception:
|
|
390
|
-
# Skip unreadable files
|
|
421
|
+
for meta_path in store.iter_meta_files():
|
|
422
|
+
data = _read_json_dict(meta_path)
|
|
423
|
+
if data is None:
|
|
391
424
|
continue
|
|
392
|
-
|
|
393
|
-
if data.get("sub_agent_state", None) is not None:
|
|
425
|
+
if data.get("sub_agent_state") is not None:
|
|
394
426
|
continue
|
|
395
|
-
sid = str(data.get("id", p.stem))
|
|
396
|
-
created = float(data.get("created_at", p.stat().st_mtime))
|
|
397
|
-
updated = float(data.get("updated_at", p.stat().st_mtime))
|
|
398
|
-
work_dir = str(data.get("work_dir", ""))
|
|
399
427
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
messages_count = int(data.get("messages_count", -1)) # -1 indicates N/A
|
|
428
|
+
sid = str(data.get("id", meta_path.parent.name))
|
|
429
|
+
created = float(data.get("created_at", meta_path.stat().st_mtime))
|
|
430
|
+
updated = float(data.get("updated_at", meta_path.stat().st_mtime))
|
|
431
|
+
work_dir = str(data.get("work_dir", ""))
|
|
405
432
|
|
|
406
|
-
|
|
407
|
-
|
|
433
|
+
user_messages_raw = data.get("user_messages")
|
|
434
|
+
if isinstance(user_messages_raw, list) and all(
|
|
435
|
+
isinstance(m, str) for m in cast(list[object], user_messages_raw)
|
|
436
|
+
):
|
|
437
|
+
user_messages = cast(list[str], user_messages_raw)
|
|
438
|
+
else:
|
|
439
|
+
user_messages = _get_user_messages(sid)
|
|
440
|
+
_maybe_backfill_user_messages(meta_path=meta_path, meta=data, user_messages=user_messages)
|
|
441
|
+
messages_count = int(data.get("messages_count", -1))
|
|
442
|
+
model_name = data.get("model_name") if isinstance(data.get("model_name"), str) else None
|
|
408
443
|
|
|
409
444
|
items.append(
|
|
410
445
|
Session.SessionMetaBrief(
|
|
@@ -412,63 +447,39 @@ class Session(BaseModel):
|
|
|
412
447
|
created_at=created,
|
|
413
448
|
updated_at=updated,
|
|
414
449
|
work_dir=work_dir,
|
|
415
|
-
path=str(
|
|
416
|
-
|
|
450
|
+
path=str(meta_path),
|
|
451
|
+
user_messages=user_messages,
|
|
417
452
|
messages_count=messages_count,
|
|
418
453
|
model_name=model_name,
|
|
419
454
|
)
|
|
420
455
|
)
|
|
421
|
-
|
|
456
|
+
|
|
422
457
|
items.sort(key=lambda d: d.updated_at, reverse=True)
|
|
423
458
|
return items
|
|
424
459
|
|
|
425
460
|
@classmethod
|
|
426
461
|
def clean_small_sessions(cls, min_messages: int = 5) -> int:
|
|
427
|
-
"""Remove sessions with fewer than min_messages messages.
|
|
428
|
-
|
|
429
|
-
Returns the number of sessions deleted.
|
|
430
|
-
"""
|
|
431
462
|
sessions = cls.list_sessions()
|
|
432
463
|
deleted_count = 0
|
|
433
|
-
|
|
464
|
+
store = get_default_store()
|
|
434
465
|
for session_meta in sessions:
|
|
435
|
-
# Skip sessions with unknown message count
|
|
436
466
|
if session_meta.messages_count < 0:
|
|
437
467
|
continue
|
|
438
468
|
if session_meta.messages_count < min_messages:
|
|
439
|
-
|
|
469
|
+
store.delete_session(session_meta.id)
|
|
440
470
|
deleted_count += 1
|
|
441
|
-
|
|
442
471
|
return deleted_count
|
|
443
472
|
|
|
444
473
|
@classmethod
|
|
445
474
|
def clean_all_sessions(cls) -> int:
|
|
446
|
-
"""Remove all sessions for the current project.
|
|
447
|
-
|
|
448
|
-
Returns the number of sessions deleted.
|
|
449
|
-
"""
|
|
450
475
|
sessions = cls.list_sessions()
|
|
451
476
|
deleted_count = 0
|
|
452
|
-
|
|
477
|
+
store = get_default_store()
|
|
453
478
|
for session_meta in sessions:
|
|
454
|
-
|
|
479
|
+
store.delete_session(session_meta.id)
|
|
455
480
|
deleted_count += 1
|
|
456
|
-
|
|
457
481
|
return deleted_count
|
|
458
482
|
|
|
459
483
|
@classmethod
|
|
460
|
-
def
|
|
461
|
-
|
|
462
|
-
sessions_dir = cls._sessions_dir()
|
|
463
|
-
messages_dir = cls._messages_dir()
|
|
464
|
-
|
|
465
|
-
# Delete session file
|
|
466
|
-
prefix = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(created_at))
|
|
467
|
-
session_file = sessions_dir / f"{prefix}-{session_id}.json"
|
|
468
|
-
if session_file.exists():
|
|
469
|
-
session_file.unlink()
|
|
470
|
-
|
|
471
|
-
# Delete messages file
|
|
472
|
-
messages_file = messages_dir / f"{prefix}-{session_id}.jsonl"
|
|
473
|
-
if messages_file.exists():
|
|
474
|
-
messages_file.unlink()
|
|
484
|
+
def exports_dir(cls) -> Path:
|
|
485
|
+
return get_default_store().paths.exports_dir
|