@predicatelabs/sdk 0.99.9

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.
Files changed (302) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +252 -0
  3. package/dist/actions.d.ts +185 -0
  4. package/dist/actions.d.ts.map +1 -0
  5. package/dist/actions.js +1120 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/agent-runtime.d.ts +352 -0
  8. package/dist/agent-runtime.d.ts.map +1 -0
  9. package/dist/agent-runtime.js +1170 -0
  10. package/dist/agent-runtime.js.map +1 -0
  11. package/dist/agent.d.ts +164 -0
  12. package/dist/agent.d.ts.map +1 -0
  13. package/dist/agent.js +408 -0
  14. package/dist/agent.js.map +1 -0
  15. package/dist/asserts/expect.d.ts +159 -0
  16. package/dist/asserts/expect.d.ts.map +1 -0
  17. package/dist/asserts/expect.js +547 -0
  18. package/dist/asserts/expect.js.map +1 -0
  19. package/dist/asserts/index.d.ts +58 -0
  20. package/dist/asserts/index.d.ts.map +1 -0
  21. package/dist/asserts/index.js +70 -0
  22. package/dist/asserts/index.js.map +1 -0
  23. package/dist/asserts/query.d.ts +199 -0
  24. package/dist/asserts/query.d.ts.map +1 -0
  25. package/dist/asserts/query.js +288 -0
  26. package/dist/asserts/query.js.map +1 -0
  27. package/dist/backends/actions.d.ts +119 -0
  28. package/dist/backends/actions.d.ts.map +1 -0
  29. package/dist/backends/actions.js +291 -0
  30. package/dist/backends/actions.js.map +1 -0
  31. package/dist/backends/browser-use-adapter.d.ts +131 -0
  32. package/dist/backends/browser-use-adapter.d.ts.map +1 -0
  33. package/dist/backends/browser-use-adapter.js +219 -0
  34. package/dist/backends/browser-use-adapter.js.map +1 -0
  35. package/dist/backends/cdp-backend.d.ts +66 -0
  36. package/dist/backends/cdp-backend.d.ts.map +1 -0
  37. package/dist/backends/cdp-backend.js +273 -0
  38. package/dist/backends/cdp-backend.js.map +1 -0
  39. package/dist/backends/index.d.ts +80 -0
  40. package/dist/backends/index.d.ts.map +1 -0
  41. package/dist/backends/index.js +101 -0
  42. package/dist/backends/index.js.map +1 -0
  43. package/dist/backends/protocol.d.ts +156 -0
  44. package/dist/backends/protocol.d.ts.map +1 -0
  45. package/dist/backends/protocol.js +16 -0
  46. package/dist/backends/protocol.js.map +1 -0
  47. package/dist/backends/sentience-context.d.ts +143 -0
  48. package/dist/backends/sentience-context.d.ts.map +1 -0
  49. package/dist/backends/sentience-context.js +359 -0
  50. package/dist/backends/sentience-context.js.map +1 -0
  51. package/dist/backends/snapshot.d.ts +188 -0
  52. package/dist/backends/snapshot.d.ts.map +1 -0
  53. package/dist/backends/snapshot.js +360 -0
  54. package/dist/backends/snapshot.js.map +1 -0
  55. package/dist/browser.d.ts +154 -0
  56. package/dist/browser.d.ts.map +1 -0
  57. package/dist/browser.js +920 -0
  58. package/dist/browser.js.map +1 -0
  59. package/dist/canonicalization.d.ts +126 -0
  60. package/dist/canonicalization.d.ts.map +1 -0
  61. package/dist/canonicalization.js +161 -0
  62. package/dist/canonicalization.js.map +1 -0
  63. package/dist/captcha/strategies.d.ts +12 -0
  64. package/dist/captcha/strategies.d.ts.map +1 -0
  65. package/dist/captcha/strategies.js +43 -0
  66. package/dist/captcha/strategies.js.map +1 -0
  67. package/dist/captcha/types.d.ts +45 -0
  68. package/dist/captcha/types.d.ts.map +1 -0
  69. package/dist/captcha/types.js +12 -0
  70. package/dist/captcha/types.js.map +1 -0
  71. package/dist/cli.d.ts +5 -0
  72. package/dist/cli.d.ts.map +1 -0
  73. package/dist/cli.js +422 -0
  74. package/dist/cli.js.map +1 -0
  75. package/dist/conversational-agent.d.ts +123 -0
  76. package/dist/conversational-agent.d.ts.map +1 -0
  77. package/dist/conversational-agent.js +341 -0
  78. package/dist/conversational-agent.js.map +1 -0
  79. package/dist/cursor-policy.d.ts +41 -0
  80. package/dist/cursor-policy.d.ts.map +1 -0
  81. package/dist/cursor-policy.js +81 -0
  82. package/dist/cursor-policy.js.map +1 -0
  83. package/dist/debugger.d.ts +28 -0
  84. package/dist/debugger.d.ts.map +1 -0
  85. package/dist/debugger.js +107 -0
  86. package/dist/debugger.js.map +1 -0
  87. package/dist/expect.d.ts +16 -0
  88. package/dist/expect.d.ts.map +1 -0
  89. package/dist/expect.js +67 -0
  90. package/dist/expect.js.map +1 -0
  91. package/dist/failure-artifacts.d.ts +95 -0
  92. package/dist/failure-artifacts.d.ts.map +1 -0
  93. package/dist/failure-artifacts.js +805 -0
  94. package/dist/failure-artifacts.js.map +1 -0
  95. package/dist/generator.d.ts +16 -0
  96. package/dist/generator.d.ts.map +1 -0
  97. package/dist/generator.js +205 -0
  98. package/dist/generator.js.map +1 -0
  99. package/dist/index.d.ts +37 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +160 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/inspector.d.ts +13 -0
  104. package/dist/inspector.d.ts.map +1 -0
  105. package/dist/inspector.js +153 -0
  106. package/dist/inspector.js.map +1 -0
  107. package/dist/llm-provider.d.ts +144 -0
  108. package/dist/llm-provider.d.ts.map +1 -0
  109. package/dist/llm-provider.js +460 -0
  110. package/dist/llm-provider.js.map +1 -0
  111. package/dist/ordinal.d.ts +90 -0
  112. package/dist/ordinal.d.ts.map +1 -0
  113. package/dist/ordinal.js +249 -0
  114. package/dist/ordinal.js.map +1 -0
  115. package/dist/overlay.d.ts +63 -0
  116. package/dist/overlay.d.ts.map +1 -0
  117. package/dist/overlay.js +102 -0
  118. package/dist/overlay.js.map +1 -0
  119. package/dist/protocols/browser-protocol.d.ts +79 -0
  120. package/dist/protocols/browser-protocol.d.ts.map +1 -0
  121. package/dist/protocols/browser-protocol.js +9 -0
  122. package/dist/protocols/browser-protocol.js.map +1 -0
  123. package/dist/query.d.ts +66 -0
  124. package/dist/query.d.ts.map +1 -0
  125. package/dist/query.js +482 -0
  126. package/dist/query.js.map +1 -0
  127. package/dist/read.d.ts +47 -0
  128. package/dist/read.d.ts.map +1 -0
  129. package/dist/read.js +128 -0
  130. package/dist/read.js.map +1 -0
  131. package/dist/recorder.d.ts +44 -0
  132. package/dist/recorder.d.ts.map +1 -0
  133. package/dist/recorder.js +262 -0
  134. package/dist/recorder.js.map +1 -0
  135. package/dist/runtime-agent.d.ts +72 -0
  136. package/dist/runtime-agent.d.ts.map +1 -0
  137. package/dist/runtime-agent.js +357 -0
  138. package/dist/runtime-agent.js.map +1 -0
  139. package/dist/screenshot.d.ts +17 -0
  140. package/dist/screenshot.d.ts.map +1 -0
  141. package/dist/screenshot.js +40 -0
  142. package/dist/screenshot.js.map +1 -0
  143. package/dist/snapshot-diff.d.ts +23 -0
  144. package/dist/snapshot-diff.d.ts.map +1 -0
  145. package/dist/snapshot-diff.js +119 -0
  146. package/dist/snapshot-diff.js.map +1 -0
  147. package/dist/snapshot.d.ts +47 -0
  148. package/dist/snapshot.d.ts.map +1 -0
  149. package/dist/snapshot.js +358 -0
  150. package/dist/snapshot.js.map +1 -0
  151. package/dist/textSearch.d.ts +64 -0
  152. package/dist/textSearch.d.ts.map +1 -0
  153. package/dist/textSearch.js +113 -0
  154. package/dist/textSearch.js.map +1 -0
  155. package/dist/tools/context.d.ts +18 -0
  156. package/dist/tools/context.d.ts.map +1 -0
  157. package/dist/tools/context.js +40 -0
  158. package/dist/tools/context.js.map +1 -0
  159. package/dist/tools/defaults.d.ts +5 -0
  160. package/dist/tools/defaults.d.ts.map +1 -0
  161. package/dist/tools/defaults.js +368 -0
  162. package/dist/tools/defaults.js.map +1 -0
  163. package/dist/tools/filesystem.d.ts +12 -0
  164. package/dist/tools/filesystem.d.ts.map +1 -0
  165. package/dist/tools/filesystem.js +137 -0
  166. package/dist/tools/filesystem.js.map +1 -0
  167. package/dist/tools/index.d.ts +5 -0
  168. package/dist/tools/index.d.ts.map +1 -0
  169. package/dist/tools/index.js +15 -0
  170. package/dist/tools/index.js.map +1 -0
  171. package/dist/tools/registry.d.ts +38 -0
  172. package/dist/tools/registry.d.ts.map +1 -0
  173. package/dist/tools/registry.js +100 -0
  174. package/dist/tools/registry.js.map +1 -0
  175. package/dist/tracing/cloud-sink.d.ts +189 -0
  176. package/dist/tracing/cloud-sink.d.ts.map +1 -0
  177. package/dist/tracing/cloud-sink.js +1067 -0
  178. package/dist/tracing/cloud-sink.js.map +1 -0
  179. package/dist/tracing/index-schema.d.ts +231 -0
  180. package/dist/tracing/index-schema.d.ts.map +1 -0
  181. package/dist/tracing/index-schema.js +235 -0
  182. package/dist/tracing/index-schema.js.map +1 -0
  183. package/dist/tracing/index.d.ts +12 -0
  184. package/dist/tracing/index.d.ts.map +1 -0
  185. package/dist/tracing/index.js +28 -0
  186. package/dist/tracing/index.js.map +1 -0
  187. package/dist/tracing/indexer.d.ts +20 -0
  188. package/dist/tracing/indexer.d.ts.map +1 -0
  189. package/dist/tracing/indexer.js +347 -0
  190. package/dist/tracing/indexer.js.map +1 -0
  191. package/dist/tracing/jsonl-sink.d.ts +51 -0
  192. package/dist/tracing/jsonl-sink.d.ts.map +1 -0
  193. package/dist/tracing/jsonl-sink.js +329 -0
  194. package/dist/tracing/jsonl-sink.js.map +1 -0
  195. package/dist/tracing/sink.d.ts +25 -0
  196. package/dist/tracing/sink.d.ts.map +1 -0
  197. package/dist/tracing/sink.js +15 -0
  198. package/dist/tracing/sink.js.map +1 -0
  199. package/dist/tracing/tracer-factory.d.ts +102 -0
  200. package/dist/tracing/tracer-factory.d.ts.map +1 -0
  201. package/dist/tracing/tracer-factory.js +375 -0
  202. package/dist/tracing/tracer-factory.js.map +1 -0
  203. package/dist/tracing/tracer.d.ts +140 -0
  204. package/dist/tracing/tracer.d.ts.map +1 -0
  205. package/dist/tracing/tracer.js +336 -0
  206. package/dist/tracing/tracer.js.map +1 -0
  207. package/dist/tracing/types.d.ts +203 -0
  208. package/dist/tracing/types.d.ts.map +1 -0
  209. package/dist/tracing/types.js +8 -0
  210. package/dist/tracing/types.js.map +1 -0
  211. package/dist/types.d.ts +422 -0
  212. package/dist/types.d.ts.map +1 -0
  213. package/dist/types.js +6 -0
  214. package/dist/types.js.map +1 -0
  215. package/dist/utils/action-executor.d.ts +25 -0
  216. package/dist/utils/action-executor.d.ts.map +1 -0
  217. package/dist/utils/action-executor.js +121 -0
  218. package/dist/utils/action-executor.js.map +1 -0
  219. package/dist/utils/browser-evaluator.d.ts +76 -0
  220. package/dist/utils/browser-evaluator.d.ts.map +1 -0
  221. package/dist/utils/browser-evaluator.js +130 -0
  222. package/dist/utils/browser-evaluator.js.map +1 -0
  223. package/dist/utils/browser.d.ts +30 -0
  224. package/dist/utils/browser.d.ts.map +1 -0
  225. package/dist/utils/browser.js +75 -0
  226. package/dist/utils/browser.js.map +1 -0
  227. package/dist/utils/element-filter.d.ts +76 -0
  228. package/dist/utils/element-filter.d.ts.map +1 -0
  229. package/dist/utils/element-filter.js +195 -0
  230. package/dist/utils/element-filter.js.map +1 -0
  231. package/dist/utils/grid-utils.d.ts +37 -0
  232. package/dist/utils/grid-utils.d.ts.map +1 -0
  233. package/dist/utils/grid-utils.js +283 -0
  234. package/dist/utils/grid-utils.js.map +1 -0
  235. package/dist/utils/llm-interaction-handler.d.ts +41 -0
  236. package/dist/utils/llm-interaction-handler.d.ts.map +1 -0
  237. package/dist/utils/llm-interaction-handler.js +171 -0
  238. package/dist/utils/llm-interaction-handler.js.map +1 -0
  239. package/dist/utils/llm-response-builder.d.ts +56 -0
  240. package/dist/utils/llm-response-builder.d.ts.map +1 -0
  241. package/dist/utils/llm-response-builder.js +130 -0
  242. package/dist/utils/llm-response-builder.js.map +1 -0
  243. package/dist/utils/selector-utils.d.ts +12 -0
  244. package/dist/utils/selector-utils.d.ts.map +1 -0
  245. package/dist/utils/selector-utils.js +32 -0
  246. package/dist/utils/selector-utils.js.map +1 -0
  247. package/dist/utils/snapshot-event-builder.d.ts +28 -0
  248. package/dist/utils/snapshot-event-builder.d.ts.map +1 -0
  249. package/dist/utils/snapshot-event-builder.js +88 -0
  250. package/dist/utils/snapshot-event-builder.js.map +1 -0
  251. package/dist/utils/snapshot-processor.d.ts +27 -0
  252. package/dist/utils/snapshot-processor.d.ts.map +1 -0
  253. package/dist/utils/snapshot-processor.js +47 -0
  254. package/dist/utils/snapshot-processor.js.map +1 -0
  255. package/dist/utils/trace-event-builder.d.ts +122 -0
  256. package/dist/utils/trace-event-builder.d.ts.map +1 -0
  257. package/dist/utils/trace-event-builder.js +365 -0
  258. package/dist/utils/trace-event-builder.js.map +1 -0
  259. package/dist/utils/trace-file-manager.d.ts +70 -0
  260. package/dist/utils/trace-file-manager.d.ts.map +1 -0
  261. package/dist/utils/trace-file-manager.js +194 -0
  262. package/dist/utils/trace-file-manager.js.map +1 -0
  263. package/dist/utils/zod.d.ts +5 -0
  264. package/dist/utils/zod.d.ts.map +1 -0
  265. package/dist/utils/zod.js +80 -0
  266. package/dist/utils/zod.js.map +1 -0
  267. package/dist/utils.d.ts +8 -0
  268. package/dist/utils.d.ts.map +1 -0
  269. package/dist/utils.js +13 -0
  270. package/dist/utils.js.map +1 -0
  271. package/dist/verification.d.ts +194 -0
  272. package/dist/verification.d.ts.map +1 -0
  273. package/dist/verification.js +530 -0
  274. package/dist/verification.js.map +1 -0
  275. package/dist/vision-executor.d.ts +18 -0
  276. package/dist/vision-executor.d.ts.map +1 -0
  277. package/dist/vision-executor.js +60 -0
  278. package/dist/vision-executor.js.map +1 -0
  279. package/dist/visual-agent.d.ts +120 -0
  280. package/dist/visual-agent.d.ts.map +1 -0
  281. package/dist/visual-agent.js +796 -0
  282. package/dist/visual-agent.js.map +1 -0
  283. package/dist/wait.d.ts +35 -0
  284. package/dist/wait.d.ts.map +1 -0
  285. package/dist/wait.js +76 -0
  286. package/dist/wait.js.map +1 -0
  287. package/package.json +94 -0
  288. package/spec/README.md +72 -0
  289. package/spec/SNAPSHOT_V1.md +208 -0
  290. package/spec/sdk-types.md +259 -0
  291. package/spec/snapshot.schema.json +148 -0
  292. package/src/extension/background.js +104 -0
  293. package/src/extension/content.js +162 -0
  294. package/src/extension/injected_api.js +1399 -0
  295. package/src/extension/manifest.json +36 -0
  296. package/src/extension/pkg/README.md +1340 -0
  297. package/src/extension/pkg/package.json +15 -0
  298. package/src/extension/pkg/sentience_core.d.ts +51 -0
  299. package/src/extension/pkg/sentience_core.js +371 -0
  300. package/src/extension/pkg/sentience_core_bg.wasm +0 -0
  301. package/src/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  302. package/src/extension/release.json +116 -0
@@ -0,0 +1,1067 @@
1
+ "use strict";
2
+ /**
3
+ * CloudTraceSink - Enterprise Cloud Upload
4
+ *
5
+ * Implements "Local Write, Batch Upload" pattern for cloud tracing
6
+ *
7
+ * PRODUCTION HARDENING:
8
+ * - Uses persistent cache directory (~/.sentience/traces/pending/) to survive crashes
9
+ * - Supports non-blocking close() to avoid hanging user scripts
10
+ * - Preserves traces locally on upload failure
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.CloudTraceSink = void 0;
47
+ const fs = __importStar(require("fs"));
48
+ const fs_1 = require("fs");
49
+ const os = __importStar(require("os"));
50
+ const path = __importStar(require("path"));
51
+ const zlib = __importStar(require("zlib"));
52
+ const https = __importStar(require("https"));
53
+ const http = __importStar(require("http"));
54
+ const url_1 = require("url");
55
+ const sink_1 = require("./sink");
56
+ /**
57
+ * Get persistent cache directory for traces
58
+ * Uses ~/.sentience/traces/pending/ (survives process crashes)
59
+ */
60
+ function getPersistentCacheDir() {
61
+ const homeDir = os.homedir();
62
+ const cacheDir = path.join(homeDir, '.sentience', 'traces', 'pending');
63
+ // Create directory if it doesn't exist
64
+ if (!fs.existsSync(cacheDir)) {
65
+ fs.mkdirSync(cacheDir, { recursive: true });
66
+ }
67
+ return cacheDir;
68
+ }
69
+ /**
70
+ * CloudTraceSink writes trace events to a local temp file,
71
+ * then uploads the complete trace to cloud storage on close()
72
+ *
73
+ * Architecture:
74
+ * 1. **Local Buffer**: Writes to temp file (zero latency, non-blocking)
75
+ * 2. **Pre-signed URL**: Uses secure pre-signed PUT URL from backend API
76
+ * 3. **Batch Upload**: Uploads complete file on close() or at intervals
77
+ * 4. **Zero Credential Exposure**: Never embeds cloud credentials in SDK
78
+ *
79
+ * This design ensures:
80
+ * - Fast agent performance (microseconds per emit, not milliseconds)
81
+ * - Security (credentials stay on backend)
82
+ * - Reliability (network issues don't crash the agent)
83
+ *
84
+ * Example:
85
+ * const sink = new CloudTraceSink(uploadUrl);
86
+ * const tracer = new Tracer(runId, sink);
87
+ * tracer.emitRunStart('SentienceAgent');
88
+ * await tracer.close(); // Uploads to cloud
89
+ */
90
+ class CloudTraceSink extends sink_1.TraceSink {
91
+ /**
92
+ * Create a new CloudTraceSink
93
+ *
94
+ * @param uploadUrl - Pre-signed PUT URL from Sentience API
95
+ * @param runId - Run ID for persistent cache naming
96
+ * @param apiKey - Sentience API key for calling /v1/traces/complete
97
+ * @param apiUrl - Sentience API base URL (default: https://api.sentienceapi.com)
98
+ * @param logger - Optional logger instance for logging file sizes and errors
99
+ */
100
+ constructor(uploadUrl, runId, apiKey, apiUrl, logger) {
101
+ super();
102
+ this.writeStream = null;
103
+ this.closed = false;
104
+ // File size tracking
105
+ this.traceFileSizeBytes = 0;
106
+ this.screenshotTotalSizeBytes = 0;
107
+ this.screenshotCount = 0; // Track number of screenshots extracted
108
+ this.indexFileSizeBytes = 0; // Track index file size
109
+ // Upload success flag
110
+ this.uploadSuccessful = false;
111
+ this.uploadUrl = uploadUrl;
112
+ this.runId = runId || `trace-${Date.now()}`;
113
+ this.apiKey = apiKey;
114
+ this.apiUrl = apiUrl || 'https://api.sentienceapi.com';
115
+ this.logger = logger;
116
+ // PRODUCTION FIX: Use persistent cache directory instead of /tmp
117
+ // This ensures traces survive process crashes!
118
+ const cacheDir = getPersistentCacheDir();
119
+ this.tempFilePath = path.join(cacheDir, `${this.runId}.jsonl`);
120
+ try {
121
+ // Open file in append mode
122
+ this.writeStream = fs.createWriteStream(this.tempFilePath, {
123
+ flags: 'a',
124
+ encoding: 'utf-8',
125
+ autoClose: true,
126
+ });
127
+ // Handle stream errors (suppress if closed)
128
+ this.writeStream.on('error', error => {
129
+ if (!this.closed) {
130
+ console.error('[CloudTraceSink] Stream error:', error);
131
+ }
132
+ });
133
+ }
134
+ catch (error) {
135
+ console.error('[CloudTraceSink] Failed to initialize sink:', error);
136
+ this.writeStream = null;
137
+ }
138
+ }
139
+ /**
140
+ * Emit a trace event to local temp file (fast, non-blocking)
141
+ *
142
+ * @param event - Trace event to emit
143
+ */
144
+ emit(event) {
145
+ if (this.closed) {
146
+ throw new Error('CloudTraceSink is closed');
147
+ }
148
+ if (!this.writeStream) {
149
+ console.error('[CloudTraceSink] Write stream not available');
150
+ return;
151
+ }
152
+ try {
153
+ const jsonStr = JSON.stringify(event);
154
+ this.writeStream.write(jsonStr + '\n');
155
+ }
156
+ catch (error) {
157
+ console.error('[CloudTraceSink] Write error:', error);
158
+ }
159
+ }
160
+ /**
161
+ * Upload data to cloud using Node's built-in https module
162
+ */
163
+ async _uploadToCloud(data) {
164
+ return new Promise((resolve, reject) => {
165
+ const url = new url_1.URL(this.uploadUrl);
166
+ const protocol = url.protocol === 'https:' ? https : http;
167
+ const options = {
168
+ hostname: url.hostname,
169
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
170
+ path: url.pathname + url.search,
171
+ method: 'PUT',
172
+ headers: {
173
+ 'Content-Type': 'application/x-gzip',
174
+ 'Content-Encoding': 'gzip',
175
+ 'Content-Length': data.length,
176
+ },
177
+ timeout: 60000, // 1 minute timeout
178
+ };
179
+ const req = protocol.request(options, res => {
180
+ // Consume response data (even if we don't use it)
181
+ res.on('data', () => { });
182
+ res.on('end', () => {
183
+ resolve(res.statusCode || 500);
184
+ });
185
+ });
186
+ req.on('error', error => {
187
+ reject(error);
188
+ });
189
+ req.on('timeout', () => {
190
+ req.destroy();
191
+ reject(new Error('Upload timeout'));
192
+ });
193
+ req.write(data);
194
+ req.end();
195
+ });
196
+ }
197
+ /**
198
+ * Upload buffered trace to cloud via pre-signed URL
199
+ *
200
+ * @param blocking - If false, upload happens in background (default: true)
201
+ *
202
+ * PRODUCTION FIX: Non-blocking mode prevents hanging user scripts
203
+ * on slow uploads (Risk #2 from production hardening plan)
204
+ */
205
+ async close(blocking = true) {
206
+ if (this.closed) {
207
+ return;
208
+ }
209
+ this.closed = true;
210
+ // Non-blocking mode: fire-and-forget background upload
211
+ if (!blocking) {
212
+ // Close the write stream synchronously
213
+ if (this.writeStream && !this.writeStream.destroyed) {
214
+ this.writeStream.end();
215
+ }
216
+ // Upload in background (don't await)
217
+ this._doUpload().catch(error => {
218
+ console.error(`❌ [Sentience] Background upload failed: ${error.message}`);
219
+ console.error(` Local trace preserved at: ${this.tempFilePath}`);
220
+ });
221
+ return;
222
+ }
223
+ // Blocking mode: wait for upload to complete
224
+ await this._doUpload();
225
+ }
226
+ /**
227
+ * Internal upload logic (called by both blocking and non-blocking close)
228
+ */
229
+ async _doUpload() {
230
+ try {
231
+ // 1. Close write stream
232
+ if (this.writeStream && !this.writeStream.destroyed) {
233
+ const stream = this.writeStream;
234
+ stream.removeAllListeners('error');
235
+ await new Promise(resolve => {
236
+ stream.end(() => {
237
+ resolve();
238
+ });
239
+ });
240
+ }
241
+ // 2. Generate index after closing file
242
+ this.generateIndex();
243
+ // 2. Check trace file exists
244
+ try {
245
+ await fs_1.promises.access(this.tempFilePath);
246
+ }
247
+ catch {
248
+ console.warn('[CloudTraceSink] Temp file does not exist, skipping upload');
249
+ return;
250
+ }
251
+ // 3. Extract screenshots from trace events
252
+ const screenshots = await this._extractScreenshotsFromTrace();
253
+ this.screenshotCount = screenshots.size;
254
+ // 4. Upload screenshots separately
255
+ if (screenshots.size > 0) {
256
+ await this._uploadScreenshots(screenshots);
257
+ }
258
+ // 5. Create cleaned trace file (without screenshot_base64)
259
+ const cleanedTracePath = this.tempFilePath.replace('.jsonl', '.cleaned.jsonl');
260
+ await this._createCleanedTrace(cleanedTracePath);
261
+ // 6. Read and compress cleaned trace
262
+ const traceData = await fs_1.promises.readFile(cleanedTracePath);
263
+ const compressedData = zlib.gzipSync(traceData);
264
+ // Measure trace file size
265
+ this.traceFileSizeBytes = compressedData.length;
266
+ // Log file sizes if logger is provided
267
+ if (this.logger) {
268
+ this.logger.info(`Trace file size: ${(this.traceFileSizeBytes / 1024 / 1024).toFixed(2)} MB`);
269
+ this.logger.info(`Screenshot total: ${(this.screenshotTotalSizeBytes / 1024 / 1024).toFixed(2)} MB`);
270
+ }
271
+ // 7. Upload cleaned trace to cloud
272
+ if (this.logger) {
273
+ this.logger.info(`Uploading trace to cloud (${compressedData.length} bytes)`);
274
+ }
275
+ const statusCode = await this._uploadToCloud(compressedData);
276
+ if (statusCode === 200) {
277
+ this.uploadSuccessful = true;
278
+ if (this.logger) {
279
+ this.logger.info('Trace uploaded successfully');
280
+ }
281
+ // Upload trace index file
282
+ await this._uploadIndex();
283
+ // Call /v1/traces/complete to report file sizes
284
+ await this._completeTrace();
285
+ // 8. Delete files on success
286
+ await this._cleanupFiles();
287
+ // Clean up temporary cleaned trace file
288
+ try {
289
+ await fs_1.promises.unlink(cleanedTracePath);
290
+ }
291
+ catch {
292
+ // Ignore cleanup errors
293
+ }
294
+ }
295
+ else {
296
+ this.uploadSuccessful = false;
297
+ console.error(`❌ [Sentience] Upload failed: HTTP ${statusCode}`);
298
+ console.error(` Local trace preserved at: ${this.tempFilePath}`);
299
+ }
300
+ }
301
+ catch (error) {
302
+ console.error(`❌ [Sentience] Error uploading trace: ${error.message}`);
303
+ console.error(` Local trace preserved at: ${this.tempFilePath}`);
304
+ // Don't throw - preserve trace locally even if upload fails
305
+ }
306
+ }
307
+ /**
308
+ * Infer final status from trace events by reading the trace file.
309
+ * @returns Final status: "success", "failure", "partial", or "unknown"
310
+ */
311
+ _inferFinalStatusFromTrace() {
312
+ try {
313
+ // Read trace file to analyze events
314
+ const traceContent = fs.readFileSync(this.tempFilePath, 'utf-8');
315
+ const lines = traceContent.split('\n').filter(line => line.trim());
316
+ const events = [];
317
+ for (const line of lines) {
318
+ try {
319
+ const event = JSON.parse(line);
320
+ events.push(event);
321
+ }
322
+ catch {
323
+ continue;
324
+ }
325
+ }
326
+ if (events.length === 0) {
327
+ return 'unknown';
328
+ }
329
+ // Check for run_end event with status
330
+ for (let i = events.length - 1; i >= 0; i--) {
331
+ const event = events[i];
332
+ if (event.type === 'run_end') {
333
+ const status = event.data?.status;
334
+ if (status === 'success' ||
335
+ status === 'failure' ||
336
+ status === 'partial' ||
337
+ status === 'unknown') {
338
+ return status;
339
+ }
340
+ }
341
+ }
342
+ // Infer from error events
343
+ const hasErrors = events.some(e => e.type === 'error');
344
+ if (hasErrors) {
345
+ // Check if there are successful steps too (partial success)
346
+ const stepEnds = events.filter(e => e.type === 'step_end');
347
+ if (stepEnds.length > 0) {
348
+ return 'partial';
349
+ }
350
+ return 'failure';
351
+ }
352
+ // If we have step_end events and no errors, likely success
353
+ const stepEnds = events.filter(e => e.type === 'step_end');
354
+ if (stepEnds.length > 0) {
355
+ return 'success';
356
+ }
357
+ return 'unknown';
358
+ }
359
+ catch {
360
+ // If we can't read the trace, default to unknown
361
+ return 'unknown';
362
+ }
363
+ }
364
+ /**
365
+ * Extract execution statistics from trace file.
366
+ * @returns Trace statistics for /v1/traces/complete
367
+ */
368
+ _extractStatsFromTrace() {
369
+ try {
370
+ // Read trace file to extract stats
371
+ const traceContent = fs.readFileSync(this.tempFilePath, 'utf-8');
372
+ const lines = traceContent.split('\n').filter(line => line.trim());
373
+ const events = [];
374
+ for (const line of lines) {
375
+ try {
376
+ const event = JSON.parse(line);
377
+ events.push(event);
378
+ }
379
+ catch {
380
+ continue;
381
+ }
382
+ }
383
+ if (events.length === 0) {
384
+ return {
385
+ total_steps: 0,
386
+ total_events: 0,
387
+ duration_ms: null,
388
+ final_status: 'unknown',
389
+ started_at: null,
390
+ ended_at: null,
391
+ };
392
+ }
393
+ // Find run_start and run_end events
394
+ const runStart = events.find(e => e.type === 'run_start');
395
+ const runEnd = events.find(e => e.type === 'run_end');
396
+ // Extract timestamps
397
+ const startedAt = runStart?.ts || null;
398
+ const endedAt = runEnd?.ts || null;
399
+ // Calculate duration
400
+ let durationMs = null;
401
+ if (startedAt && endedAt) {
402
+ try {
403
+ const startDt = new Date(startedAt);
404
+ const endDt = new Date(endedAt);
405
+ durationMs = endDt.getTime() - startDt.getTime();
406
+ }
407
+ catch {
408
+ // Ignore parse errors
409
+ }
410
+ }
411
+ // Count steps (from step_start events, only first attempt)
412
+ const stepIndices = new Set();
413
+ for (const event of events) {
414
+ if (event.type === 'step_start') {
415
+ const stepIndex = event.data?.step_index;
416
+ if (stepIndex !== undefined) {
417
+ stepIndices.add(stepIndex);
418
+ }
419
+ }
420
+ }
421
+ let totalSteps = stepIndices.size;
422
+ // If run_end has steps count, use that (more accurate)
423
+ if (runEnd) {
424
+ const stepsFromEnd = runEnd.data?.steps;
425
+ if (stepsFromEnd !== undefined) {
426
+ totalSteps = Math.max(totalSteps, stepsFromEnd);
427
+ }
428
+ }
429
+ // Count total events
430
+ const totalEvents = events.length;
431
+ // Infer final status
432
+ const finalStatus = this._inferFinalStatusFromTrace();
433
+ return {
434
+ total_steps: totalSteps,
435
+ total_events: totalEvents,
436
+ duration_ms: durationMs,
437
+ final_status: finalStatus,
438
+ started_at: startedAt,
439
+ ended_at: endedAt,
440
+ };
441
+ }
442
+ catch (error) {
443
+ this.logger?.warn(`Error extracting stats from trace: ${error.message}`);
444
+ return {
445
+ total_steps: 0,
446
+ total_events: 0,
447
+ duration_ms: null,
448
+ final_status: 'unknown',
449
+ started_at: null,
450
+ ended_at: null,
451
+ };
452
+ }
453
+ }
454
+ /**
455
+ * Call /v1/traces/complete to report file sizes and stats to gateway.
456
+ *
457
+ * This is a best-effort call - failures are logged but don't affect upload success.
458
+ */
459
+ async _completeTrace() {
460
+ if (!this.apiKey) {
461
+ // No API key - skip complete call
462
+ return;
463
+ }
464
+ return new Promise(resolve => {
465
+ const url = new url_1.URL(`${this.apiUrl}/v1/traces/complete`);
466
+ const protocol = url.protocol === 'https:' ? https : http;
467
+ // Extract stats from trace file
468
+ const stats = this._extractStatsFromTrace();
469
+ // Add file size fields
470
+ const completeStats = {
471
+ ...stats,
472
+ trace_file_size_bytes: this.traceFileSizeBytes,
473
+ screenshot_total_size_bytes: this.screenshotTotalSizeBytes,
474
+ screenshot_count: this.screenshotCount,
475
+ index_file_size_bytes: this.indexFileSizeBytes,
476
+ };
477
+ const body = JSON.stringify({
478
+ run_id: this.runId,
479
+ stats: completeStats,
480
+ });
481
+ const options = {
482
+ hostname: url.hostname,
483
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
484
+ path: url.pathname + url.search,
485
+ method: 'POST',
486
+ headers: {
487
+ 'Content-Type': 'application/json',
488
+ 'Content-Length': Buffer.byteLength(body),
489
+ Authorization: `Bearer ${this.apiKey}`,
490
+ },
491
+ timeout: 10000, // 10 second timeout
492
+ };
493
+ const req = protocol.request(options, res => {
494
+ // Consume response data
495
+ res.on('data', () => { });
496
+ res.on('end', () => {
497
+ if (res.statusCode === 200) {
498
+ this.logger?.info('Trace completion reported to gateway');
499
+ }
500
+ else {
501
+ this.logger?.warn(`Failed to report trace completion: HTTP ${res.statusCode}`);
502
+ }
503
+ resolve();
504
+ });
505
+ });
506
+ req.on('error', error => {
507
+ // Best-effort - log but don't fail
508
+ this.logger?.warn(`Error reporting trace completion: ${error.message}`);
509
+ resolve();
510
+ });
511
+ req.on('timeout', () => {
512
+ req.destroy();
513
+ this.logger?.warn('Trace completion request timeout');
514
+ resolve();
515
+ });
516
+ req.write(body);
517
+ req.end();
518
+ });
519
+ }
520
+ /**
521
+ * Generate trace index file (automatic on close)
522
+ */
523
+ generateIndex() {
524
+ try {
525
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
526
+ const { writeTraceIndex } = require('./indexer');
527
+ // Use frontend format to ensure 'step' field is present (1-based)
528
+ // Frontend derives sequence from step.step - 1, so step must be valid
529
+ const indexPath = this.tempFilePath.replace('.jsonl', '.index.json');
530
+ writeTraceIndex(this.tempFilePath, indexPath, true);
531
+ }
532
+ catch (error) {
533
+ // Non-fatal: log but don't crash
534
+ this.logger?.warn(`Failed to generate trace index: ${error.message}`);
535
+ }
536
+ }
537
+ /**
538
+ * Upload trace index file to cloud storage.
539
+ *
540
+ * Called after successful trace upload to provide fast timeline rendering.
541
+ * The index file enables O(1) step lookups without parsing the entire trace.
542
+ */
543
+ async _uploadIndex() {
544
+ // Construct index file path (same as trace file with .index.json extension)
545
+ const indexPath = this.tempFilePath.replace('.jsonl', '.index.json');
546
+ try {
547
+ // Check if index file exists
548
+ await fs_1.promises.access(indexPath);
549
+ }
550
+ catch {
551
+ this.logger?.warn('Index file not found, skipping index upload');
552
+ return;
553
+ }
554
+ try {
555
+ // Request index upload URL from API
556
+ if (!this.apiKey) {
557
+ this.logger?.info('No API key provided, skipping index upload');
558
+ return;
559
+ }
560
+ const uploadUrlResponse = await this._requestIndexUploadUrl();
561
+ if (!uploadUrlResponse) {
562
+ return;
563
+ }
564
+ // Read index file and update trace_file.path to cloud storage path
565
+ const indexContent = await fs_1.promises.readFile(indexPath, 'utf-8');
566
+ const indexJson = JSON.parse(indexContent);
567
+ // Extract cloud storage path from trace upload URL
568
+ // uploadUrl format: https://...digitaloceanspaces.com/traces/{run_id}.jsonl.gz
569
+ // Extract path: traces/{run_id}.jsonl.gz
570
+ try {
571
+ const parsedUrl = new url_1.URL(this.uploadUrl);
572
+ // Extract path after domain (e.g., /traces/run-123.jsonl.gz -> traces/run-123.jsonl.gz)
573
+ const cloudTracePath = parsedUrl.pathname.startsWith('/')
574
+ ? parsedUrl.pathname.substring(1)
575
+ : parsedUrl.pathname;
576
+ // Update trace_file.path in index
577
+ if (indexJson.trace_file && typeof indexJson.trace_file === 'object') {
578
+ indexJson.trace_file.path = cloudTracePath;
579
+ }
580
+ }
581
+ catch (error) {
582
+ this.logger?.warn(`Failed to extract cloud path from upload URL: ${error.message}`);
583
+ }
584
+ // Serialize updated index to JSON
585
+ const updatedIndexData = Buffer.from(JSON.stringify(indexJson, null, 2), 'utf-8');
586
+ const compressedIndex = zlib.gzipSync(updatedIndexData);
587
+ const indexSize = compressedIndex.length;
588
+ this.indexFileSizeBytes = indexSize; // Track index file size
589
+ this.logger?.info(`Index file size: ${(indexSize / 1024).toFixed(2)} KB`);
590
+ if (this.logger) {
591
+ this.logger.info(`Uploading trace index (${indexSize} bytes)`);
592
+ }
593
+ // Upload index to cloud storage
594
+ const statusCode = await this._uploadIndexToCloud(uploadUrlResponse, compressedIndex);
595
+ if (statusCode === 200) {
596
+ if (this.logger) {
597
+ this.logger.info('Trace index uploaded successfully');
598
+ }
599
+ // Delete local index file after successful upload
600
+ try {
601
+ await fs_1.promises.unlink(indexPath);
602
+ }
603
+ catch {
604
+ // Ignore cleanup errors
605
+ }
606
+ }
607
+ else {
608
+ this.logger?.warn(`Index upload failed: HTTP ${statusCode}`);
609
+ }
610
+ }
611
+ catch (error) {
612
+ // Non-fatal: log but don't crash
613
+ this.logger?.warn(`Error uploading trace index: ${error.message}`);
614
+ }
615
+ }
616
+ /**
617
+ * Request index upload URL from Sentience API
618
+ */
619
+ async _requestIndexUploadUrl() {
620
+ return new Promise(resolve => {
621
+ const url = new url_1.URL(`${this.apiUrl}/v1/traces/index_upload`);
622
+ const protocol = url.protocol === 'https:' ? https : http;
623
+ const body = JSON.stringify({ run_id: this.runId });
624
+ const options = {
625
+ hostname: url.hostname,
626
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
627
+ path: url.pathname + url.search,
628
+ method: 'POST',
629
+ headers: {
630
+ 'Content-Type': 'application/json',
631
+ 'Content-Length': Buffer.byteLength(body),
632
+ Authorization: `Bearer ${this.apiKey}`,
633
+ },
634
+ timeout: 10000,
635
+ };
636
+ const req = protocol.request(options, res => {
637
+ let data = '';
638
+ res.on('data', chunk => {
639
+ data += chunk;
640
+ });
641
+ res.on('end', () => {
642
+ if (res.statusCode === 200) {
643
+ try {
644
+ const response = JSON.parse(data);
645
+ resolve(response.upload_url || null);
646
+ }
647
+ catch {
648
+ this.logger?.warn('Failed to parse index upload URL response');
649
+ resolve(null);
650
+ }
651
+ }
652
+ else {
653
+ this.logger?.warn(`Failed to get index upload URL: HTTP ${res.statusCode}`);
654
+ resolve(null);
655
+ }
656
+ });
657
+ });
658
+ req.on('error', error => {
659
+ this.logger?.warn(`Error requesting index upload URL: ${error.message}`);
660
+ resolve(null);
661
+ });
662
+ req.on('timeout', () => {
663
+ req.destroy();
664
+ this.logger?.warn('Index upload URL request timeout');
665
+ resolve(null);
666
+ });
667
+ req.write(body);
668
+ req.end();
669
+ });
670
+ }
671
+ /**
672
+ * Upload index data to cloud using pre-signed URL
673
+ */
674
+ async _uploadIndexToCloud(uploadUrl, data) {
675
+ return new Promise((resolve, reject) => {
676
+ const url = new url_1.URL(uploadUrl);
677
+ const protocol = url.protocol === 'https:' ? https : http;
678
+ const options = {
679
+ hostname: url.hostname,
680
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
681
+ path: url.pathname + url.search,
682
+ method: 'PUT',
683
+ headers: {
684
+ 'Content-Type': 'application/json',
685
+ 'Content-Encoding': 'gzip',
686
+ 'Content-Length': data.length,
687
+ },
688
+ timeout: 30000, // 30 second timeout
689
+ };
690
+ const req = protocol.request(options, res => {
691
+ res.on('data', () => { });
692
+ res.on('end', () => {
693
+ resolve(res.statusCode || 500);
694
+ });
695
+ });
696
+ req.on('error', error => {
697
+ reject(error);
698
+ });
699
+ req.on('timeout', () => {
700
+ req.destroy();
701
+ reject(new Error('Index upload timeout'));
702
+ });
703
+ req.write(data);
704
+ req.end();
705
+ });
706
+ }
707
+ /**
708
+ * Normalize screenshot data by extracting base64 from data URL if needed.
709
+ *
710
+ * Handles both formats:
711
+ * - Data URL: "data:image/jpeg;base64,/9j/4AAQ..."
712
+ * - Pure base64: "/9j/4AAQ..."
713
+ *
714
+ * @param screenshotRaw - Raw screenshot data (data URL or base64)
715
+ * @param defaultFormat - Default format if not detected from data URL
716
+ * @returns Tuple of [base64String, formatString]
717
+ */
718
+ _normalizeScreenshotData(screenshotRaw, defaultFormat = 'jpeg') {
719
+ if (!screenshotRaw) {
720
+ return ['', defaultFormat];
721
+ }
722
+ // Check if it's a data URL
723
+ if (screenshotRaw.startsWith('data:image')) {
724
+ // Extract format from "data:image/jpeg;base64,..." or "data:image/png;base64,..."
725
+ try {
726
+ // Split on comma to get the base64 part
727
+ if (screenshotRaw.includes(',')) {
728
+ const [header, base64Data] = screenshotRaw.split(',', 2);
729
+ // Extract format from header: "data:image/jpeg;base64"
730
+ if (header.includes('/') && header.includes(';')) {
731
+ const formatPart = header.split('/')[1]?.split(';')[0];
732
+ if (formatPart === 'jpeg' || formatPart === 'jpg') {
733
+ return [base64Data, 'jpeg'];
734
+ }
735
+ else if (formatPart === 'png') {
736
+ return [base64Data, 'png'];
737
+ }
738
+ }
739
+ return [base64Data, defaultFormat];
740
+ }
741
+ else {
742
+ // Malformed data URL - return as-is with warning
743
+ this.logger?.warn('Malformed data URL in screenshot_base64 (missing comma)');
744
+ return [screenshotRaw, defaultFormat];
745
+ }
746
+ }
747
+ catch (error) {
748
+ this.logger?.warn(`Error parsing screenshot data URL: ${error.message}`);
749
+ return [screenshotRaw, defaultFormat];
750
+ }
751
+ }
752
+ // Already pure base64
753
+ return [screenshotRaw, defaultFormat];
754
+ }
755
+ /**
756
+ * Extract screenshots from trace events.
757
+ *
758
+ * @returns Map of sequence number to screenshot data
759
+ */
760
+ async _extractScreenshotsFromTrace() {
761
+ const screenshots = new Map();
762
+ let sequence = 0;
763
+ try {
764
+ const traceContent = await fs_1.promises.readFile(this.tempFilePath, 'utf-8');
765
+ const lines = traceContent.split('\n');
766
+ for (const line of lines) {
767
+ if (!line.trim()) {
768
+ continue;
769
+ }
770
+ try {
771
+ const event = JSON.parse(line);
772
+ // Check if this is a snapshot event with screenshot
773
+ if (event.type === 'snapshot') {
774
+ const data = event.data || {};
775
+ const screenshotRaw = data.screenshot_base64;
776
+ if (screenshotRaw) {
777
+ // Normalize: extract base64 from data URL if needed
778
+ // Handles both "data:image/jpeg;base64,..." and pure base64
779
+ const [screenshotBase64, screenshotFormat] = this._normalizeScreenshotData(screenshotRaw, data.screenshot_format || 'jpeg');
780
+ if (screenshotBase64) {
781
+ sequence += 1;
782
+ screenshots.set(sequence, {
783
+ base64: screenshotBase64,
784
+ format: screenshotFormat,
785
+ stepId: event.step_id,
786
+ });
787
+ }
788
+ }
789
+ }
790
+ }
791
+ catch {
792
+ // Skip invalid JSON lines
793
+ continue;
794
+ }
795
+ }
796
+ }
797
+ catch (error) {
798
+ this.logger?.error(`Error extracting screenshots: ${error.message}`);
799
+ }
800
+ return screenshots;
801
+ }
802
+ /**
803
+ * Create trace file without screenshot_base64 fields.
804
+ *
805
+ * @param outputPath - Path to write cleaned trace file
806
+ */
807
+ async _createCleanedTrace(outputPath) {
808
+ try {
809
+ const traceContent = await fs_1.promises.readFile(this.tempFilePath, 'utf-8');
810
+ const lines = traceContent.split('\n');
811
+ const cleanedLines = [];
812
+ for (const line of lines) {
813
+ if (!line.trim()) {
814
+ continue;
815
+ }
816
+ try {
817
+ const event = JSON.parse(line);
818
+ // Remove screenshot_base64 from snapshot events
819
+ if (event.type === 'snapshot' && event.data) {
820
+ const cleanedData = {};
821
+ for (const [key, value] of Object.entries(event.data)) {
822
+ if (key !== 'screenshot_base64' && key !== 'screenshot_format') {
823
+ cleanedData[key] = value;
824
+ }
825
+ }
826
+ event.data = cleanedData;
827
+ }
828
+ cleanedLines.push(JSON.stringify(event));
829
+ }
830
+ catch {
831
+ // Skip invalid JSON lines
832
+ continue;
833
+ }
834
+ }
835
+ await fs_1.promises.writeFile(outputPath, cleanedLines.join('\n') + '\n', 'utf-8');
836
+ }
837
+ catch (error) {
838
+ this.logger?.error(`Error creating cleaned trace: ${error.message}`);
839
+ throw error;
840
+ }
841
+ }
842
+ /**
843
+ * Request pre-signed upload URLs for screenshots from gateway.
844
+ *
845
+ * @param sequences - List of screenshot sequence numbers
846
+ * @returns Map of sequence number to upload URL
847
+ */
848
+ async _requestScreenshotUrls(sequences) {
849
+ if (!this.apiKey || sequences.length === 0) {
850
+ return new Map();
851
+ }
852
+ return new Promise(resolve => {
853
+ const url = new url_1.URL(`${this.apiUrl}/v1/screenshots/init`);
854
+ const protocol = url.protocol === 'https:' ? https : http;
855
+ const body = JSON.stringify({
856
+ run_id: this.runId,
857
+ sequences,
858
+ });
859
+ const options = {
860
+ hostname: url.hostname,
861
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
862
+ path: url.pathname + url.search,
863
+ method: 'POST',
864
+ headers: {
865
+ 'Content-Type': 'application/json',
866
+ 'Content-Length': Buffer.byteLength(body),
867
+ Authorization: `Bearer ${this.apiKey}`,
868
+ },
869
+ timeout: 10000, // 10 second timeout
870
+ };
871
+ const req = protocol.request(options, res => {
872
+ let data = '';
873
+ res.on('data', chunk => {
874
+ data += chunk;
875
+ });
876
+ res.on('end', () => {
877
+ if (res.statusCode === 200) {
878
+ try {
879
+ const response = JSON.parse(data);
880
+ const uploadUrls = response.upload_urls || {};
881
+ const urlMap = new Map();
882
+ // Gateway returns sequences as strings in JSON, convert to int keys
883
+ for (const [seqStr, url] of Object.entries(uploadUrls)) {
884
+ urlMap.set(parseInt(seqStr, 10), url);
885
+ }
886
+ resolve(urlMap);
887
+ }
888
+ catch {
889
+ this.logger?.warn('Failed to parse screenshot upload URLs response');
890
+ resolve(new Map());
891
+ }
892
+ }
893
+ else {
894
+ this.logger?.warn(`Failed to get screenshot URLs: HTTP ${res.statusCode}`);
895
+ resolve(new Map());
896
+ }
897
+ });
898
+ });
899
+ req.on('error', error => {
900
+ this.logger?.warn(`Error requesting screenshot URLs: ${error.message}`);
901
+ resolve(new Map());
902
+ });
903
+ req.on('timeout', () => {
904
+ req.destroy();
905
+ this.logger?.warn('Screenshot URLs request timeout');
906
+ resolve(new Map());
907
+ });
908
+ req.write(body);
909
+ req.end();
910
+ });
911
+ }
912
+ /**
913
+ * Upload screenshots extracted from trace events.
914
+ *
915
+ * Steps:
916
+ * 1. Request pre-signed URLs from gateway (/v1/screenshots/init)
917
+ * 2. Decode base64 to image bytes
918
+ * 3. Upload screenshots in parallel (10 concurrent workers)
919
+ * 4. Track upload progress
920
+ *
921
+ * @param screenshots - Map of sequence to screenshot data
922
+ */
923
+ async _uploadScreenshots(screenshots) {
924
+ if (screenshots.size === 0) {
925
+ return;
926
+ }
927
+ // 1. Request pre-signed URLs from gateway
928
+ const sequences = Array.from(screenshots.keys()).sort((a, b) => a - b);
929
+ const uploadUrls = await this._requestScreenshotUrls(sequences);
930
+ if (uploadUrls.size === 0) {
931
+ this.logger?.warn('No screenshot upload URLs received, skipping upload. This may indicate API key permission issue, gateway error, or network problem.');
932
+ return;
933
+ }
934
+ // 2. Upload screenshots in parallel
935
+ const uploadPromises = [];
936
+ const uploadSequences = [];
937
+ uploadUrls.forEach((url, seq) => {
938
+ const screenshotData = screenshots.get(seq);
939
+ if (!screenshotData) {
940
+ return;
941
+ }
942
+ uploadSequences.push(seq);
943
+ const uploadPromise = this._uploadSingleScreenshot(seq, url, screenshotData);
944
+ uploadPromises.push(uploadPromise);
945
+ });
946
+ // Wait for all uploads (max 10 concurrent)
947
+ const results = await Promise.allSettled(uploadPromises.slice(0, 10));
948
+ // Process remaining uploads in batches of 10
949
+ for (let i = 10; i < uploadPromises.length; i += 10) {
950
+ const batch = uploadPromises.slice(i, i + 10);
951
+ const batchResults = await Promise.allSettled(batch);
952
+ results.push(...batchResults);
953
+ }
954
+ // Count successes and failures
955
+ let uploadedCount = 0;
956
+ const failedSequences = [];
957
+ for (let i = 0; i < results.length; i++) {
958
+ const result = results[i];
959
+ if (result.status === 'fulfilled' && result.value) {
960
+ uploadedCount++;
961
+ }
962
+ else {
963
+ failedSequences.push(uploadSequences[i]);
964
+ }
965
+ }
966
+ // 3. Report results
967
+ const totalCount = uploadUrls.size;
968
+ if (uploadedCount === totalCount) {
969
+ const totalSizeMB = this.screenshotTotalSizeBytes / 1024 / 1024;
970
+ if (this.logger) {
971
+ this.logger.info(`All ${totalCount} screenshots uploaded successfully (total size: ${totalSizeMB.toFixed(2)} MB)`);
972
+ }
973
+ }
974
+ else {
975
+ if (this.logger) {
976
+ this.logger.warn(`Uploaded ${uploadedCount}/${totalCount} screenshots. Failed sequences: ${failedSequences.length > 0 ? failedSequences.join(', ') : 'none'}`);
977
+ }
978
+ }
979
+ }
980
+ /**
981
+ * Upload a single screenshot to pre-signed URL.
982
+ *
983
+ * @param sequence - Screenshot sequence number
984
+ * @param uploadUrl - Pre-signed upload URL
985
+ * @param screenshotData - Screenshot data with base64 and format
986
+ * @returns True if upload successful, false otherwise
987
+ */
988
+ async _uploadSingleScreenshot(sequence, uploadUrl, screenshotData) {
989
+ try {
990
+ // Decode base64 to image bytes
991
+ const imageBytes = Buffer.from(screenshotData.base64, 'base64');
992
+ const imageSize = imageBytes.length;
993
+ // Update total size
994
+ this.screenshotTotalSizeBytes += imageSize;
995
+ // Upload to pre-signed URL
996
+ const statusCode = await this._uploadScreenshotToCloud(uploadUrl, imageBytes, screenshotData.format);
997
+ if (statusCode === 200) {
998
+ return true;
999
+ }
1000
+ else {
1001
+ this.logger?.warn(`Screenshot ${sequence} upload failed: HTTP ${statusCode}`);
1002
+ return false;
1003
+ }
1004
+ }
1005
+ catch (error) {
1006
+ this.logger?.warn(`Screenshot ${sequence} upload error: ${error.message}`);
1007
+ return false;
1008
+ }
1009
+ }
1010
+ /**
1011
+ * Upload screenshot data to cloud using pre-signed URL
1012
+ */
1013
+ async _uploadScreenshotToCloud(uploadUrl, data, format) {
1014
+ return new Promise((resolve, reject) => {
1015
+ const url = new url_1.URL(uploadUrl);
1016
+ const protocol = url.protocol === 'https:' ? https : http;
1017
+ const options = {
1018
+ hostname: url.hostname,
1019
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
1020
+ path: url.pathname + url.search,
1021
+ method: 'PUT',
1022
+ headers: {
1023
+ 'Content-Type': `image/${format}`,
1024
+ 'Content-Length': data.length,
1025
+ },
1026
+ timeout: 30000, // 30 second timeout per screenshot
1027
+ };
1028
+ const req = protocol.request(options, res => {
1029
+ res.on('data', () => { });
1030
+ res.on('end', () => {
1031
+ resolve(res.statusCode || 500);
1032
+ });
1033
+ });
1034
+ req.on('error', error => {
1035
+ reject(error);
1036
+ });
1037
+ req.on('timeout', () => {
1038
+ req.destroy();
1039
+ reject(new Error('Screenshot upload timeout'));
1040
+ });
1041
+ req.write(data);
1042
+ req.end();
1043
+ });
1044
+ }
1045
+ /**
1046
+ * Delete local files after successful upload.
1047
+ */
1048
+ async _cleanupFiles() {
1049
+ // Delete trace file
1050
+ try {
1051
+ if (fs.existsSync(this.tempFilePath)) {
1052
+ await fs_1.promises.unlink(this.tempFilePath);
1053
+ }
1054
+ }
1055
+ catch {
1056
+ // Ignore cleanup errors
1057
+ }
1058
+ }
1059
+ /**
1060
+ * Get unique identifier for this sink
1061
+ */
1062
+ getSinkType() {
1063
+ return `CloudTraceSink(${this.uploadUrl.substring(0, 50)}...)`;
1064
+ }
1065
+ }
1066
+ exports.CloudTraceSink = CloudTraceSink;
1067
+ //# sourceMappingURL=cloud-sink.js.map