aidp 0.34.1 → 0.39.3

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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/aidp/agent_harness_adapter.rb +309 -0
  4. data/lib/aidp/cli/issue_importer.rb +20 -6
  5. data/lib/aidp/cli/jobs_command.rb +1 -1
  6. data/lib/aidp/cli/models_command.rb +9 -16
  7. data/lib/aidp/cli/prompts_command.rb +253 -0
  8. data/lib/aidp/cli/security_command.rb +4 -10
  9. data/lib/aidp/cli/storage_command.rb +277 -0
  10. data/lib/aidp/cli/temporal_command.rb +460 -0
  11. data/lib/aidp/cli.rb +24 -2
  12. data/lib/aidp/config.rb +15 -0
  13. data/lib/aidp/config_paths.rb +12 -2
  14. data/lib/aidp/database/migrations.rb +114 -0
  15. data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
  16. data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
  17. data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
  18. data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
  19. data/lib/aidp/database/repositories/job_repository.rb +278 -0
  20. data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
  21. data/lib/aidp/database/repositories/progress_repository.rb +210 -0
  22. data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
  23. data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
  24. data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
  25. data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
  26. data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
  27. data/lib/aidp/database/repositories/task_repository.rb +231 -0
  28. data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
  29. data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
  30. data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
  31. data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
  32. data/lib/aidp/database/repository.rb +166 -0
  33. data/lib/aidp/database/schema.rb +347 -0
  34. data/lib/aidp/database/storage_migrator.rb +693 -0
  35. data/lib/aidp/database.rb +139 -0
  36. data/lib/aidp/debug_mixin.rb +2 -2
  37. data/lib/aidp/execute/checkpoint.rb +26 -0
  38. data/lib/aidp/execute/checkpoint_display.rb +16 -0
  39. data/lib/aidp/execute/prompt_evaluator.rb +30 -57
  40. data/lib/aidp/execute/work_loop_runner.rb +294 -46
  41. data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
  42. data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
  43. data/lib/aidp/harness/ai_decision_engine.rb +78 -296
  44. data/lib/aidp/harness/config_schema.rb +59 -0
  45. data/lib/aidp/harness/configuration.rb +229 -10
  46. data/lib/aidp/harness/deprecation_cache.rb +53 -11
  47. data/lib/aidp/harness/enhanced_runner.rb +1 -1
  48. data/lib/aidp/harness/error_handler.rb +11 -5
  49. data/lib/aidp/harness/provider_config.rb +36 -0
  50. data/lib/aidp/harness/provider_factory.rb +26 -24
  51. data/lib/aidp/harness/provider_info.rb +9 -5
  52. data/lib/aidp/harness/provider_manager.rb +2 -2
  53. data/lib/aidp/harness/provider_metrics.rb +103 -1
  54. data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
  55. data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
  56. data/lib/aidp/harness/runner.rb +7 -2
  57. data/lib/aidp/harness/state_manager.rb +1 -1
  58. data/lib/aidp/harness/test_runner.rb +167 -11
  59. data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
  60. data/lib/aidp/harness/usage_limit.rb +179 -0
  61. data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
  62. data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
  63. data/lib/aidp/harness/usage_period.rb +234 -0
  64. data/lib/aidp/init/agent_instructions_generator.rb +317 -0
  65. data/lib/aidp/init/runner.rb +25 -4
  66. data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
  67. data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
  68. data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
  69. data/lib/aidp/interfaces/logger_interface.rb +150 -0
  70. data/lib/aidp/interfaces/ui_interface.rb +254 -0
  71. data/lib/aidp/logger.rb +25 -7
  72. data/lib/aidp/orchestration_adapter.rb +279 -0
  73. data/lib/aidp/pr_worktree_manager.rb +4 -15
  74. data/lib/aidp/prompts/feedback_collector.rb +198 -0
  75. data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
  76. data/lib/aidp/prompts/template_evolver.rb +305 -0
  77. data/lib/aidp/prompts/template_version_manager.rb +325 -0
  78. data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
  79. data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
  80. data/lib/aidp/setup/wizard.rb +616 -176
  81. data/lib/aidp/shell_executor.rb +58 -0
  82. data/lib/aidp/style_guide/selector.rb +56 -1
  83. data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
  84. data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
  85. data/lib/aidp/temporal/activities/base_activity.rb +78 -0
  86. data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
  87. data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
  88. data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
  89. data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
  90. data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
  91. data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
  92. data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
  93. data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
  94. data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
  95. data/lib/aidp/temporal/configuration.rb +169 -0
  96. data/lib/aidp/temporal/connection.rb +93 -0
  97. data/lib/aidp/temporal/worker.rb +119 -0
  98. data/lib/aidp/temporal/workflow_client.rb +171 -0
  99. data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
  100. data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
  101. data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
  102. data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
  103. data/lib/aidp/temporal.rb +120 -0
  104. data/lib/aidp/version.rb +1 -1
  105. data/lib/aidp/watch/base_processor.rb +35 -0
  106. data/lib/aidp/watch/build_processor.rb +132 -0
  107. data/lib/aidp/watch/github_state_extractor.rb +0 -8
  108. data/lib/aidp/watch/rebase_processor.rb +245 -0
  109. data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
  110. data/lib/aidp/watch/runner.rb +366 -285
  111. data/lib/aidp/watch/state_store.rb +80 -0
  112. data/lib/aidp/watch/work_item.rb +96 -0
  113. data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
  114. data/lib/aidp/watch/worktree_reconciler.rb +643 -0
  115. data/lib/aidp/workflows/guided_agent.rb +1 -1
  116. data/lib/aidp/worktree.rb +62 -0
  117. data/templates/aidp.yml.example +70 -0
  118. metadata +117 -16
  119. data/lib/aidp/analyze/json_file_storage.rb +0 -292
  120. data/lib/aidp/execute/future_work_backlog.rb +0 -411
  121. data/lib/aidp/providers/adapter.rb +0 -239
  122. data/lib/aidp/providers/aider.rb +0 -264
  123. data/lib/aidp/providers/anthropic.rb +0 -614
  124. data/lib/aidp/providers/base.rb +0 -617
  125. data/lib/aidp/providers/capability_registry.rb +0 -204
  126. data/lib/aidp/providers/codex.rb +0 -267
  127. data/lib/aidp/providers/cursor.rb +0 -304
  128. data/lib/aidp/providers/error_taxonomy.rb +0 -195
  129. data/lib/aidp/providers/gemini.rb +0 -110
  130. data/lib/aidp/providers/github_copilot.rb +0 -227
  131. data/lib/aidp/providers/kilocode.rb +0 -157
  132. data/lib/aidp/providers/opencode.rb +0 -139
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+
5
+ module Aidp
6
+ module Database
7
+ module Repositories
8
+ # Repository for harness_state table
9
+ # Replaces harness/*_state.json files
10
+ class HarnessStateRepository < Repository
11
+ def initialize(project_dir: Dir.pwd)
12
+ super(project_dir: project_dir, table_name: "harness_state")
13
+ end
14
+
15
+ # Check if state exists for mode
16
+ #
17
+ # @param mode [String, Symbol] Mode (execute, analyze, etc.)
18
+ # @return [Boolean]
19
+ def has_state?(mode)
20
+ mode_str = mode.to_s
21
+ count = query_value(
22
+ "SELECT COUNT(*) FROM harness_state WHERE project_dir = ? AND mode = ?",
23
+ [project_dir, mode_str]
24
+ )
25
+ count.positive?
26
+ end
27
+
28
+ # Load state for mode
29
+ #
30
+ # @param mode [String, Symbol] Mode
31
+ # @return [Hash] State data (empty hash if no state)
32
+ def load_state(mode)
33
+ mode_str = mode.to_s
34
+ row = query_one(
35
+ "SELECT * FROM harness_state WHERE project_dir = ? AND mode = ?",
36
+ [project_dir, mode_str]
37
+ )
38
+
39
+ return {} unless row
40
+
41
+ state = deserialize_json(row["state"]) || {}
42
+ Aidp.log_debug("harness_state_repository", "loaded",
43
+ mode: mode_str, keys: state.keys.size)
44
+ state
45
+ end
46
+
47
+ # Save state for mode
48
+ #
49
+ # @param mode [String, Symbol] Mode
50
+ # @param state_data [Hash] State to save
51
+ def save_state(mode, state_data)
52
+ mode_str = mode.to_s
53
+ now = current_timestamp
54
+
55
+ # Add metadata
56
+ state_with_metadata = state_data.merge(
57
+ mode: mode_str,
58
+ project_dir: project_dir,
59
+ saved_at: now
60
+ )
61
+
62
+ existing = query_one(
63
+ "SELECT id, version FROM harness_state WHERE project_dir = ? AND mode = ?",
64
+ [project_dir, mode_str]
65
+ )
66
+
67
+ if existing
68
+ new_version = (existing["version"] || 1) + 1
69
+ execute(
70
+ <<~SQL,
71
+ UPDATE harness_state SET
72
+ state = ?,
73
+ version = ?,
74
+ updated_at = ?
75
+ WHERE project_dir = ? AND mode = ?
76
+ SQL
77
+ [
78
+ serialize_json(state_with_metadata),
79
+ new_version,
80
+ now,
81
+ project_dir,
82
+ mode_str
83
+ ]
84
+ )
85
+ Aidp.log_debug("harness_state_repository", "updated",
86
+ mode: mode_str, version: new_version)
87
+ else
88
+ execute(
89
+ insert_sql([:project_dir, :mode, :state, :version, :created_at, :updated_at]),
90
+ [
91
+ project_dir,
92
+ mode_str,
93
+ serialize_json(state_with_metadata),
94
+ 1,
95
+ now,
96
+ now
97
+ ]
98
+ )
99
+ Aidp.log_debug("harness_state_repository", "created", mode: mode_str)
100
+ end
101
+ end
102
+
103
+ # Clear state for mode
104
+ #
105
+ # @param mode [String, Symbol] Mode
106
+ def clear_state(mode)
107
+ mode_str = mode.to_s
108
+ execute(
109
+ "DELETE FROM harness_state WHERE project_dir = ? AND mode = ?",
110
+ [project_dir, mode_str]
111
+ )
112
+ Aidp.log_debug("harness_state_repository", "cleared", mode: mode_str)
113
+ end
114
+
115
+ # Get all modes with state
116
+ #
117
+ # @return [Array<String>] Mode names
118
+ def modes_with_state
119
+ rows = query(
120
+ "SELECT DISTINCT mode FROM harness_state WHERE project_dir = ?",
121
+ [project_dir]
122
+ )
123
+ rows.map { |r| r["mode"] }
124
+ end
125
+
126
+ # Get state version for mode
127
+ #
128
+ # @param mode [String, Symbol] Mode
129
+ # @return [Integer] Version number (0 if no state)
130
+ def version(mode)
131
+ mode_str = mode.to_s
132
+ query_value(
133
+ "SELECT version FROM harness_state WHERE project_dir = ? AND mode = ?",
134
+ [project_dir, mode_str]
135
+ ) || 0
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+ require "securerandom"
5
+
6
+ module Aidp
7
+ module Database
8
+ module Repositories
9
+ # Repository for background_jobs table
10
+ # Replaces jobs/* directory (metadata, logs, PID tracking)
11
+ class JobRepository < Repository
12
+ VALID_STATUSES = %w[pending running completed failed stopped].freeze
13
+
14
+ def initialize(project_dir: Dir.pwd)
15
+ super(project_dir: project_dir, table_name: "background_jobs")
16
+ end
17
+
18
+ # Create a new job
19
+ #
20
+ # @param job_type [String] Type of job (e.g., "execute", "analyze")
21
+ # @param input [Hash] Job input/options
22
+ # @return [String] Job ID
23
+ def create(job_type:, input: {})
24
+ job_id = generate_job_id
25
+ now = current_timestamp
26
+
27
+ execute(
28
+ insert_sql([
29
+ :id, :project_dir, :job_type, :status, :options, :created_at
30
+ ]),
31
+ [
32
+ job_id,
33
+ project_dir,
34
+ job_type,
35
+ "pending",
36
+ serialize_json(input),
37
+ now
38
+ ]
39
+ )
40
+
41
+ Aidp.log_debug("job_repository", "created",
42
+ job_id: job_id, type: job_type)
43
+
44
+ job_id
45
+ end
46
+
47
+ # Start a job
48
+ #
49
+ # @param job_id [String] Job ID
50
+ # @param pid [Integer, nil] Process ID
51
+ def start(job_id, pid: nil)
52
+ now = current_timestamp
53
+
54
+ execute(
55
+ <<~SQL,
56
+ UPDATE background_jobs SET
57
+ status = 'running',
58
+ pid = ?,
59
+ started_at = ?
60
+ WHERE id = ? AND project_dir = ?
61
+ SQL
62
+ [pid, now, job_id, project_dir]
63
+ )
64
+
65
+ Aidp.log_debug("job_repository", "started", job_id: job_id, pid: pid)
66
+ end
67
+
68
+ # Complete a job
69
+ #
70
+ # @param job_id [String] Job ID
71
+ # @param output [Hash] Job output/result
72
+ def complete(job_id, output: {})
73
+ now = current_timestamp
74
+
75
+ execute(
76
+ <<~SQL,
77
+ UPDATE background_jobs SET
78
+ status = 'completed',
79
+ result = ?,
80
+ completed_at = ?
81
+ WHERE id = ? AND project_dir = ?
82
+ SQL
83
+ [serialize_json(output), now, job_id, project_dir]
84
+ )
85
+
86
+ Aidp.log_debug("job_repository", "completed", job_id: job_id)
87
+ end
88
+
89
+ # Fail a job
90
+ #
91
+ # @param job_id [String] Job ID
92
+ # @param error [String] Error message
93
+ def fail(job_id, error:)
94
+ now = current_timestamp
95
+
96
+ execute(
97
+ <<~SQL,
98
+ UPDATE background_jobs SET
99
+ status = 'failed',
100
+ error = ?,
101
+ completed_at = ?
102
+ WHERE id = ? AND project_dir = ?
103
+ SQL
104
+ [error, now, job_id, project_dir]
105
+ )
106
+
107
+ Aidp.log_debug("job_repository", "failed", job_id: job_id, error: error)
108
+ end
109
+
110
+ # Stop a job
111
+ #
112
+ # @param job_id [String] Job ID
113
+ def stop(job_id)
114
+ now = current_timestamp
115
+
116
+ execute(
117
+ <<~SQL,
118
+ UPDATE background_jobs SET
119
+ status = 'stopped',
120
+ completed_at = ?
121
+ WHERE id = ? AND project_dir = ?
122
+ SQL
123
+ [now, job_id, project_dir]
124
+ )
125
+
126
+ Aidp.log_debug("job_repository", "stopped", job_id: job_id)
127
+ end
128
+
129
+ # Find job by ID
130
+ #
131
+ # @param job_id [String] Job ID
132
+ # @return [Hash, nil] Job or nil
133
+ def find(job_id)
134
+ row = query_one(
135
+ "SELECT * FROM background_jobs WHERE id = ? AND project_dir = ?",
136
+ [job_id, project_dir]
137
+ )
138
+ deserialize_job(row)
139
+ end
140
+
141
+ # List all jobs
142
+ #
143
+ # @param status [String, nil] Filter by status
144
+ # @param limit [Integer] Maximum jobs
145
+ # @return [Array<Hash>] Jobs
146
+ def list(status: nil, limit: 50)
147
+ rows = if status
148
+ query(
149
+ <<~SQL,
150
+ SELECT * FROM background_jobs
151
+ WHERE project_dir = ? AND status = ?
152
+ ORDER BY created_at DESC
153
+ LIMIT ?
154
+ SQL
155
+ [project_dir, status, limit]
156
+ )
157
+ else
158
+ query(
159
+ <<~SQL,
160
+ SELECT * FROM background_jobs
161
+ WHERE project_dir = ?
162
+ ORDER BY created_at DESC
163
+ LIMIT ?
164
+ SQL
165
+ [project_dir, limit]
166
+ )
167
+ end
168
+
169
+ rows.map { |row| deserialize_job(row) }
170
+ end
171
+
172
+ # List running jobs
173
+ #
174
+ # @return [Array<Hash>]
175
+ def running
176
+ list(status: "running")
177
+ end
178
+
179
+ # Get job status
180
+ #
181
+ # @param job_id [String] Job ID
182
+ # @return [Hash, nil] Status info or nil
183
+ def status(job_id)
184
+ job = find(job_id)
185
+ return nil unless job
186
+
187
+ pid = job[:pid]
188
+
189
+ {
190
+ job_id: job_id,
191
+ mode: job[:job_type],
192
+ status: job[:status],
193
+ pid: pid,
194
+ running: pid && process_running?(pid),
195
+ started_at: job[:started_at],
196
+ completed_at: job[:completed_at],
197
+ error: job[:error]
198
+ }
199
+ end
200
+
201
+ # Delete a job
202
+ #
203
+ # @param job_id [String] Job ID
204
+ def delete(job_id)
205
+ execute(
206
+ "DELETE FROM background_jobs WHERE id = ? AND project_dir = ?",
207
+ [job_id, project_dir]
208
+ )
209
+ Aidp.log_debug("job_repository", "deleted", job_id: job_id)
210
+ end
211
+
212
+ # Cleanup old completed jobs
213
+ #
214
+ # @param days_to_keep [Integer] Days to retain
215
+ # @return [Integer] Deleted count
216
+ def cleanup(days_to_keep: 7)
217
+ threshold = (Time.now - (days_to_keep * 24 * 60 * 60)).strftime("%Y-%m-%d %H:%M:%S")
218
+
219
+ count = query_value(
220
+ <<~SQL,
221
+ SELECT COUNT(*) FROM background_jobs
222
+ WHERE project_dir = ? AND status IN ('completed', 'failed', 'stopped')
223
+ AND completed_at < ?
224
+ SQL
225
+ [project_dir, threshold]
226
+ ) || 0
227
+
228
+ execute(
229
+ <<~SQL,
230
+ DELETE FROM background_jobs
231
+ WHERE project_dir = ? AND status IN ('completed', 'failed', 'stopped')
232
+ AND completed_at < ?
233
+ SQL
234
+ [project_dir, threshold]
235
+ )
236
+
237
+ Aidp.log_debug("job_repository", "cleanup",
238
+ deleted: count, threshold: threshold)
239
+
240
+ count
241
+ end
242
+
243
+ private
244
+
245
+ def generate_job_id
246
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
247
+ "job_#{timestamp}_#{SecureRandom.hex(4)}"
248
+ end
249
+
250
+ def process_running?(pid)
251
+ return false unless pid
252
+
253
+ Process.kill(0, pid)
254
+ true
255
+ rescue Errno::ESRCH, Errno::EPERM
256
+ false
257
+ end
258
+
259
+ def deserialize_job(row)
260
+ return nil unless row
261
+
262
+ {
263
+ id: row["id"],
264
+ job_type: row["job_type"],
265
+ status: row["status"],
266
+ options: deserialize_json(row["options"]) || {},
267
+ result: deserialize_json(row["result"]) || {},
268
+ pid: row["pid"],
269
+ error: row["error"],
270
+ started_at: row["started_at"],
271
+ completed_at: row["completed_at"],
272
+ created_at: row["created_at"]
273
+ }
274
+ end
275
+ end
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+
5
+ module Aidp
6
+ module Database
7
+ module Repositories
8
+ # Repository for model cache
9
+ # Replaces: ~/.aidp/cache/models.json
10
+ #
11
+ # Caches discovered models from AI providers with TTL support
12
+ class ModelCacheRepository < Repository
13
+ DEFAULT_TTL = 86400 # 24 hours in seconds
14
+
15
+ def initialize(project_dir: Dir.pwd)
16
+ super(project_dir: project_dir, table_name: "model_cache")
17
+ end
18
+
19
+ # Cache models for a provider
20
+ #
21
+ # @param provider_name [String] Provider name
22
+ # @param models [Array<Hash>] Models array
23
+ # @param ttl [Integer] Time to live in seconds
24
+ # @return [Boolean] Success status
25
+ def cache_models(provider_name, models, ttl: DEFAULT_TTL)
26
+ Aidp.log_debug("model_cache_repo", "cache_models",
27
+ provider: provider_name, count: models.size, ttl: ttl)
28
+
29
+ expires_at = Time.now.utc + ttl
30
+
31
+ execute(<<~SQL, [project_dir, provider_name, serialize_json(models), current_timestamp, expires_at.strftime("%Y-%m-%d %H:%M:%S")])
32
+ INSERT INTO #{table_name} (project_dir, provider_name, models, cached_at, expires_at)
33
+ VALUES (?, ?, ?, ?, ?)
34
+ ON CONFLICT(project_dir, provider_name) DO UPDATE SET
35
+ models = excluded.models,
36
+ cached_at = excluded.cached_at,
37
+ expires_at = excluded.expires_at
38
+ SQL
39
+
40
+ Aidp.log_info("model_cache_repo", "cached_models",
41
+ provider: provider_name, count: models.size)
42
+ true
43
+ rescue => e
44
+ Aidp.log_error("model_cache_repo", "cache_models_failed",
45
+ provider: provider_name, error: e.message)
46
+ false
47
+ end
48
+
49
+ # Get cached models for a provider if not expired
50
+ #
51
+ # @param provider_name [String] Provider name
52
+ # @return [Array<Hash>, nil] Cached models or nil if expired/not found
53
+ def get_cached_models(provider_name)
54
+ Aidp.log_debug("model_cache_repo", "get_cached_models",
55
+ provider: provider_name)
56
+
57
+ row = query_one(<<~SQL, [project_dir, provider_name])
58
+ SELECT models, cached_at, expires_at
59
+ FROM #{table_name}
60
+ WHERE project_dir = ? AND provider_name = ?
61
+ SQL
62
+
63
+ return nil unless row
64
+
65
+ # Check expiration
66
+ if row["expires_at"]
67
+ # Parse as UTC since timestamps are stored in UTC
68
+ expires_at = Time.parse(row["expires_at"] + " UTC")
69
+ if Time.now.utc > expires_at
70
+ Aidp.log_debug("model_cache_repo", "cache_expired",
71
+ provider: provider_name, expires_at: expires_at)
72
+ return nil
73
+ end
74
+ end
75
+
76
+ models = deserialize_json(row["models"])
77
+ Aidp.log_debug("model_cache_repo", "cache_hit",
78
+ provider: provider_name, count: models&.size || 0)
79
+ models
80
+ end
81
+
82
+ # Invalidate cache for a specific provider
83
+ #
84
+ # @param provider_name [String] Provider name
85
+ # @return [Boolean] Success status
86
+ def invalidate(provider_name)
87
+ Aidp.log_debug("model_cache_repo", "invalidate",
88
+ provider: provider_name)
89
+
90
+ execute(<<~SQL, [project_dir, provider_name])
91
+ DELETE FROM #{table_name}
92
+ WHERE project_dir = ? AND provider_name = ?
93
+ SQL
94
+
95
+ Aidp.log_info("model_cache_repo", "invalidated",
96
+ provider: provider_name)
97
+ true
98
+ rescue => e
99
+ Aidp.log_error("model_cache_repo", "invalidate_failed",
100
+ provider: provider_name, error: e.message)
101
+ false
102
+ end
103
+
104
+ # Invalidate all cached models
105
+ #
106
+ # @return [Boolean] Success status
107
+ def invalidate_all
108
+ Aidp.log_debug("model_cache_repo", "invalidate_all")
109
+ delete_by_project
110
+ Aidp.log_info("model_cache_repo", "invalidated_all")
111
+ true
112
+ rescue => e
113
+ Aidp.log_error("model_cache_repo", "invalidate_all_failed",
114
+ error: e.message)
115
+ false
116
+ end
117
+
118
+ # Get list of providers with valid cached models
119
+ #
120
+ # @return [Array<String>] Provider names with valid caches
121
+ def cached_providers
122
+ rows = query(<<~SQL, [project_dir])
123
+ SELECT provider_name, expires_at
124
+ FROM #{table_name}
125
+ WHERE project_dir = ?
126
+ SQL
127
+
128
+ providers = []
129
+ rows.each do |row|
130
+ next unless row["expires_at"]
131
+
132
+ # Parse as UTC since timestamps are stored in UTC
133
+ expires_at = Time.parse(row["expires_at"] + " UTC")
134
+ providers << row["provider_name"] if Time.now.utc <= expires_at
135
+ end
136
+
137
+ providers
138
+ end
139
+
140
+ # Get cache statistics
141
+ #
142
+ # @return [Hash] Statistics
143
+ def stats
144
+ total = query_value(<<~SQL, [project_dir])
145
+ SELECT COUNT(*) FROM #{table_name} WHERE project_dir = ?
146
+ SQL
147
+
148
+ valid_providers = cached_providers
149
+
150
+ {
151
+ total_providers: total || 0,
152
+ cached_providers: valid_providers,
153
+ valid_count: valid_providers.size
154
+ }
155
+ end
156
+
157
+ # Clean up expired entries
158
+ #
159
+ # @return [Integer] Number of entries deleted
160
+ def cleanup_expired
161
+ result = query_value(<<~SQL, [project_dir])
162
+ SELECT COUNT(*) FROM #{table_name}
163
+ WHERE project_dir = ?
164
+ AND expires_at IS NOT NULL
165
+ AND datetime(expires_at) <= datetime('now')
166
+ SQL
167
+
168
+ execute(<<~SQL, [project_dir])
169
+ DELETE FROM #{table_name}
170
+ WHERE project_dir = ?
171
+ AND expires_at IS NOT NULL
172
+ AND datetime(expires_at) <= datetime('now')
173
+ SQL
174
+
175
+ count = result || 0
176
+ if count > 0
177
+ Aidp.log_info("model_cache_repo", "cleanup_expired",
178
+ deleted: count)
179
+ end
180
+ count
181
+ end
182
+
183
+ # Get model count for a provider
184
+ #
185
+ # @param provider_name [String] Provider name
186
+ # @return [Integer] Number of cached models
187
+ def model_count(provider_name)
188
+ models = get_cached_models(provider_name)
189
+ models&.size || 0
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end