elasticdash-test 0.1.18-alpha-12 → 0.1.18-alpha-14
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.
|
@@ -18,7 +18,6 @@ import 'dotenv/config';
|
|
|
18
18
|
* will replay from frozen data instead of hitting real services.
|
|
19
19
|
*/
|
|
20
20
|
import { pathToFileURL } from 'node:url';
|
|
21
|
-
import { interceptFetch, restoreFetch } from './interceptors/http.js';
|
|
22
21
|
const RESULT_PREFIX = '__ELASTICDASH_RESULT__:';
|
|
23
22
|
function writeResult(result) {
|
|
24
23
|
return new Promise((resolve, reject) => {
|
|
@@ -27,50 +26,73 @@ function writeResult(result) {
|
|
|
27
26
|
}
|
|
28
27
|
/**
|
|
29
28
|
* Set up frozen event replay context.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
*
|
|
30
|
+
* For HTTP events: uses **URL-based matching** via a fetch wrapper. The
|
|
31
|
+
* ID-based matching in HttpRunContext doesn't work for observability-mode
|
|
32
|
+
* reruns because wrapTool/wrapAI also consume nextId() IDs, causing the
|
|
33
|
+
* sequential counter to drift away from the frozen event IDs.
|
|
34
|
+
*
|
|
35
|
+
* For DB events: uses the HttpRunContext frozen map so wrapDB-wrapped
|
|
36
|
+
* clients can replay them by sequential ID (DB calls don't share the
|
|
37
|
+
* counter with tool/AI wrappers in the same way).
|
|
32
38
|
*/
|
|
33
39
|
async function setupFrozenContext(frozenEvents) {
|
|
34
40
|
if (frozenEvents.length === 0)
|
|
35
41
|
return;
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
42
|
+
// Always install URL-based fetch replay for HTTP events — this is the
|
|
43
|
+
// reliable matching strategy for tool reruns in subprocesses.
|
|
44
|
+
installFrozenFetchFallback(frozenEvents);
|
|
45
|
+
// Also set up HttpRunContext for DB frozen events (wrapDB uses ID-based matching).
|
|
46
|
+
const dbEvents = frozenEvents.filter(e => e.type === 'db');
|
|
47
|
+
if (dbEvents.length > 0) {
|
|
48
|
+
try {
|
|
49
|
+
const telemetryPush = await import('./interceptors/telemetry-push.js');
|
|
50
|
+
const workflowEvents = dbEvents.map((e, i) => ({
|
|
51
|
+
id: i + 1,
|
|
52
|
+
type: e.type,
|
|
53
|
+
name: e.name,
|
|
54
|
+
input: e.input,
|
|
55
|
+
output: e.output,
|
|
56
|
+
timestamp: e.timestamp,
|
|
57
|
+
durationMs: e.durationMs ?? 0,
|
|
58
|
+
streamed: e.streamed,
|
|
59
|
+
streamRaw: e.streamRaw,
|
|
60
|
+
}));
|
|
61
|
+
telemetryPush.setHttpRunContext('frozen-rerun', '');
|
|
62
|
+
const ctx = telemetryPush.getHttpRunContext();
|
|
63
|
+
if (ctx) {
|
|
64
|
+
for (const e of workflowEvents) {
|
|
65
|
+
ctx.frozenEvents.set(e.id, e);
|
|
66
|
+
}
|
|
59
67
|
}
|
|
60
68
|
}
|
|
69
|
+
catch {
|
|
70
|
+
// DB replay not available — tool will hit real DB
|
|
71
|
+
}
|
|
61
72
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
}
|
|
74
|
+
/** Saved original fetch so we can restore after the tool runs. */
|
|
75
|
+
let savedOriginalFetch = null;
|
|
76
|
+
function restoreFrozenFetch() {
|
|
77
|
+
if (savedOriginalFetch) {
|
|
78
|
+
globalThis.fetch = savedOriginalFetch;
|
|
79
|
+
savedOriginalFetch = null;
|
|
65
80
|
}
|
|
66
81
|
}
|
|
67
82
|
/**
|
|
68
|
-
*
|
|
83
|
+
* Intercept fetch for HTTP frozen events using URL-based matching.
|
|
84
|
+
* This is the primary replay mechanism for tool reruns — it matches
|
|
85
|
+
* by request URL rather than sequential ID, which is robust against
|
|
86
|
+
* ID counter drift from wrapTool/wrapAI consuming IDs.
|
|
69
87
|
*/
|
|
70
88
|
function installFrozenFetchFallback(frozenEvents) {
|
|
71
89
|
const httpEvents = frozenEvents.filter(e => e.type === 'http');
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
const frozenUrls = httpEvents.map(e => {
|
|
91
|
+
const inp = e.input;
|
|
92
|
+
return inp && typeof inp === 'object' && typeof inp.url === 'string' ? inp.url : '(no url)';
|
|
93
|
+
});
|
|
94
|
+
process.stderr.write(`[elasticdash-worker] Frozen HTTP events: ${httpEvents.length}, URLs: ${JSON.stringify(frozenUrls)}\n`);
|
|
95
|
+
savedOriginalFetch = globalThis.fetch;
|
|
74
96
|
const originalFetch = globalThis.fetch;
|
|
75
97
|
const usageCounts = new Map();
|
|
76
98
|
globalThis.fetch = async (input, init) => {
|
|
@@ -85,13 +107,21 @@ function installFrozenFetchFallback(frozenEvents) {
|
|
|
85
107
|
const callCount = usageCounts.get(url) || 0;
|
|
86
108
|
const match = matches[Math.min(callCount, matches.length - 1)];
|
|
87
109
|
usageCounts.set(url, callCount + 1);
|
|
110
|
+
process.stderr.write(`[elasticdash-worker] Frozen replay: ${url}\n`);
|
|
88
111
|
const body = typeof match.output === 'string' ? match.output : JSON.stringify(match.output);
|
|
89
112
|
return new Response(body, {
|
|
90
113
|
status: 200,
|
|
91
114
|
headers: { 'Content-Type': 'application/json', 'X-Frozen': 'true' }
|
|
92
115
|
});
|
|
93
116
|
}
|
|
94
|
-
|
|
117
|
+
process.stderr.write(`[elasticdash-worker] Fetch pass-through (no frozen match): ${url}\n`);
|
|
118
|
+
try {
|
|
119
|
+
return await originalFetch(input, init);
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
process.stderr.write(`[elasticdash-worker] Fetch FAILED for ${url}: ${e.stack || e.message || String(e)}\n`);
|
|
123
|
+
throw e;
|
|
124
|
+
}
|
|
95
125
|
};
|
|
96
126
|
}
|
|
97
127
|
async function main() {
|
|
@@ -110,15 +140,12 @@ async function main() {
|
|
|
110
140
|
return;
|
|
111
141
|
}
|
|
112
142
|
const { toolsModulePath, toolName, args, frozenEvents } = payload;
|
|
113
|
-
// Set up frozen event context before running the tool
|
|
143
|
+
// Set up frozen event context before running the tool.
|
|
144
|
+
// Uses URL-based matching for HTTP events (reliable for observability reruns)
|
|
145
|
+
// and ID-based matching for DB events.
|
|
114
146
|
const hasFrozen = frozenEvents && frozenEvents.length > 0;
|
|
115
147
|
if (hasFrozen) {
|
|
116
148
|
await setupFrozenContext(frozenEvents);
|
|
117
|
-
// Install the full fetch interceptor so frozen HTTP events are replayed.
|
|
118
|
-
// setupFrozenContext loads frozen events into HttpRunContext, but without
|
|
119
|
-
// interceptFetch() the events are never read — fetch hits real services
|
|
120
|
-
// which can fail (e.g. "Invalid URL" for relative URLs).
|
|
121
|
-
interceptFetch();
|
|
122
149
|
}
|
|
123
150
|
try {
|
|
124
151
|
let mod;
|
|
@@ -144,12 +171,13 @@ async function main() {
|
|
|
144
171
|
catch (e) {
|
|
145
172
|
const err = e;
|
|
146
173
|
const errorMsg = err.stack || err.message || String(e);
|
|
174
|
+
process.stderr.write(`[elasticdash-worker] Tool execution failed:\n${errorMsg}\n`);
|
|
147
175
|
await writeResult({ ok: false, error: errorMsg });
|
|
148
176
|
originalExit(1);
|
|
149
177
|
}
|
|
150
178
|
finally {
|
|
151
179
|
if (hasFrozen)
|
|
152
|
-
|
|
180
|
+
restoreFrozenFetch();
|
|
153
181
|
}
|
|
154
182
|
}
|
|
155
183
|
main();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-runner-worker.js","sourceRoot":"","sources":["../src/tool-runner-worker.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,CAAC;AAAC,UAAkB,CAAC,sBAAsB,GAAG,IAAI,CAAA;AAElD,iDAAiD;AACjD,OAAO,eAAe,CAAA;AAEtB;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"tool-runner-worker.js","sourceRoot":"","sources":["../src/tool-runner-worker.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -20,7 +20,6 @@ import 'dotenv/config'
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { pathToFileURL } from 'node:url'
|
|
23
|
-
import { interceptFetch, restoreFetch } from './interceptors/http.js'
|
|
24
23
|
|
|
25
24
|
const RESULT_PREFIX = '__ELASTICDASH_RESULT__:'
|
|
26
25
|
|
|
@@ -46,53 +45,78 @@ interface FrozenEvent {
|
|
|
46
45
|
|
|
47
46
|
/**
|
|
48
47
|
* Set up frozen event replay context.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
*
|
|
49
|
+
* For HTTP events: uses **URL-based matching** via a fetch wrapper. The
|
|
50
|
+
* ID-based matching in HttpRunContext doesn't work for observability-mode
|
|
51
|
+
* reruns because wrapTool/wrapAI also consume nextId() IDs, causing the
|
|
52
|
+
* sequential counter to drift away from the frozen event IDs.
|
|
53
|
+
*
|
|
54
|
+
* For DB events: uses the HttpRunContext frozen map so wrapDB-wrapped
|
|
55
|
+
* clients can replay them by sequential ID (DB calls don't share the
|
|
56
|
+
* counter with tool/AI wrappers in the same way).
|
|
51
57
|
*/
|
|
52
58
|
async function setupFrozenContext(frozenEvents: FrozenEvent[]): Promise<void> {
|
|
53
59
|
if (frozenEvents.length === 0) return
|
|
54
60
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
61
|
+
// Always install URL-based fetch replay for HTTP events — this is the
|
|
62
|
+
// reliable matching strategy for tool reruns in subprocesses.
|
|
63
|
+
installFrozenFetchFallback(frozenEvents)
|
|
64
|
+
|
|
65
|
+
// Also set up HttpRunContext for DB frozen events (wrapDB uses ID-based matching).
|
|
66
|
+
const dbEvents = frozenEvents.filter(e => e.type === 'db')
|
|
67
|
+
if (dbEvents.length > 0) {
|
|
68
|
+
try {
|
|
69
|
+
const telemetryPush = await import('./interceptors/telemetry-push.js')
|
|
70
|
+
const workflowEvents = dbEvents.map((e, i) => ({
|
|
71
|
+
id: i + 1,
|
|
72
|
+
type: e.type as 'db',
|
|
73
|
+
name: e.name,
|
|
74
|
+
input: e.input,
|
|
75
|
+
output: e.output,
|
|
76
|
+
timestamp: e.timestamp,
|
|
77
|
+
durationMs: e.durationMs ?? 0,
|
|
78
|
+
streamed: e.streamed,
|
|
79
|
+
streamRaw: e.streamRaw,
|
|
80
|
+
}))
|
|
81
|
+
telemetryPush.setHttpRunContext('frozen-rerun', '')
|
|
82
|
+
const ctx = telemetryPush.getHttpRunContext()
|
|
83
|
+
if (ctx) {
|
|
84
|
+
for (const e of workflowEvents) {
|
|
85
|
+
ctx.frozenEvents.set(e.id, e as any)
|
|
86
|
+
}
|
|
81
87
|
}
|
|
88
|
+
} catch {
|
|
89
|
+
// DB replay not available — tool will hit real DB
|
|
82
90
|
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Saved original fetch so we can restore after the tool runs. */
|
|
95
|
+
let savedOriginalFetch: typeof globalThis.fetch | null = null
|
|
96
|
+
|
|
97
|
+
function restoreFrozenFetch(): void {
|
|
98
|
+
if (savedOriginalFetch) {
|
|
99
|
+
globalThis.fetch = savedOriginalFetch
|
|
100
|
+
savedOriginalFetch = null
|
|
86
101
|
}
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
/**
|
|
90
|
-
*
|
|
105
|
+
* Intercept fetch for HTTP frozen events using URL-based matching.
|
|
106
|
+
* This is the primary replay mechanism for tool reruns — it matches
|
|
107
|
+
* by request URL rather than sequential ID, which is robust against
|
|
108
|
+
* ID counter drift from wrapTool/wrapAI consuming IDs.
|
|
91
109
|
*/
|
|
92
110
|
function installFrozenFetchFallback(frozenEvents: FrozenEvent[]): void {
|
|
93
111
|
const httpEvents = frozenEvents.filter(e => e.type === 'http')
|
|
94
|
-
if (httpEvents.length === 0) return
|
|
95
112
|
|
|
113
|
+
const frozenUrls = httpEvents.map(e => {
|
|
114
|
+
const inp = e.input as Record<string, unknown> | null
|
|
115
|
+
return inp && typeof inp === 'object' && typeof inp.url === 'string' ? inp.url : '(no url)'
|
|
116
|
+
})
|
|
117
|
+
process.stderr.write(`[elasticdash-worker] Frozen HTTP events: ${httpEvents.length}, URLs: ${JSON.stringify(frozenUrls)}\n`)
|
|
118
|
+
|
|
119
|
+
savedOriginalFetch = globalThis.fetch
|
|
96
120
|
const originalFetch = globalThis.fetch
|
|
97
121
|
const usageCounts = new Map<string, number>()
|
|
98
122
|
|
|
@@ -111,6 +135,7 @@ function installFrozenFetchFallback(frozenEvents: FrozenEvent[]): void {
|
|
|
111
135
|
const match = matches[Math.min(callCount, matches.length - 1)]
|
|
112
136
|
usageCounts.set(url, callCount + 1)
|
|
113
137
|
|
|
138
|
+
process.stderr.write(`[elasticdash-worker] Frozen replay: ${url}\n`)
|
|
114
139
|
const body = typeof match.output === 'string' ? match.output : JSON.stringify(match.output)
|
|
115
140
|
return new Response(body, {
|
|
116
141
|
status: 200,
|
|
@@ -118,7 +143,13 @@ function installFrozenFetchFallback(frozenEvents: FrozenEvent[]): void {
|
|
|
118
143
|
})
|
|
119
144
|
}
|
|
120
145
|
|
|
121
|
-
|
|
146
|
+
process.stderr.write(`[elasticdash-worker] Fetch pass-through (no frozen match): ${url}\n`)
|
|
147
|
+
try {
|
|
148
|
+
return await originalFetch(input, init)
|
|
149
|
+
} catch (e) {
|
|
150
|
+
process.stderr.write(`[elasticdash-worker] Fetch FAILED for ${url}: ${(e as Error).stack || (e as Error).message || String(e)}\n`)
|
|
151
|
+
throw e
|
|
152
|
+
}
|
|
122
153
|
}
|
|
123
154
|
}
|
|
124
155
|
|
|
@@ -141,15 +172,12 @@ async function main() {
|
|
|
141
172
|
|
|
142
173
|
const { toolsModulePath, toolName, args, frozenEvents } = payload
|
|
143
174
|
|
|
144
|
-
// Set up frozen event context before running the tool
|
|
175
|
+
// Set up frozen event context before running the tool.
|
|
176
|
+
// Uses URL-based matching for HTTP events (reliable for observability reruns)
|
|
177
|
+
// and ID-based matching for DB events.
|
|
145
178
|
const hasFrozen = frozenEvents && frozenEvents.length > 0
|
|
146
179
|
if (hasFrozen) {
|
|
147
180
|
await setupFrozenContext(frozenEvents)
|
|
148
|
-
// Install the full fetch interceptor so frozen HTTP events are replayed.
|
|
149
|
-
// setupFrozenContext loads frozen events into HttpRunContext, but without
|
|
150
|
-
// interceptFetch() the events are never read — fetch hits real services
|
|
151
|
-
// which can fail (e.g. "Invalid URL" for relative URLs).
|
|
152
|
-
interceptFetch()
|
|
153
181
|
}
|
|
154
182
|
|
|
155
183
|
try {
|
|
@@ -175,10 +203,11 @@ async function main() {
|
|
|
175
203
|
} catch (e) {
|
|
176
204
|
const err = e as Error
|
|
177
205
|
const errorMsg = err.stack || err.message || String(e)
|
|
206
|
+
process.stderr.write(`[elasticdash-worker] Tool execution failed:\n${errorMsg}\n`)
|
|
178
207
|
await writeResult({ ok: false, error: errorMsg })
|
|
179
208
|
originalExit(1)
|
|
180
209
|
} finally {
|
|
181
|
-
if (hasFrozen)
|
|
210
|
+
if (hasFrozen) restoreFrozenFetch()
|
|
182
211
|
}
|
|
183
212
|
}
|
|
184
213
|
|