amalgm 0.1.51 → 0.1.53
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 +547 -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-binaries.js +34 -9
- 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
|
@@ -19,6 +19,10 @@ function textDelta(text, data = {}) {
|
|
|
19
19
|
return event('text.delta', { streamKind: 'text', ...data, text: String(text || '') });
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function textBoundary(data = {}) {
|
|
23
|
+
return event('text.boundary', { streamKind: 'text', ...data });
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
function reasoningDelta(text, data = {}) {
|
|
23
27
|
return event('reasoning.delta', { streamKind: 'reasoning', ...data, text: String(text || '') });
|
|
24
28
|
}
|
|
@@ -181,6 +185,7 @@ module.exports = {
|
|
|
181
185
|
event,
|
|
182
186
|
reasoningDelta,
|
|
183
187
|
reasoningStarted,
|
|
188
|
+
textBoundary,
|
|
184
189
|
textDelta,
|
|
185
190
|
toolCompleted,
|
|
186
191
|
toolStarted,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { compactionFinished, compactionStarted, done, errorEvent, reasoningDelta, textDelta, toolCompleted, toolStarted, toolUpdated, usageFinal } = require('../events');
|
|
3
|
+
const { compactionFinished, compactionStarted, done, errorEvent, reasoningDelta, textBoundary, textDelta, toolCompleted, toolStarted, toolUpdated, usageFinal } = require('../events');
|
|
4
4
|
const { stringifyResult, summarizeToolOutput, summarizeToolRequest } = require('../tool-display');
|
|
5
5
|
const { normalizeError, titleForTool, toolKind } = require('../tool-shape');
|
|
6
6
|
const { normalizeUsage } = require('../usage');
|
|
@@ -186,6 +186,10 @@ function isCodexCompactionItem(item = {}) {
|
|
|
186
186
|
return item?.type === 'contextCompaction';
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
function isCodexAgentMessageItem(item = {}) {
|
|
190
|
+
return item?.type === 'agentMessage';
|
|
191
|
+
}
|
|
192
|
+
|
|
189
193
|
function normalizeCodexNotification(msg, providerSessionId, state) {
|
|
190
194
|
const method = msg.method;
|
|
191
195
|
const p = msg.params || msg.payload || {};
|
|
@@ -210,6 +214,14 @@ function normalizeCodexNotification(msg, providerSessionId, state) {
|
|
|
210
214
|
...raw,
|
|
211
215
|
})];
|
|
212
216
|
}
|
|
217
|
+
if (method === 'item/started' && isCodexAgentMessageItem(p.item)) {
|
|
218
|
+
return [textBoundary({
|
|
219
|
+
providerSessionId,
|
|
220
|
+
itemId: p.item.id || undefined,
|
|
221
|
+
reason: 'agent_message_started',
|
|
222
|
+
...raw,
|
|
223
|
+
})];
|
|
224
|
+
}
|
|
213
225
|
if (method === 'item/agentMessage/delta' && codexDelta(msg, p)) return [textDelta(codexDelta(msg, p), { providerSessionId, ...raw })];
|
|
214
226
|
if ((method === 'item/reasoning/delta' || method === 'item/reasoning/textDelta' || method === 'item/reasoning/summaryTextDelta') && codexDelta(msg, p)) {
|
|
215
227
|
return [reasoningDelta(codexDelta(msg, p), { providerSessionId, ...raw })];
|
|
@@ -66,6 +66,7 @@ class PartAccumulator {
|
|
|
66
66
|
constructor() {
|
|
67
67
|
this.parts = [];
|
|
68
68
|
this.textBuffer = '';
|
|
69
|
+
this.currentTextPart = null;
|
|
69
70
|
this.reasoningBuffer = '';
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -74,12 +75,21 @@ class PartAccumulator {
|
|
|
74
75
|
if (e.type === 'text.delta') {
|
|
75
76
|
this.finishReasoning(e.ts);
|
|
76
77
|
this.textBuffer += e.text || '';
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
else
|
|
78
|
+
if (this.currentTextPart) {
|
|
79
|
+
this.currentTextPart.text = this.textBuffer;
|
|
80
|
+
} else {
|
|
81
|
+
this.currentTextPart = { type: 'text', text: this.textBuffer };
|
|
82
|
+
this.parts.push(this.currentTextPart);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (e.type === 'text.boundary') {
|
|
87
|
+
this.finishReasoning(e.ts);
|
|
88
|
+
this.resetText();
|
|
80
89
|
return;
|
|
81
90
|
}
|
|
82
91
|
if (e.type === 'reasoning.delta') {
|
|
92
|
+
this.resetText();
|
|
83
93
|
this.reasoningBuffer += e.text || '';
|
|
84
94
|
const last = this.parts[this.parts.length - 1];
|
|
85
95
|
if (last?.type === 'reasoning' && last.state === 'streaming') {
|
|
@@ -94,7 +104,7 @@ class PartAccumulator {
|
|
|
94
104
|
}
|
|
95
105
|
if (e.type === 'tool.started') {
|
|
96
106
|
this.finishReasoning(e.ts);
|
|
97
|
-
this.
|
|
107
|
+
this.resetText();
|
|
98
108
|
const existing = this.findTool(e.toolCallId);
|
|
99
109
|
if (existing) {
|
|
100
110
|
existing.input = { ...existing.input, ...(e.input || {}) };
|
|
@@ -164,7 +174,7 @@ class PartAccumulator {
|
|
|
164
174
|
}
|
|
165
175
|
if (e.type === 'compaction.started') {
|
|
166
176
|
this.finishReasoning(e.ts);
|
|
167
|
-
this.
|
|
177
|
+
this.resetText();
|
|
168
178
|
const part = {
|
|
169
179
|
type: 'compaction',
|
|
170
180
|
phase: 'started',
|
|
@@ -182,7 +192,7 @@ class PartAccumulator {
|
|
|
182
192
|
}
|
|
183
193
|
if (e.type === 'compaction.finished') {
|
|
184
194
|
this.finishReasoning(e.ts);
|
|
185
|
-
this.
|
|
195
|
+
this.resetText();
|
|
186
196
|
const existing = this.findOpenCompaction();
|
|
187
197
|
const finishedAt = compactionIso(e.ts);
|
|
188
198
|
const startedAt = existing?.startedAt || undefined;
|
|
@@ -230,6 +240,11 @@ class PartAccumulator {
|
|
|
230
240
|
this.reasoningBuffer = '';
|
|
231
241
|
}
|
|
232
242
|
|
|
243
|
+
resetText() {
|
|
244
|
+
this.textBuffer = '';
|
|
245
|
+
this.currentTextPart = null;
|
|
246
|
+
}
|
|
247
|
+
|
|
233
248
|
finalize() {
|
|
234
249
|
const ts = Date.now();
|
|
235
250
|
this.finishReasoning(ts);
|
|
@@ -66,6 +66,9 @@ function toAgentStreamEvent(e, context = {}) {
|
|
|
66
66
|
if (e.type === 'text.delta') {
|
|
67
67
|
return { _type: 'update', eventTimestamp: e.ts, sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: e.text || '' } };
|
|
68
68
|
}
|
|
69
|
+
if (e.type === 'text.boundary') {
|
|
70
|
+
return { _type: 'update', eventTimestamp: e.ts, sessionUpdate: 'agent_message_boundary', itemId: e.itemId || null, reason: e.reason || null };
|
|
71
|
+
}
|
|
69
72
|
if (e.type === 'reasoning.delta') {
|
|
70
73
|
return { _type: 'update', eventTimestamp: e.ts, sessionUpdate: 'agent_thought_chunk', content: { type: 'text', text: e.text || '' } };
|
|
71
74
|
}
|
|
@@ -1,10 +1,69 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert/strict');
|
|
4
|
+
const os = require('node:os');
|
|
4
5
|
const test = require('node:test');
|
|
5
6
|
const { authEnvelope, runtimeEnv } = require('../auth');
|
|
6
7
|
|
|
7
|
-
test('
|
|
8
|
+
test('amalgm auth uses a pinned CLI home across sessions and proxy token refreshes', () => {
|
|
9
|
+
const first = authEnvelope({
|
|
10
|
+
harness: 'codex',
|
|
11
|
+
authMethod: 'amalgm',
|
|
12
|
+
sessionId: 'session-one',
|
|
13
|
+
proxyToken: 'proxy-token-one',
|
|
14
|
+
localBaseUrl: 'http://127.0.0.1:8084',
|
|
15
|
+
proxyBaseUrl: 'https://proxy.example.test',
|
|
16
|
+
amalgmDir: '/tmp/amalgm-test',
|
|
17
|
+
userId: 'user-123',
|
|
18
|
+
});
|
|
19
|
+
const second = authEnvelope({
|
|
20
|
+
harness: 'codex',
|
|
21
|
+
authMethod: 'amalgm',
|
|
22
|
+
sessionId: 'session-two',
|
|
23
|
+
proxyToken: 'proxy-token-two',
|
|
24
|
+
localBaseUrl: 'http://127.0.0.1:8084',
|
|
25
|
+
proxyBaseUrl: 'https://proxy.example.test',
|
|
26
|
+
amalgmDir: '/tmp/amalgm-test',
|
|
27
|
+
userId: 'user-123',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
assert.notEqual(first.tokenFingerprint, second.tokenFingerprint);
|
|
31
|
+
assert.equal(first.runtimeHome, second.runtimeHome);
|
|
32
|
+
assert.match(first.runtimeHome, /\/tmp\/amalgm-test\/cli-homes\/codex\/amalgm-[0-9a-f]{16}$/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('opencode amalgm auth uses a pinned CLI home across sessions and proxy token refreshes', () => {
|
|
36
|
+
const first = authEnvelope({
|
|
37
|
+
harness: 'opencode',
|
|
38
|
+
authMethod: 'amalgm',
|
|
39
|
+
sessionId: 'session-one',
|
|
40
|
+
proxyToken: 'proxy-token-one',
|
|
41
|
+
localBaseUrl: 'http://127.0.0.1:8084',
|
|
42
|
+
proxyBaseUrl: 'https://proxy.example.test',
|
|
43
|
+
amalgmDir: '/tmp/amalgm-test',
|
|
44
|
+
userId: 'user-123',
|
|
45
|
+
});
|
|
46
|
+
const second = authEnvelope({
|
|
47
|
+
harness: 'opencode',
|
|
48
|
+
authMethod: 'amalgm',
|
|
49
|
+
sessionId: 'session-two',
|
|
50
|
+
proxyToken: 'proxy-token-two',
|
|
51
|
+
localBaseUrl: 'http://127.0.0.1:8084',
|
|
52
|
+
proxyBaseUrl: 'https://proxy.example.test',
|
|
53
|
+
amalgmDir: '/tmp/amalgm-test',
|
|
54
|
+
userId: 'user-123',
|
|
55
|
+
});
|
|
56
|
+
const env = runtimeEnv({ harness: 'opencode', authMethod: 'amalgm', auth: first }, { PATH: '/bin' });
|
|
57
|
+
|
|
58
|
+
assert.notEqual(first.tokenFingerprint, second.tokenFingerprint);
|
|
59
|
+
assert.equal(first.runtimeHome, second.runtimeHome);
|
|
60
|
+
assert.match(first.runtimeHome, /\/tmp\/amalgm-test\/cli-homes\/opencode\/amalgm-[0-9a-f]{16}$/);
|
|
61
|
+
assert.equal(env.HOME, first.runtimeHome);
|
|
62
|
+
assert.equal(env.OPENCODE_HOME, first.runtimeHome);
|
|
63
|
+
assert.equal(env.OPENCODE_CONFIG_DIR, first.runtimeHome);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('claude provider auth uses the native user home with CLAUDE_CONFIG_DIR unset', () => {
|
|
8
67
|
const envelope = authEnvelope({
|
|
9
68
|
harness: 'claude_code',
|
|
10
69
|
authMethod: 'provider_auth',
|
|
@@ -12,7 +71,7 @@ test('claude provider auth keeps the user home environment', () => {
|
|
|
12
71
|
amalgmDir: '/tmp/amalgm-test',
|
|
13
72
|
});
|
|
14
73
|
|
|
15
|
-
assert.equal(envelope.runtimeHome,
|
|
74
|
+
assert.equal(envelope.runtimeHome, os.homedir());
|
|
16
75
|
|
|
17
76
|
const env = runtimeEnv({
|
|
18
77
|
harness: 'claude_code',
|
|
@@ -25,12 +84,12 @@ test('claude provider auth keeps the user home environment', () => {
|
|
|
25
84
|
ANTHROPIC_API_KEY: 'should-not-leak',
|
|
26
85
|
});
|
|
27
86
|
|
|
28
|
-
assert.equal(env.HOME,
|
|
29
|
-
assert.equal(env.CLAUDE_CONFIG_DIR,
|
|
87
|
+
assert.equal(env.HOME, os.homedir());
|
|
88
|
+
assert.equal(env.CLAUDE_CONFIG_DIR, undefined);
|
|
30
89
|
assert.equal(env.ANTHROPIC_API_KEY, undefined);
|
|
31
90
|
});
|
|
32
91
|
|
|
33
|
-
test('codex provider auth
|
|
92
|
+
test('codex provider auth uses a pinned provider home', () => {
|
|
34
93
|
const envelope = authEnvelope({
|
|
35
94
|
harness: 'codex',
|
|
36
95
|
authMethod: 'provider_auth',
|
|
@@ -38,5 +97,24 @@ test('codex provider auth still receives an isolated runtime home', () => {
|
|
|
38
97
|
amalgmDir: '/tmp/amalgm-test',
|
|
39
98
|
});
|
|
40
99
|
|
|
41
|
-
assert.match(envelope.runtimeHome, /\/tmp\/amalgm-test\/
|
|
100
|
+
assert.match(envelope.runtimeHome, /\/tmp\/amalgm-test\/cli-homes\/codex\/provider-[0-9a-f]{16}$/);
|
|
101
|
+
assert.equal(envelope.runtimeHome.includes('/runtime/session-test/'), false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('stored CLI home path wins over derived path', () => {
|
|
105
|
+
const envelope = authEnvelope({
|
|
106
|
+
harness: 'codex',
|
|
107
|
+
authMethod: 'amalgm',
|
|
108
|
+
sessionId: 'session-test',
|
|
109
|
+
proxyToken: 'proxy-token',
|
|
110
|
+
localBaseUrl: 'http://127.0.0.1:8084',
|
|
111
|
+
proxyBaseUrl: 'https://proxy.example.test',
|
|
112
|
+
amalgmDir: '/tmp/amalgm-test',
|
|
113
|
+
userId: 'user-123',
|
|
114
|
+
cliHomePath: '/tmp/amalgm-test/cli-homes/codex/manual-profile',
|
|
115
|
+
authProfileId: 'manual-profile',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
assert.equal(envelope.authProfileId, 'manual-profile');
|
|
119
|
+
assert.equal(envelope.runtimeHome, '/tmp/amalgm-test/cli-homes/codex/manual-profile');
|
|
42
120
|
});
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const { ChatCore } = require('../engine');
|
|
6
|
+
const { done, textDelta } = require('../events');
|
|
7
|
+
const { TurnStore } = require('../stores');
|
|
8
|
+
|
|
9
|
+
function payload(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
codeSessionId: 'session-test',
|
|
12
|
+
assistantMessageId: 'assistant-test',
|
|
13
|
+
userMessageId: 'user-test',
|
|
14
|
+
userId: 'user-test',
|
|
15
|
+
agentId: 'claude_code',
|
|
16
|
+
authMethod: 'amalgm',
|
|
17
|
+
modelId: 'anthropic/claude-opus-4.7',
|
|
18
|
+
cwd: process.cwd(),
|
|
19
|
+
prompt: 'hello',
|
|
20
|
+
proxyToken: 'proxy-token',
|
|
21
|
+
proxyBaseUrl: 'https://proxy.example.test',
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function runtime() {
|
|
27
|
+
return {
|
|
28
|
+
get: () => null,
|
|
29
|
+
stop: async () => {},
|
|
30
|
+
destroy: async () => true,
|
|
31
|
+
async *prompt() {
|
|
32
|
+
yield textDelta('hi');
|
|
33
|
+
yield done({ providerSessionId: 'provider-test' });
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function waitTick() {
|
|
39
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('title generation waits for user save and does not block a fast turn', async () => {
|
|
43
|
+
let resolveUserSave;
|
|
44
|
+
let resolveTitle;
|
|
45
|
+
let titleStarted = false;
|
|
46
|
+
const frames = [];
|
|
47
|
+
const db = {
|
|
48
|
+
saveUserMessage: async () => new Promise((resolve) => {
|
|
49
|
+
resolveUserSave = () => resolve(true);
|
|
50
|
+
}),
|
|
51
|
+
ensureAssistantMessage: async () => true,
|
|
52
|
+
saveAssistantMessage: async () => true,
|
|
53
|
+
mergeSessionMetadata: async () => {},
|
|
54
|
+
generateAndSaveTitle: async (_sessionId, _prompt, onTitle) => {
|
|
55
|
+
titleStarted = true;
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
resolveTitle = () => {
|
|
58
|
+
onTitle('Generated Title');
|
|
59
|
+
resolve('Generated Title');
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
logUsage: async () => {},
|
|
64
|
+
};
|
|
65
|
+
const core = new ChatCore({
|
|
66
|
+
db,
|
|
67
|
+
runtime: runtime(),
|
|
68
|
+
turns: new TurnStore(),
|
|
69
|
+
options: {
|
|
70
|
+
beginTurn: async () => {},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const run = core.runTurn(payload(), (frame) => frames.push(frame));
|
|
75
|
+
await waitTick();
|
|
76
|
+
|
|
77
|
+
assert.equal(titleStarted, false, 'title generation should not race ahead of the user message save');
|
|
78
|
+
assert.equal(frames.some((frame) => frame.includes('"complete"')), false, 'complete waits for final persistence');
|
|
79
|
+
|
|
80
|
+
resolveUserSave();
|
|
81
|
+
await run;
|
|
82
|
+
assert.equal(frames.some((frame) => frame.includes('"complete"')), true, 'turn completes after persistence settles');
|
|
83
|
+
assert.equal(frames.some((frame) => frame.includes('"title_generated"')), false, 'pending title should not hold the stream open');
|
|
84
|
+
await waitTick();
|
|
85
|
+
assert.equal(titleStarted, true, 'title generation should start after the user message save settles');
|
|
86
|
+
|
|
87
|
+
resolveTitle();
|
|
88
|
+
await waitTick();
|
|
89
|
+
|
|
90
|
+
assert.equal(frames.some((frame) => frame.includes('"title_generated"')), false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('complete frame waits for assistant message save', async () => {
|
|
94
|
+
let resolveAssistantSave;
|
|
95
|
+
const frames = [];
|
|
96
|
+
const db = {
|
|
97
|
+
saveUserMessage: async () => true,
|
|
98
|
+
ensureAssistantMessage: async () => true,
|
|
99
|
+
saveAssistantMessage: async () => new Promise((resolve) => {
|
|
100
|
+
resolveAssistantSave = () => resolve(true);
|
|
101
|
+
}),
|
|
102
|
+
mergeSessionMetadata: async () => {},
|
|
103
|
+
logUsage: async () => {},
|
|
104
|
+
};
|
|
105
|
+
const core = new ChatCore({
|
|
106
|
+
db,
|
|
107
|
+
runtime: runtime(),
|
|
108
|
+
turns: new TurnStore(),
|
|
109
|
+
options: {
|
|
110
|
+
beginTurn: async () => {},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const run = core.runTurn(payload(), (frame) => frames.push(frame));
|
|
115
|
+
await waitTick();
|
|
116
|
+
|
|
117
|
+
assert.equal(frames.some((frame) => frame.includes('agent_message_chunk')), true, 'content should still stream immediately');
|
|
118
|
+
assert.equal(frames.some((frame) => frame.includes('"complete"')), false, 'complete must not outrun assistant save');
|
|
119
|
+
assert.equal(core.active('session-test').active, true, 'turn remains active while persistence is pending');
|
|
120
|
+
|
|
121
|
+
resolveAssistantSave();
|
|
122
|
+
await run;
|
|
123
|
+
|
|
124
|
+
assert.equal(frames.some((frame) => frame.includes('"complete"')), true, 'complete is emitted after assistant save');
|
|
125
|
+
assert.equal(core.turns.active('session-test'), null, 'temp/raw state clears after save and complete');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('provider streams while user save is pending but assistant save waits', async () => {
|
|
129
|
+
let resolveUserSave;
|
|
130
|
+
let resolveAssistantSave;
|
|
131
|
+
const order = [];
|
|
132
|
+
const db = {
|
|
133
|
+
saveUserMessage: async () => new Promise((resolve) => {
|
|
134
|
+
order.push('saveUserMessage:start');
|
|
135
|
+
resolveUserSave = () => {
|
|
136
|
+
order.push('saveUserMessage:done');
|
|
137
|
+
resolve(true);
|
|
138
|
+
};
|
|
139
|
+
}),
|
|
140
|
+
ensureAssistantMessage: async () => {
|
|
141
|
+
order.push('ensureAssistantMessage');
|
|
142
|
+
return true;
|
|
143
|
+
},
|
|
144
|
+
saveAssistantMessage: async () => new Promise((resolve) => {
|
|
145
|
+
order.push('saveAssistantMessage');
|
|
146
|
+
resolveAssistantSave = () => resolve(true);
|
|
147
|
+
}),
|
|
148
|
+
mergeSessionMetadata: async () => {},
|
|
149
|
+
logUsage: async () => {},
|
|
150
|
+
};
|
|
151
|
+
const fakeRuntime = {
|
|
152
|
+
get: () => null,
|
|
153
|
+
stop: async () => {},
|
|
154
|
+
destroy: async () => true,
|
|
155
|
+
async *prompt() {
|
|
156
|
+
order.push('prompt');
|
|
157
|
+
yield textDelta('hi');
|
|
158
|
+
yield done({ providerSessionId: 'provider-test' });
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
const core = new ChatCore({
|
|
162
|
+
db,
|
|
163
|
+
runtime: fakeRuntime,
|
|
164
|
+
turns: new TurnStore(),
|
|
165
|
+
options: {
|
|
166
|
+
beginTurn: async () => {},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const run = core.runTurn(payload(), () => {});
|
|
171
|
+
await waitTick();
|
|
172
|
+
|
|
173
|
+
assert.deepEqual(order, ['saveUserMessage:start', 'prompt'], 'provider can stream while user save is pending');
|
|
174
|
+
|
|
175
|
+
resolveUserSave();
|
|
176
|
+
await waitTick();
|
|
177
|
+
|
|
178
|
+
assert.deepEqual(order, [
|
|
179
|
+
'saveUserMessage:start',
|
|
180
|
+
'prompt',
|
|
181
|
+
'saveUserMessage:done',
|
|
182
|
+
'saveAssistantMessage',
|
|
183
|
+
]);
|
|
184
|
+
assert.equal(resolveAssistantSave !== undefined, true);
|
|
185
|
+
resolveAssistantSave();
|
|
186
|
+
await run;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('stop waits for cancelled turn persistence', async () => {
|
|
190
|
+
let releaseRuntime;
|
|
191
|
+
let resolveAssistantSave;
|
|
192
|
+
const frames = [];
|
|
193
|
+
const fakeRuntime = {
|
|
194
|
+
get: () => null,
|
|
195
|
+
stop: async () => {
|
|
196
|
+
if (releaseRuntime) releaseRuntime();
|
|
197
|
+
},
|
|
198
|
+
destroy: async () => true,
|
|
199
|
+
async *prompt() {
|
|
200
|
+
yield textDelta('partial', { providerSessionId: 'provider-test' });
|
|
201
|
+
await new Promise((resolve) => {
|
|
202
|
+
releaseRuntime = resolve;
|
|
203
|
+
});
|
|
204
|
+
yield done({ providerSessionId: 'provider-test', stopReason: 'cancelled' });
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const db = {
|
|
208
|
+
saveUserMessage: async () => true,
|
|
209
|
+
ensureAssistantMessage: async () => true,
|
|
210
|
+
saveAssistantMessage: async () => new Promise((resolve) => {
|
|
211
|
+
resolveAssistantSave = () => resolve(true);
|
|
212
|
+
}),
|
|
213
|
+
mergeSessionMetadata: async () => {},
|
|
214
|
+
logUsage: async () => {},
|
|
215
|
+
};
|
|
216
|
+
const core = new ChatCore({
|
|
217
|
+
db,
|
|
218
|
+
runtime: fakeRuntime,
|
|
219
|
+
turns: new TurnStore(),
|
|
220
|
+
options: {
|
|
221
|
+
beginTurn: async () => {},
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const turn = core.runTurn(payload(), (frame) => frames.push(frame));
|
|
226
|
+
await waitTick();
|
|
227
|
+
|
|
228
|
+
let stopResolved = false;
|
|
229
|
+
const stop = core.stop('session-test').then((result) => {
|
|
230
|
+
stopResolved = true;
|
|
231
|
+
return result;
|
|
232
|
+
});
|
|
233
|
+
await waitTick();
|
|
234
|
+
|
|
235
|
+
assert.equal(stopResolved, false, 'stop should wait for cancelled turn persistence');
|
|
236
|
+
assert.equal(frames.some((frame) => frame.includes('"stopReason":"cancelled"')), false, 'cancelled complete waits for save');
|
|
237
|
+
|
|
238
|
+
resolveAssistantSave();
|
|
239
|
+
assert.equal(await stop, true);
|
|
240
|
+
const result = await turn;
|
|
241
|
+
|
|
242
|
+
assert.equal(result.stopReason, 'cancelled');
|
|
243
|
+
assert.equal(stopResolved, true);
|
|
244
|
+
assert.equal(frames.some((frame) => frame.includes('"stopReason":"cancelled"')), true);
|
|
245
|
+
assert.equal(core.turns.latest('session-test'), null, 'cancelled raw-temp clears after save');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('new turn waits for active turn finalization before starting', async () => {
|
|
249
|
+
let resolveFirstSave;
|
|
250
|
+
let stopCalls = 0;
|
|
251
|
+
const prompts = [];
|
|
252
|
+
const dbCalls = [];
|
|
253
|
+
const fakeRuntime = {
|
|
254
|
+
get: () => null,
|
|
255
|
+
stop: async () => {
|
|
256
|
+
stopCalls += 1;
|
|
257
|
+
},
|
|
258
|
+
destroy: async () => true,
|
|
259
|
+
async *prompt(_contract, input) {
|
|
260
|
+
prompts.push(input.text);
|
|
261
|
+
yield textDelta(input.text, { providerSessionId: 'provider-test' });
|
|
262
|
+
yield done({ providerSessionId: 'provider-test' });
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
const db = {
|
|
266
|
+
saveUserMessage: async (config) => {
|
|
267
|
+
dbCalls.push(['saveUserMessage', config.userMessageId]);
|
|
268
|
+
return true;
|
|
269
|
+
},
|
|
270
|
+
ensureAssistantMessage: async () => true,
|
|
271
|
+
saveAssistantMessage: async (config) => {
|
|
272
|
+
dbCalls.push(['saveAssistantMessage', config.assistantMessageId]);
|
|
273
|
+
if (config.assistantMessageId === 'assistant-test') {
|
|
274
|
+
return new Promise((resolve) => {
|
|
275
|
+
resolveFirstSave = () => resolve(true);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
},
|
|
280
|
+
mergeSessionMetadata: async () => {},
|
|
281
|
+
logUsage: async () => {},
|
|
282
|
+
};
|
|
283
|
+
const core = new ChatCore({
|
|
284
|
+
db,
|
|
285
|
+
runtime: fakeRuntime,
|
|
286
|
+
turns: new TurnStore(),
|
|
287
|
+
options: {
|
|
288
|
+
beginTurn: async () => {},
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const first = core.runTurn(payload({ prompt: 'first' }), () => {});
|
|
293
|
+
await waitTick();
|
|
294
|
+
|
|
295
|
+
const second = core.runTurn(payload({
|
|
296
|
+
userMessageId: 'user-test-2',
|
|
297
|
+
assistantMessageId: 'assistant-test-2',
|
|
298
|
+
prompt: 'second',
|
|
299
|
+
}), () => {});
|
|
300
|
+
await waitTick();
|
|
301
|
+
|
|
302
|
+
assert.deepEqual(prompts, ['first'], 'second provider turn should not start before first finalizes');
|
|
303
|
+
assert.equal(dbCalls.some((call) => call[0] === 'saveUserMessage' && call[1] === 'user-test-2'), false, 'second turn persistence waits too');
|
|
304
|
+
assert.equal(stopCalls, 0, 'finished stream should be awaited, not stopped during final save');
|
|
305
|
+
|
|
306
|
+
resolveFirstSave();
|
|
307
|
+
await first;
|
|
308
|
+
await second;
|
|
309
|
+
|
|
310
|
+
assert.deepEqual(prompts, ['first', 'second']);
|
|
311
|
+
assert.equal(dbCalls.some((call) => call[0] === 'saveUserMessage' && call[1] === 'user-test-2'), true);
|
|
312
|
+
});
|
|
@@ -9,6 +9,7 @@ const { ClaudeAdapter } = require('../adapters/claude');
|
|
|
9
9
|
const { __private: codexPrivate } = require('../adapters/codex');
|
|
10
10
|
const {
|
|
11
11
|
claudeNativeHookSettings,
|
|
12
|
+
syncClaudeNativeConfig,
|
|
12
13
|
syncCodexNativeConfig,
|
|
13
14
|
} = require('../tooling/native-config');
|
|
14
15
|
|
|
@@ -125,3 +126,25 @@ test('claude extracts native hooks without enabling full filesystem settings', (
|
|
|
125
126
|
assert.equal(options.settings.hooks.UserPromptSubmit[0].hooks[0].command, 'echo recall');
|
|
126
127
|
});
|
|
127
128
|
});
|
|
129
|
+
|
|
130
|
+
test('claude native sync skips debug symlinks', () => {
|
|
131
|
+
withNativeHome((home) => {
|
|
132
|
+
const source = path.join(home, '.claude');
|
|
133
|
+
fs.mkdirSync(path.join(source, 'debug'), { recursive: true });
|
|
134
|
+
fs.writeFileSync(path.join(source, 'settings.json'), '{"hooks":{}}');
|
|
135
|
+
const debugFile = path.join(source, 'debug', 'trace.txt');
|
|
136
|
+
fs.writeFileSync(debugFile, 'debug');
|
|
137
|
+
fs.symlinkSync(debugFile, path.join(source, 'debug', 'latest'));
|
|
138
|
+
|
|
139
|
+
const runtimeHome = path.join(home, 'runtime-home');
|
|
140
|
+
fs.mkdirSync(path.join(runtimeHome, 'debug'), { recursive: true });
|
|
141
|
+
fs.symlinkSync(debugFile, path.join(runtimeHome, 'debug', 'latest'));
|
|
142
|
+
|
|
143
|
+
const result = syncClaudeNativeConfig(runtimeHome);
|
|
144
|
+
|
|
145
|
+
assert.equal(result.sourceDir, source);
|
|
146
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'settings.json')), true);
|
|
147
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'debug', 'trace.txt')), false);
|
|
148
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'debug', 'latest')), false);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -84,10 +84,10 @@ function titleFromMcp(tool, input = {}) {
|
|
|
84
84
|
if (name === 'browser_type') return input.text ? `Typing "${compactText(input.text, 60)}"` : 'Typing in browser';
|
|
85
85
|
if (name === 'browser_click') return 'Clicking in browser';
|
|
86
86
|
if (name === 'browser_wait_for_load_state') return 'Waiting for page load';
|
|
87
|
-
if (name === '
|
|
88
|
-
if (name === '
|
|
89
|
-
if (name === '
|
|
90
|
-
if (name === '
|
|
87
|
+
if (name === 'apps_register') return input.name ? `Registering app ${compactText(input.name, 60)}` : 'Registering app';
|
|
88
|
+
if (name === 'apps_start') return 'Starting app';
|
|
89
|
+
if (name === 'apps_stop') return 'Stopping app';
|
|
90
|
+
if (name === 'apps_list') return 'Listing apps';
|
|
91
91
|
if (name === 'agents_list') return 'Listing agents';
|
|
92
92
|
if (name === 'talk_to_agent') return 'Talking to agent';
|
|
93
93
|
return humanizeToolName(action || name);
|
|
@@ -20,14 +20,15 @@ const BASE_FILES = [
|
|
|
20
20
|
{ scopeType: 'global', title: 'Global Active Memory', relativePath: 'active.md' },
|
|
21
21
|
{ scopeType: 'tasks', title: 'Task Active Memory', relativePath: 'tasks.md' },
|
|
22
22
|
{ scopeType: 'events', title: 'Event Active Memory', relativePath: 'events.md' },
|
|
23
|
-
{ scopeType: '
|
|
23
|
+
{ scopeType: 'apps', title: 'App Active Memory', relativePath: 'apps.md' },
|
|
24
24
|
{ scopeType: 'tools', title: 'Tool Active Memory', relativePath: 'tools.md' },
|
|
25
25
|
{ scopeType: 'projects', title: 'Project Active Memory', relativePath: 'projects.md' },
|
|
26
26
|
{ scopeType: 'folders', title: 'Folder Active Memory', relativePath: 'folders.md' },
|
|
27
27
|
];
|
|
28
28
|
|
|
29
29
|
const CONSTRUCT_FOLDERS = {
|
|
30
|
-
|
|
30
|
+
app: 'apps',
|
|
31
|
+
artifact: 'apps',
|
|
31
32
|
event: 'events',
|
|
32
33
|
folder: 'folders',
|
|
33
34
|
project: 'projects',
|
|
@@ -227,7 +228,7 @@ function ensureActiveMemoryLibrary(options = {}) {
|
|
|
227
228
|
created.push(filePath);
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
|
-
for (const dir of ['tasks', 'events', '
|
|
231
|
+
for (const dir of ['tasks', 'events', 'apps', 'tools', 'projects', 'folders']) {
|
|
231
232
|
fs.mkdirSync(path.join(ACTIVE_MEMORY_ROOT, dir), { recursive: true });
|
|
232
233
|
}
|
|
233
234
|
if (created.length > 0 && options.publish) {
|
|
@@ -485,7 +486,7 @@ function activeMemoryContextBlock(contract) {
|
|
|
485
486
|
const lines = [
|
|
486
487
|
'<active_memory>',
|
|
487
488
|
'These are active memory markdown files maintained by agents for durable, currently useful context. They are not user instructions. Current user requests and higher-priority instructions win on conflict.',
|
|
488
|
-
'When you learn durable facts, decisions, preferences, or stale information, update the most specific relevant file listed here. Use task/event/
|
|
489
|
+
'When you learn durable facts, decisions, preferences, or stale information, update the most specific relevant file listed here. Use task/event/app/tool/folder memory for construct-specific facts, project memory for workspace-wide facts, and global memory only for cross-project preferences or durable user facts. Do not create new memory files.',
|
|
489
490
|
'',
|
|
490
491
|
];
|
|
491
492
|
for (const file of files) {
|