amalgm 0.1.40 → 0.1.43
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/lib/cli.js +27 -0
- package/lib/process-cleanup.js +213 -0
- package/lib/service.js +33 -40
- package/lib/supervisor.js +20 -0
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +9 -0
- package/runtime/scripts/amalgm-mcp/events/executor.js +46 -4
- package/runtime/scripts/amalgm-mcp/events/rest.js +2 -1
- package/runtime/scripts/amalgm-mcp/events/tools.js +6 -2
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +54 -3
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +6 -2
- package/runtime/scripts/chat-core/tooling/active-memory.js +182 -5
- package/runtime/scripts/chat-core/tooling/passive-memory.js +576 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +6 -1
- package/runtime/scripts/fs-watcher.js +45 -10
- package/runtime/scripts/port-monitor.js +54 -16
package/lib/cli.js
CHANGED
|
@@ -28,6 +28,11 @@ const {
|
|
|
28
28
|
stopService,
|
|
29
29
|
uninstallService,
|
|
30
30
|
} = require('./service');
|
|
31
|
+
const {
|
|
32
|
+
cleanupStaleRuntimeProcesses,
|
|
33
|
+
runtimeServiceProcesses,
|
|
34
|
+
supervisorProcesses,
|
|
35
|
+
} = require('./process-cleanup');
|
|
31
36
|
const {
|
|
32
37
|
deleteComputerAuth,
|
|
33
38
|
loadComputerRecord,
|
|
@@ -846,6 +851,8 @@ async function start(options) {
|
|
|
846
851
|
await stop();
|
|
847
852
|
}
|
|
848
853
|
|
|
854
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
855
|
+
|
|
849
856
|
const blockedPorts = await unavailableServicePorts();
|
|
850
857
|
if (blockedPorts.length > 0) {
|
|
851
858
|
const detail = blockedPorts.map(([name, port]) => `${name} (${port})`).join(', ');
|
|
@@ -928,6 +935,7 @@ async function stop() {
|
|
|
928
935
|
} catch {
|
|
929
936
|
// noop
|
|
930
937
|
}
|
|
938
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
931
939
|
console.log('Amalgm is not running.');
|
|
932
940
|
return;
|
|
933
941
|
}
|
|
@@ -951,6 +959,7 @@ async function stop() {
|
|
|
951
959
|
} catch {
|
|
952
960
|
// noop
|
|
953
961
|
}
|
|
962
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
954
963
|
console.log('Amalgm stopped.');
|
|
955
964
|
}
|
|
956
965
|
|
|
@@ -1028,6 +1037,14 @@ async function status() {
|
|
|
1028
1037
|
}
|
|
1029
1038
|
console.log(`HMAC proxy: ${proxyTokenFromRecord(record) ? 'configured' : 'not configured'}`);
|
|
1030
1039
|
|
|
1040
|
+
const runtimeChildren = runtimeServiceProcesses();
|
|
1041
|
+
const runtimeSupervisors = supervisorProcesses();
|
|
1042
|
+
if (runtimeChildren.length > 5 || runtimeSupervisors.length > 1) {
|
|
1043
|
+
console.log(
|
|
1044
|
+
`Warning: multiple Amalgm runtime processes detected (${runtimeSupervisors.length} supervisor, ${runtimeChildren.length} services). Run \`amalgm stop && amalgm start\`.`,
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1031
1048
|
for (const [name, port] of servicePorts()) {
|
|
1032
1049
|
const ok = await checkHttp(port);
|
|
1033
1050
|
console.log(`${name.padEnd(13)} ${ok ? 'up' : 'down'} http://127.0.0.1:${port}`);
|
|
@@ -1101,6 +1118,16 @@ async function doctor() {
|
|
|
1101
1118
|
);
|
|
1102
1119
|
}
|
|
1103
1120
|
add('daemon', daemonRunning, daemonRunning ? `pid ${pid}` : 'stopped', daemonRunning ? 'ok' : 'warn');
|
|
1121
|
+
const runtimeChildren = runtimeServiceProcesses();
|
|
1122
|
+
const runtimeSupervisors = supervisorProcesses();
|
|
1123
|
+
const expectedServices = daemonRunning ? 5 : 0;
|
|
1124
|
+
const cleanProcessShape = runtimeSupervisors.length <= 1 && runtimeChildren.length <= expectedServices;
|
|
1125
|
+
add(
|
|
1126
|
+
'runtime processes',
|
|
1127
|
+
cleanProcessShape,
|
|
1128
|
+
`${runtimeSupervisors.length} supervisor, ${runtimeChildren.length} services`,
|
|
1129
|
+
cleanProcessShape ? 'ok' : 'warn',
|
|
1130
|
+
);
|
|
1104
1131
|
if (lastShutdown?.signal || lastShutdown?.reason) {
|
|
1105
1132
|
add(
|
|
1106
1133
|
'last shutdown',
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
AMALGM_DIR,
|
|
9
|
+
PACKAGE_ROOT,
|
|
10
|
+
RUNTIME_DIR,
|
|
11
|
+
} = require('./paths');
|
|
12
|
+
|
|
13
|
+
const RUNTIME_SERVICE_MARKERS = [
|
|
14
|
+
path.join(RUNTIME_DIR, 'scripts', 'port-monitor.js'),
|
|
15
|
+
path.join(RUNTIME_DIR, 'scripts', 'fs-watcher.js'),
|
|
16
|
+
path.join(RUNTIME_DIR, 'scripts', 'chat-server.js'),
|
|
17
|
+
path.join(RUNTIME_DIR, 'scripts', 'local-gateway.js'),
|
|
18
|
+
path.join(RUNTIME_DIR, 'scripts', 'amalgm-mcp', 'index.js'),
|
|
19
|
+
].map((value) => value.replace(/\\/g, '/'));
|
|
20
|
+
const RUNTIME_SERVICE_SUFFIXES = [
|
|
21
|
+
'runtime/scripts/port-monitor.js',
|
|
22
|
+
'runtime/scripts/fs-watcher.js',
|
|
23
|
+
'runtime/scripts/chat-server.js',
|
|
24
|
+
'runtime/scripts/local-gateway.js',
|
|
25
|
+
'runtime/scripts/amalgm-mcp/index.js',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const SUPERVISOR_MARKER = path.join(PACKAGE_ROOT, 'bin', 'amalgm.js').replace(/\\/g, '/');
|
|
29
|
+
|
|
30
|
+
function isPidRunning(pid) {
|
|
31
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
32
|
+
try {
|
|
33
|
+
process.kill(pid, 0);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function waitForExit(pid, timeoutMs = 5000) {
|
|
41
|
+
const deadline = Date.now() + timeoutMs;
|
|
42
|
+
while (Date.now() < deadline && isPidRunning(pid)) {
|
|
43
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
44
|
+
}
|
|
45
|
+
return !isPidRunning(pid);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readProcString(file) {
|
|
49
|
+
try {
|
|
50
|
+
return fs.readFileSync(file).toString('utf8');
|
|
51
|
+
} catch {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readProcCommand(pid) {
|
|
57
|
+
const cmdline = readProcString(`/proc/${pid}/cmdline`).replace(/\0/g, ' ').trim();
|
|
58
|
+
if (cmdline) return cmdline;
|
|
59
|
+
try {
|
|
60
|
+
const result = spawnSync('ps', ['-p', String(pid), '-o', 'command='], {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
timeout: 1000,
|
|
63
|
+
});
|
|
64
|
+
return String(result.stdout || '').trim();
|
|
65
|
+
} catch {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readProcEnv(pid) {
|
|
71
|
+
const raw = readProcString(`/proc/${pid}/environ`);
|
|
72
|
+
if (!raw) return null;
|
|
73
|
+
const env = {};
|
|
74
|
+
for (const entry of raw.split('\0')) {
|
|
75
|
+
if (!entry) continue;
|
|
76
|
+
const index = entry.indexOf('=');
|
|
77
|
+
if (index <= 0) continue;
|
|
78
|
+
env[entry.slice(0, index)] = entry.slice(index + 1);
|
|
79
|
+
}
|
|
80
|
+
return env;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function listProcProcesses() {
|
|
84
|
+
try {
|
|
85
|
+
return fs.readdirSync('/proc')
|
|
86
|
+
.filter((entry) => /^\d+$/.test(entry))
|
|
87
|
+
.map((entry) => Number(entry))
|
|
88
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0)
|
|
89
|
+
.map((pid) => ({
|
|
90
|
+
pid,
|
|
91
|
+
command: readProcCommand(pid),
|
|
92
|
+
env: readProcEnv(pid),
|
|
93
|
+
}))
|
|
94
|
+
.filter((item) => item.command);
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function listPsProcesses() {
|
|
101
|
+
try {
|
|
102
|
+
const result = spawnSync('ps', ['-eo', 'pid=,command='], {
|
|
103
|
+
encoding: 'utf8',
|
|
104
|
+
timeout: 2000,
|
|
105
|
+
});
|
|
106
|
+
if (result.status !== 0) return [];
|
|
107
|
+
return String(result.stdout || '')
|
|
108
|
+
.split('\n')
|
|
109
|
+
.map((line) => {
|
|
110
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
111
|
+
if (!match) return null;
|
|
112
|
+
return {
|
|
113
|
+
pid: Number(match[1]),
|
|
114
|
+
command: match[2],
|
|
115
|
+
env: null,
|
|
116
|
+
};
|
|
117
|
+
})
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
} catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function listProcesses() {
|
|
125
|
+
return listProcProcesses() || listPsProcesses();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizedCommand(command) {
|
|
129
|
+
return String(command || '').replace(/\\/g, '/');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function envMatchesAmalgmDir(env) {
|
|
133
|
+
if (!env || !env.AMALGM_DIR) return true;
|
|
134
|
+
return path.resolve(env.AMALGM_DIR) === path.resolve(AMALGM_DIR);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function commandLooksLikeRuntimeService(command) {
|
|
138
|
+
const normalized = normalizedCommand(command);
|
|
139
|
+
return RUNTIME_SERVICE_MARKERS.some((marker) => normalized.includes(marker))
|
|
140
|
+
|| RUNTIME_SERVICE_SUFFIXES.some((marker) => normalized.includes(marker));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function commandLooksLikeSupervisor(command) {
|
|
144
|
+
const normalized = normalizedCommand(command);
|
|
145
|
+
return normalized.includes(SUPERVISOR_MARKER) && /\brun\b/.test(normalized);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function runtimeServiceProcesses(options = {}) {
|
|
149
|
+
const exclude = new Set([process.pid, ...(options.excludePids || [])].filter(Number.isInteger));
|
|
150
|
+
return listProcesses()
|
|
151
|
+
.filter((item) => !exclude.has(item.pid))
|
|
152
|
+
.filter((item) => commandLooksLikeRuntimeService(item.command))
|
|
153
|
+
.filter((item) => envMatchesAmalgmDir(item.env));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function supervisorProcesses(options = {}) {
|
|
157
|
+
const exclude = new Set([process.pid, ...(options.excludePids || [])].filter(Number.isInteger));
|
|
158
|
+
return listProcesses()
|
|
159
|
+
.filter((item) => !exclude.has(item.pid))
|
|
160
|
+
.filter((item) => commandLooksLikeSupervisor(item.command))
|
|
161
|
+
.filter((item) => envMatchesAmalgmDir(item.env));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function terminateProcesses(processes, options = {}) {
|
|
165
|
+
const logger = options.logger || null;
|
|
166
|
+
const unique = [...new Map(processes.map((item) => [item.pid, item])).values()]
|
|
167
|
+
.filter((item) => isPidRunning(item.pid));
|
|
168
|
+
if (unique.length === 0) return [];
|
|
169
|
+
|
|
170
|
+
for (const item of unique) {
|
|
171
|
+
try {
|
|
172
|
+
process.kill(item.pid, 'SIGTERM');
|
|
173
|
+
} catch {
|
|
174
|
+
// It may have exited between discovery and shutdown.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const survivors = [];
|
|
179
|
+
for (const item of unique) {
|
|
180
|
+
if (!waitForExit(item.pid, options.termTimeoutMs || 3000)) survivors.push(item);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const item of survivors) {
|
|
184
|
+
try {
|
|
185
|
+
process.kill(item.pid, 'SIGKILL');
|
|
186
|
+
} catch {
|
|
187
|
+
// noop
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const item of survivors) waitForExit(item.pid, options.killTimeoutMs || 1500);
|
|
191
|
+
|
|
192
|
+
const stopped = unique.filter((item) => !isPidRunning(item.pid));
|
|
193
|
+
if (stopped.length > 0 && logger?.log) {
|
|
194
|
+
logger.log(`cleaned stale Amalgm runtime process(es): ${stopped.map((item) => item.pid).join(', ')}`);
|
|
195
|
+
}
|
|
196
|
+
return stopped;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function cleanupStaleRuntimeProcesses(options = {}) {
|
|
200
|
+
const stale = runtimeServiceProcesses(options);
|
|
201
|
+
const supervisors = options.includeSupervisors ? supervisorProcesses(options) : [];
|
|
202
|
+
return terminateProcesses([...stale, ...supervisors], options);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
cleanupStaleRuntimeProcesses,
|
|
207
|
+
commandLooksLikeRuntimeService,
|
|
208
|
+
commandLooksLikeSupervisor,
|
|
209
|
+
isPidRunning,
|
|
210
|
+
runtimeServiceProcesses,
|
|
211
|
+
supervisorProcesses,
|
|
212
|
+
terminateProcesses,
|
|
213
|
+
};
|
package/lib/service.js
CHANGED
|
@@ -15,6 +15,9 @@ const {
|
|
|
15
15
|
SERVICE_STATE_FILE,
|
|
16
16
|
SERVICE_STOP_FILE,
|
|
17
17
|
} = require('./paths');
|
|
18
|
+
const {
|
|
19
|
+
cleanupStaleRuntimeProcesses,
|
|
20
|
+
} = require('./process-cleanup');
|
|
18
21
|
|
|
19
22
|
const PACKAGE_VERSION = require('../package.json').version;
|
|
20
23
|
const SERVICE_LABEL = 'ai.amalgm.runtime';
|
|
@@ -250,6 +253,7 @@ function writePortableScript(localOnly) {
|
|
|
250
253
|
`LOCK_DIR=${shellQuote(path.join(SERVICE_DIR, 'lock'))}`,
|
|
251
254
|
`NODE_BIN=${shellQuote(process.execPath)}`,
|
|
252
255
|
`AMALGM_BIN=${shellQuote(AMALGM_BIN)}`,
|
|
256
|
+
`NODE_WATCHDOG=${shellQuote(SERVICE_NODE_SCRIPT_FILE)}`,
|
|
253
257
|
`DAEMON_LOG=${shellQuote(path.join(LOG_DIR, 'daemon.log'))}`,
|
|
254
258
|
`SERVICE_LOG=${shellQuote(SERVICE_LOG_FILE)}`,
|
|
255
259
|
`RESTART_DELAY=${shellQuote(String(process.env.AMALGM_SERVICE_RESTART_DELAY || '5'))}`,
|
|
@@ -261,42 +265,7 @@ function writePortableScript(localOnly) {
|
|
|
261
265
|
|
|
262
266
|
lines.push(
|
|
263
267
|
'mkdir -p "$AMALGM_DIR" "$LOG_DIR"',
|
|
264
|
-
'
|
|
265
|
-
' existing="$(cat "$PID_FILE" 2>/dev/null | sed -n \'s/.*"pid"[[:space:]]*:[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p\')"',
|
|
266
|
-
' [ -n "$existing" ] || existing="$(cat "$PID_FILE" 2>/dev/null || true)"',
|
|
267
|
-
' if [ -n "$existing" ] && kill -0 "$existing" 2>/dev/null; then',
|
|
268
|
-
' exit 0',
|
|
269
|
-
' fi',
|
|
270
|
-
' rm -rf "$LOCK_DIR"',
|
|
271
|
-
' mkdir "$LOCK_DIR" || exit 1',
|
|
272
|
-
'fi',
|
|
273
|
-
'child_pid=""',
|
|
274
|
-
'cleanup() {',
|
|
275
|
-
' touch "$STOP_FILE" 2>/dev/null || true',
|
|
276
|
-
' if [ -n "$child_pid" ] && kill -0 "$child_pid" 2>/dev/null; then',
|
|
277
|
-
' kill "$child_pid" 2>/dev/null || true',
|
|
278
|
-
' wait "$child_pid" 2>/dev/null || true',
|
|
279
|
-
' fi',
|
|
280
|
-
' rm -f "$PID_FILE"',
|
|
281
|
-
' rm -rf "$LOCK_DIR"',
|
|
282
|
-
' exit 0',
|
|
283
|
-
'}',
|
|
284
|
-
'trap cleanup TERM INT HUP',
|
|
285
|
-
'printf \'{"pid":%s,"started_at":"%s","backend":"portable"}\\n\' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$PID_FILE"',
|
|
286
|
-
'rm -f "$STOP_FILE"',
|
|
287
|
-
'while [ ! -f "$STOP_FILE" ]; do',
|
|
288
|
-
' printf \'[%s] [service] starting amalgm run\\n\' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$SERVICE_LOG"',
|
|
289
|
-
' "$NODE_BIN" "$AMALGM_BIN" run >> "$DAEMON_LOG" 2>&1 &',
|
|
290
|
-
' child_pid=$!',
|
|
291
|
-
' wait "$child_pid"',
|
|
292
|
-
' code=$?',
|
|
293
|
-
' child_pid=""',
|
|
294
|
-
' printf \'[%s] [service] amalgm run exited code=%s\\n\' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$code" >> "$SERVICE_LOG"',
|
|
295
|
-
' [ -f "$STOP_FILE" ] && break',
|
|
296
|
-
' sleep "$RESTART_DELAY"',
|
|
297
|
-
'done',
|
|
298
|
-
'rm -f "$PID_FILE"',
|
|
299
|
-
'rm -rf "$LOCK_DIR"',
|
|
268
|
+
'exec "$NODE_BIN" "$NODE_WATCHDOG"',
|
|
300
269
|
);
|
|
301
270
|
|
|
302
271
|
fs.writeFileSync(SERVICE_SCRIPT_FILE, `${lines.join('\n')}\n`, { mode: 0o700 });
|
|
@@ -330,6 +299,9 @@ function writePortableNodeScript(localOnly) {
|
|
|
330
299
|
const fs = require('fs');
|
|
331
300
|
const path = require('path');
|
|
332
301
|
const { spawn } = require('child_process');
|
|
302
|
+
const {
|
|
303
|
+
cleanupStaleRuntimeProcesses,
|
|
304
|
+
} = require(path.join(path.dirname(${JSON.stringify(AMALGM_BIN)}), '..', 'lib', 'process-cleanup'));
|
|
333
305
|
|
|
334
306
|
const config = ${JSON.stringify(config, null, 2)};
|
|
335
307
|
let child = null;
|
|
@@ -411,19 +383,39 @@ function cleanup(exitCode = 0) {
|
|
|
411
383
|
if (child && !child.killed) {
|
|
412
384
|
try { child.kill('SIGKILL'); } catch {}
|
|
413
385
|
}
|
|
386
|
+
cleanupStaleRuntimeProcesses({
|
|
387
|
+
excludePids: [process.pid],
|
|
388
|
+
includeSupervisors: true,
|
|
389
|
+
logger: { log: (message) => append(config.serviceLog, message) },
|
|
390
|
+
});
|
|
414
391
|
finish(exitCode);
|
|
415
392
|
}, 5000);
|
|
416
393
|
child.once('exit', () => {
|
|
417
394
|
clearTimeout(timer);
|
|
395
|
+
cleanupStaleRuntimeProcesses({
|
|
396
|
+
excludePids: [process.pid],
|
|
397
|
+
includeSupervisors: true,
|
|
398
|
+
logger: { log: (message) => append(config.serviceLog, message) },
|
|
399
|
+
});
|
|
418
400
|
finish(exitCode);
|
|
419
401
|
});
|
|
420
402
|
return;
|
|
421
403
|
}
|
|
404
|
+
cleanupStaleRuntimeProcesses({
|
|
405
|
+
excludePids: [process.pid],
|
|
406
|
+
includeSupervisors: true,
|
|
407
|
+
logger: { log: (message) => append(config.serviceLog, message) },
|
|
408
|
+
});
|
|
422
409
|
finish(exitCode);
|
|
423
410
|
}
|
|
424
411
|
|
|
425
412
|
function launch() {
|
|
426
413
|
if (stopping || fs.existsSync(config.stopFile)) return cleanup(0);
|
|
414
|
+
cleanupStaleRuntimeProcesses({
|
|
415
|
+
excludePids: [process.pid],
|
|
416
|
+
includeSupervisors: true,
|
|
417
|
+
logger: { log: (message) => append(config.serviceLog, message) },
|
|
418
|
+
});
|
|
427
419
|
append(config.serviceLog, 'starting amalgm run');
|
|
428
420
|
const fd = fs.openSync(config.daemonLog, 'a');
|
|
429
421
|
child = spawn(config.nodeBin, [config.amalgmBin, 'run'], {
|
|
@@ -655,10 +647,8 @@ function startPortable() {
|
|
|
655
647
|
removeFile(SERVICE_STOP_FILE);
|
|
656
648
|
ensureDir(LOG_DIR, 0o700);
|
|
657
649
|
const logFd = fs.openSync(SERVICE_LOG_FILE, 'a');
|
|
658
|
-
const command = process.
|
|
659
|
-
const args =
|
|
660
|
-
? [state?.files?.nodeScript || SERVICE_NODE_SCRIPT_FILE]
|
|
661
|
-
: [state?.files?.script || SERVICE_SCRIPT_FILE];
|
|
650
|
+
const command = process.execPath;
|
|
651
|
+
const args = [state?.files?.nodeScript || SERVICE_NODE_SCRIPT_FILE];
|
|
662
652
|
const child = spawn(command, args, {
|
|
663
653
|
detached: true,
|
|
664
654
|
env: {
|
|
@@ -682,6 +672,7 @@ function stopDaemonProcess() {
|
|
|
682
672
|
const pid = readPidFile(PID_FILE);
|
|
683
673
|
if (!isPidRunning(pid)) {
|
|
684
674
|
removeFile(PID_FILE);
|
|
675
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
685
676
|
return false;
|
|
686
677
|
}
|
|
687
678
|
try {
|
|
@@ -701,6 +692,7 @@ function stopDaemonProcess() {
|
|
|
701
692
|
}
|
|
702
693
|
}
|
|
703
694
|
removeFile(PID_FILE);
|
|
695
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
704
696
|
return true;
|
|
705
697
|
}
|
|
706
698
|
|
|
@@ -720,6 +712,7 @@ function stopPortable() {
|
|
|
720
712
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
721
713
|
}
|
|
722
714
|
removeFile(SERVICE_PID_FILE);
|
|
715
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
723
716
|
}
|
|
724
717
|
|
|
725
718
|
function startService(options = {}) {
|
package/lib/supervisor.js
CHANGED
|
@@ -26,6 +26,11 @@ const {
|
|
|
26
26
|
ensureAgentCommandShims,
|
|
27
27
|
ensureNativeBinaries,
|
|
28
28
|
} = require('../runtime/scripts/chat-core/tooling/native-binaries');
|
|
29
|
+
const {
|
|
30
|
+
cleanupStaleRuntimeProcesses,
|
|
31
|
+
isPidRunning,
|
|
32
|
+
supervisorProcesses,
|
|
33
|
+
} = require('./process-cleanup');
|
|
29
34
|
const PACKAGE_VERSION = require('../package.json').version;
|
|
30
35
|
|
|
31
36
|
function ensureDir(dir, mode = 0o700) {
|
|
@@ -408,6 +413,21 @@ async function startSupervisor(options = {}) {
|
|
|
408
413
|
ensureDir(AMALGM_DIR, 0o700);
|
|
409
414
|
ensureDir(LOG_DIR, 0o700);
|
|
410
415
|
|
|
416
|
+
const activeSupervisors = supervisorProcesses({ excludePids: [process.pid] });
|
|
417
|
+
if (activeSupervisors.length > 0) {
|
|
418
|
+
const pids = activeSupervisors
|
|
419
|
+
.filter((item) => isPidRunning(item.pid))
|
|
420
|
+
.map((item) => item.pid);
|
|
421
|
+
if (pids.length > 0) {
|
|
422
|
+
throw new Error(`Amalgm runtime is already running (pid ${pids.join(', ')}). Run \`amalgm stop\` first.`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
cleanupStaleRuntimeProcesses({
|
|
427
|
+
excludePids: [process.pid],
|
|
428
|
+
logger: options.foreground ? console : null,
|
|
429
|
+
});
|
|
430
|
+
|
|
411
431
|
const nativeResult = ensureNativeBinaries({
|
|
412
432
|
logger: console,
|
|
413
433
|
quiet: !options.foreground,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amalgm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"sync-runtime": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
18
18
|
"prepack": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
19
19
|
"pack:dry": "npm pack --dry-run",
|
|
20
|
-
"check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/service.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/fs-watcher.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js"
|
|
20
|
+
"check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/service.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/fs-watcher.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20"
|
|
@@ -26,6 +26,13 @@ const stopping = new Set();
|
|
|
26
26
|
const restartTimers = new Map();
|
|
27
27
|
|
|
28
28
|
const ARTIFACT_ENV_ALLOWLIST = [
|
|
29
|
+
'AMALGM_BIND_HOST',
|
|
30
|
+
'AMALGM_DIR',
|
|
31
|
+
'AMALGM_GATEWAY_PORT',
|
|
32
|
+
'AMALGM_MCP_PORT',
|
|
33
|
+
'AMALGM_RUNTIME_TOKEN',
|
|
34
|
+
'AMALGM_WORKSPACES_DIR',
|
|
35
|
+
'CHAT_SERVER_PORT',
|
|
29
36
|
'PATH',
|
|
30
37
|
'HOME',
|
|
31
38
|
'USER',
|
|
@@ -81,6 +88,8 @@ function buildEnv(artifact) {
|
|
|
81
88
|
AMALGM_ARTIFACT_ID: artifact.id,
|
|
82
89
|
AMALGM_ARTIFACT_REF: artifact.artifactRef,
|
|
83
90
|
AMALGM_ARTIFACT_URL: artifact.publicUrl,
|
|
91
|
+
AMALGM_CHAT_SERVER_URL: `http://127.0.0.1:${process.env.CHAT_SERVER_PORT || 8084}`,
|
|
92
|
+
AMALGM_MCP_URL: `http://127.0.0.1:${process.env.AMALGM_MCP_PORT || 8083}`,
|
|
84
93
|
};
|
|
85
94
|
}
|
|
86
95
|
|
|
@@ -26,6 +26,39 @@ function harnessToAgent(harness) {
|
|
|
26
26
|
return map[harness] || 'claude';
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function normalizeEffort(value) {
|
|
30
|
+
if (typeof value !== 'string') return null;
|
|
31
|
+
const normalized = value.trim().toLowerCase().replace(/_/g, '-');
|
|
32
|
+
if (normalized === 'low') return 'low';
|
|
33
|
+
if (normalized === 'medium') return 'medium';
|
|
34
|
+
if (normalized === 'high') return 'high';
|
|
35
|
+
if (normalized === 'xhigh' || normalized === 'extra-high' || normalized === 'extra high') return 'xhigh';
|
|
36
|
+
if (normalized === 'max') return 'max';
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sanitizeRunModelSettings(settings, harness) {
|
|
41
|
+
if (!settings || typeof settings !== 'object') return {};
|
|
42
|
+
const effort = normalizeEffort(settings.effort || settings.reasoningEffort || settings.reasoning);
|
|
43
|
+
const allowedEfforts =
|
|
44
|
+
harness === 'claude_code'
|
|
45
|
+
? new Set(['low', 'medium', 'high', 'xhigh', 'max'])
|
|
46
|
+
: harness === 'codex'
|
|
47
|
+
? new Set(['low', 'medium', 'high', 'xhigh'])
|
|
48
|
+
: new Set();
|
|
49
|
+
return {
|
|
50
|
+
...(effort && allowedEfforts.has(effort) ? { effort } : {}),
|
|
51
|
+
...(harness === 'claude_code' && settings.fastMode === true ? { fastMode: true } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function modelIdForSettings(modelId, harness, modelSettings) {
|
|
56
|
+
if (harness !== 'codex' || !modelSettings.effort || typeof modelId !== 'string') return modelId;
|
|
57
|
+
return modelId
|
|
58
|
+
.replace(/(?::thinking-|-thinking-)(low|medium|high|xhigh)$/i, '')
|
|
59
|
+
.replace(/\/(low|medium|high|xhigh)$/i, '');
|
|
60
|
+
}
|
|
61
|
+
|
|
29
62
|
function resolveEventRequest(eventDef, projectPath) {
|
|
30
63
|
const harness =
|
|
31
64
|
(eventDef.chatInput && eventDef.chatInput.agent && eventDef.chatInput.agent.harness)
|
|
@@ -58,6 +91,10 @@ function resolveEventRequest(eventDef, projectPath) {
|
|
|
58
91
|
templateChatInput,
|
|
59
92
|
authMethod,
|
|
60
93
|
model,
|
|
94
|
+
modelSettings: sanitizeRunModelSettings(
|
|
95
|
+
eventDef.modelSettings || (eventDef.chatInput && eventDef.chatInput.modelSettings),
|
|
96
|
+
harness,
|
|
97
|
+
),
|
|
61
98
|
};
|
|
62
99
|
}
|
|
63
100
|
|
|
@@ -75,7 +112,7 @@ async function executeArtifactEvent(artifactOrTrigger, eventDef, payload, opts =
|
|
|
75
112
|
const userMessageId = crypto.randomUUID();
|
|
76
113
|
const assistantMessageId = crypto.randomUUID();
|
|
77
114
|
const startedAt = new Date().toISOString();
|
|
78
|
-
const { harness, templateChatInput, authMethod, model } = resolveEventRequest(eventDef, projectPath);
|
|
115
|
+
const { harness, templateChatInput, authMethod, model, modelSettings } = resolveEventRequest(eventDef, projectPath);
|
|
79
116
|
const payloadText = JSON.stringify(payload);
|
|
80
117
|
const chatInput = withChatInputText(templateChatInput, (text) => text.replaceAll('{payload}', payloadText));
|
|
81
118
|
const legacy = chatInputToLegacyFields(chatInput, {
|
|
@@ -87,8 +124,12 @@ async function executeArtifactEvent(artifactOrTrigger, eventDef, payload, opts =
|
|
|
87
124
|
projectPath,
|
|
88
125
|
});
|
|
89
126
|
const cwd = legacy.cwd || DEFAULT_CWD;
|
|
90
|
-
const modelSelection = resolveModelSelection(
|
|
127
|
+
const modelSelection = resolveModelSelection(
|
|
128
|
+
harness,
|
|
129
|
+
modelIdForSettings(legacy.modelId || model, harness, modelSettings),
|
|
130
|
+
);
|
|
91
131
|
const mcpServers = await buildLocalMcpServerConfigs(legacy.mcpAppIds || []);
|
|
132
|
+
const reasoningEffort = modelSettings.effort || modelSelection.reasoningEffort;
|
|
92
133
|
|
|
93
134
|
console.log(
|
|
94
135
|
`[AmalgmMCP:Event] Starting agent run for ${artifactOrTrigger.id}:${eventDef.name} (session: ${codeSessionId}, cwd: ${cwd})`,
|
|
@@ -135,7 +176,8 @@ async function executeArtifactEvent(artifactOrTrigger, eventDef, payload, opts =
|
|
|
135
176
|
agentId: harnessToAgent(harness),
|
|
136
177
|
modelId: modelSelection.modelId || legacy.modelId || null,
|
|
137
178
|
cliModel: modelSelection.cliModel || null,
|
|
138
|
-
...(
|
|
179
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
180
|
+
...(modelSettings.fastMode ? { fastMode: true } : {}),
|
|
139
181
|
cwd,
|
|
140
182
|
authMethod: legacy.authMethod || authMethod,
|
|
141
183
|
mcpServers,
|
|
@@ -156,7 +198,7 @@ async function executeArtifactEvent(artifactOrTrigger, eventDef, payload, opts =
|
|
|
156
198
|
if (hasSupabase()) {
|
|
157
199
|
supabasePatch('sessions', 'id', codeSessionId, {
|
|
158
200
|
last_message_at: new Date().toISOString(),
|
|
159
|
-
status: '
|
|
201
|
+
status: 'complete',
|
|
160
202
|
new_messages: true,
|
|
161
203
|
}).catch(() => {});
|
|
162
204
|
}
|
|
@@ -109,7 +109,7 @@ async function handleList(req, sendJson) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
async function handleCreate(body, sendJson) {
|
|
112
|
-
const { name, description, source, event, agent_prompt, projectPath, harness, model, authMethod, chatInput } = body;
|
|
112
|
+
const { name, description, source, event, agent_prompt, projectPath, harness, model, modelSettings, authMethod, chatInput } = body;
|
|
113
113
|
if (!name || !source || !event || (!agent_prompt && !chatInput)) {
|
|
114
114
|
return sendJson(400, { error: 'name, source, event, and agent_prompt or chatInput are required' });
|
|
115
115
|
}
|
|
@@ -128,6 +128,7 @@ async function handleCreate(body, sendJson) {
|
|
|
128
128
|
projectPath: projectPath || null,
|
|
129
129
|
harness: harness || null,
|
|
130
130
|
model: model || null,
|
|
131
|
+
modelSettings: modelSettings || null,
|
|
131
132
|
authMethod: authMethod || null,
|
|
132
133
|
chatInput: chatInput || null,
|
|
133
134
|
createdAt: new Date().toISOString(),
|
|
@@ -131,12 +131,13 @@ module.exports = [
|
|
|
131
131
|
projectPath: { type: 'string', description: 'Working directory path for the agent run.' },
|
|
132
132
|
harness: { type: 'string', description: 'Agent harness for the event run.' },
|
|
133
133
|
model: { type: 'string', description: 'Model ID for the event run.' },
|
|
134
|
+
modelSettings: { type: 'object', description: 'Advanced model settings such as effort and fastMode.' },
|
|
134
135
|
authMethod: { type: 'string', description: 'Auth method for the event run.' },
|
|
135
136
|
chatInput: { type: 'object', description: 'Shared chat input shape for the event run.' },
|
|
136
137
|
},
|
|
137
138
|
required: ['name', 'source', 'event'],
|
|
138
139
|
},
|
|
139
|
-
async handler({ name, description, source, event, agent_prompt, projectPath, harness, model, authMethod, chatInput }) {
|
|
140
|
+
async handler({ name, description, source, event, agent_prompt, projectPath, harness, model, modelSettings, authMethod, chatInput }) {
|
|
140
141
|
if (!name || (!agent_prompt && !chatInput)) return errorResult('name and agent_prompt or chatInput are required');
|
|
141
142
|
await hydrateModelPreferences();
|
|
142
143
|
const data = loadEventTriggers();
|
|
@@ -153,6 +154,7 @@ module.exports = [
|
|
|
153
154
|
projectPath: projectPath || null,
|
|
154
155
|
harness: harness || null,
|
|
155
156
|
model: model || null,
|
|
157
|
+
modelSettings: modelSettings || null,
|
|
156
158
|
authMethod: authMethod || null,
|
|
157
159
|
chatInput: chatInput || null,
|
|
158
160
|
createdAt: new Date().toISOString(),
|
|
@@ -226,12 +228,13 @@ module.exports = [
|
|
|
226
228
|
projectPath: { type: 'string' },
|
|
227
229
|
harness: { type: 'string' },
|
|
228
230
|
model: { type: 'string' },
|
|
231
|
+
modelSettings: { type: 'object' },
|
|
229
232
|
authMethod: { type: 'string' },
|
|
230
233
|
chatInput: { type: 'object' },
|
|
231
234
|
},
|
|
232
235
|
required: ['trigger_id'],
|
|
233
236
|
},
|
|
234
|
-
async handler({ trigger_id, name, description, source, event, agent_prompt, enabled, projectPath, harness, model, authMethod, chatInput }) {
|
|
237
|
+
async handler({ trigger_id, name, description, source, event, agent_prompt, enabled, projectPath, harness, model, modelSettings, authMethod, chatInput }) {
|
|
235
238
|
const data = loadEventTriggers();
|
|
236
239
|
const trigger = data.triggers.find((t) => t.id === trigger_id);
|
|
237
240
|
if (!trigger) return errorResult(`Trigger not found: ${trigger_id}`);
|
|
@@ -247,6 +250,7 @@ module.exports = [
|
|
|
247
250
|
if (projectPath !== undefined) trigger.projectPath = projectPath || null;
|
|
248
251
|
if (harness !== undefined) trigger.harness = harness || null;
|
|
249
252
|
if (model !== undefined) trigger.model = model || null;
|
|
253
|
+
if (modelSettings !== undefined) trigger.modelSettings = modelSettings || null;
|
|
250
254
|
if (authMethod !== undefined) trigger.authMethod = authMethod || null;
|
|
251
255
|
if (chatInput !== undefined) trigger.chatInput = chatInput || null;
|
|
252
256
|
trigger.updatedAt = new Date().toISOString();
|