-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathmtime_tracker.hpp
More file actions
186 lines (161 loc) · 6.89 KB
/
Copy pathmtime_tracker.hpp
File metadata and controls
186 lines (161 loc) · 6.89 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
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <chrono>
#include <map>
#include <memory>
#include <mutex>
#include <filesystem>
#include <optional>
#include <tuple>
#include <vector>
namespace acecode {
struct FileReadEditMetadata {
std::string encoding;
std::string line_ending;
int start_line = 0;
int end_line = 0;
bool lossy = false;
size_t lossy_replacement_count = 0;
};
// Tracks file modification times to detect concurrent external edits.
// Thread-safe: guarded by internal mutex.
class MtimeTracker {
public:
using clock = std::filesystem::file_time_type;
class FileWriteGuard {
public:
explicit FileWriteGuard(std::shared_ptr<std::mutex> mutex);
FileWriteGuard(FileWriteGuard&&) noexcept = default;
FileWriteGuard& operator=(FileWriteGuard&&) noexcept = delete;
FileWriteGuard(const FileWriteGuard&) = delete;
FileWriteGuard& operator=(const FileWriteGuard&) = delete;
private:
std::shared_ptr<std::mutex> mutex_;
std::unique_lock<std::mutex> lock_;
};
enum class ReadBaselineStatus {
Ok,
NotRead,
UnsafeRead,
ExternallyModified
};
struct ReadBaselineCheck {
ReadBaselineStatus status = ReadBaselineStatus::Ok;
bool content_unchanged_after_mtime_change = false;
};
struct ReadObservationKey {
std::string path;
int start_line = 0;
int end_line = 0;
bool byte_mode = false;
uint64_t byte_offset = 0;
size_t max_bytes = 0;
bool operator<(const ReadObservationKey& other) const {
return std::tie(path, start_line, end_line, byte_mode,
byte_offset, max_bytes) <
std::tie(other.path, other.start_line, other.end_line,
other.byte_mode, other.byte_offset, other.max_bytes);
}
bool operator==(const ReadObservationKey& other) const {
return path == other.path &&
start_line == other.start_line &&
end_line == other.end_line &&
byte_mode == other.byte_mode &&
byte_offset == other.byte_offset &&
max_bytes == other.max_bytes;
}
};
struct ReadObservation {
clock mtime;
std::string tool_call_id;
std::string persisted_output_path;
};
// Record the mtime of a file at the time it was read or observed by the agent.
void record_read(const std::string& path);
// Record a full or partial read. Full reads keep content so later edit checks can
// distinguish real external changes from timestamp-only churn.
void record_read(const std::string& path, const std::string& content, bool partial);
void record_read(const std::string& path,
const std::string& normalized_content,
bool partial,
const FileReadEditMetadata& metadata);
// Seed a full-file baseline from resumed transcript history. The stored
// mtime is intentionally stale so validation re-checks current content
// before allowing a write.
void seed_transcript_read_baseline(const std::string& path,
const std::string& normalized_content,
const FileReadEditMetadata& metadata);
// Claude Code style read-observation cache: repeated file_read calls for
// the same requested range can return a compact unchanged stub when the
// file mtime has not changed since the previous actual read.
bool has_unchanged_read_observation(
const std::string& path,
int start_line,
int end_line,
bool byte_mode = false,
uint64_t byte_offset = 0,
size_t max_bytes = 0
) const;
std::optional<ReadObservation> unchanged_read_observation(
const std::string& path,
int start_line,
int end_line,
bool byte_mode = false,
uint64_t byte_offset = 0,
size_t max_bytes = 0
) const;
void record_read_observation(const std::string& path,
int start_line,
int end_line,
bool byte_mode = false,
uint64_t byte_offset = 0,
size_t max_bytes = 0);
void record_read_observation_result(const std::string& path,
int start_line,
int end_line,
const std::string& tool_call_id,
const std::string& persisted_output_path,
bool byte_mode = false,
uint64_t byte_offset = 0,
size_t max_bytes = 0);
void invalidate_read_observations(const std::string& path);
void clear_read_observations();
// A human or another trusted UI wrote the file outside the agent tool
// protocol. Drop both the edit baseline and repeated-read observations so
// the agent must read the new bytes before a later file_edit/file_write.
void invalidate_agent_read_state(const std::string& path);
// Check if a file has been externally modified since the last recorded read.
// Returns true if the file was modified externally (mtime changed).
// Returns false if no record exists or mtime is unchanged.
bool was_externally_modified(const std::string& path) const;
// Require a prior non-lossy read baseline before editing. Full reads keep content
// so timestamp-only churn can still be accepted; ranged reads are accepted only
// while the recorded mtime is unchanged.
ReadBaselineCheck validate_read_baseline_for_edit(
const std::string& path,
const std::string& current_content
) const;
// Update the record after a successful write (so subsequent edits don't false-alarm).
void record_write(const std::string& path);
void record_write(const std::string& path, const std::string& content);
std::optional<FileReadEditMetadata> read_metadata(const std::string& path) const;
// Serialize validate-and-write sections per file so concurrent tool calls cannot
// interleave after the stale-file check.
FileWriteGuard acquire_write_guard(const std::string& path);
static MtimeTracker& instance();
private:
struct Record {
clock mtime;
bool partial = false;
std::optional<std::string> content;
std::optional<FileReadEditMetadata> metadata;
};
mutable std::mutex mu_;
std::map<std::string, Record> records_;
std::map<ReadObservationKey, ReadObservation> read_observations_;
std::vector<ReadObservationKey> read_observation_lru_;
std::map<std::string, std::weak_ptr<std::mutex>> file_locks_;
};
} // namespace acecode