amalgm 0.1.51 → 0.1.52
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/tunnel-events.js +48 -23
- package/package.json +2 -2
- package/runtime/lib/harnesses.js +12 -4
- package/runtime/scripts/amalgm-mcp/agents/store.js +5 -5
- package/runtime/scripts/amalgm-mcp/{artifacts → apps}/advertise.js +39 -24
- package/runtime/scripts/amalgm-mcp/apps/rest.js +144 -0
- package/runtime/scripts/amalgm-mcp/apps/store.js +171 -0
- package/runtime/scripts/amalgm-mcp/apps/supervisor.js +439 -0
- package/runtime/scripts/amalgm-mcp/apps/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/automations/cell-references.js +237 -0
- package/runtime/scripts/amalgm-mcp/automations/context.js +41 -0
- package/runtime/scripts/amalgm-mcp/automations/rest.js +148 -0
- package/runtime/scripts/amalgm-mcp/automations/runner.js +613 -0
- package/runtime/scripts/amalgm-mcp/automations/scheduler.js +90 -0
- package/runtime/scripts/amalgm-mcp/automations/store.js +1125 -0
- package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +177 -0
- package/runtime/scripts/amalgm-mcp/automations/tools.js +418 -0
- package/runtime/scripts/amalgm-mcp/automations/validator.js +225 -0
- package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +505 -0
- package/runtime/scripts/amalgm-mcp/browser/electron-bridge.js +222 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +13 -631
- package/runtime/scripts/amalgm-mcp/browser/tools.js +9 -7
- package/runtime/scripts/amalgm-mcp/config.js +33 -48
- package/runtime/scripts/amalgm-mcp/deps.js +1 -31
- package/runtime/scripts/amalgm-mcp/events/ingress.js +50 -42
- package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +169 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +45 -14
- package/runtime/scripts/amalgm-mcp/events/store.js +106 -57
- package/runtime/scripts/amalgm-mcp/index.js +12 -14
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +229 -65
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +13 -27
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +2 -3
- package/runtime/scripts/amalgm-mcp/server/http.js +106 -56
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +1 -1
- package/runtime/scripts/amalgm-mcp/state/db.js +119 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +16 -3
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +348 -0
- package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +23 -0
- package/runtime/scripts/amalgm-mcp/tests/workflows-store-runner.test.js +67 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +16 -3
- package/runtime/scripts/amalgm-mcp/workflows/compiler.js +222 -0
- package/runtime/scripts/amalgm-mcp/workflows/runner.js +593 -0
- package/runtime/scripts/amalgm-mcp/workflows/store.js +237 -0
- package/runtime/scripts/chat-core/adapters/claude.js +2 -1
- package/runtime/scripts/chat-core/auth.js +82 -12
- package/runtime/scripts/chat-core/contract.js +5 -1
- package/runtime/scripts/chat-core/engine.js +103 -62
- package/runtime/scripts/chat-core/event-schema.js +8 -0
- package/runtime/scripts/chat-core/events.js +5 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +13 -1
- package/runtime/scripts/chat-core/parts.js +21 -6
- package/runtime/scripts/chat-core/sse.js +3 -0
- package/runtime/scripts/chat-core/tests/auth.test.js +84 -6
- package/runtime/scripts/chat-core/tests/engine.test.js +312 -0
- package/runtime/scripts/chat-core/tests/native-config.test.js +23 -0
- package/runtime/scripts/chat-core/tool-shape.js +4 -4
- package/runtime/scripts/chat-core/tooling/active-memory.js +5 -4
- package/runtime/scripts/chat-core/tooling/native-config.js +34 -3
- package/runtime/scripts/local-gateway.js +34 -27
- package/runtime/scripts/platform-context.txt +76 -94
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +0 -103
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +0 -157
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +0 -439
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +0 -176
- package/runtime/scripts/amalgm-mcp/events/executor.js +0 -258
- package/runtime/scripts/amalgm-mcp/events/rest.js +0 -214
- package/runtime/scripts/amalgm-mcp/events/tools.js +0 -323
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +0 -110
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +0 -416
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workflow storage — ~/.amalgm/workflows.json.
|
|
5
|
+
*
|
|
6
|
+
* Workflows are the executable primitive. Event triggers match inbound events
|
|
7
|
+
* and point at one or more workflow ids; scheduled tasks remain a separate
|
|
8
|
+
* working path.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const { STORAGE_DIR, WORKFLOWS_FILE } = require('../config');
|
|
13
|
+
const { ensureDir, readJson, writeJsonAtomic } = require('../lib/storage');
|
|
14
|
+
const { appendStateEvent } = require('../state/events');
|
|
15
|
+
const { compileWorkflowText } = require('./compiler');
|
|
16
|
+
|
|
17
|
+
function nowIso() {
|
|
18
|
+
return new Date().toISOString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeStringList(values) {
|
|
22
|
+
if (!Array.isArray(values)) return [];
|
|
23
|
+
return [...new Set(values
|
|
24
|
+
.filter((value) => typeof value === 'string')
|
|
25
|
+
.map((value) => value.trim())
|
|
26
|
+
.filter(Boolean))];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeWorkflowRecord(workflow) {
|
|
30
|
+
const workflowText = String(workflow.workflowText || workflow.workflow || '').trim();
|
|
31
|
+
let workflowIr = workflow.workflowIr && workflow.workflowIr.version === 1 ? workflow.workflowIr : null;
|
|
32
|
+
let compilerErrors = null;
|
|
33
|
+
|
|
34
|
+
if (workflowText) {
|
|
35
|
+
try {
|
|
36
|
+
workflowIr = compileWorkflowText(workflowText);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
workflowIr = null;
|
|
39
|
+
compilerErrors = [error.message || String(error)];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
id: workflow.id || crypto.randomUUID(),
|
|
45
|
+
name: workflow.name || 'Untitled workflow',
|
|
46
|
+
description: workflow.description || '',
|
|
47
|
+
enabled: workflow.enabled !== false,
|
|
48
|
+
projectPath: workflow.projectPath || null,
|
|
49
|
+
workflowText,
|
|
50
|
+
workflowIr,
|
|
51
|
+
compilerErrors,
|
|
52
|
+
allowlist: workflow.allowlist && typeof workflow.allowlist === 'object' ? workflow.allowlist : {},
|
|
53
|
+
limits: workflow.limits && typeof workflow.limits === 'object' ? workflow.limits : {},
|
|
54
|
+
triggerIds: normalizeStringList(workflow.triggerIds),
|
|
55
|
+
internal: workflow.internal === true,
|
|
56
|
+
createdAt: workflow.createdAt || nowIso(),
|
|
57
|
+
updatedAt: workflow.updatedAt || null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ensureWorkflowDirs() {
|
|
62
|
+
ensureDir(STORAGE_DIR);
|
|
63
|
+
if (!readJson(WORKFLOWS_FILE, null)) {
|
|
64
|
+
writeJsonAtomic(WORKFLOWS_FILE, { version: 1, workflows: [] });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function migrateWorkflowsData(data) {
|
|
69
|
+
let changed = false;
|
|
70
|
+
const rawWorkflows = Array.isArray(data?.workflows) ? data.workflows : [];
|
|
71
|
+
const workflows = rawWorkflows
|
|
72
|
+
.filter((workflow) => workflow && (workflow.workflowText || workflow.workflow || workflow.workflowIr))
|
|
73
|
+
.map((workflow) => {
|
|
74
|
+
const normalized = normalizeWorkflowRecord(workflow);
|
|
75
|
+
if (JSON.stringify(normalized) !== JSON.stringify(workflow)) changed = true;
|
|
76
|
+
return normalized;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (workflows.length !== rawWorkflows.length) changed = true;
|
|
80
|
+
if (data?.version !== 1) changed = true;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
changed,
|
|
84
|
+
data: {
|
|
85
|
+
version: 1,
|
|
86
|
+
workflows,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function loadWorkflows() {
|
|
92
|
+
ensureWorkflowDirs();
|
|
93
|
+
const raw = readJson(WORKFLOWS_FILE, { version: 1, workflows: [] });
|
|
94
|
+
const migrated = migrateWorkflowsData(raw);
|
|
95
|
+
if (migrated.changed) writeJsonAtomic(WORKFLOWS_FILE, migrated.data);
|
|
96
|
+
return migrated.data;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function publishWorkflowsChange(data, source = 'workflows') {
|
|
100
|
+
try {
|
|
101
|
+
appendStateEvent({
|
|
102
|
+
resource: 'workflows',
|
|
103
|
+
op: 'replace',
|
|
104
|
+
value: Array.isArray(data?.workflows) ? data.workflows : [],
|
|
105
|
+
source,
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.warn('[Workflows] Local Live Store publish failed:', error.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function saveWorkflows(data, options = {}) {
|
|
113
|
+
ensureWorkflowDirs();
|
|
114
|
+
const normalized = migrateWorkflowsData(data).data;
|
|
115
|
+
writeJsonAtomic(WORKFLOWS_FILE, normalized);
|
|
116
|
+
publishWorkflowsChange(normalized, options.source || 'workflows:save');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getWorkflowById(workflowId) {
|
|
120
|
+
if (!workflowId) return null;
|
|
121
|
+
return loadWorkflows().workflows.find((workflow) => workflow.id === workflowId) || null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createWorkflow(input) {
|
|
125
|
+
const data = loadWorkflows();
|
|
126
|
+
const workflow = normalizeWorkflowRecord({
|
|
127
|
+
...input,
|
|
128
|
+
id: input.id || crypto.randomUUID(),
|
|
129
|
+
createdAt: input.createdAt || nowIso(),
|
|
130
|
+
updatedAt: input.updatedAt || null,
|
|
131
|
+
});
|
|
132
|
+
if (!workflow.workflowText) throw new Error('workflowText is required.');
|
|
133
|
+
if (workflow.compilerErrors?.length) throw new Error(workflow.compilerErrors.join('\n'));
|
|
134
|
+
data.workflows.push(workflow);
|
|
135
|
+
saveWorkflows(data, { source: 'workflow:create' });
|
|
136
|
+
return workflow;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function updateWorkflow(workflowId, updates) {
|
|
140
|
+
const data = loadWorkflows();
|
|
141
|
+
const index = data.workflows.findIndex((workflow) => workflow.id === workflowId);
|
|
142
|
+
if (index === -1) throw new Error(`Workflow not found: ${workflowId}`);
|
|
143
|
+
const workflow = normalizeWorkflowRecord({
|
|
144
|
+
...data.workflows[index],
|
|
145
|
+
...updates,
|
|
146
|
+
id: workflowId,
|
|
147
|
+
updatedAt: nowIso(),
|
|
148
|
+
});
|
|
149
|
+
if (!workflow.workflowText) throw new Error('workflowText is required.');
|
|
150
|
+
if (workflow.compilerErrors?.length) throw new Error(workflow.compilerErrors.join('\n'));
|
|
151
|
+
data.workflows[index] = workflow;
|
|
152
|
+
saveWorkflows(data, { source: 'workflow:update' });
|
|
153
|
+
return workflow;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function deleteWorkflow(workflowId) {
|
|
157
|
+
const data = loadWorkflows();
|
|
158
|
+
const index = data.workflows.findIndex((workflow) => workflow.id === workflowId);
|
|
159
|
+
if (index === -1) throw new Error(`Workflow not found: ${workflowId}`);
|
|
160
|
+
const [deleted] = data.workflows.splice(index, 1);
|
|
161
|
+
saveWorkflows(data, { source: 'workflow:delete' });
|
|
162
|
+
return deleted;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function upsertWorkflowFromTrigger(trigger) {
|
|
166
|
+
if (!trigger?.workflowText && !trigger?.workflow && !trigger?.workflowIr) return null;
|
|
167
|
+
const data = loadWorkflows();
|
|
168
|
+
const workflowId = trigger.workflowId || `workflow:${trigger.id}`;
|
|
169
|
+
const index = data.workflows.findIndex((workflow) => workflow.id === workflowId);
|
|
170
|
+
const existing = index === -1 ? null : data.workflows[index];
|
|
171
|
+
const next = normalizeWorkflowRecord({
|
|
172
|
+
...(existing || {}),
|
|
173
|
+
id: workflowId,
|
|
174
|
+
name: trigger.workflowName || trigger.name || 'Untitled workflow',
|
|
175
|
+
description: trigger.description || '',
|
|
176
|
+
enabled: trigger.enabled !== false,
|
|
177
|
+
projectPath: trigger.projectPath || null,
|
|
178
|
+
workflowText: trigger.workflowText || trigger.workflow || '',
|
|
179
|
+
workflowIr: trigger.workflowIr || null,
|
|
180
|
+
allowlist: trigger.allowlist || {},
|
|
181
|
+
limits: trigger.limits || {},
|
|
182
|
+
triggerIds: [trigger.id],
|
|
183
|
+
internal: trigger.internal === true,
|
|
184
|
+
createdAt: index === -1 ? (trigger.createdAt || nowIso()) : existing.createdAt,
|
|
185
|
+
updatedAt: index === -1 ? null : existing.updatedAt,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (existing && JSON.stringify(existing) === JSON.stringify(next)) return existing;
|
|
189
|
+
if (existing) next.updatedAt = nowIso();
|
|
190
|
+
if (index === -1) data.workflows.push(next);
|
|
191
|
+
else data.workflows[index] = next;
|
|
192
|
+
saveWorkflows(data, { source: 'workflow:migrate-trigger' });
|
|
193
|
+
return next;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function syncWorkflowTriggerLinks(triggers) {
|
|
197
|
+
const data = loadWorkflows();
|
|
198
|
+
let changed = false;
|
|
199
|
+
|
|
200
|
+
const triggerIdsByWorkflow = new Map();
|
|
201
|
+
for (const trigger of Array.isArray(triggers) ? triggers : []) {
|
|
202
|
+
const triggerId = String(trigger?.id || '').trim();
|
|
203
|
+
if (!triggerId) continue;
|
|
204
|
+
const workflowIds = normalizeStringList(trigger.workflowIds);
|
|
205
|
+
for (const workflowId of workflowIds) {
|
|
206
|
+
if (!triggerIdsByWorkflow.has(workflowId)) triggerIdsByWorkflow.set(workflowId, []);
|
|
207
|
+
triggerIdsByWorkflow.get(workflowId).push(triggerId);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const workflows = data.workflows.map((workflow) => {
|
|
212
|
+
const nextTriggerIds = normalizeStringList(
|
|
213
|
+
triggerIdsByWorkflow.has(workflow.id) ? triggerIdsByWorkflow.get(workflow.id) : [],
|
|
214
|
+
);
|
|
215
|
+
if (JSON.stringify(nextTriggerIds) === JSON.stringify(workflow.triggerIds || [])) return workflow;
|
|
216
|
+
changed = true;
|
|
217
|
+
return {
|
|
218
|
+
...workflow,
|
|
219
|
+
triggerIds: nextTriggerIds,
|
|
220
|
+
updatedAt: nowIso(),
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (changed) saveWorkflows({ ...data, workflows }, { source: 'workflow:sync-trigger-links' });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
createWorkflow,
|
|
229
|
+
deleteWorkflow,
|
|
230
|
+
getWorkflowById,
|
|
231
|
+
loadWorkflows,
|
|
232
|
+
normalizeWorkflowRecord,
|
|
233
|
+
saveWorkflows,
|
|
234
|
+
syncWorkflowTriggerLinks,
|
|
235
|
+
updateWorkflow,
|
|
236
|
+
upsertWorkflowFromTrigger,
|
|
237
|
+
};
|
|
@@ -8,7 +8,7 @@ const { normalizeClaudeMessage, usageRecordsFromClaudeResult, usageFromClaude }
|
|
|
8
8
|
const { recordNativeEvent } = require('../recorder');
|
|
9
9
|
const { toClaudeMcpServers } = require('../tooling/mcp-bundle');
|
|
10
10
|
const { bundledClaudeBinary } = require('../tooling/native-binaries');
|
|
11
|
-
const { claudeNativeHookSettings } = require('../tooling/native-config');
|
|
11
|
+
const { claudeNativeHookSettings, syncNativeHarnessConfig } = require('../tooling/native-config');
|
|
12
12
|
const { importPackage } = require('../tooling/package-import');
|
|
13
13
|
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
14
14
|
|
|
@@ -32,6 +32,7 @@ class ClaudeAdapter {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
options(contract, extra = {}) {
|
|
35
|
+
syncNativeHarnessConfig(contract);
|
|
35
36
|
const systemPrompt = composeSystemPrompt(contract);
|
|
36
37
|
const pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_BINARY || bundledClaudeBinary();
|
|
37
38
|
const settings = {
|
|
@@ -69,25 +69,80 @@ function coerceAuth(harness, requested) {
|
|
|
69
69
|
return requested && supported.includes(requested) ? requested : supported[0];
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (authMethod === 'provider_auth' && harness !== 'codex') return null;
|
|
76
|
-
const suffix = tokenFingerprint || 'default';
|
|
77
|
-
return path.join(amalgmDir, 'runtime', sessionId, harness, suffix);
|
|
72
|
+
function safePathSegment(value) {
|
|
73
|
+
const clean = String(value || '').trim().replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
74
|
+
return clean || 'default';
|
|
78
75
|
}
|
|
79
76
|
|
|
80
|
-
function
|
|
81
|
-
if (
|
|
77
|
+
function findStableJsonIdentity(value, depth = 0) {
|
|
78
|
+
if (!value || typeof value !== 'object' || depth > 4) return null;
|
|
79
|
+
const keys = ['account_id', 'accountId', 'user_id', 'userId', 'email', 'login', 'username', 'sub'];
|
|
80
|
+
for (const key of keys) {
|
|
81
|
+
const candidate = value[key];
|
|
82
|
+
if (typeof candidate === 'string' && candidate.trim()) return `${key}:${candidate.trim()}`;
|
|
83
|
+
}
|
|
84
|
+
for (const child of Object.values(value)) {
|
|
85
|
+
const found = findStableJsonIdentity(child, depth + 1);
|
|
86
|
+
if (found) return found;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readJsonIdentity(file) {
|
|
82
92
|
try {
|
|
83
|
-
const raw = fs.readFileSync(
|
|
84
|
-
return
|
|
93
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
94
|
+
return findStableJsonIdentity(JSON.parse(raw));
|
|
85
95
|
} catch {
|
|
86
96
|
return null;
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
function
|
|
100
|
+
function providerAuthIdentity(harness) {
|
|
101
|
+
if (harness === 'codex') {
|
|
102
|
+
return readJsonIdentity(path.join(os.homedir(), '.codex', 'auth.json')) || 'codex:provider-default';
|
|
103
|
+
}
|
|
104
|
+
if (harness === 'claude_code') {
|
|
105
|
+
return readJsonIdentity(path.join(os.homedir(), '.claude.json'))
|
|
106
|
+
|| readJsonIdentity(path.join(os.homedir(), '.claude', 'auth.json'))
|
|
107
|
+
|| 'claude_code:provider-default';
|
|
108
|
+
}
|
|
109
|
+
return `${harness || 'provider'}:provider-default`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function deriveAuthProfileId({ method, harness, userId, tokenFingerprint }) {
|
|
113
|
+
if (method === 'amalgm') {
|
|
114
|
+
return `amalgm-${fingerprint(userId || process.env.AMALGM_USER_ID || 'local') || 'local'}`;
|
|
115
|
+
}
|
|
116
|
+
if (method === 'byok') {
|
|
117
|
+
return `byok-${tokenFingerprint || 'default'}`;
|
|
118
|
+
}
|
|
119
|
+
if (method === 'provider_auth') {
|
|
120
|
+
return `provider-${fingerprint(providerAuthIdentity(harness)) || 'default'}`;
|
|
121
|
+
}
|
|
122
|
+
return 'default';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function runtimeHome({ amalgmDir, harness, authProfileId }) {
|
|
126
|
+
return path.join(amalgmDir, 'cli-homes', safePathSegment(harness), safePathSegment(authProfileId));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function nativeProviderHome(harness) {
|
|
130
|
+
if (harness === 'claude_code') return os.homedir();
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function pinnedRuntimeHome(amalgmDir, cliHomePath) {
|
|
135
|
+
if (typeof cliHomePath !== 'string' || !cliHomePath.trim()) return null;
|
|
136
|
+
const root = path.resolve(amalgmDir);
|
|
137
|
+
const resolved = path.resolve(cliHomePath.trim());
|
|
138
|
+
return resolved === root || resolved.startsWith(`${root}${path.sep}`) ? resolved : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function providerAuthFingerprint(harness) {
|
|
142
|
+
return fingerprint(providerAuthIdentity(harness));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken, proxyBaseUrl, amalgmDir, userId, credentialId, modelId, cliHomePath, authProfileId }) {
|
|
91
146
|
const method = coerceAuth(harness, authMethod);
|
|
92
147
|
const byok = readByokFile(amalgmDir);
|
|
93
148
|
let baseUrl = null;
|
|
@@ -116,6 +171,7 @@ function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken
|
|
|
116
171
|
} else if (method === 'provider_auth') {
|
|
117
172
|
tokenFingerprint = providerAuthFingerprint(harness);
|
|
118
173
|
}
|
|
174
|
+
const profileId = authProfileId || deriveAuthProfileId({ method, harness, userId, tokenFingerprint });
|
|
119
175
|
return {
|
|
120
176
|
method,
|
|
121
177
|
baseUrl,
|
|
@@ -125,7 +181,11 @@ function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken
|
|
|
125
181
|
forwardBaseUrl,
|
|
126
182
|
tokenRef,
|
|
127
183
|
tokenFingerprint,
|
|
128
|
-
|
|
184
|
+
authProfileId: profileId,
|
|
185
|
+
runtimeHome:
|
|
186
|
+
nativeProviderHome(method === 'provider_auth' ? harness : null)
|
|
187
|
+
|| pinnedRuntimeHome(amalgmDir, cliHomePath)
|
|
188
|
+
|| runtimeHome({ amalgmDir, harness, authProfileId: profileId }),
|
|
129
189
|
};
|
|
130
190
|
}
|
|
131
191
|
|
|
@@ -142,11 +202,21 @@ function runtimeEnv(contract, baseEnv = process.env) {
|
|
|
142
202
|
for (const key of ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', 'AI_GATEWAY_API_KEY', 'AI_GATEWAY_BASE_URL']) {
|
|
143
203
|
delete env[key];
|
|
144
204
|
}
|
|
205
|
+
if (contract.harness === 'claude_code' && contract.authMethod === 'provider_auth') {
|
|
206
|
+
env.HOME = os.homedir();
|
|
207
|
+
delete env.CLAUDE_CONFIG_DIR;
|
|
208
|
+
return {
|
|
209
|
+
...env,
|
|
210
|
+
IS_SANDBOX: '1',
|
|
211
|
+
...(baseEnv.AMALGM_RUNTIME_TOKEN ? { AMALGM_RUNTIME_TOKEN: baseEnv.AMALGM_RUNTIME_TOKEN } : {}),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
145
214
|
if (contract.auth.runtimeHome) {
|
|
146
215
|
fs.mkdirSync(contract.auth.runtimeHome, { recursive: true });
|
|
147
216
|
env.HOME = contract.auth.runtimeHome;
|
|
148
217
|
env.CODEX_HOME = contract.auth.runtimeHome;
|
|
149
218
|
env.CLAUDE_CONFIG_DIR = contract.auth.runtimeHome;
|
|
219
|
+
env.OPENCODE_HOME = contract.auth.runtimeHome;
|
|
150
220
|
env.OPENCODE_CONFIG_DIR = contract.auth.runtimeHome;
|
|
151
221
|
}
|
|
152
222
|
env.IS_SANDBOX = '1';
|
|
@@ -326,6 +326,7 @@ function createContract(payload, options = {}) {
|
|
|
326
326
|
const cwd = resolveCwd(payload.cwd, options);
|
|
327
327
|
const localBaseUrl = options.localBaseUrl || `http://127.0.0.1:${options.port || runtimePort('chat-server')}`;
|
|
328
328
|
const origin = normalizeOrigin({ ...payload, cwd });
|
|
329
|
+
const userId = payload.userId || options.userId || 'unknown-user';
|
|
329
330
|
const auth = authEnvelope({
|
|
330
331
|
harness,
|
|
331
332
|
authMethod,
|
|
@@ -334,12 +335,15 @@ function createContract(payload, options = {}) {
|
|
|
334
335
|
proxyToken: options.proxyToken || PROXY_TOKEN,
|
|
335
336
|
proxyBaseUrl: options.proxyBaseUrl || PROXY_BASE_URL || 'https://amalgm-api-proxy-v2.fly.dev',
|
|
336
337
|
amalgmDir: options.amalgmDir || AMALGM_DIR || path.join(process.env.HOME || '.', '.amalgm'),
|
|
338
|
+
userId,
|
|
337
339
|
credentialId: payload.credentialId || payload.byokCredentialId || null,
|
|
338
340
|
modelId,
|
|
341
|
+
cliHomePath: payload.cliHomePath || payload.cli_home_path || null,
|
|
342
|
+
authProfileId: payload.authProfileId || payload.auth_profile_id || null,
|
|
339
343
|
});
|
|
340
344
|
const contract = {
|
|
341
345
|
sessionId,
|
|
342
|
-
userId
|
|
346
|
+
userId,
|
|
343
347
|
computerId: payload.computerId || process.env.AMALGM_CONTAINER_ID || 'local-computer',
|
|
344
348
|
assistantMessageId,
|
|
345
349
|
userMessageId: payload.userMessageId || null,
|
|
@@ -15,6 +15,13 @@ function warnIfDbNotSaved(result, label) {
|
|
|
15
15
|
return true;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function assertDbSaved(result, label) {
|
|
19
|
+
if (result === false || result === null) {
|
|
20
|
+
throw new Error(`${label} was not saved; keeping temp/raw stream for reconnect`);
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
async function bestEffortDbSave(label, fn) {
|
|
19
26
|
try {
|
|
20
27
|
return warnIfDbNotSaved(await fn(), label);
|
|
@@ -43,6 +50,23 @@ async function withDeadline(label, promise, ms) {
|
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
function scheduleTitleGeneration({ db, userMessageSave, contract, input, turns, turnId, writeFrame }) {
|
|
54
|
+
if (typeof db.generateAndSaveTitle !== 'function' || !input.text) return null;
|
|
55
|
+
return Promise.resolve(userMessageSave)
|
|
56
|
+
.then((saved) => {
|
|
57
|
+
if (saved === false || saved === null) return null;
|
|
58
|
+
return db.generateAndSaveTitle(contract.sessionId, input.text, (title) => {
|
|
59
|
+
const frame = titleFrame({ sessionId: contract.sessionId, title });
|
|
60
|
+
const chunk = turns.addChunk(turnId, (index) => withSseId(frame, index));
|
|
61
|
+
if (writeFrame && chunk) writeFrame(chunk.data);
|
|
62
|
+
});
|
|
63
|
+
})
|
|
64
|
+
.catch((err) => {
|
|
65
|
+
console.warn('[ChatCore] Title generation failed:', err.message);
|
|
66
|
+
return null;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
46
70
|
class ChatCore {
|
|
47
71
|
constructor({ db, runtime, turns, options = {} }) {
|
|
48
72
|
this.db = db;
|
|
@@ -72,20 +96,31 @@ class ChatCore {
|
|
|
72
96
|
};
|
|
73
97
|
}
|
|
74
98
|
|
|
75
|
-
async
|
|
99
|
+
async finalizeActiveTurn(sessionId) {
|
|
76
100
|
const entry = this.turns.active(sessionId);
|
|
77
101
|
if (!entry) return false;
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
if (!entry.streamEnded) {
|
|
103
|
+
this.turns.mark(entry.turnId, 'cancelling');
|
|
104
|
+
await this.runtime.stop(sessionId);
|
|
105
|
+
}
|
|
106
|
+
if (entry.finalized && typeof entry.finalized.then === 'function') {
|
|
107
|
+
await entry.finalized;
|
|
108
|
+
}
|
|
80
109
|
return true;
|
|
81
110
|
}
|
|
82
111
|
|
|
112
|
+
async stop(sessionId) {
|
|
113
|
+
return this.finalizeActiveTurn(sessionId);
|
|
114
|
+
}
|
|
115
|
+
|
|
83
116
|
async destroy(sessionId) {
|
|
84
117
|
this.contracts.delete(sessionId);
|
|
85
118
|
return this.runtime.destroy(sessionId);
|
|
86
119
|
}
|
|
87
120
|
|
|
88
121
|
async runTurn(payload, writeFrame) {
|
|
122
|
+
const sessionId = payload.codeSessionId || payload.sessionId;
|
|
123
|
+
if (sessionId) await this.finalizeActiveTurn(sessionId);
|
|
89
124
|
const contract = this.contractFor(payload);
|
|
90
125
|
await this.beginTurn(contract);
|
|
91
126
|
const input = normalizeInput(payload);
|
|
@@ -121,10 +156,6 @@ class ChatCore {
|
|
|
121
156
|
console.warn('[ChatCore] Failed to load cold-resume conversation history:', err.message);
|
|
122
157
|
}
|
|
123
158
|
}
|
|
124
|
-
const assistantPlaceholderSave = bestEffortDbSave(
|
|
125
|
-
'Assistant placeholder',
|
|
126
|
-
() => this.db.ensureAssistantMessage(msgConfig),
|
|
127
|
-
);
|
|
128
159
|
this.db.mergeSessionMetadata(contract.sessionId, {
|
|
129
160
|
contract: {
|
|
130
161
|
userId: contract.userId,
|
|
@@ -148,6 +179,7 @@ class ChatCore {
|
|
|
148
179
|
auth: {
|
|
149
180
|
baseUrl: contract.auth.baseUrl,
|
|
150
181
|
tokenFingerprint: contract.auth.tokenFingerprint,
|
|
182
|
+
authProfileId: contract.auth.authProfileId,
|
|
151
183
|
runtimeHome: contract.auth.runtimeHome,
|
|
152
184
|
},
|
|
153
185
|
},
|
|
@@ -161,19 +193,23 @@ class ChatCore {
|
|
|
161
193
|
});
|
|
162
194
|
entry.persistence = {
|
|
163
195
|
userMessageSaved: null,
|
|
164
|
-
assistantPlaceholderSaved: null,
|
|
165
196
|
assistantMessageSaved: null,
|
|
166
197
|
};
|
|
198
|
+
entry.streamEnded = false;
|
|
199
|
+
let resolveFinalized = null;
|
|
200
|
+
entry.finalized = new Promise((resolve) => {
|
|
201
|
+
resolveFinalized = resolve;
|
|
202
|
+
});
|
|
167
203
|
|
|
168
|
-
|
|
169
|
-
this.db
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
204
|
+
scheduleTitleGeneration({
|
|
205
|
+
db: this.db,
|
|
206
|
+
userMessageSave,
|
|
207
|
+
contract,
|
|
208
|
+
input,
|
|
209
|
+
turns: this.turns,
|
|
210
|
+
turnId,
|
|
211
|
+
writeFrame,
|
|
212
|
+
});
|
|
177
213
|
|
|
178
214
|
const parts = new PartAccumulator();
|
|
179
215
|
let usage = null;
|
|
@@ -181,8 +217,10 @@ class ChatCore {
|
|
|
181
217
|
let providerSessionId = contract.providerSessionId || null;
|
|
182
218
|
let stopReason = 'end_turn';
|
|
183
219
|
let sawDone = false;
|
|
220
|
+
let finalDoneEvent = null;
|
|
184
221
|
|
|
185
|
-
const append = (e) => {
|
|
222
|
+
const append = (e, options = {}) => {
|
|
223
|
+
const emitFrame = options.emitFrame !== false;
|
|
186
224
|
if (e.providerSessionId) providerSessionId = e.providerSessionId;
|
|
187
225
|
if (e.type === 'usage.final') {
|
|
188
226
|
usage = e.usage;
|
|
@@ -191,62 +229,65 @@ class ChatCore {
|
|
|
191
229
|
if (e.type === 'done') {
|
|
192
230
|
sawDone = true;
|
|
193
231
|
stopReason = e.stopReason || stopReason;
|
|
232
|
+
finalDoneEvent = e;
|
|
194
233
|
}
|
|
195
234
|
parts.apply(e);
|
|
235
|
+
if (!emitFrame) return;
|
|
196
236
|
const frame = frameFor(e, { sessionId: contract.sessionId, providerSessionId, usage });
|
|
197
237
|
const chunk = this.turns.addChunk(turnId, (index) => withSseId(frame, index));
|
|
198
238
|
if (writeFrame && chunk) writeFrame(chunk.data);
|
|
199
239
|
};
|
|
200
240
|
|
|
201
241
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
242
|
+
try {
|
|
243
|
+
const stream = await this.runtime.prompt(contract, runtimeInput);
|
|
244
|
+
for await (const e of stream) append(e, { emitFrame: e.type !== 'done' });
|
|
245
|
+
const current = this.turns.get(turnId);
|
|
246
|
+
if (!sawDone) {
|
|
247
|
+
append(done({
|
|
248
|
+
providerSessionId,
|
|
249
|
+
stopReason: current?.status === 'cancelling' ? 'cancelled' : stopReason,
|
|
250
|
+
}), { emitFrame: false });
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
append(errorEvent(err.message, { providerSessionId }));
|
|
254
|
+
append(done({ providerSessionId, stopReason: 'error' }), { emitFrame: false });
|
|
255
|
+
stopReason = 'error';
|
|
210
256
|
}
|
|
211
|
-
|
|
212
|
-
append(errorEvent(err.message, { providerSessionId }));
|
|
213
|
-
append(done({ providerSessionId, stopReason: 'error' }));
|
|
214
|
-
stopReason = 'error';
|
|
215
|
-
}
|
|
257
|
+
entry.streamEnded = true;
|
|
216
258
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
259
|
+
const savedParts = parts.finalize();
|
|
260
|
+
this.turns.setParts(turnId, savedParts);
|
|
261
|
+
const finalizePersistence = async () => {
|
|
262
|
+
const userMessageSaved = await userMessageSave;
|
|
263
|
+
assertDbSaved(userMessageSaved, 'User message');
|
|
264
|
+
const assistantMessageSaved = await this.db.saveAssistantMessage(msgConfig, savedParts, providerSessionId, usage);
|
|
265
|
+
if (entry.persistence) {
|
|
266
|
+
entry.persistence.userMessageSaved = userMessageSaved;
|
|
267
|
+
entry.persistence.assistantMessageSaved = assistantMessageSaved;
|
|
268
|
+
}
|
|
269
|
+
assertDbSaved(assistantMessageSaved, 'Assistant message');
|
|
270
|
+
await logTurnUsage({
|
|
271
|
+
db: this.db,
|
|
272
|
+
contract,
|
|
273
|
+
messageId: contract.assistantMessageId,
|
|
274
|
+
usage,
|
|
275
|
+
usageRecords,
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
try {
|
|
279
|
+
await finalizePersistence();
|
|
280
|
+
} catch (err) {
|
|
239
281
|
this.turns.mark(turnId, 'save_failed');
|
|
240
|
-
|
|
241
|
-
this.turns.mark(turnId, stopReason === 'error' ? 'error' : stopReason === 'cancelled' ? 'cancelled' : 'complete');
|
|
242
|
-
this.turns.clear(turnId);
|
|
282
|
+
throw err;
|
|
243
283
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
284
|
+
append(finalDoneEvent || done({ providerSessionId, stopReason }));
|
|
285
|
+
this.turns.mark(turnId, stopReason === 'error' ? 'error' : stopReason === 'cancelled' ? 'cancelled' : 'complete');
|
|
286
|
+
this.turns.clear(turnId);
|
|
287
|
+
return { providerSessionId, usage, parts: savedParts, stopReason };
|
|
288
|
+
} finally {
|
|
289
|
+
if (resolveFinalized) resolveFinalized();
|
|
290
|
+
}
|
|
250
291
|
}
|
|
251
292
|
}
|
|
252
293
|
|
|
@@ -96,6 +96,13 @@ const textDeltaEvent = baseEvent.extend({
|
|
|
96
96
|
streamKind: z.literal('text').optional(),
|
|
97
97
|
}).strict();
|
|
98
98
|
|
|
99
|
+
const textBoundaryEvent = baseEvent.extend({
|
|
100
|
+
type: z.literal('text.boundary'),
|
|
101
|
+
streamKind: z.literal('text').optional(),
|
|
102
|
+
itemId: z.string().optional(),
|
|
103
|
+
reason: z.string().optional(),
|
|
104
|
+
}).strict();
|
|
105
|
+
|
|
99
106
|
const reasoningDeltaEvent = baseEvent.extend({
|
|
100
107
|
type: z.literal('reasoning.delta'),
|
|
101
108
|
text: z.string(),
|
|
@@ -194,6 +201,7 @@ const compactionFinishedEvent = baseEvent.extend({
|
|
|
194
201
|
|
|
195
202
|
const amalgmEventSchema = z.discriminatedUnion('type', [
|
|
196
203
|
textDeltaEvent,
|
|
204
|
+
textBoundaryEvent,
|
|
197
205
|
reasoningDeltaEvent,
|
|
198
206
|
toolStartedEvent,
|
|
199
207
|
toolUpdatedEvent,
|