@synergenius/flow-weaver-pack-weaver 0.9.7 → 0.9.9

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 (224) hide show
  1. package/dist/bot/agent-loop.d.ts +20 -0
  2. package/dist/bot/agent-loop.d.ts.map +1 -0
  3. package/dist/bot/agent-loop.js +331 -0
  4. package/dist/bot/agent-loop.js.map +1 -0
  5. package/dist/bot/agent-provider.d.ts.map +1 -1
  6. package/dist/bot/agent-provider.js +3 -2
  7. package/dist/bot/agent-provider.js.map +1 -1
  8. package/dist/bot/approvals.js +17 -8
  9. package/dist/bot/approvals.js.map +1 -1
  10. package/dist/bot/assistant-core.d.ts +17 -0
  11. package/dist/bot/assistant-core.d.ts.map +1 -1
  12. package/dist/bot/assistant-core.js +418 -60
  13. package/dist/bot/assistant-core.js.map +1 -1
  14. package/dist/bot/assistant-tools.d.ts +1 -1
  15. package/dist/bot/assistant-tools.d.ts.map +1 -1
  16. package/dist/bot/assistant-tools.js +283 -9
  17. package/dist/bot/assistant-tools.js.map +1 -1
  18. package/dist/bot/bot-agent-channel.d.ts.map +1 -1
  19. package/dist/bot/bot-agent-channel.js +2 -0
  20. package/dist/bot/bot-agent-channel.js.map +1 -1
  21. package/dist/bot/bot-manager.d.ts +4 -0
  22. package/dist/bot/bot-manager.d.ts.map +1 -1
  23. package/dist/bot/bot-manager.js +72 -27
  24. package/dist/bot/bot-manager.js.map +1 -1
  25. package/dist/bot/conversation-store.d.ts +6 -5
  26. package/dist/bot/conversation-store.d.ts.map +1 -1
  27. package/dist/bot/conversation-store.js +98 -42
  28. package/dist/bot/conversation-store.js.map +1 -1
  29. package/dist/bot/cost-store.d.ts +3 -0
  30. package/dist/bot/cost-store.d.ts.map +1 -1
  31. package/dist/bot/cost-store.js +21 -10
  32. package/dist/bot/cost-store.js.map +1 -1
  33. package/dist/bot/cost-tracker.d.ts.map +1 -1
  34. package/dist/bot/cost-tracker.js +14 -1
  35. package/dist/bot/cost-tracker.js.map +1 -1
  36. package/dist/bot/cron-parser.d.ts.map +1 -1
  37. package/dist/bot/cron-parser.js +2 -0
  38. package/dist/bot/cron-parser.js.map +1 -1
  39. package/dist/bot/cron-scheduler.d.ts.map +1 -1
  40. package/dist/bot/cron-scheduler.js +1 -0
  41. package/dist/bot/cron-scheduler.js.map +1 -1
  42. package/dist/bot/device-connection.d.ts +13 -0
  43. package/dist/bot/device-connection.d.ts.map +1 -0
  44. package/dist/bot/device-connection.js +102 -0
  45. package/dist/bot/device-connection.js.map +1 -0
  46. package/dist/bot/error-classifier.d.ts.map +1 -1
  47. package/dist/bot/error-classifier.js +5 -0
  48. package/dist/bot/error-classifier.js.map +1 -1
  49. package/dist/bot/file-lock.d.ts.map +1 -1
  50. package/dist/bot/file-lock.js +13 -3
  51. package/dist/bot/file-lock.js.map +1 -1
  52. package/dist/bot/file-watcher.d.ts.map +1 -1
  53. package/dist/bot/file-watcher.js +1 -0
  54. package/dist/bot/file-watcher.js.map +1 -1
  55. package/dist/bot/genesis-prompt-context.d.ts +5 -0
  56. package/dist/bot/genesis-prompt-context.d.ts.map +1 -1
  57. package/dist/bot/genesis-prompt-context.js +55 -0
  58. package/dist/bot/genesis-prompt-context.js.map +1 -1
  59. package/dist/bot/genesis-store.d.ts +4 -0
  60. package/dist/bot/genesis-store.d.ts.map +1 -1
  61. package/dist/bot/genesis-store.js +79 -12
  62. package/dist/bot/genesis-store.js.map +1 -1
  63. package/dist/bot/improve-loop.d.ts +46 -0
  64. package/dist/bot/improve-loop.d.ts.map +1 -0
  65. package/dist/bot/improve-loop.js +592 -0
  66. package/dist/bot/improve-loop.js.map +1 -0
  67. package/dist/bot/insight-engine.d.ts +12 -0
  68. package/dist/bot/insight-engine.d.ts.map +1 -0
  69. package/dist/bot/insight-engine.js +256 -0
  70. package/dist/bot/insight-engine.js.map +1 -0
  71. package/dist/bot/knowledge-store.d.ts.map +1 -1
  72. package/dist/bot/knowledge-store.js +4 -1
  73. package/dist/bot/knowledge-store.js.map +1 -1
  74. package/dist/bot/pipeline-runner.d.ts.map +1 -1
  75. package/dist/bot/pipeline-runner.js +12 -4
  76. package/dist/bot/pipeline-runner.js.map +1 -1
  77. package/dist/bot/project-model.d.ts +25 -0
  78. package/dist/bot/project-model.d.ts.map +1 -0
  79. package/dist/bot/project-model.js +372 -0
  80. package/dist/bot/project-model.js.map +1 -0
  81. package/dist/bot/response-formatter.js +2 -3
  82. package/dist/bot/response-formatter.js.map +1 -1
  83. package/dist/bot/run-store.d.ts.map +1 -1
  84. package/dist/bot/run-store.js +10 -2
  85. package/dist/bot/run-store.js.map +1 -1
  86. package/dist/bot/safe-path.d.ts +1 -1
  87. package/dist/bot/safe-path.d.ts.map +1 -1
  88. package/dist/bot/safe-path.js +20 -1
  89. package/dist/bot/safe-path.js.map +1 -1
  90. package/dist/bot/safety.d.ts +10 -2
  91. package/dist/bot/safety.d.ts.map +1 -1
  92. package/dist/bot/safety.js +45 -2
  93. package/dist/bot/safety.js.map +1 -1
  94. package/dist/bot/session-state.d.ts +4 -0
  95. package/dist/bot/session-state.d.ts.map +1 -1
  96. package/dist/bot/session-state.js +52 -9
  97. package/dist/bot/session-state.js.map +1 -1
  98. package/dist/bot/slash-commands.d.ts.map +1 -1
  99. package/dist/bot/slash-commands.js +109 -3
  100. package/dist/bot/slash-commands.js.map +1 -1
  101. package/dist/bot/steering-engine.d.ts +67 -0
  102. package/dist/bot/steering-engine.d.ts.map +1 -0
  103. package/dist/bot/steering-engine.js +198 -0
  104. package/dist/bot/steering-engine.js.map +1 -0
  105. package/dist/bot/step-executor.d.ts.map +1 -1
  106. package/dist/bot/step-executor.js +62 -25
  107. package/dist/bot/step-executor.js.map +1 -1
  108. package/dist/bot/system-prompt.d.ts.map +1 -1
  109. package/dist/bot/system-prompt.js +5 -2
  110. package/dist/bot/system-prompt.js.map +1 -1
  111. package/dist/bot/task-queue.d.ts +6 -1
  112. package/dist/bot/task-queue.d.ts.map +1 -1
  113. package/dist/bot/task-queue.js +43 -4
  114. package/dist/bot/task-queue.js.map +1 -1
  115. package/dist/bot/tool-registry.d.ts +1 -1
  116. package/dist/bot/tool-registry.d.ts.map +1 -1
  117. package/dist/bot/tool-registry.js +65 -4
  118. package/dist/bot/tool-registry.js.map +1 -1
  119. package/dist/bot/trust-calculator.d.ts +34 -0
  120. package/dist/bot/trust-calculator.d.ts.map +1 -0
  121. package/dist/bot/trust-calculator.js +67 -0
  122. package/dist/bot/trust-calculator.js.map +1 -0
  123. package/dist/bot/types.d.ts +97 -0
  124. package/dist/bot/types.d.ts.map +1 -1
  125. package/dist/bot/update-checker.d.ts +21 -0
  126. package/dist/bot/update-checker.d.ts.map +1 -0
  127. package/dist/bot/update-checker.js +129 -0
  128. package/dist/bot/update-checker.js.map +1 -0
  129. package/dist/bot/weaver-tools.d.ts.map +1 -1
  130. package/dist/bot/weaver-tools.js +11 -4
  131. package/dist/bot/weaver-tools.js.map +1 -1
  132. package/dist/cli-bridge.d.ts +2 -0
  133. package/dist/cli-bridge.d.ts.map +1 -1
  134. package/dist/cli-bridge.js +3 -1
  135. package/dist/cli-bridge.js.map +1 -1
  136. package/dist/cli-handlers.d.ts +10 -1
  137. package/dist/cli-handlers.d.ts.map +1 -1
  138. package/dist/cli-handlers.js +141 -24
  139. package/dist/cli-handlers.js.map +1 -1
  140. package/dist/cli.d.ts +3 -0
  141. package/dist/cli.d.ts.map +1 -0
  142. package/dist/cli.js +749 -0
  143. package/dist/cli.js.map +1 -0
  144. package/dist/docs/weaver-config.md +15 -9
  145. package/dist/handlers/on-execution-completed.d.ts +11 -0
  146. package/dist/handlers/on-execution-completed.d.ts.map +1 -0
  147. package/dist/handlers/on-execution-completed.js +25 -0
  148. package/dist/handlers/on-execution-completed.js.map +1 -0
  149. package/dist/mcp-tools.d.ts.map +1 -1
  150. package/dist/mcp-tools.js +33 -0
  151. package/dist/mcp-tools.js.map +1 -1
  152. package/dist/node-types/genesis-approve.d.ts.map +1 -1
  153. package/dist/node-types/genesis-approve.js +28 -3
  154. package/dist/node-types/genesis-approve.js.map +1 -1
  155. package/dist/node-types/genesis-observe.d.ts.map +1 -1
  156. package/dist/node-types/genesis-observe.js +23 -13
  157. package/dist/node-types/genesis-observe.js.map +1 -1
  158. package/dist/node-types/genesis-propose.d.ts.map +1 -1
  159. package/dist/node-types/genesis-propose.js +8 -0
  160. package/dist/node-types/genesis-propose.js.map +1 -1
  161. package/dist/node-types/genesis-update-history.d.ts.map +1 -1
  162. package/dist/node-types/genesis-update-history.js +13 -0
  163. package/dist/node-types/genesis-update-history.js.map +1 -1
  164. package/dist/templates/weaver-template.d.ts +11 -0
  165. package/dist/templates/weaver-template.d.ts.map +1 -0
  166. package/dist/templates/weaver-template.js +53 -0
  167. package/dist/templates/weaver-template.js.map +1 -0
  168. package/dist/workflows/weaver-bot-session.d.ts +65 -0
  169. package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
  170. package/dist/workflows/weaver-bot-session.js +68 -0
  171. package/dist/workflows/weaver-bot-session.js.map +1 -0
  172. package/dist/workflows/weaver.d.ts +24 -0
  173. package/dist/workflows/weaver.d.ts.map +1 -0
  174. package/dist/workflows/weaver.js +28 -0
  175. package/dist/workflows/weaver.js.map +1 -0
  176. package/flowweaver.manifest.json +28 -1
  177. package/package.json +6 -3
  178. package/src/bot/agent-provider.ts +3 -2
  179. package/src/bot/approvals.ts +16 -8
  180. package/src/bot/assistant-core.ts +420 -63
  181. package/src/bot/assistant-tools.ts +291 -9
  182. package/src/bot/bot-agent-channel.ts +2 -0
  183. package/src/bot/bot-manager.ts +70 -29
  184. package/src/bot/conversation-store.ts +87 -42
  185. package/src/bot/cost-store.ts +20 -9
  186. package/src/bot/cost-tracker.ts +13 -1
  187. package/src/bot/cron-parser.ts +1 -0
  188. package/src/bot/cron-scheduler.ts +1 -0
  189. package/src/bot/device-connection.ts +102 -0
  190. package/src/bot/error-classifier.ts +5 -0
  191. package/src/bot/file-lock.ts +12 -2
  192. package/src/bot/file-watcher.ts +1 -0
  193. package/src/bot/genesis-prompt-context.ts +61 -0
  194. package/src/bot/genesis-store.ts +68 -16
  195. package/src/bot/improve-loop.ts +651 -0
  196. package/src/bot/insight-engine.ts +273 -0
  197. package/src/bot/knowledge-store.ts +4 -1
  198. package/src/bot/pipeline-runner.ts +11 -6
  199. package/src/bot/project-model.ts +404 -0
  200. package/src/bot/response-formatter.ts +2 -3
  201. package/src/bot/run-store.ts +5 -2
  202. package/src/bot/safe-path.ts +20 -1
  203. package/src/bot/safety.ts +57 -3
  204. package/src/bot/session-state.ts +47 -7
  205. package/src/bot/slash-commands.ts +111 -3
  206. package/src/bot/steering-engine.ts +233 -0
  207. package/src/bot/step-executor.ts +66 -26
  208. package/src/bot/system-prompt.ts +5 -2
  209. package/src/bot/task-queue.ts +40 -4
  210. package/src/bot/tool-registry.ts +67 -5
  211. package/src/bot/trust-calculator.ts +87 -0
  212. package/src/bot/types.ts +104 -0
  213. package/src/bot/update-checker.ts +138 -0
  214. package/src/bot/weaver-tools.ts +10 -4
  215. package/src/cli-bridge.ts +4 -1
  216. package/src/cli-handlers.ts +150 -29
  217. package/src/handlers/on-execution-completed.ts +30 -0
  218. package/src/mcp-tools.ts +38 -0
  219. package/src/node-types/genesis-approve.ts +28 -3
  220. package/src/node-types/genesis-observe.ts +23 -12
  221. package/src/node-types/genesis-propose.ts +8 -0
  222. package/src/node-types/genesis-update-history.ts +12 -0
  223. package/src/ui/evolution-panel.tsx +96 -0
  224. package/src/ui/insights-widget.tsx +77 -0
@@ -49,8 +49,8 @@ export class ConversationStore {
49
49
  fs.mkdirSync(this.baseDir, { recursive: true });
50
50
  }
51
51
 
52
- create(projectDir: string): ConversationRecord {
53
- const id = crypto.randomUUID().slice(0, 8);
52
+ async create(projectDir: string): Promise<ConversationRecord> {
53
+ const id = crypto.randomUUID().replace(/-/g, '').slice(0, 16);
54
54
  const now = Date.now();
55
55
  const record: ConversationRecord = {
56
56
  id,
@@ -67,10 +67,12 @@ export class ConversationStore {
67
67
  const convDir = path.join(this.baseDir, id);
68
68
  fs.mkdirSync(convDir, { recursive: true });
69
69
 
70
- // Add to index (sync write — no lock needed for create, index is new or we're the only writer)
71
- const index = this.readIndex();
72
- index.unshift(record);
73
- this.writeIndexUnsafe(index);
70
+ // Add to index under file lock for concurrent safety
71
+ await withFileLock(this.indexPath, () => {
72
+ const index = this.readIndex();
73
+ index.unshift(record);
74
+ this.writeIndexAtomic(index);
75
+ });
74
76
 
75
77
  return record;
76
78
  }
@@ -89,11 +91,13 @@ export class ConversationStore {
89
91
  return index.length > 0 ? index[0] : null;
90
92
  }
91
93
 
92
- delete(id: string): void {
93
- // Remove from index (sync delete doesn't need lock)
94
- const index = this.readIndex();
95
- const filtered = index.filter(c => c.id !== id);
96
- this.writeIndexUnsafe(filtered);
94
+ async delete(id: string): Promise<void> {
95
+ // Remove from index under file lock for concurrent safety
96
+ await withFileLock(this.indexPath, () => {
97
+ const index = this.readIndex();
98
+ const filtered = index.filter(c => c.id !== id);
99
+ this.writeIndexAtomic(filtered);
100
+ });
97
101
 
98
102
  // Remove files
99
103
  const convDir = path.join(this.baseDir, id);
@@ -108,6 +112,7 @@ export class ConversationStore {
108
112
 
109
113
  const content = fs.readFileSync(msgPath, 'utf-8');
110
114
  const messages: AgentMessage[] = [];
115
+ let corruptCount = 0;
111
116
 
112
117
  for (const line of content.split('\n')) {
113
118
  if (!line.trim()) continue;
@@ -121,33 +126,43 @@ export class ConversationStore {
121
126
  if (stored.toolCallId) msg.toolCallId = stored.toolCallId;
122
127
  messages.push(msg);
123
128
  } catch {
124
- // Skip corrupt lines
129
+ corruptCount++;
125
130
  }
126
131
  }
127
132
 
133
+ if (corruptCount > 0) {
134
+ console.warn(` (Warning: skipped ${corruptCount} corrupt message(s) in conversation ${id})`);
135
+ }
136
+
128
137
  return messages;
129
138
  }
130
139
 
131
- appendMessages(id: string, messages: AgentMessage[]): void {
132
- if (messages.length === 0) return;
140
+ appendMessages(id: string, messages: AgentMessage[]): boolean {
141
+ if (messages.length === 0) return true;
133
142
 
134
- const convDir = path.join(this.baseDir, id);
135
- fs.mkdirSync(convDir, { recursive: true });
136
- const msgPath = path.join(convDir, 'messages.ndjson');
137
-
138
- const lines = messages.map(m => {
139
- const stored: StoredMessage = {
140
- role: m.role,
141
- content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
142
- timestamp: Date.now(),
143
- tokens: Math.ceil((typeof m.content === 'string' ? m.content.length : JSON.stringify(m.content).length) / CHARS_PER_TOKEN),
144
- };
145
- if (m.toolCalls) stored.toolCalls = m.toolCalls;
146
- if (m.toolCallId) stored.toolCallId = m.toolCallId;
147
- return JSON.stringify(stored);
148
- });
143
+ try {
144
+ const convDir = path.join(this.baseDir, id);
145
+ fs.mkdirSync(convDir, { recursive: true });
146
+ const msgPath = path.join(convDir, 'messages.ndjson');
147
+
148
+ const lines = messages.map(m => {
149
+ const stored: StoredMessage = {
150
+ role: m.role,
151
+ content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
152
+ timestamp: Date.now(),
153
+ tokens: Math.ceil((typeof m.content === 'string' ? m.content.length : JSON.stringify(m.content).length) / CHARS_PER_TOKEN),
154
+ };
155
+ if (m.toolCalls) stored.toolCalls = m.toolCalls;
156
+ if (m.toolCallId) stored.toolCallId = m.toolCallId;
157
+ return JSON.stringify(stored);
158
+ });
149
159
 
150
- fs.appendFileSync(msgPath, lines.join('\n') + '\n');
160
+ fs.appendFileSync(msgPath, lines.join('\n') + '\n');
161
+ return true;
162
+ } catch (err) {
163
+ process.stderr.write(`[weaver] failed to persist ${messages.length} message(s) for conversation ${id}: ${err instanceof Error ? err.message : err}\n`);
164
+ return false;
165
+ }
151
166
  }
152
167
 
153
168
  async updateAfterTurn(id: string, newMessages: AgentMessage[], tokensUsed: number): Promise<void> {
@@ -167,12 +182,16 @@ export class ConversationStore {
167
182
  index.unshift(record);
168
183
  }
169
184
 
170
- // Cap index size
185
+ // Cap index size — clean up evicted conversation directories
171
186
  if (index.length > MAX_INDEX_SIZE) {
172
- index.splice(MAX_INDEX_SIZE);
187
+ const evicted = index.splice(MAX_INDEX_SIZE);
188
+ for (const conv of evicted) {
189
+ const convDir = path.join(this.baseDir, conv.id);
190
+ try { fs.rmSync(convDir, { recursive: true, force: true }); } catch { /* best effort */ }
191
+ }
173
192
  }
174
193
 
175
- this.writeIndexUnsafe(index);
194
+ this.writeIndexAtomic(index);
176
195
  });
177
196
  }
178
197
 
@@ -214,7 +233,7 @@ export class ConversationStore {
214
233
  const record = index.find(c => c.id === id);
215
234
  if (record) {
216
235
  record.title = title.slice(0, 80).replace(/\n/g, ' ').trim();
217
- this.writeIndexUnsafe(index);
236
+ this.writeIndexAtomic(index);
218
237
  }
219
238
  });
220
239
  }
@@ -225,7 +244,7 @@ export class ConversationStore {
225
244
  const record = index.find(c => c.id === id);
226
245
  if (record && !record.botIds.includes(botId)) {
227
246
  record.botIds.push(botId);
228
- this.writeIndexUnsafe(index);
247
+ this.writeIndexAtomic(index);
229
248
  }
230
249
  });
231
250
  }
@@ -238,17 +257,43 @@ export class ConversationStore {
238
257
  return JSON.parse(fs.readFileSync(this.indexPath, 'utf-8'));
239
258
  } catch (err) {
240
259
  if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] conversation index parse failed: ${err}\n`);
241
- return [];
260
+ // Try backup
261
+ return this.readBackupIndex();
242
262
  }
243
263
  }
244
264
 
245
- private async writeIndex(index: ConversationRecord[]): Promise<void> {
246
- await withFileLock(this.indexPath, () => {
247
- this.writeIndexUnsafe(index);
248
- });
265
+ private readBackupIndex(): ConversationRecord[] {
266
+ const backupPath = this.indexPath + '.bak';
267
+ if (!fs.existsSync(backupPath)) return [];
268
+ try {
269
+ const data = JSON.parse(fs.readFileSync(backupPath, 'utf-8'));
270
+ // Restore backup to main index
271
+ try { this.writeIndexAtomic(data); } catch { /* best effort */ }
272
+ return data;
273
+ } catch (err) {
274
+ if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] conversation backup parse failed: ${err}\n`);
275
+ return [];
276
+ }
249
277
  }
250
278
 
251
- private writeIndexUnsafe(index: ConversationRecord[]): void {
252
- fs.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
279
+ /** Atomic write: serialize to temp file, backup existing, rename into place. */
280
+ private writeIndexAtomic(index: ConversationRecord[]): void {
281
+ const tmpPath = this.indexPath + `.tmp.${process.pid}`;
282
+ const backupPath = this.indexPath + '.bak';
283
+ const content = JSON.stringify(index, null, 2);
284
+
285
+ // Write to temp file first
286
+ fs.writeFileSync(tmpPath, content);
287
+
288
+ // Backup current index if it exists
289
+ if (fs.existsSync(this.indexPath)) {
290
+ try { fs.copyFileSync(this.indexPath, backupPath); } catch { /* best effort */ }
291
+ }
292
+
293
+ // Atomic rename
294
+ fs.renameSync(tmpPath, this.indexPath);
295
+
296
+ // Always update backup after successful write
297
+ try { fs.copyFileSync(this.indexPath, backupPath); } catch { /* best effort */ }
253
298
  }
254
299
  }
@@ -3,6 +3,7 @@ import * as path from 'node:path';
3
3
  import * as os from 'node:os';
4
4
  import type { CostRecord, CostSummary } from './types.js';
5
5
  import { parseNdjson } from './safe-json.js';
6
+ import { withFileLock } from './file-lock.js';
6
7
 
7
8
  const MAX_ENTRIES = 10_000;
8
9
  const CAP_CHECK_INTERVAL = 100;
@@ -11,6 +12,7 @@ export class CostStore {
11
12
  private readonly dir: string;
12
13
  private readonly filePath: string;
13
14
  private appendCount = 0;
15
+ private pendingCap: Promise<void> | null = null;
14
16
 
15
17
  constructor(dir?: string) {
16
18
  this.dir = dir ?? process.env.WEAVER_DATA_DIR ?? path.join(os.homedir(), '.weaver');
@@ -22,10 +24,15 @@ export class CostStore {
22
24
  fs.appendFileSync(this.filePath, JSON.stringify(record) + '\n', 'utf-8');
23
25
  this.appendCount++;
24
26
  if (this.appendCount % CAP_CHECK_INTERVAL === 0) {
25
- this.enforceCap();
27
+ this.pendingCap = this.enforceCap().catch(() => {}).finally(() => { this.pendingCap = null; });
26
28
  }
27
29
  }
28
30
 
31
+ /** Wait for any in-flight cap enforcement to finish (used in tests). */
32
+ async waitForPendingCap(): Promise<void> {
33
+ if (this.pendingCap) await this.pendingCap;
34
+ }
35
+
29
36
  query(filters?: { since?: number; model?: string }): CostRecord[] {
30
37
  if (!fs.existsSync(this.filePath)) return [];
31
38
 
@@ -75,17 +82,21 @@ export class CostStore {
75
82
  return summary;
76
83
  }
77
84
 
78
- private enforceCap(): void {
85
+ private async enforceCap(): Promise<void> {
79
86
  if (!fs.existsSync(this.filePath)) return;
80
87
 
81
- const content = fs.readFileSync(this.filePath, 'utf-8');
82
- const lines = content.split('\n').filter((l) => l.trim().length > 0);
88
+ await withFileLock(this.filePath, () => {
89
+ if (!fs.existsSync(this.filePath)) return;
90
+
91
+ const content = fs.readFileSync(this.filePath, 'utf-8');
92
+ const lines = content.split('\n').filter((l) => l.trim().length > 0);
83
93
 
84
- if (lines.length <= MAX_ENTRIES) return;
94
+ if (lines.length <= MAX_ENTRIES) return;
85
95
 
86
- const kept = lines.slice(lines.length - MAX_ENTRIES);
87
- const tmpPath = this.filePath + '.tmp';
88
- fs.writeFileSync(tmpPath, kept.join('\n') + '\n', 'utf-8');
89
- fs.renameSync(tmpPath, this.filePath);
96
+ const kept = lines.slice(lines.length - MAX_ENTRIES);
97
+ const tmpPath = this.filePath + '.tmp';
98
+ fs.writeFileSync(tmpPath, kept.join('\n') + '\n', 'utf-8');
99
+ fs.renameSync(tmpPath, this.filePath);
100
+ });
90
101
  }
91
102
  }
@@ -33,7 +33,19 @@ export class CostTracker {
33
33
  }
34
34
 
35
35
  static estimateCost(model: string, usage: TokenUsage): number {
36
- const pricing = MODEL_PRICING[model];
36
+ let pricing = MODEL_PRICING[model];
37
+ if (!pricing) {
38
+ // Fallback: prefix match for model variants with date suffixes
39
+ // e.g., 'claude-sonnet-4-6-20260301' matches 'claude-sonnet-4-6'
40
+ // Use longest matching prefix to avoid ambiguity.
41
+ let bestKey = '';
42
+ for (const key of Object.keys(MODEL_PRICING)) {
43
+ if (model.startsWith(key) && key.length > bestKey.length) {
44
+ bestKey = key;
45
+ }
46
+ }
47
+ if (bestKey) pricing = MODEL_PRICING[bestKey];
48
+ }
37
49
  if (!pricing) return 0;
38
50
 
39
51
  return (
@@ -38,6 +38,7 @@ function parseField(token: string, min: number, max: number): CronField {
38
38
  end = max;
39
39
  } else if (rangeStr!.includes('-')) {
40
40
  [start, end] = rangeStr!.split('-').map(Number) as [number, number];
41
+ if (isNaN(start) || isNaN(end)) throw new Error(`Invalid range in "${part}"`);
41
42
  } else {
42
43
  start = parseInt(rangeStr!, 10);
43
44
  end = max;
@@ -15,6 +15,7 @@ export class CronScheduler extends EventEmitter {
15
15
  }
16
16
 
17
17
  start(): void {
18
+ this.stop();
18
19
  this.stopped = false;
19
20
  this.scheduleNext();
20
21
  }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Device Connection — Weaver-specific handlers for the core DeviceConnection.
3
+ *
4
+ * The transport (WebSocket, heartbeat, reconnect) lives in @synergenius/flow-weaver/agent.
5
+ * This module registers handlers for file operations, health, insights, improve status.
6
+ */
7
+
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
11
+
12
+ // Re-export the core DeviceConnection for convenience
13
+ export { DeviceConnection } from '@synergenius/flow-weaver/agent';
14
+ export type { DeviceConnectionOptions, DeviceInfo, DeviceEvent } from '@synergenius/flow-weaver/agent';
15
+
16
+ /**
17
+ * Register weaver-specific request handlers on a DeviceConnection.
18
+ */
19
+ export function registerWeaverHandlers(
20
+ conn: import('@synergenius/flow-weaver/agent').DeviceConnection,
21
+ projectDir: string,
22
+ ): void {
23
+ // Advertise weaver capabilities
24
+ conn.addCapability('file_read');
25
+ conn.addCapability('file_list');
26
+ conn.addCapability('health');
27
+ conn.addCapability('insights');
28
+ conn.addCapability('improve');
29
+ conn.addCapability('assistant');
30
+
31
+ // File read
32
+ conn.onRequest('file:read', async (_method, params) => {
33
+ const filePath = path.resolve(projectDir, String(params.path ?? ''));
34
+ if (!filePath.startsWith(projectDir)) throw new Error('Path outside project directory');
35
+ if (!fs.existsSync(filePath)) throw new Error('File not found');
36
+ const stat = fs.statSync(filePath);
37
+ if (stat.isDirectory()) {
38
+ return { type: 'directory', entries: fs.readdirSync(filePath) };
39
+ }
40
+ if (stat.size > 1_048_576) throw new Error('File too large (>1MB)');
41
+ return { type: 'file', content: fs.readFileSync(filePath, 'utf-8') };
42
+ });
43
+
44
+ // File list
45
+ conn.onRequest('file:list', async (_method, params) => {
46
+ const dirPath = path.resolve(projectDir, String(params.path ?? '.'));
47
+ if (!dirPath.startsWith(projectDir)) throw new Error('Path outside project directory');
48
+ if (!fs.existsSync(dirPath)) throw new Error('Directory not found');
49
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
50
+ return entries
51
+ .filter(e => !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'dist')
52
+ .map(e => ({
53
+ name: e.name,
54
+ type: e.isDirectory() ? 'directory' : 'file',
55
+ path: path.relative(projectDir, path.join(dirPath, e.name)),
56
+ }));
57
+ });
58
+
59
+ // Project health
60
+ conn.onRequest('health', async () => {
61
+ try {
62
+ const { ProjectModelStore } = await import('./project-model.js');
63
+ const pms = new ProjectModelStore(projectDir);
64
+ const model = await pms.getOrBuild();
65
+ return { health: model.health, trust: model.trust, cost: model.cost, bots: model.bots };
66
+ } catch {
67
+ return { error: 'Project model not available' };
68
+ }
69
+ });
70
+
71
+ // Insights
72
+ conn.onRequest('insights', async () => {
73
+ try {
74
+ const { ProjectModelStore } = await import('./project-model.js');
75
+ const { InsightEngine } = await import('./insight-engine.js');
76
+ const model = await new ProjectModelStore(projectDir).getOrBuild();
77
+ return new InsightEngine().analyze(model);
78
+ } catch {
79
+ return [];
80
+ }
81
+ });
82
+
83
+ // Improve status
84
+ conn.onRequest('improve:status', async () => {
85
+ try {
86
+ const summaryDir = path.join(os.homedir(), '.weaver', 'improve');
87
+ if (!fs.existsSync(summaryDir)) return { running: false, lastRun: null };
88
+ const files = fs.readdirSync(summaryDir).filter(f => f.endsWith('.json')).sort().reverse();
89
+ if (files.length === 0) return { running: false, lastRun: null };
90
+ const latest = JSON.parse(fs.readFileSync(path.join(summaryDir, files[0]!), 'utf-8'));
91
+ let running = false;
92
+ try {
93
+ const { execFileSync } = await import('node:child_process');
94
+ const worktrees = execFileSync('git', ['worktree', 'list'], { encoding: 'utf-8', cwd: projectDir });
95
+ running = worktrees.includes('weaver-improve');
96
+ } catch { /* git not available */ }
97
+ return { running, lastRun: latest };
98
+ } catch {
99
+ return { running: false, lastRun: null };
100
+ }
101
+ });
102
+ }
@@ -16,7 +16,12 @@ const PATTERNS: Array<{ pattern: RegExp; isTransient: boolean; guidance: string;
16
16
  { pattern: /EPIPE|ENOTFOUND/i, isTransient: true, guidance: 'Network error.', category: 'network' },
17
17
  { pattern: /401|authentication|invalid.*key/i, isTransient: false, guidance: 'Authentication failed. Check API key or run "weaver init".', category: 'auth' },
18
18
  { pattern: /403|forbidden/i, isTransient: false, guidance: 'Access denied. API key may lack permissions.', category: 'auth' },
19
+ { pattern: /400|bad request/i, isTransient: false, guidance: 'Bad request (400). Check input payload or parameters.', category: 'parse' },
20
+ { pattern: /404|not found/i, isTransient: false, guidance: 'Resource not found (404). Check URL or resource ID.', category: 'network' },
21
+ { pattern: /408|request timeout/i, isTransient: true, guidance: 'Request timeout (408). Will auto-retry.', category: 'timeout' },
22
+ { pattern: /422|unprocessable/i, isTransient: false, guidance: 'Unprocessable entity (422). Input is well-formed but semantically invalid.', category: 'parse' },
19
23
  { pattern: /429|rate.?limit|too many requests/i, isTransient: true, guidance: 'Rate limited. Wait a few minutes or reduce --parallel.', category: 'rate-limit' },
24
+ { pattern: /500|internal server error/i, isTransient: true, guidance: 'Internal server error (500). Will auto-retry.', category: 'network' },
20
25
  { pattern: /502|bad gateway/i, isTransient: true, guidance: 'Server error (502). Will auto-retry.', category: 'network' },
21
26
  { pattern: /503|service unavailable|overloaded/i, isTransient: true, guidance: 'Service overloaded. Will auto-retry.', category: 'network' },
22
27
  { pattern: /504|gateway timeout/i, isTransient: true, guidance: 'Gateway timeout (504). Will auto-retry.', category: 'timeout' },
@@ -58,8 +58,18 @@ export async function withFileLock<T>(
58
58
  continue;
59
59
  }
60
60
  } catch {
61
- // Can't read lock info, try to clean up
62
- try { fs.rmSync(lockDir, { recursive: true }); } catch { /* ignore */ }
61
+ // Can't read lock info lock holder may still be writing it.
62
+ // Only clean up if the lock directory itself is stale.
63
+ try {
64
+ const stat = fs.statSync(lockDir);
65
+ if (Date.now() - stat.mtimeMs > staleMs) {
66
+ try { fs.rmSync(lockDir, { recursive: true }); } catch { /* ignore */ }
67
+ }
68
+ } catch { /* lockDir gone, will retry */ }
69
+
70
+ if (attempt < retries) {
71
+ await new Promise(r => setTimeout(r, retryWait));
72
+ }
63
73
  continue;
64
74
  }
65
75
 
@@ -25,6 +25,7 @@ export class FileWatcher extends EventEmitter {
25
25
  }
26
26
 
27
27
  start(): void {
28
+ this.stop();
28
29
  this.stopped = false;
29
30
  this.lastMtime = this.getMtime();
30
31
  this.attachFsWatch();
@@ -133,3 +133,64 @@ export async function getWorkflowDescription(filePath: string): Promise<string>
133
133
  return '(workflow description unavailable)';
134
134
  }
135
135
  }
136
+
137
+ /**
138
+ * Builds intelligence context from the project model for Genesis proposals.
139
+ * Includes health scores, insights, operation effectiveness, and user preferences.
140
+ */
141
+ export async function getGenesisInsightContext(projectDir: string): Promise<string> {
142
+ try {
143
+ const { ProjectModelStore } = await import('./project-model.js');
144
+ const pms = new ProjectModelStore(projectDir);
145
+ const model = await pms.getOrBuild();
146
+
147
+ const lines: string[] = ['## Project Intelligence', ''];
148
+ lines.push(`Health: ${model.health.overall}/100`);
149
+ lines.push(`Trust Phase: ${model.trust.phase}`);
150
+ lines.push('');
151
+
152
+ // Top failure patterns as genesis candidates
153
+ const candidates = model.failurePatterns
154
+ .filter(f => !f.transient && f.occurrences >= 3)
155
+ .slice(0, 3);
156
+ if (candidates.length > 0) {
157
+ lines.push('### Top Issues (Genesis Candidates)');
158
+ for (const c of candidates) {
159
+ lines.push(`- [${c.occurrences}x] ${c.pattern} (${c.category})`);
160
+ }
161
+ lines.push('');
162
+ }
163
+
164
+ // Operation effectiveness
165
+ const ops = Object.entries(model.evolution.byOperationType);
166
+ if (ops.length > 0) {
167
+ lines.push('### Operation Effectiveness');
168
+ for (const [op, stats] of ops) {
169
+ lines.push(`- ${op}: ${Math.round(stats.effectiveness * 100)}% effective (${stats.applied}/${stats.proposed})`);
170
+ }
171
+ lines.push('');
172
+ }
173
+
174
+ // User preferences
175
+ if (model.userPreferences.autoApprovePatterns.length > 0) {
176
+ lines.push(`Auto-approve patterns: ${model.userPreferences.autoApprovePatterns.join(', ')}`);
177
+ }
178
+ if (model.userPreferences.neverApprovePatterns.length > 0) {
179
+ lines.push(`Never-approve patterns: ${model.userPreferences.neverApprovePatterns.join(', ')}`);
180
+ }
181
+
182
+ // Bot performance
183
+ const weakBots = model.bots.filter(b => b.successRate < 0.5 && b.totalTasksRun > 3);
184
+ if (weakBots.length > 0) {
185
+ lines.push('');
186
+ lines.push('### Underperforming Bots');
187
+ for (const b of weakBots) {
188
+ lines.push(`- ${b.name}: ${Math.round(b.successRate * 100)}% success (${b.totalTasksRun} tasks)`);
189
+ }
190
+ }
191
+
192
+ return lines.join('\n');
193
+ } catch {
194
+ return '';
195
+ }
196
+ }
@@ -33,9 +33,13 @@ export class GenesisStore {
33
33
  fs.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8');
34
34
  return { ...DEFAULT_CONFIG };
35
35
  }
36
- const content = fs.readFileSync(configPath, 'utf-8');
37
- const raw = jsonParseOr(content, {} as Record<string, unknown>, 'genesis config');
38
- return { ...DEFAULT_CONFIG, ...raw };
36
+ try {
37
+ const content = fs.readFileSync(configPath, 'utf-8');
38
+ const raw = jsonParseOr(content, {} as Record<string, unknown>, 'genesis config');
39
+ return { ...DEFAULT_CONFIG, ...raw };
40
+ } catch {
41
+ return { ...DEFAULT_CONFIG };
42
+ }
39
43
  }
40
44
 
41
45
  saveConfig(config: GenesisConfig): void {
@@ -45,18 +49,26 @@ export class GenesisStore {
45
49
 
46
50
  loadHistory(): GenesisHistory {
47
51
  const historyPath = path.join(this.genesisDir, 'history.json');
52
+ const fallback: GenesisHistory = { configHash: '', cycles: [] };
48
53
  if (!fs.existsSync(historyPath)) {
49
- return { configHash: '', cycles: [] };
54
+ return fallback;
55
+ }
56
+ try {
57
+ const content = fs.readFileSync(historyPath, 'utf-8');
58
+ const parsed = jsonParseOr<GenesisHistory>(content, null as unknown as GenesisHistory, 'genesis history');
59
+ if (parsed && Array.isArray(parsed.cycles)) return parsed;
60
+ // Primary file corrupt — try backup
61
+ return this.readJsonBackup<GenesisHistory>(historyPath, fallback);
62
+ } catch {
63
+ return this.readJsonBackup<GenesisHistory>(historyPath, fallback);
50
64
  }
51
- const content = fs.readFileSync(historyPath, 'utf-8');
52
- return jsonParseOr<GenesisHistory>(content, { configHash: '', cycles: [] }, 'genesis history');
53
65
  }
54
66
 
55
67
  appendCycle(cycle: GenesisCycleRecord): void {
56
68
  const history = this.loadHistory();
57
69
  history.cycles.push(cycle);
58
70
  this.ensureDirs();
59
- fs.writeFileSync(path.join(this.genesisDir, 'history.json'), JSON.stringify(history, null, 2), 'utf-8');
71
+ this.writeJsonAtomic(path.join(this.genesisDir, 'history.json'), history);
60
72
  }
61
73
 
62
74
  saveSnapshot(cycleId: string, content: string): string {
@@ -82,8 +94,12 @@ export class GenesisStore {
82
94
  getLastFingerprint(): GenesisFingerprint | null {
83
95
  const fpPath = path.join(this.genesisDir, 'fingerprint.json');
84
96
  if (!fs.existsSync(fpPath)) return null;
85
- const content = fs.readFileSync(fpPath, 'utf-8');
86
- return jsonParseOr<GenesisFingerprint | null>(content, null, 'genesis fingerprint');
97
+ try {
98
+ const content = fs.readFileSync(fpPath, 'utf-8');
99
+ return jsonParseOr<GenesisFingerprint | null>(content, null, 'genesis fingerprint');
100
+ } catch {
101
+ return null;
102
+ }
87
103
  }
88
104
 
89
105
  getRecentOutcomes(count: number): string[] {
@@ -142,19 +158,21 @@ export class GenesisStore {
142
158
  loadSelfHistory(): GenesisSelfMigrationRecord[] {
143
159
  const histPath = path.join(this.genesisDir, 'self-history.json');
144
160
  if (!fs.existsSync(histPath)) return [];
145
- const content = fs.readFileSync(histPath, 'utf-8');
146
- return jsonParseOr<GenesisSelfMigrationRecord[]>(content, [], 'genesis self-history');
161
+ try {
162
+ const content = fs.readFileSync(histPath, 'utf-8');
163
+ const parsed = jsonParseOr<GenesisSelfMigrationRecord[]>(content, null as unknown as GenesisSelfMigrationRecord[], 'genesis self-history');
164
+ if (Array.isArray(parsed)) return parsed;
165
+ return this.readJsonBackup<GenesisSelfMigrationRecord[]>(histPath, []);
166
+ } catch {
167
+ return this.readJsonBackup<GenesisSelfMigrationRecord[]>(histPath, []);
168
+ }
147
169
  }
148
170
 
149
171
  appendSelfMigration(record: GenesisSelfMigrationRecord): void {
150
172
  const records = this.loadSelfHistory();
151
173
  records.push(record);
152
174
  this.ensureDirs();
153
- fs.writeFileSync(
154
- path.join(this.genesisDir, 'self-history.json'),
155
- JSON.stringify(records, null, 2),
156
- 'utf-8',
157
- );
175
+ this.writeJsonAtomic(path.join(this.genesisDir, 'self-history.json'), records);
158
176
  }
159
177
 
160
178
  getSelfFailureCount(): number {
@@ -167,6 +185,40 @@ export class GenesisStore {
167
185
  return count;
168
186
  }
169
187
 
188
+ /** Atomic write: serialize to temp file, backup existing, rename into place. */
189
+ private writeJsonAtomic(filePath: string, data: unknown): void {
190
+ const tmpPath = filePath + `.tmp.${process.pid}`;
191
+ const backupPath = filePath + '.bak';
192
+ const content = JSON.stringify(data, null, 2);
193
+
194
+ fs.writeFileSync(tmpPath, content, 'utf-8');
195
+
196
+ // Backup current file before overwriting
197
+ if (fs.existsSync(filePath)) {
198
+ try { fs.copyFileSync(filePath, backupPath); } catch { /* best effort */ }
199
+ }
200
+
201
+ fs.renameSync(tmpPath, filePath);
202
+
203
+ // Update backup after successful write
204
+ try { fs.copyFileSync(filePath, backupPath); } catch { /* best effort */ }
205
+ }
206
+
207
+ /** Read from .bak file when primary is corrupt. */
208
+ private readJsonBackup<T>(filePath: string, fallback: T): T {
209
+ const backupPath = filePath + '.bak';
210
+ if (!fs.existsSync(backupPath)) return fallback;
211
+ try {
212
+ const content = fs.readFileSync(backupPath, 'utf-8');
213
+ const data = JSON.parse(content) as T;
214
+ // Restore backup to primary
215
+ try { this.writeJsonAtomic(filePath, data); } catch { /* best effort */ }
216
+ return data;
217
+ } catch {
218
+ return fallback;
219
+ }
220
+ }
221
+
170
222
  static hashFile(filePath: string): string {
171
223
  const content = fs.readFileSync(filePath);
172
224
  return crypto.createHash('sha256').update(content).digest('hex');