@tradejs/cli 3.1.29-beta.261 → 3.1.29-beta.263

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 (32) hide show
  1. package/README.md +47 -8
  2. package/dist/cli.js +990 -1448
  3. package/dist/lib/aiPocketSearch.js +16 -0
  4. package/dist/lib/aiPocketSearchCli.js +3 -3
  5. package/dist/lib/jevMicroReferences.js +73 -0
  6. package/dist/lib/jevResearch.js +11 -0
  7. package/dist/lib/runtimeEvidenceArtifacts.js +3 -416
  8. package/dist/lib/runtimeEvidenceCompositionSnapshots.js +42 -211
  9. package/dist/lib/runtimeEvidenceCompositions.js +33 -310
  10. package/dist/lib/runtimeEvidenceDeployment.js +3 -315
  11. package/dist/lib/runtimeEvidenceProducer.js +3 -77
  12. package/dist/lib/runtimeEvidenceSync.js +32 -299
  13. package/dist/lib/runtimeFeedbackArtifacts.js +3 -545
  14. package/dist/lib/runtimeFeedbackNotification.js +7 -195
  15. package/dist/lib/runtimeFeedbackReplay.js +26 -525
  16. package/dist/lib/runtimeFeedbackSync.js +26 -219
  17. package/dist/lib/runtimeScorecard.js +5 -163
  18. package/dist/scripts/aiPocketSearch.js +101 -23
  19. package/dist/scripts/backtest.js +10 -1
  20. package/dist/scripts/jev.js +11 -0
  21. package/dist/scripts/mcpWorker.js +273 -0
  22. package/dist/scripts/replay.js +14 -250
  23. package/dist/scripts/replayRunner.js +14 -250
  24. package/dist/scripts/runtimeEvidence.js +47 -597
  25. package/dist/scripts/runtimeEvidenceSync.js +29 -296
  26. package/dist/scripts/runtimeFeedbackNotify.js +10 -188
  27. package/dist/scripts/runtimeFeedbackReplay.js +29 -528
  28. package/dist/scripts/runtimeFeedbackSync.js +28 -221
  29. package/dist/scripts/runtimeScorecard.js +88 -503
  30. package/dist/scripts/signals.js +55 -224
  31. package/dist/scripts/signalsDaemon.js +55 -224
  32. package/package.json +9 -8
@@ -74,7 +74,7 @@ var resolveAiPocketCadenceProfile = ({
74
74
  validationRows = [],
75
75
  testRows = [],
76
76
  mode = "auto",
77
- validationSplit = 0.25,
77
+ validationSplit = 0,
78
78
  minSupport,
79
79
  minEvents,
80
80
  minValidationSupport,
@@ -124,8 +124,8 @@ var AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS = {
124
124
  minProfitFactor: "1.2",
125
125
  minWinRate: "0",
126
126
  minTotalProfit: "0",
127
- validationSplit: "0.25",
128
- testSplit: "0"
127
+ validationSplit: "0",
128
+ testSplit: "0.4"
129
129
  };
130
130
  var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
131
131
  const timestamps = [
@@ -166,11 +166,6 @@ var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0
166
166
  testRows: rows.filter((row) => isIn(testTimestamps, row.timestamp))
167
167
  };
168
168
  };
169
- var splitAiPocketCoverageRowsByTimestamp = (rows, family, validationSplit, testSplit = 0) => splitAiPocketResearchRowsByTimestamp(
170
- rows.filter((row) => row.featureCoverage?.[family] === true),
171
- validationSplit,
172
- testSplit
173
- );
174
169
  var sealAiPocketTestPartition = (split, sealed) => {
175
170
  const finiteTimestamps = split.testRows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp));
176
171
  return {
@@ -436,17 +431,17 @@ var resolveAiPocketSearchCommandOptions = ({
436
431
  longName: "validationSplit",
437
432
  shortName: "V"
438
433
  }) ?? flags2.validationSplit,
439
- 0.25
440
- ) : 0.25;
434
+ 0
435
+ ) : 0;
441
436
  const testSplit = hasCliOption2(argv, "testSplit", "T") ? normalizeRatio(
442
437
  readAiPocketSearchCliOption({
443
438
  argv,
444
439
  longName: "testSplit",
445
440
  shortName: "T"
446
441
  }) ?? flags2.testSplit,
447
- 0
448
- ) : 0;
449
- const sealTest = Boolean(flags2.sealTest);
442
+ 0.4
443
+ ) : 0.4;
444
+ const sealTest = flags2.sealTest == null ? true : Boolean(flags2.sealTest);
450
445
  if (sealTest && testSplit <= 0) {
451
446
  throw new Error("--sealTest requires a positive --testSplit");
452
447
  }
@@ -2649,6 +2644,22 @@ var buildAiPocketMarkdownReport = ({
2649
2644
  "",
2650
2645
  markdownTable(pocketTableHeaders, pocketRows(pocketSearch.positivePockets)),
2651
2646
  "",
2647
+ "## Development Stability (inside discovery, not independent validation)",
2648
+ "",
2649
+ markdownTable(
2650
+ ["Condition", "Status", "Block", "Events", "PnL", "PF"],
2651
+ pocketSearch.positivePockets.flatMap(
2652
+ (pocket) => (pocket.stability?.folds ?? []).map((fold, index) => [
2653
+ pocket.condition,
2654
+ pocket.stability?.status,
2655
+ index + 1,
2656
+ fold.summary.events,
2657
+ formatMdNumber(fold.summary.totalProfit),
2658
+ formatMdNumber(fold.summary.profitFactor)
2659
+ ])
2660
+ )
2661
+ ),
2662
+ "",
2652
2663
  "## Top Loss Pockets",
2653
2664
  "",
2654
2665
  markdownTable(pocketTableHeaders, pocketRows(pocketSearch.negativePockets)),
@@ -4099,6 +4110,62 @@ var summarizeAiTrainEvaluationsByQualityThreshold = (evaluations, thresholds = [
4099
4110
  )
4100
4111
  }));
4101
4112
 
4113
+ // src/lib/aiPocketSearch/stability.ts
4114
+ var assessDevelopmentStability = (result, options) => {
4115
+ const minEvents = options.minEventsPerFold ?? 5;
4116
+ const timestamps = [...new Set(options.timestamps)].sort((a, b) => a - b);
4117
+ const boundaries = [0, 1, 2, 3].map(
4118
+ (index) => index === 3 ? timestamps.at(-1) == null ? Number.POSITIVE_INFINITY : timestamps.at(-1) + 1 : timestamps[Math.floor(timestamps.length * index / 3)] ?? Number.POSITIVE_INFINITY
4119
+ );
4120
+ const blocks = [0, 1, 2].map((index) => ({
4121
+ start: boundaries[index],
4122
+ end: boundaries[index + 1],
4123
+ rows: options.developmentRows.filter(
4124
+ (row) => row.timestamp != null && row.timestamp >= boundaries[index] && row.timestamp < boundaries[index + 1]
4125
+ ),
4126
+ baseline: options.baselineRows.filter(
4127
+ (row) => row.timestamp != null && row.timestamp >= boundaries[index] && row.timestamp < boundaries[index + 1]
4128
+ )
4129
+ }));
4130
+ for (const pocket of result.positivePockets) {
4131
+ const folds = blocks.map((block) => {
4132
+ const mask = buildPredicateListMask(block.rows, pocket.predicates).mask;
4133
+ const selected = block.rows.filter((_, index) => mask[index] === 1);
4134
+ const summary = summarizeAiPocketRows(selected);
4135
+ const baseline = summarizeAiPocketRows(block.baseline);
4136
+ const objectiveSummary = result.objective === "add-to-gate" ? summarizeAiPocketRows([
4137
+ .../* @__PURE__ */ new Set([...block.baseline, ...selected])
4138
+ ]) : summary;
4139
+ return {
4140
+ startTimestamp: Number.isFinite(block.start) ? block.start : null,
4141
+ endTimestamp: Number.isFinite(block.end) ? block.end : null,
4142
+ summary,
4143
+ objectiveSummary,
4144
+ baseline
4145
+ };
4146
+ });
4147
+ const supported = folds.filter((fold) => fold.summary.events >= minEvents);
4148
+ const unstable = supported.some(
4149
+ (fold) => fold.objectiveSummary.totalProfit < (result.objective === "add-to-gate" ? fold.baseline.totalProfit : 0) || (fold.objectiveSummary.profitFactor ?? (fold.objectiveSummary.grossLoss === 0 ? Infinity : 0)) < 1
4150
+ );
4151
+ const status = unstable ? "unstable" : supported.length < 3 ? "insufficient-support" : "stable";
4152
+ pocket.stability = {
4153
+ status,
4154
+ role: "development-only diagnostic",
4155
+ minEventsPerFold: minEvents,
4156
+ folds
4157
+ };
4158
+ if (status !== "stable") {
4159
+ pocket.readiness = "research-only";
4160
+ pocket.readinessReasons.push(`development stability: ${status}`);
4161
+ }
4162
+ }
4163
+ const rank = { stable: 0, "insufficient-support": 1, unstable: 2 };
4164
+ result.positivePockets.sort(
4165
+ (a, b) => rank[a.stability.status] - rank[b.stability.status]
4166
+ );
4167
+ };
4168
+
4102
4169
  // src/lib/aiPocketSearch/research.ts
4103
4170
  var resolveScopeRows = (rows, scope) => {
4104
4171
  if (scope === "approved") {
@@ -4194,7 +4261,7 @@ var runAiPocketSearchResearch = ({
4194
4261
  maxEventCountShare,
4195
4262
  explicitMaxEventCountShare
4196
4263
  });
4197
- return searchAiPockets(train, {
4264
+ const result = searchAiPockets(train, {
4198
4265
  minSupport: cadenceProfile.minSupport,
4199
4266
  minProfitFactor,
4200
4267
  minTotalProfit,
@@ -4224,6 +4291,17 @@ var runAiPocketSearchResearch = ({
4224
4291
  progressInterval: 250,
4225
4292
  onProgress: (progress) => onProgress?.({ label, progress })
4226
4293
  });
4294
+ assessDevelopmentStability(result, {
4295
+ developmentRows: [...train, ...validation],
4296
+ baselineRows: [...trainBaseline, ...validationBaseline],
4297
+ // Every provider/scope uses the same outer development timestamps.
4298
+ timestamps: [
4299
+ ...new Set(
4300
+ fullSplit.trainRows.concat(fullSplit.validationRows).map((row) => row.timestamp)
4301
+ )
4302
+ ].sort((a, b) => a - b)
4303
+ });
4304
+ return result;
4227
4305
  };
4228
4306
  const search = runSearch({
4229
4307
  label: "full",
@@ -4243,12 +4321,12 @@ var runAiPocketSearchResearch = ({
4243
4321
  const coverageSearches = [];
4244
4322
  if (coverageMode === "auto") {
4245
4323
  for (const family of ["cmc", "coinalyze"]) {
4246
- const cohortSplit = splitAiPocketCoverageRowsByTimestamp(
4247
- directionRows,
4248
- family,
4249
- validationSplit,
4250
- testSplit
4251
- );
4324
+ const covered = (row) => row.featureCoverage?.[family] === true;
4325
+ const cohortSplit = {
4326
+ trainRows: fullSplit.trainRows.filter(covered),
4327
+ validationRows: fullSplit.validationRows.filter(covered),
4328
+ testRows: fullSplit.testRows.filter(covered)
4329
+ };
4252
4330
  const sealedCohortSplit = sealAiPocketTestPartition(
4253
4331
  cohortSplit,
4254
4332
  sealTest
@@ -4581,18 +4659,18 @@ import_args.default.option(
4581
4659
  );
4582
4660
  import_args.default.option(
4583
4661
  ["V", "validationSplit"],
4584
- "Trailing time-ordered scope share reserved for validation (0 disables)",
4662
+ "Optional legacy tuning share; default 0, stability is checked inside development",
4585
4663
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.validationSplit
4586
4664
  );
4587
4665
  import_args.default.option(
4588
4666
  ["T", "testSplit"],
4589
- "Trailing timestamp-grouped scope share withheld as untouched test",
4667
+ "Trailing timestamp-grouped test share; default 0.4 (outer 60/40)",
4590
4668
  AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.testSplit
4591
4669
  );
4592
4670
  import_args.default.option(
4593
4671
  ["w", "sealTest"],
4594
4672
  "Reserve test bounds without exposing its rows or economics to discovery",
4595
- false
4673
+ true
4596
4674
  );
4597
4675
  import_args.default.option(
4598
4676
  ["N", "minValidationSupport"],
@@ -74,6 +74,10 @@ var import_userSettings = require("@tradejs/infra/userSettings");
74
74
  var import_jev3 = require("@tradejs/node/jev");
75
75
  var resolveBacktestAiExportEnabled = (flags2) => Boolean(flags2.ai);
76
76
  var resolveBacktestJev = async (flags2, userName2, projectRoot2) => {
77
+ if (flags2.jevQuestionSet != null)
78
+ throw new Error(
79
+ "--jevQuestionSet was removed; --jev always uses the current nine-question set"
80
+ );
77
81
  if (!flags2.jev) {
78
82
  if (flags2.jevRecorded || flags2.jevModelFile)
79
83
  throw new Error("Jev options require --jev");
@@ -4325,6 +4329,11 @@ import_args.default.option(
4325
4329
  "Skip per-test artifact persistence and keep only in-memory summary/AI-ML dataset output",
4326
4330
  false
4327
4331
  );
4332
+ import_args.default.option(
4333
+ ["Y", "persistResults"],
4334
+ "Persist lightweight result summaries even in fast mode",
4335
+ false
4336
+ );
4328
4337
  import_args.default.option(
4329
4338
  "researchTrace",
4330
4339
  "Write compact setup/entry/exit/skip research trace JSONL",
@@ -5635,7 +5644,7 @@ var finishBacktest = async (testSuite) => {
5635
5644
  }
5636
5645
  console.log(import_chalk10.default.gray(`full report: ${markdownReportPath}`));
5637
5646
  console.log("");
5638
- if (!isFastMode) {
5647
+ if (!isFastMode || flags.persistResults) {
5639
5648
  await (0, import_redis5.setData)(
5640
5649
  import_redis5.redisKeys.backtestResults(userName, flags.config, timestamp),
5641
5650
  {
@@ -172,6 +172,17 @@ var runJevResearch = async (options) => {
172
172
  throw new Error(
173
173
  "Export exceeds 20000 samples; select a bounded recording directory"
174
174
  );
175
+ await (0, import_jev2.evaluateJevInput)({
176
+ input: record.input,
177
+ config: {
178
+ source: "recorded",
179
+ mode: "observe",
180
+ provider: record.provider,
181
+ recordsDir: options.recordsDir
182
+ },
183
+ userName: options.userName,
184
+ projectRoot: options.projectRoot
185
+ });
175
186
  const outcome = byRecord.get(record.id);
176
187
  rows2.push({
177
188
  schema: "tradejs-jev-study/v4",
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/scripts/mcpWorker.ts
21
+ var mcpWorker_exports = {};
22
+ __export(mcpWorker_exports, {
23
+ main: () => main
24
+ });
25
+ module.exports = __toCommonJS(mcpWorker_exports);
26
+ var import_node_child_process = require("child_process");
27
+ var import_node_crypto = require("crypto");
28
+ var import_promises = require("fs/promises");
29
+ var import_redis = require("@tradejs/infra/redis");
30
+ var import_mcp = require("@tradejs/node/mcp");
31
+ var pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
32
+ var leaseKey = "worker:lease";
33
+ var LEASE_SECONDS = 15;
34
+ var JOB_TIMEOUT_MS = 60 * 60 * 1e3;
35
+ var main = async (options = {}) => {
36
+ const owner = (0, import_node_crypto.randomUUID)();
37
+ if (!await import_redis.mcpStorage.put(leaseKey, owner, LEASE_SECONDS, true))
38
+ throw new Error("Another MCP worker owns this storage");
39
+ let stopping = false;
40
+ let child;
41
+ let renewing = false;
42
+ let leaseLost = false;
43
+ let manifestHash = "";
44
+ const signalChild = (signal) => {
45
+ if (!child?.pid) return;
46
+ try {
47
+ process.kill(
48
+ process.platform === "win32" ? child.pid : -child.pid,
49
+ signal
50
+ );
51
+ } catch {
52
+ }
53
+ };
54
+ const heartbeatValue = () => ({
55
+ observedAt: Date.now(),
56
+ packageManifestSha256: manifestHash,
57
+ replayAvailable: Boolean(process.env.MCP_REPLAY_REDIS_HOST)
58
+ });
59
+ const stop = () => {
60
+ stopping = true;
61
+ signalChild("SIGTERM");
62
+ };
63
+ options.signal?.addEventListener("abort", stop, { once: true });
64
+ if (options.signal?.aborted) stop();
65
+ process.once("SIGINT", stop);
66
+ process.once("SIGTERM", stop);
67
+ const heartbeat = setInterval(async () => {
68
+ if (renewing || leaseLost) return;
69
+ renewing = true;
70
+ try {
71
+ if (!await import_redis.mcpStorage.compare(leaseKey, owner, owner, LEASE_SECONDS))
72
+ throw new Error("Worker lease lost");
73
+ await import_redis.mcpStorage.put("worker:heartbeat", heartbeatValue(), LEASE_SECONDS);
74
+ } catch {
75
+ leaseLost = true;
76
+ stop();
77
+ } finally {
78
+ renewing = false;
79
+ }
80
+ }, 2e3);
81
+ try {
82
+ manifestHash = await (0, import_mcp.mcpPackageManifestHash)();
83
+ await import_redis.mcpStorage.put("worker:heartbeat", heartbeatValue(), LEASE_SECONDS);
84
+ let cursor = "0";
85
+ do {
86
+ const page = await import_redis.mcpStorage.scan("jobs:", cursor);
87
+ cursor = page.cursor;
88
+ for (const key of page.ids) {
89
+ const job = await import_redis.mcpStorage.get(key);
90
+ if (job?.status === "running")
91
+ await import_redis.mcpStorage.compare(
92
+ key,
93
+ job,
94
+ {
95
+ ...job,
96
+ status: "failed",
97
+ error: "Worker restarted; this job was not replayed",
98
+ finishedAt: Date.now(),
99
+ updatedAt: Date.now()
100
+ },
101
+ import_mcp.MCP_JOB_TTL
102
+ );
103
+ }
104
+ } while (cursor !== "0" && !stopping);
105
+ console.log(
106
+ "MCP worker ready (one job at a time, cached history, no live order commands)"
107
+ );
108
+ while (!stopping) {
109
+ let queued;
110
+ cursor = "0";
111
+ do {
112
+ const page = await import_redis.mcpStorage.scan("jobs:", cursor);
113
+ cursor = page.cursor;
114
+ for (const key of page.ids) {
115
+ const job = await import_redis.mcpStorage.get(key);
116
+ if (job?.status === "queued" && (!queued || job.createdAt < queued.createdAt))
117
+ queued = job;
118
+ }
119
+ } while (cursor !== "0" && !stopping);
120
+ if (!queued) {
121
+ await pause(1e3);
122
+ continue;
123
+ }
124
+ const jobKey = (0, import_mcp.mcpJobStorageId)(queued.userName, queued.id);
125
+ const started = {
126
+ ...queued,
127
+ status: "running",
128
+ startedAt: Date.now(),
129
+ updatedAt: Date.now()
130
+ };
131
+ if (!await import_redis.mcpStorage.compare(jobKey, queued, started, import_mcp.MCP_JOB_TTL))
132
+ continue;
133
+ if (started.packageManifestSha256 !== manifestHash) {
134
+ await import_redis.mcpStorage.compare(
135
+ jobKey,
136
+ started,
137
+ {
138
+ ...started,
139
+ status: "failed",
140
+ error: "Package composition changed since submission",
141
+ finishedAt: Date.now()
142
+ },
143
+ import_mcp.MCP_JOB_TTL
144
+ );
145
+ continue;
146
+ }
147
+ await (0, import_promises.mkdir)((0, import_mcp.mcpJobDirectory)(started), { recursive: true });
148
+ if (started.kind === "backtest")
149
+ await (0, import_redis.setDataStrict)(
150
+ import_redis.redisKeys.backtestConfig(
151
+ started.userName,
152
+ String(started.request.frozenConfigId)
153
+ ),
154
+ started.request.resolvedConfig,
155
+ { expire: import_mcp.MCP_JOB_TTL }
156
+ );
157
+ const jobEnv = {
158
+ ...process.env,
159
+ NODE_OPTIONS: "--max-old-space-size=1024",
160
+ MCP_WORKER: "true",
161
+ BACKTEST_WORKER_HEAP_MB: "768",
162
+ BACKTEST_MAX_PARALLEL: "1",
163
+ AI_ENABLED: "false",
164
+ ML_ENABLED: "false",
165
+ MAKE_ORDERS: "false",
166
+ TRADEJS_EXTERNAL_ORDER_PLACEMENT: "false"
167
+ };
168
+ if (started.kind === "runtime-feedback-replay") {
169
+ for (const name of Object.keys(jobEnv)) {
170
+ if (/secret|token|api.?key|private.?key|cookie|credential/i.test(name))
171
+ delete jobEnv[name];
172
+ }
173
+ Object.assign(jobEnv, {
174
+ DOTENV_CONFIG_PATH: "/dev/null",
175
+ REDIS_HOST: process.env.MCP_REPLAY_REDIS_HOST,
176
+ REDIS_PORT: "6379",
177
+ REDIS_DB: "0",
178
+ RUNTIME_FEEDBACK_ISOLATED_REDIS: "true",
179
+ PGOPTIONS: "-c default_transaction_read_only=on",
180
+ TRADEJS_TIMESCALE_READ_ONLY: "true"
181
+ });
182
+ }
183
+ child = (0, import_node_child_process.spawn)(process.execPath, [process.argv[1], ...started.args], {
184
+ cwd: process.env.PROJECT_CWD || process.cwd(),
185
+ env: jobEnv,
186
+ detached: process.platform !== "win32",
187
+ stdio: ["ignore", "pipe", "pipe"]
188
+ });
189
+ const currentChild = child;
190
+ let timedOut = false;
191
+ const timeout = setTimeout(() => {
192
+ timedOut = true;
193
+ signalChild("SIGTERM");
194
+ }, JOB_TIMEOUT_MS);
195
+ const logs = [];
196
+ const log = (chunk) => {
197
+ const lines = chunk.toString().replace(/\u001b\[[0-9;]*m/g, "").split(/[\r\n]+/).filter(Boolean);
198
+ logs.push(
199
+ ...lines.map(
200
+ (line) => line.slice(0, 1e3).replace(
201
+ /((?:secret|password|api.?key|token|authorization)\s*[:=]\s*)\S+/gi,
202
+ "$1[redacted]"
203
+ )
204
+ )
205
+ );
206
+ if (logs.length > 150) logs.splice(0, logs.length - 150);
207
+ };
208
+ currentChild.stdout?.on("data", log);
209
+ currentChild.stderr?.on("data", log);
210
+ let killAt;
211
+ const monitor = setInterval(async () => {
212
+ try {
213
+ const latest = await import_redis.mcpStorage.get(jobKey);
214
+ if (!latest || latest.cancelRequested || stopping || timedOut) {
215
+ killAt ??= Date.now();
216
+ signalChild(Date.now() - killAt > 8e3 ? "SIGKILL" : "SIGTERM");
217
+ }
218
+ if (latest && !leaseLost)
219
+ await import_redis.mcpStorage.compare(
220
+ jobKey,
221
+ latest,
222
+ { ...latest, logs: [...logs], updatedAt: Date.now() },
223
+ import_mcp.MCP_JOB_TTL
224
+ );
225
+ } catch {
226
+ leaseLost = true;
227
+ stop();
228
+ }
229
+ }, 1e3);
230
+ const outcome = await new Promise((resolve) => {
231
+ currentChild.once(
232
+ "error",
233
+ () => resolve({ code: null, error: "Unable to start research process" })
234
+ );
235
+ currentChild.once("exit", (code) => resolve({ code }));
236
+ });
237
+ clearTimeout(timeout);
238
+ clearInterval(monitor);
239
+ child = void 0;
240
+ if (!leaseLost) {
241
+ for (let attempt = 0; attempt < 4; attempt++) {
242
+ const latest = await import_redis.mcpStorage.get(jobKey);
243
+ if (!latest) break;
244
+ const cancelled = latest.cancelRequested;
245
+ const next = {
246
+ ...latest,
247
+ logs,
248
+ status: cancelled ? "cancelled" : outcome.code === 0 && !timedOut && !stopping ? "completed" : "failed",
249
+ exitCode: outcome.code,
250
+ updatedAt: Date.now(),
251
+ finishedAt: Date.now(),
252
+ ...cancelled ? {} : timedOut ? { error: "Job exceeded one hour" } : stopping ? { error: "Worker stopped; this job was not replayed" } : outcome.code !== 0 ? { error: outcome.error || "Research process failed" } : {}
253
+ };
254
+ if (await import_redis.mcpStorage.compare(jobKey, latest, next, import_mcp.MCP_JOB_TTL))
255
+ break;
256
+ }
257
+ }
258
+ }
259
+ } finally {
260
+ clearInterval(heartbeat);
261
+ options.signal?.removeEventListener("abort", stop);
262
+ process.removeListener("SIGINT", stop);
263
+ process.removeListener("SIGTERM", stop);
264
+ if (await import_redis.mcpStorage.release(leaseKey, owner))
265
+ await import_redis.mcpStorage.remove("worker:heartbeat");
266
+ }
267
+ if (leaseLost)
268
+ throw new Error("Worker lease lost; active computation was stopped");
269
+ };
270
+ // Annotate the CommonJS export names for ESM import in node:
271
+ 0 && (module.exports = {
272
+ main
273
+ });