-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathsession_storage.hpp
More file actions
226 lines (190 loc) · 10.6 KB
/
Copy pathsession_storage.hpp
File metadata and controls
226 lines (190 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
#pragma once
#include "../provider/llm_provider.hpp"
#include "todo_state.hpp"
#include <cstddef>
#include <functional>
#include <string>
#include <vector>
namespace acecode {
struct SessionLoadDiagnostics {
std::size_t malformed_complete_records = 0;
bool ignored_partial_tail = false;
bool recovered_unterminated_record = false;
bool recovered() const {
return malformed_complete_records > 0 ||
ignored_partial_tail ||
recovered_unterminated_record;
}
};
struct SessionLoadResult {
std::vector<ChatMessage> messages;
SessionLoadDiagnostics diagnostics;
};
// Worktree 会话状态(enter_worktree 工具 / --worktree 启动写入)。
// worktree_path 非空 = 该会话当前工作在 linked worktree 里;resume 时恢复
// (worktree 目录已被外部删除则清空)。老 meta 没有该字段时读出为全空。
struct WorktreeSessionInfo {
std::string original_cwd; // 进 worktree 前的会话 cwd
std::string worktree_path; // <主仓根>/.acecode/worktrees/<slug>
std::string worktree_name; // slug(用户名或随机生成)
std::string worktree_branch; // worktree-<slug>
std::string original_head_commit; // 创建基线 SHA;exit 时变更计数的比较基点
bool active() const { return !worktree_path.empty(); }
};
struct SessionMeta {
std::string id;
std::string cwd;
std::string created_at; // ISO 8601
std::string updated_at; // ISO 8601
int message_count = 0;
std::string summary;
std::string provider;
std::string model;
std::string model_preset; // optional saved_models name for this session
std::string title; // optional window title; empty = unset
// "user" = explicit rename, "user-cleared" = explicit empty title,
// "generated" = hidden title generator, "legacy" = pre-provenance
// metadata that already had a title.
std::string title_source;
std::string input_draft; // optional unsubmitted chat input draft; empty = unset
std::string permission_mode = "default"; // default | accept-edits | plan | yolo
std::string pre_plan_permission_mode; // previous non-plan mode while permission_mode == plan
int turn_count = 0; // visible user turns, excluding internal hidden prompts
TokenUsage last_token_usage;
TokenUsage session_token_usage;
std::vector<TodoItem> todos;
// Web fork 相关元数据(openspec session-fork capability)。
// 空字符串 = 这个 session 不是从其它 session 分叉出来的。
// 老 meta 文件没有这两个字段时读出来就是空,序列化时也省略。
std::string forked_from; // 源 session id
std::string fork_message_id; // 在源 session 哪条消息上分叉(含此条)
// spawn_subagent 派生的子代理会话:记录父会话 id。非空 = 子会话,
// 不出现在常规会话列表/侧栏,只出现在父会话的「后台任务」面板。
// 持久化(与 subagent_depth 的 runtime-only 语义不同):daemon 重启后
// 子会话依然被识别为后台任务,而不是泄漏进侧栏。空时序列化省略。
std::string parent_session_id;
// Current expert identity for this session. Normal conversations may switch
// it between turns; member is non-empty only for a validated team sub-agent.
std::string expert_id;
std::string expert_member_id;
// Daemon-owned LOOP provenance. This is display/history metadata only:
// resuming the session does not reactivate LOOP execution policy.
// Empty loop_id means this session was not directly created by a LOOP run.
std::string loop_id;
std::string loop_run_id;
// Sidebar archive state. Missing field in legacy metadata is false.
bool archived = false;
// Session was intentionally created outside the workspace list. It still
// has a cwd for tool execution/storage, but UI should not bind it to a
// workspace.
bool no_workspace = false;
// 会话当前的 worktree 状态。inactive(worktree_path 为空)时序列化省略。
WorktreeSessionInfo worktree;
};
class SessionStorage {
public:
// Compute a project hash from working directory path.
// Returns first 16 hex chars of a hash of the canonical path.
static std::string compute_project_hash(const std::string& cwd);
// Generate a new session ID: YYYYMMDD-HHMMSS-<4 hex random>
static std::string generate_session_id();
// Get the project directory for a given CWD: ~/.acecode/projects/<hash>/
static std::string get_project_dir(const std::string& cwd);
// Append a single ChatMessage as one JSONL line to a session file.
// Returns false if the record could not be durably handed to the stream.
// If a previous crash left an unterminated tail, a newline is inserted
// first so the new record cannot be swallowed by that damaged fragment.
static bool append_message(const std::string& session_path, const ChatMessage& msg);
// Rewrite a session JSONL file with all messages using one stream.
static void write_messages(const std::string& session_path,
const std::vector<ChatMessage>& messages);
// Load all messages from a JSONL session file.
// Skips malformed records while preserving later valid records.
static std::vector<ChatMessage> load_messages(const std::string& session_path);
// Detailed form used by resume/diagnostic surfaces. A valid final record
// without a newline is recovered; an invalid final fragment is ignored.
static SessionLoadResult load_messages_with_diagnostics(
const std::string& session_path);
// Write session metadata to a .meta.json file. Returns true only when the
// atomic replacement succeeds.
static bool write_meta(const std::string& meta_path, const SessionMeta& meta);
// Read session metadata from a .meta.json file.
static SessionMeta read_meta(const std::string& meta_path);
// List canonical sessions in a project directory, sorted by updated_at descending.
// PID-suffixed files are incompatible old data and are ignored.
static std::vector<SessionMeta> list_sessions(const std::string& project_dir);
// Metadata-only variant for global discovery/index maintenance. Unlike
// list_sessions(), it never opens JSONL transcripts to backfill legacy
// summary/count fields. A non-empty cancellation check is observed before
// every directory entry so callers can stop even inside a project that
// contains thousands of sessions; already-read metadata is returned.
static std::vector<SessionMeta> list_session_metadata(
const std::string& project_dir,
const std::function<bool()>& should_cancel = {});
// list_session_metadata() 的分页结果。sessions 已按 updated_at 降序,
// 同一秒内按 id 降序保证顺序确定。
struct MetadataPage {
std::vector<SessionMeta> sessions;
// 目录里候选 .meta.json 的总数。accept 过滤之前的上界,截断时
// 用它给调用方一个「至少这么多」的量级。
std::size_t candidate_files = 0;
// 通过 accept 的条数。只有 exhausted 为 true 时才是全量精确值。
std::size_t accepted = 0;
// 是否读完了全部候选文件。false 表示提前停在了 limit 上,
// 或者被 should_cancel 打断。
bool exhausted = true;
};
// 只取最新 N 条的元数据枚举。侧边栏折叠列表一次只显示 5 行,但
// list_session_metadata() 会把项目目录里每个 .meta.json 都打开一遍 ——
// 上千会话的目录在 Windows 上(每次 open/close 都过一遍杀软)实测要
// 数秒,而数据总量只有几百 KB,开销几乎全在 syscall 次数上。
//
// 这里先只枚举文件名和 mtime(Windows 的 FindNextFile 顺带返回,不产生
// 额外 syscall),按 mtime 降序后只打开需要的那几个文件。meta 每次落盘
// 都会重写,所以 mtime 与文件内的 updated_at 一致;为容忍两者的细微
// 偏差,实际会多读一小段余量,最终仍按 updated_at 排序后截断。
//
// limit <= 0 等价于 list_session_metadata():读全部并返回精确计数。
// accept 为空表示不过滤。被过滤掉的条目不占用 limit 名额,所以调用方
// 可以安全地在这里做 archived / 子会话过滤。
static MetadataPage list_session_metadata_page(
const std::string& project_dir,
int limit,
const std::function<bool(const SessionMeta&)>& accept = {},
const std::function<bool()>& should_cancel = {});
// Canonical session file record used by resume/web history paths.
struct SessionFileCandidate {
std::string jsonl_path;
std::string meta_path;
int pid = 0; // Always 0 for canonical files.
std::int64_t mtime = 0; // file_clock tick count, used for sorting if needed.
};
// Find the canonical `<session_id>.jsonl` file in project_dir.
// PID-suffixed files are incompatible old data and are not returned.
static std::vector<SessionFileCandidate> find_session_files(
const std::string& project_dir, const std::string& session_id);
// Detect incompatible old `<session-id>-<pid>.jsonl` or `.meta.json` data.
// If session_id is empty, checks whether any such old data exists in project_dir.
static bool has_incompatible_pid_session_files(
const std::string& project_dir, const std::string& session_id = "");
// Get the JSONL file path for a session.
// Default and pid <= 0 return canonical `<dir>/<id>.jsonl`.
// pid > 0 returns an old PID-suffixed path for tests/diagnostics only.
static std::string session_path(const std::string& project_dir,
const std::string& session_id,
int pid = 0);
// Get the meta file path for a session. pid semantics match session_path.
static std::string meta_path(const std::string& project_dir,
const std::string& session_id,
int pid = 0);
// Get current time as ISO 8601 string (UTC)
static std::string now_iso8601();
// Permanently delete a session's disk data: `<id>.jsonl`, `<id>.meta.json`
// and the per-session `<id>/` directory (persisted tool results etc.).
// Metadata is removed last so a partial failure remains discoverable and
// retryable. Callers own policy guard rails (archived/sub-agent, not busy).
static bool purge_session_files(const std::string& project_dir,
const std::string& session_id,
std::string* error = nullptr);
};
} // namespace acecode