@sesamespace/hivemind 0.8.13 → 0.10.0
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.
- package/README.md +2 -1
- package/dist/{chunk-MLY4VFOO.js → chunk-BHCDOHSK.js} +3 -3
- package/dist/{chunk-PFZO67E2.js → chunk-DPLCEMEC.js} +2 -2
- package/dist/{chunk-HTLHMXAL.js → chunk-FBQBBAPZ.js} +2 -2
- package/dist/{chunk-NSTTILSN.js → chunk-FK6WYXRM.js} +79 -2
- package/dist/chunk-FK6WYXRM.js.map +1 -0
- package/dist/{chunk-LJHJGDKY.js → chunk-ICSJNKI6.js} +62 -2
- package/dist/chunk-ICSJNKI6.js.map +1 -0
- package/dist/{chunk-4Y7A25UG.js → chunk-IXBIAX76.js} +2 -2
- package/dist/{chunk-ZM7RK5YV.js → chunk-M3A2WRXM.js} +560 -37
- package/dist/chunk-M3A2WRXM.js.map +1 -0
- package/dist/commands/fleet.js +3 -3
- package/dist/commands/init.js +3 -3
- package/dist/commands/start.js +3 -3
- package/dist/commands/upgrade.js +1 -1
- package/dist/commands/watchdog.js +3 -3
- package/dist/dashboard.html +873 -131
- package/dist/index.js +2 -2
- package/dist/main.js +375 -7
- package/dist/main.js.map +1 -1
- package/dist/start.js +1 -1
- package/install.sh +162 -0
- package/package.json +24 -23
- package/packages/memory/Cargo.lock +6480 -0
- package/packages/memory/Cargo.toml +21 -0
- package/packages/memory/src/src/context.rs +179 -0
- package/packages/memory/src/src/embeddings.rs +51 -0
- package/packages/memory/src/src/main.rs +887 -0
- package/packages/memory/src/src/promotion.rs +808 -0
- package/packages/memory/src/src/scoring.rs +142 -0
- package/packages/memory/src/src/store.rs +460 -0
- package/packages/memory/src/src/tasks.rs +321 -0
- package/.pnpmrc.json +0 -1
- package/AUTO-DEBUG-DESIGN.md +0 -267
- package/DASHBOARD-PLAN.md +0 -206
- package/MEMORY-ENHANCEMENT-PLAN.md +0 -211
- package/TOOL-USE-DESIGN.md +0 -173
- package/dist/chunk-LJHJGDKY.js.map +0 -1
- package/dist/chunk-NSTTILSN.js.map +0 -1
- package/dist/chunk-ZM7RK5YV.js.map +0 -1
- package/docs/TOOL-PARITY-PLAN.md +0 -191
- package/src/memory/dashboard-integration.ts +0 -295
- package/src/memory/index.ts +0 -187
- package/src/memory/performance-test.ts +0 -208
- package/src/memory/processors/agent-sync.ts +0 -312
- package/src/memory/processors/command-learner.ts +0 -298
- package/src/memory/processors/memory-api-client.ts +0 -105
- package/src/memory/processors/message-flow-integration.ts +0 -168
- package/src/memory/processors/research-digester.ts +0 -204
- package/test-caitlin-access.md +0 -11
- /package/dist/{chunk-MLY4VFOO.js.map → chunk-BHCDOHSK.js.map} +0 -0
- /package/dist/{chunk-PFZO67E2.js.map → chunk-DPLCEMEC.js.map} +0 -0
- /package/dist/{chunk-HTLHMXAL.js.map → chunk-FBQBBAPZ.js.map} +0 -0
- /package/dist/{chunk-4Y7A25UG.js.map → chunk-IXBIAX76.js.map} +0 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
use arrow_array::{RecordBatch, RecordBatchIterator, StringArray};
|
|
3
|
+
use arrow_schema::{DataType, Field, Schema};
|
|
4
|
+
use chrono::Utc;
|
|
5
|
+
use futures::stream::TryStreamExt;
|
|
6
|
+
use lancedb::{connection::Connection, query::ExecutableQuery, query::QueryBase, Table};
|
|
7
|
+
use serde::{Deserialize, Serialize};
|
|
8
|
+
use std::sync::Arc;
|
|
9
|
+
|
|
10
|
+
const TASKS_TABLE: &str = "tasks";
|
|
11
|
+
|
|
12
|
+
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
13
|
+
pub struct TaskRecord {
|
|
14
|
+
pub id: String,
|
|
15
|
+
pub context_name: String,
|
|
16
|
+
pub title: String,
|
|
17
|
+
pub description: String,
|
|
18
|
+
pub status: String,
|
|
19
|
+
pub blocked_by: String, // JSON array stored as string
|
|
20
|
+
pub created_at: String,
|
|
21
|
+
pub updated_at: String,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[derive(Debug, Deserialize)]
|
|
25
|
+
pub struct TaskInput {
|
|
26
|
+
pub context_name: String,
|
|
27
|
+
pub title: String,
|
|
28
|
+
pub description: String,
|
|
29
|
+
#[serde(default = "default_status")]
|
|
30
|
+
pub status: String,
|
|
31
|
+
#[serde(default)]
|
|
32
|
+
pub blocked_by: Vec<String>,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn default_status() -> String {
|
|
36
|
+
"planned".to_string()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[derive(Debug, Deserialize)]
|
|
40
|
+
pub struct TaskUpdate {
|
|
41
|
+
pub status: Option<String>,
|
|
42
|
+
pub title: Option<String>,
|
|
43
|
+
pub description: Option<String>,
|
|
44
|
+
pub blocked_by: Option<Vec<String>>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub struct TaskStore {
|
|
48
|
+
db: Connection,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl TaskStore {
|
|
52
|
+
pub async fn new(db: Connection) -> Result<Self> {
|
|
53
|
+
let store = Self { db };
|
|
54
|
+
store.ensure_table().await?;
|
|
55
|
+
Ok(store)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn schema() -> Arc<Schema> {
|
|
59
|
+
Arc::new(Schema::new(vec![
|
|
60
|
+
Field::new("id", DataType::Utf8, false),
|
|
61
|
+
Field::new("context_name", DataType::Utf8, false),
|
|
62
|
+
Field::new("title", DataType::Utf8, false),
|
|
63
|
+
Field::new("description", DataType::Utf8, false),
|
|
64
|
+
Field::new("status", DataType::Utf8, false),
|
|
65
|
+
Field::new("blocked_by", DataType::Utf8, false),
|
|
66
|
+
Field::new("created_at", DataType::Utf8, false),
|
|
67
|
+
Field::new("updated_at", DataType::Utf8, false),
|
|
68
|
+
]))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async fn ensure_table(&self) -> Result<()> {
|
|
72
|
+
let names = self.db.table_names().execute().await?;
|
|
73
|
+
if !names.contains(&TASKS_TABLE.to_string()) {
|
|
74
|
+
let schema = Self::schema();
|
|
75
|
+
let batch = RecordBatch::new_empty(schema.clone());
|
|
76
|
+
let batches = RecordBatchIterator::new(vec![Ok(batch)], schema);
|
|
77
|
+
self.db
|
|
78
|
+
.create_table(TASKS_TABLE, Box::new(batches))
|
|
79
|
+
.execute()
|
|
80
|
+
.await?;
|
|
81
|
+
tracing::info!("Created tasks table");
|
|
82
|
+
}
|
|
83
|
+
Ok(())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pub async fn create_task(&self, input: TaskInput) -> Result<TaskRecord> {
|
|
87
|
+
let id = uuid::Uuid::new_v4().to_string();
|
|
88
|
+
let now = Utc::now().to_rfc3339();
|
|
89
|
+
let blocked_by_json = serde_json::to_string(&input.blocked_by)?;
|
|
90
|
+
|
|
91
|
+
let task = TaskRecord {
|
|
92
|
+
id: id.clone(),
|
|
93
|
+
context_name: input.context_name.clone(),
|
|
94
|
+
title: input.title.clone(),
|
|
95
|
+
description: input.description.clone(),
|
|
96
|
+
status: input.status,
|
|
97
|
+
blocked_by: blocked_by_json.clone(),
|
|
98
|
+
created_at: now.clone(),
|
|
99
|
+
updated_at: now.clone(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
let schema = Self::schema();
|
|
103
|
+
let batch = RecordBatch::try_new(
|
|
104
|
+
schema.clone(),
|
|
105
|
+
vec![
|
|
106
|
+
Arc::new(StringArray::from(vec![task.id.as_str()])),
|
|
107
|
+
Arc::new(StringArray::from(vec![task.context_name.as_str()])),
|
|
108
|
+
Arc::new(StringArray::from(vec![task.title.as_str()])),
|
|
109
|
+
Arc::new(StringArray::from(vec![task.description.as_str()])),
|
|
110
|
+
Arc::new(StringArray::from(vec![task.status.as_str()])),
|
|
111
|
+
Arc::new(StringArray::from(vec![blocked_by_json.as_str()])),
|
|
112
|
+
Arc::new(StringArray::from(vec![task.created_at.as_str()])),
|
|
113
|
+
Arc::new(StringArray::from(vec![task.updated_at.as_str()])),
|
|
114
|
+
],
|
|
115
|
+
)?;
|
|
116
|
+
|
|
117
|
+
let table = self.db.open_table(TASKS_TABLE).execute().await?;
|
|
118
|
+
let batches = RecordBatchIterator::new(vec![Ok(batch)], schema);
|
|
119
|
+
table.add(Box::new(batches)).execute().await?;
|
|
120
|
+
|
|
121
|
+
tracing::debug!("Created task {} in context {}", task.id, task.context_name);
|
|
122
|
+
Ok(task)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pub async fn list_tasks(
|
|
126
|
+
&self,
|
|
127
|
+
context: &str,
|
|
128
|
+
status_filter: Option<&str>,
|
|
129
|
+
) -> Result<Vec<TaskRecord>> {
|
|
130
|
+
let table = self.db.open_table(TASKS_TABLE).execute().await?;
|
|
131
|
+
|
|
132
|
+
let filter = match status_filter {
|
|
133
|
+
Some(status) => format!("context_name = '{}' AND status = '{}'", context, status),
|
|
134
|
+
None => format!("context_name = '{}'", context),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
let results = table.query().only_if(filter).execute().await?;
|
|
138
|
+
|
|
139
|
+
let mut tasks = Vec::new();
|
|
140
|
+
let batches: Vec<RecordBatch> = results.try_collect().await?;
|
|
141
|
+
|
|
142
|
+
for batch in &batches {
|
|
143
|
+
let ids = batch
|
|
144
|
+
.column_by_name("id")
|
|
145
|
+
.unwrap()
|
|
146
|
+
.as_any()
|
|
147
|
+
.downcast_ref::<StringArray>()
|
|
148
|
+
.unwrap();
|
|
149
|
+
let ctx_names = batch
|
|
150
|
+
.column_by_name("context_name")
|
|
151
|
+
.unwrap()
|
|
152
|
+
.as_any()
|
|
153
|
+
.downcast_ref::<StringArray>()
|
|
154
|
+
.unwrap();
|
|
155
|
+
let titles = batch
|
|
156
|
+
.column_by_name("title")
|
|
157
|
+
.unwrap()
|
|
158
|
+
.as_any()
|
|
159
|
+
.downcast_ref::<StringArray>()
|
|
160
|
+
.unwrap();
|
|
161
|
+
let descriptions = batch
|
|
162
|
+
.column_by_name("description")
|
|
163
|
+
.unwrap()
|
|
164
|
+
.as_any()
|
|
165
|
+
.downcast_ref::<StringArray>()
|
|
166
|
+
.unwrap();
|
|
167
|
+
let statuses = batch
|
|
168
|
+
.column_by_name("status")
|
|
169
|
+
.unwrap()
|
|
170
|
+
.as_any()
|
|
171
|
+
.downcast_ref::<StringArray>()
|
|
172
|
+
.unwrap();
|
|
173
|
+
let blocked_bys = batch
|
|
174
|
+
.column_by_name("blocked_by")
|
|
175
|
+
.unwrap()
|
|
176
|
+
.as_any()
|
|
177
|
+
.downcast_ref::<StringArray>()
|
|
178
|
+
.unwrap();
|
|
179
|
+
let created_ats = batch
|
|
180
|
+
.column_by_name("created_at")
|
|
181
|
+
.unwrap()
|
|
182
|
+
.as_any()
|
|
183
|
+
.downcast_ref::<StringArray>()
|
|
184
|
+
.unwrap();
|
|
185
|
+
let updated_ats = batch
|
|
186
|
+
.column_by_name("updated_at")
|
|
187
|
+
.unwrap()
|
|
188
|
+
.as_any()
|
|
189
|
+
.downcast_ref::<StringArray>()
|
|
190
|
+
.unwrap();
|
|
191
|
+
|
|
192
|
+
for i in 0..batch.num_rows() {
|
|
193
|
+
tasks.push(TaskRecord {
|
|
194
|
+
id: ids.value(i).to_string(),
|
|
195
|
+
context_name: ctx_names.value(i).to_string(),
|
|
196
|
+
title: titles.value(i).to_string(),
|
|
197
|
+
description: descriptions.value(i).to_string(),
|
|
198
|
+
status: statuses.value(i).to_string(),
|
|
199
|
+
blocked_by: blocked_bys.value(i).to_string(),
|
|
200
|
+
created_at: created_ats.value(i).to_string(),
|
|
201
|
+
updated_at: updated_ats.value(i).to_string(),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Sort by created_at
|
|
207
|
+
tasks.sort_by(|a, b| a.created_at.cmp(&b.created_at));
|
|
208
|
+
|
|
209
|
+
Ok(tasks)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
pub async fn get_task(&self, id: &str) -> Result<Option<TaskRecord>> {
|
|
213
|
+
let table = self.db.open_table(TASKS_TABLE).execute().await?;
|
|
214
|
+
let results = table
|
|
215
|
+
.query()
|
|
216
|
+
.only_if(format!("id = '{}'", id))
|
|
217
|
+
.execute()
|
|
218
|
+
.await?;
|
|
219
|
+
|
|
220
|
+
let batches: Vec<RecordBatch> = results.try_collect().await?;
|
|
221
|
+
for batch in &batches {
|
|
222
|
+
if batch.num_rows() > 0 {
|
|
223
|
+
let ids = batch.column_by_name("id").unwrap().as_any().downcast_ref::<StringArray>().unwrap();
|
|
224
|
+
let ctx_names = batch.column_by_name("context_name").unwrap().as_any().downcast_ref::<StringArray>().unwrap();
|
|
225
|
+
let titles = batch.column_by_name("title").unwrap().as_any().downcast_ref::<StringArray>().unwrap();
|
|
226
|
+
let descriptions = batch.column_by_name("description").unwrap().as_any().downcast_ref::<StringArray>().unwrap();
|
|
227
|
+
let statuses = batch.column_by_name("status").unwrap().as_any().downcast_ref::<StringArray>().unwrap();
|
|
228
|
+
let blocked_bys = batch.column_by_name("blocked_by").unwrap().as_any().downcast_ref::<StringArray>().unwrap();
|
|
229
|
+
let created_ats = batch.column_by_name("created_at").unwrap().as_any().downcast_ref::<StringArray>().unwrap();
|
|
230
|
+
let updated_ats = batch.column_by_name("updated_at").unwrap().as_any().downcast_ref::<StringArray>().unwrap();
|
|
231
|
+
|
|
232
|
+
return Ok(Some(TaskRecord {
|
|
233
|
+
id: ids.value(0).to_string(),
|
|
234
|
+
context_name: ctx_names.value(0).to_string(),
|
|
235
|
+
title: titles.value(0).to_string(),
|
|
236
|
+
description: descriptions.value(0).to_string(),
|
|
237
|
+
status: statuses.value(0).to_string(),
|
|
238
|
+
blocked_by: blocked_bys.value(0).to_string(),
|
|
239
|
+
created_at: created_ats.value(0).to_string(),
|
|
240
|
+
updated_at: updated_ats.value(0).to_string(),
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
Ok(None)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
pub async fn update_task(&self, id: &str, update: TaskUpdate) -> Result<Option<TaskRecord>> {
|
|
249
|
+
let existing = self.get_task(id).await?;
|
|
250
|
+
let Some(mut task) = existing else {
|
|
251
|
+
return Ok(None);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Apply updates
|
|
255
|
+
if let Some(status) = update.status {
|
|
256
|
+
task.status = status;
|
|
257
|
+
}
|
|
258
|
+
if let Some(title) = update.title {
|
|
259
|
+
task.title = title;
|
|
260
|
+
}
|
|
261
|
+
if let Some(description) = update.description {
|
|
262
|
+
task.description = description;
|
|
263
|
+
}
|
|
264
|
+
if let Some(blocked_by) = update.blocked_by {
|
|
265
|
+
task.blocked_by = serde_json::to_string(&blocked_by)?;
|
|
266
|
+
}
|
|
267
|
+
task.updated_at = Utc::now().to_rfc3339();
|
|
268
|
+
|
|
269
|
+
// Delete and reinsert
|
|
270
|
+
let table = self.db.open_table(TASKS_TABLE).execute().await?;
|
|
271
|
+
table.delete(&format!("id = '{}'", id)).await?;
|
|
272
|
+
|
|
273
|
+
let schema = Self::schema();
|
|
274
|
+
let batch = RecordBatch::try_new(
|
|
275
|
+
schema.clone(),
|
|
276
|
+
vec![
|
|
277
|
+
Arc::new(StringArray::from(vec![task.id.as_str()])),
|
|
278
|
+
Arc::new(StringArray::from(vec![task.context_name.as_str()])),
|
|
279
|
+
Arc::new(StringArray::from(vec![task.title.as_str()])),
|
|
280
|
+
Arc::new(StringArray::from(vec![task.description.as_str()])),
|
|
281
|
+
Arc::new(StringArray::from(vec![task.status.as_str()])),
|
|
282
|
+
Arc::new(StringArray::from(vec![task.blocked_by.as_str()])),
|
|
283
|
+
Arc::new(StringArray::from(vec![task.created_at.as_str()])),
|
|
284
|
+
Arc::new(StringArray::from(vec![task.updated_at.as_str()])),
|
|
285
|
+
],
|
|
286
|
+
)?;
|
|
287
|
+
|
|
288
|
+
let batches = RecordBatchIterator::new(vec![Ok(batch)], schema);
|
|
289
|
+
table.add(Box::new(batches)).execute().await?;
|
|
290
|
+
|
|
291
|
+
Ok(Some(task))
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// Get the next available task for a context:
|
|
295
|
+
/// - Status is "planned" (not active/complete/archived)
|
|
296
|
+
/// - Not blocked by any incomplete tasks
|
|
297
|
+
pub async fn get_next_task(&self, context: &str) -> Result<Option<TaskRecord>> {
|
|
298
|
+
let planned = self.list_tasks(context, Some("planned")).await?;
|
|
299
|
+
let all_tasks = self.list_tasks(context, None).await?;
|
|
300
|
+
|
|
301
|
+
// Build a set of complete task IDs
|
|
302
|
+
let complete_ids: std::collections::HashSet<String> = all_tasks
|
|
303
|
+
.iter()
|
|
304
|
+
.filter(|t| t.status == "complete" || t.status == "archived")
|
|
305
|
+
.map(|t| t.id.clone())
|
|
306
|
+
.collect();
|
|
307
|
+
|
|
308
|
+
for task in planned {
|
|
309
|
+
let blocked_by: Vec<String> =
|
|
310
|
+
serde_json::from_str(&task.blocked_by).unwrap_or_default();
|
|
311
|
+
|
|
312
|
+
// Task is available if all blockers are complete
|
|
313
|
+
let is_blocked = blocked_by.iter().any(|b| !complete_ids.contains(b));
|
|
314
|
+
if !is_blocked {
|
|
315
|
+
return Ok(Some(task));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
Ok(None)
|
|
320
|
+
}
|
|
321
|
+
}
|
package/.pnpmrc.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"onlyBuiltDependencies":["better-sqlite3"]}
|
package/AUTO-DEBUG-DESIGN.md
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
# Auto-Debug Agent — Design Doc
|
|
2
|
-
|
|
3
|
-
## Vision
|
|
4
|
-
|
|
5
|
-
A Hivemind background processor that watches agent logs, detects errors, diagnoses root causes, and submits fixes as PRs — creating a self-healing codebase. Any fleet agent (e.g. Caitlin) can run this processor to continuously improve the system while using it.
|
|
6
|
-
|
|
7
|
-
## How It Works
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
Logs → Detect → Deduplicate → Diagnose → Fix → PR → Verify
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
### 1. Log Watcher (`log-watcher` processor)
|
|
14
|
-
|
|
15
|
-
Tails all Hivemind log files:
|
|
16
|
-
- `/tmp/hivemind-agent.log`
|
|
17
|
-
- `/tmp/hivemind-error.log`
|
|
18
|
-
- `/tmp/hivemind-watchdog.log`
|
|
19
|
-
- `/tmp/hivemind-memory.log`
|
|
20
|
-
- `/tmp/hivemind-memory-error.log`
|
|
21
|
-
|
|
22
|
-
**Detection rules:**
|
|
23
|
-
- Stack traces (multiline, starts with `Error:` or `at ...`)
|
|
24
|
-
- Uncaught exceptions / unhandled rejections
|
|
25
|
-
- Repeated warnings (same message 3+ times in 5 min)
|
|
26
|
-
- Process crashes (exit codes ≠ 0)
|
|
27
|
-
- Health check failures logged by watchdog
|
|
28
|
-
- Memory daemon errors
|
|
29
|
-
- Sesame connection failures
|
|
30
|
-
|
|
31
|
-
**Output:** `ErrorEvent` objects with:
|
|
32
|
-
```typescript
|
|
33
|
-
interface ErrorEvent {
|
|
34
|
-
id: string; // hash of normalized stack trace
|
|
35
|
-
timestamp: Date;
|
|
36
|
-
source: "agent" | "watchdog" | "memory";
|
|
37
|
-
level: "error" | "crash" | "repeated-warning";
|
|
38
|
-
message: string;
|
|
39
|
-
stackTrace?: string;
|
|
40
|
-
logFile: string;
|
|
41
|
-
lineNumber: number;
|
|
42
|
-
occurrences: number; // count within dedup window
|
|
43
|
-
context: string[]; // surrounding log lines (±10)
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### 2. Deduplication & Prioritization
|
|
48
|
-
|
|
49
|
-
Not every error deserves a PR. The processor maintains a local error registry:
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
interface ErrorRegistry {
|
|
53
|
-
// key: error id (normalized stack hash)
|
|
54
|
-
errors: Map<string, {
|
|
55
|
-
firstSeen: Date;
|
|
56
|
-
lastSeen: Date;
|
|
57
|
-
totalOccurrences: number;
|
|
58
|
-
status: "new" | "investigating" | "fix-submitted" | "resolved" | "wont-fix";
|
|
59
|
-
prUrl?: string;
|
|
60
|
-
severity: number; // 0-10, auto-calculated
|
|
61
|
-
}>;
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**Severity scoring:**
|
|
66
|
-
- Crash/uncaught exception: +5
|
|
67
|
-
- Occurs on every startup: +3
|
|
68
|
-
- Frequency (>10/hour): +2
|
|
69
|
-
- Affects Sesame connectivity: +2
|
|
70
|
-
- Memory daemon related: +1
|
|
71
|
-
- Already has a PR open: -10 (skip)
|
|
72
|
-
- Marked wont-fix: -10 (skip)
|
|
73
|
-
|
|
74
|
-
**Threshold:** Only investigate errors with severity ≥ 3.
|
|
75
|
-
|
|
76
|
-
### 3. Diagnosis Engine
|
|
77
|
-
|
|
78
|
-
When an error crosses the threshold, the processor:
|
|
79
|
-
|
|
80
|
-
1. **Locate source:** Parse stack trace → map to source files in the repo
|
|
81
|
-
2. **Gather context:** Use `code-indexer` data to understand the file, function, and dependencies
|
|
82
|
-
3. **Check git blame:** Was this recently changed? By whom?
|
|
83
|
-
4. **Search for related:** Query `task-tracker` for related tasks, check if a fix is already in progress
|
|
84
|
-
5. **Build diagnosis prompt:** Assemble all context into a structured prompt for the agent's LLM
|
|
85
|
-
|
|
86
|
-
The diagnosis is done by the agent itself (not a separate LLM call) — it feeds into the agent's normal message processing as an internal task.
|
|
87
|
-
|
|
88
|
-
### 4. Fix Generation
|
|
89
|
-
|
|
90
|
-
Once diagnosed, the agent:
|
|
91
|
-
|
|
92
|
-
1. Creates a feature branch: `auto-fix/{error-id-short}`
|
|
93
|
-
2. Makes the code change
|
|
94
|
-
3. Runs `tsup` to verify build
|
|
95
|
-
4. Runs tests if available
|
|
96
|
-
5. Commits with a structured message:
|
|
97
|
-
```
|
|
98
|
-
fix(auto-debug): {short description}
|
|
99
|
-
|
|
100
|
-
Error: {original error message}
|
|
101
|
-
Occurrences: {count} over {timespan}
|
|
102
|
-
Source: {log file}
|
|
103
|
-
|
|
104
|
-
Root cause: {diagnosis}
|
|
105
|
-
|
|
106
|
-
Auto-generated by hivemind auto-debug processor
|
|
107
|
-
```
|
|
108
|
-
6. Pushes branch and opens PR
|
|
109
|
-
|
|
110
|
-
### 5. PR Template
|
|
111
|
-
|
|
112
|
-
```markdown
|
|
113
|
-
## 🤖 Auto-Debug Fix
|
|
114
|
-
|
|
115
|
-
**Error:** `{error message}`
|
|
116
|
-
**Source:** `{file}:{line}`
|
|
117
|
-
**Frequency:** {count} occurrences over {timespan}
|
|
118
|
-
**Severity:** {score}/10
|
|
119
|
-
|
|
120
|
-
### Diagnosis
|
|
121
|
-
{LLM-generated root cause analysis}
|
|
122
|
-
|
|
123
|
-
### Fix
|
|
124
|
-
{Description of what changed and why}
|
|
125
|
-
|
|
126
|
-
### Log Sample
|
|
127
|
-
```
|
|
128
|
-
{relevant log lines}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Verification
|
|
132
|
-
- [ ] Build passes (`tsup`)
|
|
133
|
-
- [ ] Error no longer reproduces (monitored for 30 min post-deploy)
|
|
134
|
-
|
|
135
|
-
---
|
|
136
|
-
*Auto-generated by hivemind auto-debug processor. Review before merging.*
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### 6. Post-Fix Monitoring
|
|
140
|
-
|
|
141
|
-
After a PR is merged and deployed:
|
|
142
|
-
- Watch for the same error ID in logs
|
|
143
|
-
- If it doesn't recur within 1 hour → mark as `resolved`
|
|
144
|
-
- If it recurs → reopen with additional context, bump severity
|
|
145
|
-
|
|
146
|
-
## Architecture
|
|
147
|
-
|
|
148
|
-
### Processor Class
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
class LogWatcher extends BackgroundProcess {
|
|
152
|
-
name = "log-watcher";
|
|
153
|
-
interval = 10_000; // check every 10s
|
|
154
|
-
|
|
155
|
-
private tailPositions: Map<string, number>; // track file offsets
|
|
156
|
-
private errorRegistry: ErrorRegistry;
|
|
157
|
-
private activeBranches: Set<string>;
|
|
158
|
-
|
|
159
|
-
async run(context: ProcessContext): Promise<ProcessResult> {
|
|
160
|
-
// 1. Read new log lines since last position
|
|
161
|
-
const newLines = this.readNewLines();
|
|
162
|
-
|
|
163
|
-
// 2. Detect errors
|
|
164
|
-
const errors = this.detectErrors(newLines);
|
|
165
|
-
|
|
166
|
-
// 3. Deduplicate and score
|
|
167
|
-
const actionable = this.processErrors(errors);
|
|
168
|
-
|
|
169
|
-
// 4. For high-severity new errors, queue investigation
|
|
170
|
-
for (const error of actionable) {
|
|
171
|
-
await this.queueInvestigation(error, context);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return { itemsProcessed: newLines.length, errors: [] };
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### Integration Points
|
|
180
|
-
|
|
181
|
-
- **code-indexer:** Provides file/function context for diagnosis
|
|
182
|
-
- **task-tracker:** Tracks fix progress, prevents duplicate work
|
|
183
|
-
- **agent-sync:** In fleet mode, coordinates so multiple agents don't fix the same bug
|
|
184
|
-
- **Sesame:** Posts status updates to a designated channel (e.g. `#hivemind-debug`)
|
|
185
|
-
- **GitHub:** Opens PRs via `gh` CLI
|
|
186
|
-
|
|
187
|
-
### Fleet Coordination
|
|
188
|
-
|
|
189
|
-
When multiple agents run this processor:
|
|
190
|
-
1. Error registry is shared via Sesame vault or a shared memory namespace
|
|
191
|
-
2. Lock mechanism: first agent to claim an error ID owns the investigation
|
|
192
|
-
3. Other agents provide additional log samples if they see the same error
|
|
193
|
-
4. Any agent can review/approve another agent's PR
|
|
194
|
-
|
|
195
|
-
## Configuration
|
|
196
|
-
|
|
197
|
-
```toml
|
|
198
|
-
[processors.log-watcher]
|
|
199
|
-
enabled = true
|
|
200
|
-
interval_ms = 10_000
|
|
201
|
-
severity_threshold = 3
|
|
202
|
-
log_files = [
|
|
203
|
-
"/tmp/hivemind-agent.log",
|
|
204
|
-
"/tmp/hivemind-error.log",
|
|
205
|
-
"/tmp/hivemind-watchdog.log",
|
|
206
|
-
"/tmp/hivemind-memory.log",
|
|
207
|
-
]
|
|
208
|
-
auto_pr = true # false = diagnose only, don't submit PRs
|
|
209
|
-
repo = "baileydavis2026/hivemind"
|
|
210
|
-
branch_prefix = "auto-fix"
|
|
211
|
-
notify_channel = "hivemind-debug" # Sesame channel for status updates
|
|
212
|
-
max_concurrent_fixes = 2 # don't overwhelm with PRs
|
|
213
|
-
cooldown_minutes = 30 # min time between PRs for same error class
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
## Safety & Guardrails
|
|
217
|
-
|
|
218
|
-
1. **Human review required:** PRs are never auto-merged. A human must approve.
|
|
219
|
-
2. **Scope limits:** Auto-debug only touches files in the Hivemind repo, never system files or other projects.
|
|
220
|
-
3. **Rate limiting:** Max 2 concurrent fix branches, 30-min cooldown per error class.
|
|
221
|
-
4. **Severity gating:** Only acts on errors above threshold — ignores transient/cosmetic issues.
|
|
222
|
-
5. **Rollback awareness:** If a fix introduces new errors, the processor detects this and comments on the PR.
|
|
223
|
-
6. **No secrets in PRs:** Log context is sanitized (API keys, tokens stripped) before inclusion in PR descriptions.
|
|
224
|
-
|
|
225
|
-
## Implementation Plan
|
|
226
|
-
|
|
227
|
-
### Phase 1: Log Watcher (detection only)
|
|
228
|
-
- Implement `LogWatcher` processor
|
|
229
|
-
- Error detection, deduplication, registry
|
|
230
|
-
- Sesame channel notifications for new errors
|
|
231
|
-
- Dashboard integration (error list view)
|
|
232
|
-
|
|
233
|
-
### Phase 2: Diagnosis
|
|
234
|
-
- Stack trace → source mapping
|
|
235
|
-
- Integration with code-indexer for context
|
|
236
|
-
- LLM-powered root cause analysis
|
|
237
|
-
- Diagnosis reports posted to Sesame
|
|
238
|
-
|
|
239
|
-
### Phase 3: Auto-Fix PRs
|
|
240
|
-
- Branch creation, code changes, PR submission
|
|
241
|
-
- Build verification
|
|
242
|
-
- Post-merge monitoring
|
|
243
|
-
- Fleet coordination (error locking)
|
|
244
|
-
|
|
245
|
-
### Phase 4: Learning
|
|
246
|
-
- Track fix success rate per error pattern
|
|
247
|
-
- Build pattern library (common fixes for common errors)
|
|
248
|
-
- Skip LLM for known patterns → direct fix
|
|
249
|
-
- Feed insights back to MEMORY-ENHANCEMENT-PLAN
|
|
250
|
-
|
|
251
|
-
## Success Metrics
|
|
252
|
-
|
|
253
|
-
- **Detection rate:** % of real errors caught vs. total errors in logs
|
|
254
|
-
- **False positive rate:** % of investigations that led to wont-fix
|
|
255
|
-
- **Fix success rate:** % of merged PRs that resolved the error
|
|
256
|
-
- **Time to fix:** From first error occurrence to PR merged
|
|
257
|
-
- **Regression rate:** % of fixes that introduced new errors
|
|
258
|
-
|
|
259
|
-
## Why This Works
|
|
260
|
-
|
|
261
|
-
The fastest path to a bug-free system is having the system's own users (agents) fix bugs as they encounter them. Every agent running Hivemind becomes a contributor to Hivemind's stability. The more agents deployed, the faster bugs are found and fixed. It's a flywheel:
|
|
262
|
-
|
|
263
|
-
```
|
|
264
|
-
More agents → More log coverage → More bugs found → More fixes → More stable → More agents
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
This is the software equivalent of an immune system — the codebase develops antibodies to its own failure modes.
|