112 return absl::OkStatus();
115 for (
const auto& entry :
121 if (!entry.is_directory()) {
125 const std::string proposal_id = entry.path().filename().string();
131 bool metadata_loaded =
false;
132 const std::filesystem::path metadata_path = entry.path() /
"metadata.json";
134 if (std::filesystem::exists(metadata_path, ec) && !ec) {
135 std::ifstream metadata_file(metadata_path);
136 if (metadata_file.is_open()) {
138 nlohmann::json metadata_json;
139 metadata_file >> metadata_json;
141 metadata.
id = metadata_json.value(
"id", proposal_id);
142 if (metadata.
id.empty()) {
143 metadata.
id = proposal_id;
145 metadata.
sandbox_id = metadata_json.value(
"sandbox_id",
"");
147 if (metadata_json.contains(
"sandbox_directory") &&
148 metadata_json[
"sandbox_directory"].is_string()) {
150 metadata_json[
"sandbox_directory"].get<std::string>());
155 if (metadata_json.contains(
"sandbox_rom_path") &&
156 metadata_json[
"sandbox_rom_path"].is_string()) {
158 metadata_json[
"sandbox_rom_path"].get<std::string>());
163 metadata.
description = metadata_json.value(
"description",
"");
164 metadata.
prompt = metadata_json.value(
"prompt",
"");
166 ParseStatus(metadata_json.value(
"status",
"pending"));
168 int64_t created_at_millis = metadata_json.value<int64_t>(
169 "created_at_millis", TimeToMillis(absl::Now()));
170 metadata.
created_at = absl::FromUnixMillis(created_at_millis);
172 int64_t reviewed_at_millis =
173 metadata_json.value<int64_t>(
"reviewed_at_millis", 0);
174 metadata.
reviewed_at = OptionalTimeFromMillis(reviewed_at_millis);
176 std::string diff_path =
177 metadata_json.value(
"diff_path", std::string(
"diff.txt"));
178 std::string log_path =
179 metadata_json.value(
"log_path", std::string(
"execution.log"));
180 metadata.
diff_path = entry.path() / diff_path;
181 metadata.
log_path = entry.path() / log_path;
183 metadata.
bytes_changed = metadata_json.value(
"bytes_changed", 0);
185 metadata_json.value(
"commands_executed", 0);
188 if (metadata_json.contains(
"screenshots") &&
189 metadata_json[
"screenshots"].is_array()) {
190 for (
const auto& screenshot : metadata_json[
"screenshots"]) {
191 if (screenshot.is_string()) {
193 entry.path() / screenshot.get<std::string>());
204 metadata_loaded =
true;
205 }
catch (
const std::exception& ex) {
206 std::cerr <<
"Warning: Failed to parse metadata for proposal "
207 << proposal_id <<
": " << ex.what() <<
"\n";
212 if (!metadata_loaded) {
213 std::filesystem::path log_path = entry.path() /
"execution.log";
214 if (!std::filesystem::exists(log_path, ec) || ec) {
218 std::filesystem::path diff_path = entry.path() /
"diff.txt";
220 absl::Time created_at = absl::Now();
221 if (proposal_id.starts_with(
"proposal-")) {
222 std::string time_str = proposal_id.substr(9, 15);
224 if (absl::ParseTime(
"%Y%m%dT%H%M%S", time_str, &created_at, &error)) {
229 auto ftime = std::filesystem::last_write_time(log_path, ec);
232 std::chrono::time_point_cast<std::chrono::system_clock::duration>(
233 ftime - std::filesystem::file_time_type::clock::now() +
234 std::chrono::system_clock::now());
235 auto time_t_value = std::chrono::system_clock::to_time_t(sctp);
236 created_at = absl::FromTimeT(time_t_value);
242 .sandbox_directory = std::filesystem::path(),
243 .sandbox_rom_path = std::filesystem::path(),
244 .description =
"Loaded from disk",
247 .created_at = created_at,
248 .reviewed_at = std::nullopt,
249 .diff_path = diff_path,
250 .log_path = log_path,
253 .commands_executed = 0,
256 if (std::filesystem::exists(diff_path, ec) && !ec) {
258 static_cast<int>(std::filesystem::file_size(diff_path, ec));
261 for (
const auto& file :
262 std::filesystem::directory_iterator(entry.path(), ec)) {
266 if (file.path().extension() ==
".png" ||
267 file.path().extension() ==
".jpg" ||
268 file.path().extension() ==
".jpeg") {
275 if (!write_status.ok()) {
276 std::cerr <<
"Warning: Failed to persist metadata for legacy proposal "
277 << proposal_id <<
": " << write_status.message() <<
"\n";
284 return absl::OkStatus();
302 const std::filesystem::path& sandbox_rom_path,
303 absl::string_view prompt,
304 absl::string_view description) {
305 std::unique_lock<std::mutex> lock(
mutex_);
313 if (!std::filesystem::create_directories(proposal_dir, ec) && ec) {
314 return absl::InternalError(
315 absl::StrCat(
"Failed to create proposal directory at ",
316 proposal_dir.string(),
": ", ec.message()));
322 .sandbox_id = std::string(sandbox_id),
323 .sandbox_directory = sandbox_rom_path.empty()
324 ? std::filesystem::path()
325 : sandbox_rom_path.parent_path(),
326 .sandbox_rom_path = sandbox_rom_path,
327 .description = std::string(description),
328 .prompt = std::string(prompt),
330 .created_at = absl::Now(),
331 .reviewed_at = std::nullopt,
332 .diff_path = proposal_dir /
"diff.txt",
333 .log_path = proposal_dir /
"execution.log",
336 .commands_executed = 0,
345 absl::string_view diff_content) {
346 std::lock_guard<std::mutex> lock(
mutex_);
349 return absl::NotFoundError(
350 absl::StrCat(
"Proposal not found: ", proposal_id));
353 std::ofstream diff_file(it->second.diff_path, std::ios::out);
354 if (!diff_file.is_open()) {
355 return absl::InternalError(absl::StrCat(
"Failed to open diff file: ",
356 it->second.diff_path.string()));
359 diff_file << diff_content;
363 it->second.bytes_changed =
static_cast<int>(diff_content.size());
367 return absl::OkStatus();
371 absl::string_view log_entry) {
372 std::lock_guard<std::mutex> lock(
mutex_);
375 return absl::NotFoundError(
376 absl::StrCat(
"Proposal not found: ", proposal_id));
379 std::ofstream log_file(it->second.log_path, std::ios::out | std::ios::app);
380 if (!log_file.is_open()) {
381 return absl::InternalError(absl::StrCat(
"Failed to open log file: ",
382 it->second.log_path.string()));
385 log_file << absl::FormatTime(
"[%Y-%m-%d %H:%M:%S] ", absl::Now(),
386 absl::LocalTimeZone())
387 << log_entry <<
"\n";
390 return absl::OkStatus();
456 std::optional<ProposalStatus> filter_status)
const {
457 std::unique_lock<std::mutex> lock(
mutex_);
466 std::cerr <<
"Warning: Failed to load proposals from disk: "
467 << status.message() <<
"\n";
471 std::vector<ProposalMetadata> result;
473 for (
const auto& [
id, metadata] :
proposals_) {
474 if (!filter_status.has_value() || metadata.status == *filter_status) {
475 result.push_back(metadata);
480 std::sort(result.begin(), result.end(),
482 return a.created_at > b.created_at;
512 if (!std::filesystem::exists(proposal_dir, ec) || ec) {
513 return absl::NotFoundError(
514 absl::StrCat(
"Proposal directory missing for ", metadata.
id));
517 auto relative_to_proposal = [&](
const std::filesystem::path& path) {
519 return std::string();
521 std::error_code relative_error;
523 std::filesystem::relative(path, proposal_dir, relative_error);
524 if (!relative_error) {
525 return relative_path.generic_string();
527 return path.generic_string();
530 nlohmann::json metadata_json;
531 metadata_json[
"id"] = metadata.
id;
532 metadata_json[
"sandbox_id"] = metadata.
sandbox_id;
534 metadata_json[
"sandbox_directory"] =
538 metadata_json[
"sandbox_rom_path"] =
541 metadata_json[
"description"] = metadata.
description;
542 metadata_json[
"prompt"] = metadata.
prompt;
543 metadata_json[
"status"] = StatusToString(metadata.
status);
544 metadata_json[
"created_at_millis"] = TimeToMillis(metadata.
created_at);
545 metadata_json[
"reviewed_at_millis"] =
548 metadata_json[
"diff_path"] = relative_to_proposal(metadata.
diff_path);
549 metadata_json[
"log_path"] = relative_to_proposal(metadata.
log_path);
553 nlohmann::json screenshots_json = nlohmann::json::array();
554 for (
const auto& screenshot : metadata.
screenshots) {
555 screenshots_json.push_back(relative_to_proposal(screenshot));
557 metadata_json[
"screenshots"] = std::move(screenshots_json);
559 std::ofstream metadata_file(proposal_dir /
"metadata.json", std::ios::out);
560 if (!metadata_file.is_open()) {
561 return absl::InternalError(absl::StrCat(
562 "Failed to write metadata file for proposal ", metadata.
id));
565 metadata_file << metadata_json.dump(2);
566 metadata_file.close();
567 if (!metadata_file) {
568 return absl::InternalError(absl::StrCat(
569 "Failed to flush metadata file for proposal ", metadata.
id));
572 return absl::OkStatus();