11defmodule Store.Project.Conversation.TaskListStatusMigration do
22 @ moduledoc """
3- Helpers to heal and migrate conversation task-list statuses and shapes.
3+ Helpers to best-effort heal and migrate conversation task-list statuses and shapes.
44
55 These functions are intentionally minimal and safe: they only alter the
66 `tasks` map of the decoded conversation JSON if legacy shapes are detected
77 (for example the value is a bare list instead of a map), or if a `status`
8- field is missing or non-canonical. The migration is performed in-place via
9- an atomic write that preserves the original timestamp prefix used by the
10- Store.Project.Conversation format.
8+ field is missing or non-canonical. Repairs are applied in-memory, and the
9+ module attempts an atomic write of the repaired JSON back to disk, but if
10+ persistence fails, processing continues without raising. The timestamp prefix
11+ used by the Store.Project.Conversation format is preserved.
1112 """
1213
1314 @ doc """
1415 Inspect the decoded JSON map `original_json_map` for legacy task-list
15- shapes and/or non-canonical status values. If repairs are needed, atomically
16- write the repaired JSON back to disk using the provided `conversation` and
17- the original `timestamp_str` (which is the unix timestamp string prefix).
16+ shapes and/or non-canonical status values. If repairs are needed, apply them
17+ in-memory and attempt to atomically persist the repaired JSON to disk using
18+ the provided `conversation` and the original `timestamp_str` (the unix
19+ timestamp string prefix).
20+
21+ Persistence is best-effort: if the write fails (for example due to concurrent
22+ access or filesystem issues), this function will warn and continue without
23+ raising so callers can keep reading the conversation.
1824
1925 This function is safe to call from Store.Project.Conversation.read/1; it is
2026 conservative and only writes when it detects a change.
@@ -56,7 +62,17 @@ defmodule Store.Project.Conversation.TaskListStatusMigration do
5662 end )
5763
5864 if changed do
59- write_file_with_ts ( conversation , timestamp_str , repaired )
65+ case write_file_with_ts ( conversation , timestamp_str , repaired ) do
66+ :ok ->
67+ :ok
68+
69+ { :error , reason } ->
70+ UI . warn (
71+ "Could not persist healed tasks for conversation #{ conversation . id } : #{ inspect ( reason ) } "
72+ )
73+
74+ :ok
75+ end
6076 else
6177 :ok
6278 end
@@ -82,14 +98,41 @@ defmodule Store.Project.Conversation.TaskListStatusMigration do
8298 end
8399 end
84100
101+ @ spec write_file_with_ts ( Store.Project.Conversation . t ( ) , String . t ( ) , map ( ) ) ::
102+ :ok | { :error , term ( ) }
85103 defp write_file_with_ts ( conversation , timestamp_str , data_map ) do
86104 conversation . project_home
87105 |> Path . join ( "conversations" )
88106 |> File . mkdir_p ( )
89107
90- json = SafeJson . encode! ( data_map )
91108 tmp = conversation . store_path <> ".tmp"
92- File . write! ( tmp , "#{ timestamp_str } :" <> json )
93- File . rename! ( tmp , conversation . store_path )
109+
110+ with { :ok , json } <- SafeJson . encode ( data_map ) ,
111+ :ok <- File . write ( tmp , "#{ timestamp_str } :" <> json ) ,
112+ :ok <- safe_rename ( tmp , conversation . store_path ) do
113+ :ok
114+ else
115+ { :error , reason } ->
116+ File . rm ( tmp )
117+ { :error , reason }
118+ end
119+ end
120+
121+ @ spec safe_rename ( String . t ( ) , String . t ( ) ) :: :ok | { :error , term ( ) }
122+ defp safe_rename ( tmp , dest ) do
123+ case File . rename ( tmp , dest ) do
124+ :ok ->
125+ :ok
126+
127+ { :error , :enoent } ->
128+ if File . exists? ( tmp ) do
129+ { :error , :dest_missing }
130+ else
131+ { :error , :tmp_missing }
132+ end
133+
134+ { :error , reason } ->
135+ { :error , reason }
136+ end
94137 end
95138end
0 commit comments