amalgm 0.1.48 → 0.1.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -49,7 +49,7 @@ function cleanString(value) {
49
49
  // Chat server (local Next.js/Electron) — same process tree; no cloud hop.
50
50
  const CHAT_SERVER_URL = `http://localhost:${runtimePort('chat-server')}`;
51
51
 
52
- const SCHEDULER_INTERVAL_MS = 30_000;
52
+ const SCHEDULER_INTERVAL_MS = 60_000;
53
53
 
54
54
  // Default working directory for automated runs (was /workspace in the container).
55
55
  const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || os.homedir();
@@ -12,6 +12,7 @@ const os = require('os');
12
12
  const { AMALGM_COMPUTER_ID, AMALGM_USER_ID, DEFAULT_CWD } = require('../config');
13
13
  const { runThroughChatServer } = require('../lib/chat-runner');
14
14
  const { hasSupabase, supabaseInsert, supabasePatch } = require('../lib/supabase');
15
+ const { recordEventRun } = require('./store');
15
16
  const {
16
17
  chatInputToLegacyFields,
17
18
  normalizeChatInput,
@@ -124,6 +125,7 @@ async function executeArtifactEvent(artifactOrTrigger, eventDef, payload, opts =
124
125
  projectPath,
125
126
  });
126
127
  const cwd = legacy.cwd || DEFAULT_CWD;
128
+ const runTriggerId = triggerId || artifactOrTrigger.id;
127
129
  const modelSelection = resolveModelSelection(
128
130
  harness,
129
131
  modelIdForSettings(legacy.modelId || model, harness, modelSettings),
@@ -135,6 +137,17 @@ async function executeArtifactEvent(artifactOrTrigger, eventDef, payload, opts =
135
137
  `[AmalgmMCP:Event] Starting agent run for ${artifactOrTrigger.id}:${eventDef.name} (session: ${codeSessionId}, cwd: ${cwd})`,
136
138
  );
137
139
 
140
+ recordEventRun(runTriggerId, {
141
+ runId: codeSessionId,
142
+ sessionId: codeSessionId,
143
+ triggerName: eventDef.name,
144
+ startedAt,
145
+ status: 'running',
146
+ harness,
147
+ projectPath: projectPath || null,
148
+ prompt: legacy.prompt,
149
+ }, { source: 'event_runs:start' });
150
+
138
151
  if (hasSupabase()) {
139
152
  try {
140
153
  const metadata = {
@@ -195,6 +208,16 @@ async function executeArtifactEvent(artifactOrTrigger, eventDef, payload, opts =
195
208
  `[AmalgmMCP:Event] ${artifactOrTrigger.id}:${eventDef.name} completed in ${durationMs}ms (output: ${outputText.length} chars)`,
196
209
  );
197
210
 
211
+ recordEventRun(runTriggerId, {
212
+ runId: codeSessionId,
213
+ sessionId: codeSessionId,
214
+ triggerName: eventDef.name,
215
+ finishedAt: new Date().toISOString(),
216
+ status: 'completed',
217
+ durationMs,
218
+ outputLength: outputText.length,
219
+ }, { source: 'event_runs:complete' });
220
+
198
221
  if (hasSupabase()) {
199
222
  supabasePatch('sessions', 'id', codeSessionId, {
200
223
  last_message_at: new Date().toISOString(),
@@ -207,6 +230,14 @@ async function executeArtifactEvent(artifactOrTrigger, eventDef, payload, opts =
207
230
  `[AmalgmMCP:Event] ${artifactOrTrigger.id}:${eventDef.name} failed:`,
208
231
  err.message,
209
232
  );
233
+ recordEventRun(runTriggerId, {
234
+ runId: codeSessionId,
235
+ sessionId: codeSessionId,
236
+ triggerName: eventDef.name,
237
+ finishedAt: new Date().toISOString(),
238
+ status: 'failed',
239
+ error: err.message,
240
+ }, { source: 'event_runs:failed' });
210
241
  if (hasSupabase()) {
211
242
  supabasePatch('sessions', 'id', codeSessionId, {
212
243
  last_message_at: new Date().toISOString(),
@@ -4,6 +4,7 @@
4
4
 
5
5
  const { EVENT_TRIGGERS_FILE, STORAGE_DIR } = require('../config');
6
6
  const { ensureDir, readJson, writeJsonAtomic } = require('../lib/storage');
7
+ const { openLocalDb } = require('../state/db');
7
8
  const {
8
9
  chatInputToLegacyFields,
9
10
  getChatInputText,
@@ -11,7 +12,22 @@ const {
11
12
  } = require('../../../lib/chatInput');
12
13
  const { DEFAULT_SELECTED_MODELS, getSelectedModel } = require('../lib/prefs');
13
14
  const credentialAdapter = require('../../credential-adapter');
14
- const { appendStateEvent } = require('../state/events');
15
+ const { appendStateEvent, insertStateEvent, publishStateEvent } = require('../state/events');
16
+
17
+ const DEFAULT_RUN_LIMIT = 100;
18
+
19
+ function nowIso() {
20
+ return new Date().toISOString();
21
+ }
22
+
23
+ function parseJson(value, fallback = null) {
24
+ if (typeof value !== 'string' || !value) return fallback;
25
+ try {
26
+ return JSON.parse(value);
27
+ } catch {
28
+ return fallback;
29
+ }
30
+ }
15
31
 
16
32
  function normalizeStoredTrigger(trigger) {
17
33
  const harness =
@@ -78,6 +94,7 @@ function migrateTriggersData(data) {
78
94
 
79
95
  function ensureTriggersDirs() {
80
96
  ensureDir(STORAGE_DIR);
97
+ openLocalDb();
81
98
  if (!readJson(EVENT_TRIGGERS_FILE, null)) {
82
99
  writeJsonAtomic(EVENT_TRIGGERS_FILE, { triggers: [] });
83
100
  }
@@ -110,4 +127,187 @@ function saveEventTriggers(data, options = {}) {
110
127
  publishEventTriggersChange(data, options.source || 'event-triggers:save');
111
128
  }
112
129
 
113
- module.exports = { ensureTriggersDirs, loadEventTriggers, saveEventTriggers };
130
+ function rowToEventRun(row) {
131
+ if (!row) return null;
132
+ const parsed = parseJson(row.run_json, {});
133
+ return {
134
+ ...parsed,
135
+ id: row.id,
136
+ runId: row.id,
137
+ triggerId: row.trigger_id,
138
+ status: row.status,
139
+ startedAt: row.started_at || parsed.startedAt || null,
140
+ finishedAt: row.finished_at || parsed.finishedAt || null,
141
+ sessionId: row.session_id || parsed.sessionId || null,
142
+ harness: row.harness || parsed.harness || null,
143
+ projectPath: row.project_path || parsed.projectPath || null,
144
+ error: row.error || parsed.error || null,
145
+ createdAt: row.created_at,
146
+ updatedAt: row.updated_at,
147
+ };
148
+ }
149
+
150
+ function eventRunRowParams(run) {
151
+ const timestamp = nowIso();
152
+ const createdAt = run.createdAt || timestamp;
153
+ const updatedAt = run.updatedAt || timestamp;
154
+ const runJson = {
155
+ ...run,
156
+ id: run.id,
157
+ runId: run.id,
158
+ triggerId: run.triggerId,
159
+ status: run.status || 'running',
160
+ startedAt: run.startedAt || null,
161
+ finishedAt: run.finishedAt || null,
162
+ sessionId: run.sessionId || null,
163
+ harness: run.harness || null,
164
+ projectPath: run.projectPath || null,
165
+ error: run.error || null,
166
+ createdAt,
167
+ updatedAt,
168
+ };
169
+ return {
170
+ id: run.id,
171
+ trigger_id: run.triggerId,
172
+ status: run.status || 'running',
173
+ started_at: run.startedAt || null,
174
+ finished_at: run.finishedAt || null,
175
+ session_id: run.sessionId || null,
176
+ harness: run.harness || null,
177
+ project_path: run.projectPath || null,
178
+ error: run.error || null,
179
+ run_json: JSON.stringify(runJson),
180
+ created_at: createdAt,
181
+ updated_at: updatedAt,
182
+ };
183
+ }
184
+
185
+ function upsertEventRunRow(db, run) {
186
+ const params = eventRunRowParams(run);
187
+ db.prepare(`
188
+ INSERT INTO event_runs (
189
+ id,
190
+ trigger_id,
191
+ status,
192
+ started_at,
193
+ finished_at,
194
+ session_id,
195
+ harness,
196
+ project_path,
197
+ error,
198
+ run_json,
199
+ created_at,
200
+ updated_at
201
+ )
202
+ VALUES (
203
+ @id,
204
+ @trigger_id,
205
+ @status,
206
+ @started_at,
207
+ @finished_at,
208
+ @session_id,
209
+ @harness,
210
+ @project_path,
211
+ @error,
212
+ @run_json,
213
+ @created_at,
214
+ @updated_at
215
+ )
216
+ ON CONFLICT(id) DO UPDATE SET
217
+ trigger_id = excluded.trigger_id,
218
+ status = excluded.status,
219
+ started_at = excluded.started_at,
220
+ finished_at = excluded.finished_at,
221
+ session_id = excluded.session_id,
222
+ harness = excluded.harness,
223
+ project_path = excluded.project_path,
224
+ error = excluded.error,
225
+ run_json = excluded.run_json,
226
+ updated_at = excluded.updated_at
227
+ `).run(params);
228
+ }
229
+
230
+ function mergeEventRunEntry(existing, triggerId, entry) {
231
+ const timestamp = nowIso();
232
+ const startedAt = entry.startedAt || existing?.startedAt || timestamp;
233
+ const finishedAt = entry.finishedAt || existing?.finishedAt || null;
234
+ const events = Array.isArray(existing?.events) ? existing.events.slice(-99) : [];
235
+ events.push({ ...entry, recordedAt: timestamp });
236
+
237
+ return {
238
+ ...(existing || {}),
239
+ id: entry.runId || entry.id || existing?.id || entry.sessionId || timestamp,
240
+ runId: entry.runId || entry.id || existing?.id || entry.sessionId || null,
241
+ triggerId,
242
+ triggerName: entry.triggerName || existing?.triggerName || null,
243
+ status: entry.status || existing?.status || 'running',
244
+ startedAt,
245
+ finishedAt,
246
+ sessionId: entry.sessionId || existing?.sessionId || null,
247
+ harness: entry.harness || existing?.harness || null,
248
+ projectPath: entry.projectPath || existing?.projectPath || null,
249
+ error: entry.error || existing?.error || null,
250
+ durationMs: entry.durationMs ?? existing?.durationMs ?? null,
251
+ outputLength: entry.outputLength ?? existing?.outputLength ?? null,
252
+ prompt: entry.prompt || existing?.prompt || null,
253
+ events,
254
+ createdAt: existing?.createdAt || startedAt,
255
+ updatedAt: timestamp,
256
+ };
257
+ }
258
+
259
+ function listEventRuns(options = {}) {
260
+ ensureTriggersDirs();
261
+ const limit = Math.max(1, Math.min(Number(options.limit) || DEFAULT_RUN_LIMIT, 1000));
262
+ const db = openLocalDb();
263
+ const rows = options.triggerId
264
+ ? db.prepare(`
265
+ SELECT * FROM event_runs
266
+ WHERE trigger_id = ?
267
+ ORDER BY datetime(updated_at) DESC, datetime(started_at) DESC
268
+ LIMIT ?
269
+ `).all(options.triggerId, limit)
270
+ : db.prepare(`
271
+ SELECT * FROM event_runs
272
+ ORDER BY datetime(updated_at) DESC, datetime(started_at) DESC
273
+ LIMIT ?
274
+ `).all(limit);
275
+ return rows.map(rowToEventRun).filter(Boolean);
276
+ }
277
+
278
+ function recordEventRun(triggerId, entry, options = {}) {
279
+ if (!triggerId || (!entry?.runId && !entry?.id && !entry?.sessionId)) return null;
280
+ ensureTriggersDirs();
281
+
282
+ const db = openLocalDb();
283
+ const source = options.source || entry.source || 'event_runs:record';
284
+ let run = null;
285
+ let event = null;
286
+
287
+ db.transaction(() => {
288
+ const runId = entry.runId || entry.id || entry.sessionId;
289
+ const existingRow = db.prepare('SELECT * FROM event_runs WHERE id = ?').get(runId);
290
+ const existing = rowToEventRun(existingRow);
291
+ run = mergeEventRunEntry(existing, triggerId, { ...entry, runId });
292
+ upsertEventRunRow(db, run);
293
+ run = rowToEventRun(db.prepare('SELECT * FROM event_runs WHERE id = ?').get(run.id));
294
+ event = insertStateEvent(db, {
295
+ resource: 'event_runs',
296
+ op: existing ? 'update' : 'insert',
297
+ id: run.id,
298
+ value: run,
299
+ source,
300
+ });
301
+ })();
302
+
303
+ if (event) publishStateEvent(event);
304
+ return run;
305
+ }
306
+
307
+ module.exports = {
308
+ ensureTriggersDirs,
309
+ listEventRuns,
310
+ loadEventTriggers,
311
+ recordEventRun,
312
+ saveEventTriggers,
313
+ };