@superatomai/sdk-node 0.0.50-dsp → 0.0.51-dsp

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/dist/index.d.mts CHANGED
@@ -291,6 +291,14 @@ type MatchTier = 'high' | 'near' | 'edit' | 'none';
291
291
  */
292
292
  interface ScriptBinding {
293
293
  recipeId: string;
294
+ /**
295
+ * The recipe's own version number at the moment THIS turn ran — not
296
+ * necessarily the recipe's current version. A later edit bumps the live
297
+ * version, so without this a past turn's binding would silently point at
298
+ * whatever the recipe looks like today instead of what actually produced
299
+ * that answer. Absent on bindings created before this field existed.
300
+ */
301
+ version?: number;
294
302
  /** Params the script ran with — the edit's starting point. */
295
303
  params: Record<string, any>;
296
304
  /** Recipe name at bind time (matcher catalog + user-facing confirmation). */
@@ -2548,6 +2556,18 @@ declare class ScriptStore {
2548
2556
  search(prompt: string, limit?: number): Promise<ScriptRecipe[]>;
2549
2557
  /** Fetch one recipe by id with its body loaded from disk. */
2550
2558
  get(id: string): Promise<ScriptRecipe | null>;
2559
+ /**
2560
+ * Fetch one recipe by id, pinned to a SPECIFIC past version rather than
2561
+ * whatever is live today. Needed to render a past turn's conversation
2562
+ * history accurately after the script has since been edited — get() alone
2563
+ * always returns the CURRENT body, which may no longer be what that turn
2564
+ * actually ran.
2565
+ *
2566
+ * Returns null (never a silently-wrong version) when the requested version
2567
+ * isn't the current one and its archived body can't be found — e.g. the
2568
+ * archive was cleaned up after the whole recipe was later deleted.
2569
+ */
2570
+ getVersion(id: string, version: number): Promise<ScriptRecipe | null>;
2551
2571
  /** Create or update a recipe (metadata upsert + body write when changed). */
2552
2572
  save(recipe: ScriptRecipe): Promise<void>;
2553
2573
  /**
@@ -3470,7 +3490,14 @@ declare class Thread {
3470
3490
  * See backend/docs/SCRIPT-EDIT-DESIGN.md § Component 1.
3471
3491
  */
3472
3492
  getScriptBindings(limit?: number, currentUIBlockId?: string): Record<string, any>[];
3473
- getConversationContext(limit?: number, currentUIBlockId?: string): string;
3493
+ /**
3494
+ * @param getScriptBody Resolves a script-backed turn's EXACT script, pinned
3495
+ * to the version that turn actually ran — never today's (possibly
3496
+ * since-edited) live body. Owned by the caller (needs a ScriptStore, which
3497
+ * Thread doesn't hold) — omit it and every turn falls back to the
3498
+ * no-script summary below.
3499
+ */
3500
+ getConversationContext(limit?: number, currentUIBlockId?: string, getScriptBody?: (recipeId: string, version: number) => Promise<string | null>): Promise<string>;
3474
3501
  /**
3475
3502
  * Convert Thread to JSON-serializable object
3476
3503
  */
package/dist/index.d.ts CHANGED
@@ -291,6 +291,14 @@ type MatchTier = 'high' | 'near' | 'edit' | 'none';
291
291
  */
292
292
  interface ScriptBinding {
293
293
  recipeId: string;
294
+ /**
295
+ * The recipe's own version number at the moment THIS turn ran — not
296
+ * necessarily the recipe's current version. A later edit bumps the live
297
+ * version, so without this a past turn's binding would silently point at
298
+ * whatever the recipe looks like today instead of what actually produced
299
+ * that answer. Absent on bindings created before this field existed.
300
+ */
301
+ version?: number;
294
302
  /** Params the script ran with — the edit's starting point. */
295
303
  params: Record<string, any>;
296
304
  /** Recipe name at bind time (matcher catalog + user-facing confirmation). */
@@ -2548,6 +2556,18 @@ declare class ScriptStore {
2548
2556
  search(prompt: string, limit?: number): Promise<ScriptRecipe[]>;
2549
2557
  /** Fetch one recipe by id with its body loaded from disk. */
2550
2558
  get(id: string): Promise<ScriptRecipe | null>;
2559
+ /**
2560
+ * Fetch one recipe by id, pinned to a SPECIFIC past version rather than
2561
+ * whatever is live today. Needed to render a past turn's conversation
2562
+ * history accurately after the script has since been edited — get() alone
2563
+ * always returns the CURRENT body, which may no longer be what that turn
2564
+ * actually ran.
2565
+ *
2566
+ * Returns null (never a silently-wrong version) when the requested version
2567
+ * isn't the current one and its archived body can't be found — e.g. the
2568
+ * archive was cleaned up after the whole recipe was later deleted.
2569
+ */
2570
+ getVersion(id: string, version: number): Promise<ScriptRecipe | null>;
2551
2571
  /** Create or update a recipe (metadata upsert + body write when changed). */
2552
2572
  save(recipe: ScriptRecipe): Promise<void>;
2553
2573
  /**
@@ -3470,7 +3490,14 @@ declare class Thread {
3470
3490
  * See backend/docs/SCRIPT-EDIT-DESIGN.md § Component 1.
3471
3491
  */
3472
3492
  getScriptBindings(limit?: number, currentUIBlockId?: string): Record<string, any>[];
3473
- getConversationContext(limit?: number, currentUIBlockId?: string): string;
3493
+ /**
3494
+ * @param getScriptBody Resolves a script-backed turn's EXACT script, pinned
3495
+ * to the version that turn actually ran — never today's (possibly
3496
+ * since-edited) live body. Owned by the caller (needs a ScriptStore, which
3497
+ * Thread doesn't hold) — omit it and every turn falls back to the
3498
+ * no-script summary below.
3499
+ */
3500
+ getConversationContext(limit?: number, currentUIBlockId?: string, getScriptBody?: (recipeId: string, version: number) => Promise<string | null>): Promise<string>;
3474
3501
  /**
3475
3502
  * Convert Thread to JSON-serializable object
3476
3503
  */
package/dist/index.js CHANGED
@@ -1502,7 +1502,14 @@ var Thread = class {
1502
1502
  }
1503
1503
  return out;
1504
1504
  }
1505
- getConversationContext(limit = 5, currentUIBlockId) {
1505
+ /**
1506
+ * @param getScriptBody Resolves a script-backed turn's EXACT script, pinned
1507
+ * to the version that turn actually ran — never today's (possibly
1508
+ * since-edited) live body. Owned by the caller (needs a ScriptStore, which
1509
+ * Thread doesn't hold) — omit it and every turn falls back to the
1510
+ * no-script summary below.
1511
+ */
1512
+ async getConversationContext(limit = 5, currentUIBlockId, getScriptBody) {
1506
1513
  if (limit === 0) {
1507
1514
  return "";
1508
1515
  }
@@ -1512,53 +1519,62 @@ var Thread = class {
1512
1519
  }
1513
1520
  const recentBlocks = allBlocks.slice(-limit);
1514
1521
  const contextLines = [];
1515
- recentBlocks.forEach((block, index) => {
1516
- const questionNum = index + 1;
1522
+ for (const block of recentBlocks) {
1517
1523
  const question = block.getUserQuestion();
1518
1524
  const metadata = block.getComponentMetadata();
1519
1525
  const programAnswer = block.getProgramAnswer();
1520
1526
  const textResponse = block.getTextResponse();
1521
- let assistantResponse = "";
1527
+ const scriptBinding = block.getScriptBinding();
1522
1528
  const responseParts = [];
1529
+ let analysisText = "";
1523
1530
  const hasProgramAnswer = programAnswer && programAnswer.trim().length > 0;
1524
1531
  if (hasProgramAnswer) {
1525
1532
  const trimmed = programAnswer.trim();
1526
- responseParts.push(truncateKeepingMethodLine(trimmed, 2e3));
1533
+ analysisText = truncateKeepingMethodLine(trimmed, 2e3);
1527
1534
  } else {
1528
1535
  const hasTextResponse = textResponse && textResponse.trim().length > 0;
1529
1536
  if (hasTextResponse) {
1530
1537
  const cleaned = cleanAssistantText(textResponse);
1531
1538
  if (cleaned) {
1532
- const trimmedText = cleaned.length > 800 ? cleaned.substring(0, 800) + "...[truncated]" : cleaned;
1533
- responseParts.push(trimmedText);
1539
+ analysisText = cleaned.length > 800 ? cleaned.substring(0, 800) + "...[truncated]" : cleaned;
1534
1540
  }
1535
1541
  }
1536
1542
  }
1537
- const scriptBinding = block.getScriptBinding();
1538
- if (scriptBinding?.params && Object.keys(scriptBinding.params).length > 0) {
1539
- responseParts.push(`Parameters used: ${JSON.stringify(scriptBinding.params)}`);
1540
- }
1541
- const hasComponent = metadata && Object.keys(metadata).length > 0 && metadata.type;
1542
- if (hasComponent) {
1543
- const label = metadata.name ? `${metadata.type} \u2014 ${metadata.name}` : metadata.type;
1544
- responseParts.push(`[Rendered: ${label}]`);
1545
- const innerComponents = metadata.props?.config?.components;
1546
- if (Array.isArray(innerComponents) && innerComponents.length > 0) {
1547
- const summary = innerComponents.map((c) => c?.props?.title ? `${c.name} ("${c.props.title}")` : c?.name).filter(Boolean).join(", ");
1548
- if (summary) responseParts.push(` Components: ${summary}`);
1543
+ if (analysisText) responseParts.push(analysisText);
1544
+ let scriptAttached = false;
1545
+ if (scriptBinding?.recipeId && scriptBinding.version !== void 0 && getScriptBody) {
1546
+ try {
1547
+ const body = await getScriptBody(scriptBinding.recipeId, scriptBinding.version);
1548
+ if (body) {
1549
+ responseParts.push(`Script used:
1550
+ ${body}`);
1551
+ scriptAttached = true;
1552
+ }
1553
+ } catch {
1549
1554
  }
1550
1555
  }
1551
- if (responseParts.length > 0) {
1552
- assistantResponse = responseParts.join("\n");
1553
- } else {
1554
- assistantResponse = "No response generated";
1556
+ if (!scriptAttached) {
1557
+ if (scriptBinding?.params && Object.keys(scriptBinding.params).length > 0) {
1558
+ responseParts.push(`Parameters used: ${JSON.stringify(scriptBinding.params)}`);
1559
+ }
1560
+ const hasComponent = metadata && Object.keys(metadata).length > 0 && metadata.type;
1561
+ if (hasComponent) {
1562
+ const label = metadata.name ? `${metadata.type} \u2014 ${metadata.name}` : metadata.type;
1563
+ responseParts.push(`[Rendered: ${label}]`);
1564
+ const innerComponents = metadata.props?.config?.components;
1565
+ if (Array.isArray(innerComponents) && innerComponents.length > 0) {
1566
+ const summary = innerComponents.map((c) => c?.props?.title ? `${c.name} ("${c.props.title}")` : c?.name).filter(Boolean).join(", ");
1567
+ if (summary) responseParts.push(` Components: ${summary}`);
1568
+ }
1569
+ }
1555
1570
  }
1571
+ const assistantResponse = responseParts.length > 0 ? responseParts.join("\n") : "No response generated";
1556
1572
  contextLines.push(`User:
1557
1573
  ${question}`);
1558
1574
  contextLines.push(`Assistant:
1559
1575
  ${assistantResponse}`);
1560
1576
  contextLines.push("---");
1561
- });
1577
+ }
1562
1578
  return contextLines.join("\n").trim();
1563
1579
  }
1564
1580
  /**
@@ -10232,16 +10248,20 @@ ${formatted}`;
10232
10248
  } catch (execError) {
10233
10249
  const errorMsg = execError instanceof Error ? execError.message : typeof execError === "object" && execError !== null ? execError.message || execError.error || JSON.stringify(execError) : String(execError);
10234
10250
  logger.warn(`[SourceAgent:${this.tool.name}] Tool execution failed (attempt ${this.attempts}/${this.config.maxRetries}): ${errorMsg}`);
10251
+ let hint = "";
10252
+ if (this.extractSourceType() === "netsuite" && /"o:errorCode"\s*:\s*"UNEXPECTED_ERROR"/.test(errorMsg)) {
10253
+ hint = ` This gives no column/syntax detail \u2014 it usually means a column that doesn't exist on that table (wrong field name), or a column referenced outside the CTE that selects it (a CTE only exposes columns it projects). Recheck every column name against the schema.`;
10254
+ }
10235
10255
  if (this.streamBuffer.hasCallback()) {
10236
10256
  const querySeconds = ((Date.now() - queryStartTime) / 1e3).toFixed(1);
10237
10257
  this.streamBuffer.write(`__QUERY_TIMER_DONE_${querySeconds}__
10238
10258
 
10239
10259
  `);
10240
- this.streamBuffer.write(`\u274C **Query failed:** ${errorMsg}
10260
+ this.streamBuffer.write(`\u274C **Query failed:** ${errorMsg}${hint}
10241
10261
 
10242
10262
  `);
10243
10263
  }
10244
- return `\u274C ERROR: ${errorMsg}
10264
+ return `\u274C ERROR: ${errorMsg}${hint}
10245
10265
 
10246
10266
  Analyze the error and try again with a corrected query.`;
10247
10267
  }
@@ -11254,13 +11274,34 @@ function findHardcodedComponentText(scriptBody) {
11254
11274
  }
11255
11275
  return violations;
11256
11276
  }
11277
+ function findHardcodedEntityNames(source, entityDisplayNames) {
11278
+ const names = entityDisplayNames.filter((n) => n && n.trim().length >= 2);
11279
+ if (!names.length) return [];
11280
+ const violations = [];
11281
+ const seen = /* @__PURE__ */ new Set();
11282
+ for (const chunk of literalChunks(source)) {
11283
+ const trimmed = chunk.trim();
11284
+ if (!trimmed) continue;
11285
+ for (const name of names) {
11286
+ if (seen.has(name)) continue;
11287
+ if (trimmed.toLowerCase().includes(name.toLowerCase())) {
11288
+ seen.add(name);
11289
+ violations.push({
11290
+ text: trimmed.slice(0, 120),
11291
+ reason: `"${name}" \u2014 a name resolved via resolve_entities this turn \u2014 is written as fixed text. Read it from \`data\`/\`datasets\` instead (e.g. \`data[0]?.<name_column>\`), or filter/join on the resolved id (\`params.<name>Id\`) rather than the name string \u2014 a name typed as a literal is exactly as wrong on replay as a number typed as a literal: the id a future run resolves will differ, but the printed/filtered name will not.`
11292
+ });
11293
+ }
11294
+ }
11295
+ }
11296
+ return violations;
11297
+ }
11257
11298
  function formatLintError(violations) {
11258
11299
  if (!violations.length) return null;
11259
11300
  const lines = violations.map((v) => ` \u2022 "${v.text}" \u2014 ${v.reason}`).join("\n");
11260
- return `The analysis body hardcodes ${violations.length} number${violations.length > 1 ? "s" : ""} as text:
11301
+ return `The analysis body hardcodes ${violations.length} value${violations.length > 1 ? "s" : ""} as text:
11261
11302
  ${lines}
11262
11303
 
11263
- FIX: every figure must come from the data via a fmt helper \u2014 fmt.money, fmt.num, fmt.pct, fmt.delta, fmt.count, fmt.date, fmt.range. A number typed into a string is frozen at authoring time and will be wrong on replay when the data changes. Rewording will not clear this \u2014 only moving the number out of the string will. A constant the code itself uses is fine once interpolated from a variable; a figure read off the data is not.`;
11304
+ FIX: every figure must come from the data via a fmt helper \u2014 fmt.money, fmt.num, fmt.pct, fmt.delta, fmt.count, fmt.date, fmt.range \u2014 and every resolved name must come from \`data\`/\`datasets\`, not be typed in. A value typed into a string is frozen at authoring time and will be wrong on replay when the data or params change. Rewording will not clear this \u2014 only moving the value out of the string will. A constant the code itself uses is fine once interpolated from a variable; a figure or name read off the data is not.`;
11264
11305
  }
11265
11306
 
11266
11307
  // src/userResponse/scripts/component-validator.ts
@@ -12031,7 +12072,8 @@ ${catalog}`,
12031
12072
  this.renderComponentAttempts++;
12032
12073
  const hardcodedAggregates = findHardcodedAggregates(body);
12033
12074
  const hardcodedComponentText = findHardcodedComponentText(body);
12034
- const componentViolations = [...hardcodedAggregates, ...hardcodedComponentText];
12075
+ const hardcodedEntityNames = findHardcodedEntityNames(body, this.resolvedEntities.map((e) => e.displayName));
12076
+ const componentViolations = [...hardcodedAggregates, ...hardcodedComponentText, ...hardcodedEntityNames];
12035
12077
  if (componentViolations.length > 0) {
12036
12078
  const named = componentViolations.map((v) => `"${v.text}"`).join(", ");
12037
12079
  const repeatedTexts = componentViolations.map((v) => v.text).filter((t) => this.lastComponentsViolationTexts.includes(t));
@@ -12114,7 +12156,10 @@ Now write your answer for the user. Do not restate the component list.`;
12114
12156
  return `Reached the max of ${MAX_ANALYSIS_ATTEMPTS} write_analysis attempts. Write your final analysis as prose instead, using the values from the script result exactly as shown.`;
12115
12157
  }
12116
12158
  this.scriptState.analysisAttempts++;
12117
- const violations = lintAnalysisBody(body);
12159
+ const violations = [
12160
+ ...lintAnalysisBody(body),
12161
+ ...findHardcodedEntityNames(body, this.resolvedEntities.map((e) => e.displayName))
12162
+ ];
12118
12163
  const repeatedTexts = violations.map((v) => v.text).filter((t) => this.scriptState.lastAnalysisViolationTexts.includes(t));
12119
12164
  this.scriptState.lastAnalysisViolationTexts = violations.map((v) => v.text);
12120
12165
  const lintError = formatLintError(violations);
@@ -12192,6 +12237,14 @@ FIX: derive every such value from the data itself \u2014 \`data.length\`, a real
12192
12237
  ${lines}
12193
12238
 
12194
12239
  FIX: build the title/description with a template literal that reads the value from \`data\`/\`datasets\` at render time (e.g. \` \\\`Total: \\\${fmt.money(datasets.summary[0].total)}\\\` \`), never a number typed directly into the string.`;
12240
+ }
12241
+ const hardcodedEntityNames = findHardcodedEntityNames(scriptBody, this.resolvedEntities.map((e) => e.displayName));
12242
+ if (hardcodedEntityNames.length > 0) {
12243
+ const lines = hardcodedEntityNames.map((v) => ` \u2022 "${v.text}" \u2014 ${v.reason}`).join("\n");
12244
+ return `write_script rejected: the script hardcodes ${hardcodedEntityNames.length} resolved entity name(s) as fixed text:
12245
+ ${lines}
12246
+
12247
+ FIX: read the name from \`data\`/\`datasets\` at render time, or filter/join on the resolved id (\`params.<name>Id\`) instead of the name string.`;
12195
12248
  }
12196
12249
  const draft = await this.scriptStore.saveDraft({
12197
12250
  recipeId: this.scriptState.recipeId ?? void 0,
@@ -13397,6 +13450,22 @@ function stripNamedExport(body, fnName) {
13397
13450
  }
13398
13451
  return body.slice(0, start);
13399
13452
  }
13453
+ function extractNamedExport(body, fnName) {
13454
+ const start = body.search(new RegExp(`export\\s+(?:async\\s+)?function\\s+${fnName}\\b`));
13455
+ if (start === -1) return null;
13456
+ const open = body.indexOf("{", start);
13457
+ if (open === -1) return null;
13458
+ let depth = 0;
13459
+ for (let i = open; i < body.length; i++) {
13460
+ const c = body[i];
13461
+ if (c === "{") depth++;
13462
+ else if (c === "}") {
13463
+ depth--;
13464
+ if (depth === 0) return body.slice(start, i + 1);
13465
+ }
13466
+ }
13467
+ return null;
13468
+ }
13400
13469
  function normalizeScriptBody(scriptBody) {
13401
13470
  let i = 0;
13402
13471
  const n = scriptBody.length;
@@ -13496,6 +13565,37 @@ var ScriptStore = class {
13496
13565
  return null;
13497
13566
  }
13498
13567
  }
13568
+ /**
13569
+ * Fetch one recipe by id, pinned to a SPECIFIC past version rather than
13570
+ * whatever is live today. Needed to render a past turn's conversation
13571
+ * history accurately after the script has since been edited — get() alone
13572
+ * always returns the CURRENT body, which may no longer be what that turn
13573
+ * actually ran.
13574
+ *
13575
+ * Returns null (never a silently-wrong version) when the requested version
13576
+ * isn't the current one and its archived body can't be found — e.g. the
13577
+ * archive was cleaned up after the whole recipe was later deleted.
13578
+ */
13579
+ async getVersion(id, version) {
13580
+ if (!this.store) return null;
13581
+ try {
13582
+ const row = await this.store.getById(id);
13583
+ if (!row) return null;
13584
+ if (version === row.version) {
13585
+ return this.rowToRecipe(row, this.readBody(row.fileBase));
13586
+ }
13587
+ const archivePath = path6.join(this.archiveDir, `${row.fileBase}.v${version}.ts`);
13588
+ if (!fs6.existsSync(archivePath)) {
13589
+ logger.warn(`[ScriptStore] getVersion(${id}, v${version}) \u2014 archived body not found, recipe is now at v${row.version}`);
13590
+ return null;
13591
+ }
13592
+ const body = fs6.readFileSync(archivePath, "utf-8");
13593
+ return this.rowToRecipe(row, body);
13594
+ } catch (err) {
13595
+ logger.warn(`[ScriptStore] getVersion(${id}, v${version}) failed: ${err}`);
13596
+ return null;
13597
+ }
13598
+ }
13499
13599
  // ============================================
13500
13600
  // Write
13501
13601
  // ============================================
@@ -13535,7 +13635,20 @@ var ScriptStore = class {
13535
13635
  const reuse = existing && existing.status === "draft" && existing.turnId === input.turnId;
13536
13636
  const id = reuse ? existing.id : `script_${Date.now()}_${(0, import_crypto5.randomBytes)(3).toString("hex")}`;
13537
13637
  const fileBase = reuse ? existing.fileBase : input.parentId ? `${this.toSlug(input.name)}-${id.slice(-6)}` : await this.computeFileBase(input.name, id);
13538
- const normalized = normalizeScriptBody(input.scriptBody);
13638
+ let normalized = normalizeScriptBody(input.scriptBody);
13639
+ if (reuse) {
13640
+ const existingBody = this.readBody(fileBase);
13641
+ if (existingBody) {
13642
+ for (const fnName of ["getComponents", "getAnalysis"]) {
13643
+ if (new RegExp(`export\\s+(?:async\\s+)?function\\s+${fnName}\\b`).test(normalized)) continue;
13644
+ const carried = extractNamedExport(existingBody, fnName);
13645
+ if (carried) normalized = `${normalized.trimEnd()}
13646
+
13647
+ ${carried.trim()}
13648
+ `;
13649
+ }
13650
+ }
13651
+ }
13539
13652
  const bodyHash = this.hash(normalized);
13540
13653
  this.writeBody(fileBase, normalized);
13541
13654
  const recipe = {
@@ -17785,6 +17898,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${scriptMatch
17785
17898
  // ("use mode instead") can edit THIS script.
17786
17899
  scriptBinding: {
17787
17900
  recipeId: scriptMatch.recipe.id,
17901
+ version: scriptMatch.recipe.version,
17788
17902
  params: scriptMatch.extractedParams || {},
17789
17903
  name: scriptMatch.recipe.name,
17790
17904
  columns: scriptResult.executedQueries?.[0]?.data?.[0] ? Object.keys(scriptResult.executedQueries[0].data[0]) : void 0
@@ -18212,6 +18326,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${carriedRequ
18212
18326
  if (committed) {
18213
18327
  scriptBinding = {
18214
18328
  recipeId: committed.id,
18329
+ version: committed.version,
18215
18330
  params: {},
18216
18331
  name: committed.name,
18217
18332
  columns: authored.executedQueries?.[0]?.data?.[0] ? Object.keys(authored.executedQueries[0].data[0]) : void 0
@@ -18269,6 +18384,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${carriedRequ
18269
18384
  }
18270
18385
  scriptBinding = {
18271
18386
  recipeId: authored.recipeId,
18387
+ version: promoted?.version,
18272
18388
  params: {},
18273
18389
  name: authored.name,
18274
18390
  columns: authored.executedQueries?.[0]?.data?.[0] ? Object.keys(authored.executedQueries[0].data[0]) : void 0
@@ -18587,7 +18703,7 @@ var CONTEXT_CONFIG = {
18587
18703
  * Set to 0 to disable conversation history
18588
18704
  * Higher values provide more context but may increase token usage
18589
18705
  */
18590
- MAX_CONVERSATION_CONTEXT_BLOCKS: 3
18706
+ MAX_CONVERSATION_CONTEXT_BLOCKS: 2
18591
18707
  };
18592
18708
 
18593
18709
  // src/handlers/user-prompt-request.ts
@@ -18685,7 +18801,12 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
18685
18801
  }
18686
18802
  const requestStartTime = Date.now();
18687
18803
  logger.info(`Starting user prompt request with ${components.length} components`);
18688
- const conversationHistory = thread.getConversationContext(CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS, existingUiBlockId);
18804
+ const conversationScriptStore = new ScriptStore({ collections, projectId: process.env.SUPERATOM_PROJECT_ID || "" });
18805
+ const conversationHistory = await thread.getConversationContext(
18806
+ CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS,
18807
+ existingUiBlockId,
18808
+ async (recipeId, version) => (await conversationScriptStore.getVersion(recipeId, version))?.scriptBody ?? null
18809
+ );
18689
18810
  const MAX_EDIT_CANDIDATES = 5;
18690
18811
  let activeScriptBindings = thread.getScriptBindings(MAX_EDIT_CANDIDATES, existingUiBlockId);
18691
18812
  if (activeScriptBindings.length === 0 && collections?.["conversation-history"]?.["threadScriptBindings"]) {
@@ -19333,7 +19454,7 @@ async function handleActionsRequest(data, sendMessage, anthropicApiKey, groqApiK
19333
19454
  logger.debug(`[ACTIONS_REQ ${id}] Component: ${component?.name || "unknown"} (${component?.type || "unknown"})`);
19334
19455
  logger.debug(`[ACTIONS_REQ ${id}] Component data available: ${componentData ? "yes" : "no"}`);
19335
19456
  logger.debug(`[ACTIONS_REQ ${id}] Extracting conversation history (max ${CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS} blocks)`);
19336
- const conversationHistory = thread.getConversationContext(CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS, uiBlockId);
19457
+ const conversationHistory = await thread.getConversationContext(CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS, uiBlockId);
19337
19458
  const historyLineCount = conversationHistory.split("\n").filter((l) => l.trim()).length;
19338
19459
  logger.info(`[ACTIONS_REQ ${id}] Conversation history extracted: ${historyLineCount} lines`);
19339
19460
  logger.debug(`[ACTIONS_REQ ${id}] Conversation history preview:
package/dist/index.mjs CHANGED
@@ -1443,7 +1443,14 @@ var Thread = class {
1443
1443
  }
1444
1444
  return out;
1445
1445
  }
1446
- getConversationContext(limit = 5, currentUIBlockId) {
1446
+ /**
1447
+ * @param getScriptBody Resolves a script-backed turn's EXACT script, pinned
1448
+ * to the version that turn actually ran — never today's (possibly
1449
+ * since-edited) live body. Owned by the caller (needs a ScriptStore, which
1450
+ * Thread doesn't hold) — omit it and every turn falls back to the
1451
+ * no-script summary below.
1452
+ */
1453
+ async getConversationContext(limit = 5, currentUIBlockId, getScriptBody) {
1447
1454
  if (limit === 0) {
1448
1455
  return "";
1449
1456
  }
@@ -1453,53 +1460,62 @@ var Thread = class {
1453
1460
  }
1454
1461
  const recentBlocks = allBlocks.slice(-limit);
1455
1462
  const contextLines = [];
1456
- recentBlocks.forEach((block, index) => {
1457
- const questionNum = index + 1;
1463
+ for (const block of recentBlocks) {
1458
1464
  const question = block.getUserQuestion();
1459
1465
  const metadata = block.getComponentMetadata();
1460
1466
  const programAnswer = block.getProgramAnswer();
1461
1467
  const textResponse = block.getTextResponse();
1462
- let assistantResponse = "";
1468
+ const scriptBinding = block.getScriptBinding();
1463
1469
  const responseParts = [];
1470
+ let analysisText = "";
1464
1471
  const hasProgramAnswer = programAnswer && programAnswer.trim().length > 0;
1465
1472
  if (hasProgramAnswer) {
1466
1473
  const trimmed = programAnswer.trim();
1467
- responseParts.push(truncateKeepingMethodLine(trimmed, 2e3));
1474
+ analysisText = truncateKeepingMethodLine(trimmed, 2e3);
1468
1475
  } else {
1469
1476
  const hasTextResponse = textResponse && textResponse.trim().length > 0;
1470
1477
  if (hasTextResponse) {
1471
1478
  const cleaned = cleanAssistantText(textResponse);
1472
1479
  if (cleaned) {
1473
- const trimmedText = cleaned.length > 800 ? cleaned.substring(0, 800) + "...[truncated]" : cleaned;
1474
- responseParts.push(trimmedText);
1480
+ analysisText = cleaned.length > 800 ? cleaned.substring(0, 800) + "...[truncated]" : cleaned;
1475
1481
  }
1476
1482
  }
1477
1483
  }
1478
- const scriptBinding = block.getScriptBinding();
1479
- if (scriptBinding?.params && Object.keys(scriptBinding.params).length > 0) {
1480
- responseParts.push(`Parameters used: ${JSON.stringify(scriptBinding.params)}`);
1481
- }
1482
- const hasComponent = metadata && Object.keys(metadata).length > 0 && metadata.type;
1483
- if (hasComponent) {
1484
- const label = metadata.name ? `${metadata.type} \u2014 ${metadata.name}` : metadata.type;
1485
- responseParts.push(`[Rendered: ${label}]`);
1486
- const innerComponents = metadata.props?.config?.components;
1487
- if (Array.isArray(innerComponents) && innerComponents.length > 0) {
1488
- const summary = innerComponents.map((c) => c?.props?.title ? `${c.name} ("${c.props.title}")` : c?.name).filter(Boolean).join(", ");
1489
- if (summary) responseParts.push(` Components: ${summary}`);
1484
+ if (analysisText) responseParts.push(analysisText);
1485
+ let scriptAttached = false;
1486
+ if (scriptBinding?.recipeId && scriptBinding.version !== void 0 && getScriptBody) {
1487
+ try {
1488
+ const body = await getScriptBody(scriptBinding.recipeId, scriptBinding.version);
1489
+ if (body) {
1490
+ responseParts.push(`Script used:
1491
+ ${body}`);
1492
+ scriptAttached = true;
1493
+ }
1494
+ } catch {
1490
1495
  }
1491
1496
  }
1492
- if (responseParts.length > 0) {
1493
- assistantResponse = responseParts.join("\n");
1494
- } else {
1495
- assistantResponse = "No response generated";
1497
+ if (!scriptAttached) {
1498
+ if (scriptBinding?.params && Object.keys(scriptBinding.params).length > 0) {
1499
+ responseParts.push(`Parameters used: ${JSON.stringify(scriptBinding.params)}`);
1500
+ }
1501
+ const hasComponent = metadata && Object.keys(metadata).length > 0 && metadata.type;
1502
+ if (hasComponent) {
1503
+ const label = metadata.name ? `${metadata.type} \u2014 ${metadata.name}` : metadata.type;
1504
+ responseParts.push(`[Rendered: ${label}]`);
1505
+ const innerComponents = metadata.props?.config?.components;
1506
+ if (Array.isArray(innerComponents) && innerComponents.length > 0) {
1507
+ const summary = innerComponents.map((c) => c?.props?.title ? `${c.name} ("${c.props.title}")` : c?.name).filter(Boolean).join(", ");
1508
+ if (summary) responseParts.push(` Components: ${summary}`);
1509
+ }
1510
+ }
1496
1511
  }
1512
+ const assistantResponse = responseParts.length > 0 ? responseParts.join("\n") : "No response generated";
1497
1513
  contextLines.push(`User:
1498
1514
  ${question}`);
1499
1515
  contextLines.push(`Assistant:
1500
1516
  ${assistantResponse}`);
1501
1517
  contextLines.push("---");
1502
- });
1518
+ }
1503
1519
  return contextLines.join("\n").trim();
1504
1520
  }
1505
1521
  /**
@@ -10173,16 +10189,20 @@ ${formatted}`;
10173
10189
  } catch (execError) {
10174
10190
  const errorMsg = execError instanceof Error ? execError.message : typeof execError === "object" && execError !== null ? execError.message || execError.error || JSON.stringify(execError) : String(execError);
10175
10191
  logger.warn(`[SourceAgent:${this.tool.name}] Tool execution failed (attempt ${this.attempts}/${this.config.maxRetries}): ${errorMsg}`);
10192
+ let hint = "";
10193
+ if (this.extractSourceType() === "netsuite" && /"o:errorCode"\s*:\s*"UNEXPECTED_ERROR"/.test(errorMsg)) {
10194
+ hint = ` This gives no column/syntax detail \u2014 it usually means a column that doesn't exist on that table (wrong field name), or a column referenced outside the CTE that selects it (a CTE only exposes columns it projects). Recheck every column name against the schema.`;
10195
+ }
10176
10196
  if (this.streamBuffer.hasCallback()) {
10177
10197
  const querySeconds = ((Date.now() - queryStartTime) / 1e3).toFixed(1);
10178
10198
  this.streamBuffer.write(`__QUERY_TIMER_DONE_${querySeconds}__
10179
10199
 
10180
10200
  `);
10181
- this.streamBuffer.write(`\u274C **Query failed:** ${errorMsg}
10201
+ this.streamBuffer.write(`\u274C **Query failed:** ${errorMsg}${hint}
10182
10202
 
10183
10203
  `);
10184
10204
  }
10185
- return `\u274C ERROR: ${errorMsg}
10205
+ return `\u274C ERROR: ${errorMsg}${hint}
10186
10206
 
10187
10207
  Analyze the error and try again with a corrected query.`;
10188
10208
  }
@@ -11195,13 +11215,34 @@ function findHardcodedComponentText(scriptBody) {
11195
11215
  }
11196
11216
  return violations;
11197
11217
  }
11218
+ function findHardcodedEntityNames(source, entityDisplayNames) {
11219
+ const names = entityDisplayNames.filter((n) => n && n.trim().length >= 2);
11220
+ if (!names.length) return [];
11221
+ const violations = [];
11222
+ const seen = /* @__PURE__ */ new Set();
11223
+ for (const chunk of literalChunks(source)) {
11224
+ const trimmed = chunk.trim();
11225
+ if (!trimmed) continue;
11226
+ for (const name of names) {
11227
+ if (seen.has(name)) continue;
11228
+ if (trimmed.toLowerCase().includes(name.toLowerCase())) {
11229
+ seen.add(name);
11230
+ violations.push({
11231
+ text: trimmed.slice(0, 120),
11232
+ reason: `"${name}" \u2014 a name resolved via resolve_entities this turn \u2014 is written as fixed text. Read it from \`data\`/\`datasets\` instead (e.g. \`data[0]?.<name_column>\`), or filter/join on the resolved id (\`params.<name>Id\`) rather than the name string \u2014 a name typed as a literal is exactly as wrong on replay as a number typed as a literal: the id a future run resolves will differ, but the printed/filtered name will not.`
11233
+ });
11234
+ }
11235
+ }
11236
+ }
11237
+ return violations;
11238
+ }
11198
11239
  function formatLintError(violations) {
11199
11240
  if (!violations.length) return null;
11200
11241
  const lines = violations.map((v) => ` \u2022 "${v.text}" \u2014 ${v.reason}`).join("\n");
11201
- return `The analysis body hardcodes ${violations.length} number${violations.length > 1 ? "s" : ""} as text:
11242
+ return `The analysis body hardcodes ${violations.length} value${violations.length > 1 ? "s" : ""} as text:
11202
11243
  ${lines}
11203
11244
 
11204
- FIX: every figure must come from the data via a fmt helper \u2014 fmt.money, fmt.num, fmt.pct, fmt.delta, fmt.count, fmt.date, fmt.range. A number typed into a string is frozen at authoring time and will be wrong on replay when the data changes. Rewording will not clear this \u2014 only moving the number out of the string will. A constant the code itself uses is fine once interpolated from a variable; a figure read off the data is not.`;
11245
+ FIX: every figure must come from the data via a fmt helper \u2014 fmt.money, fmt.num, fmt.pct, fmt.delta, fmt.count, fmt.date, fmt.range \u2014 and every resolved name must come from \`data\`/\`datasets\`, not be typed in. A value typed into a string is frozen at authoring time and will be wrong on replay when the data or params change. Rewording will not clear this \u2014 only moving the value out of the string will. A constant the code itself uses is fine once interpolated from a variable; a figure or name read off the data is not.`;
11205
11246
  }
11206
11247
 
11207
11248
  // src/userResponse/scripts/component-validator.ts
@@ -11972,7 +12013,8 @@ ${catalog}`,
11972
12013
  this.renderComponentAttempts++;
11973
12014
  const hardcodedAggregates = findHardcodedAggregates(body);
11974
12015
  const hardcodedComponentText = findHardcodedComponentText(body);
11975
- const componentViolations = [...hardcodedAggregates, ...hardcodedComponentText];
12016
+ const hardcodedEntityNames = findHardcodedEntityNames(body, this.resolvedEntities.map((e) => e.displayName));
12017
+ const componentViolations = [...hardcodedAggregates, ...hardcodedComponentText, ...hardcodedEntityNames];
11976
12018
  if (componentViolations.length > 0) {
11977
12019
  const named = componentViolations.map((v) => `"${v.text}"`).join(", ");
11978
12020
  const repeatedTexts = componentViolations.map((v) => v.text).filter((t) => this.lastComponentsViolationTexts.includes(t));
@@ -12055,7 +12097,10 @@ Now write your answer for the user. Do not restate the component list.`;
12055
12097
  return `Reached the max of ${MAX_ANALYSIS_ATTEMPTS} write_analysis attempts. Write your final analysis as prose instead, using the values from the script result exactly as shown.`;
12056
12098
  }
12057
12099
  this.scriptState.analysisAttempts++;
12058
- const violations = lintAnalysisBody(body);
12100
+ const violations = [
12101
+ ...lintAnalysisBody(body),
12102
+ ...findHardcodedEntityNames(body, this.resolvedEntities.map((e) => e.displayName))
12103
+ ];
12059
12104
  const repeatedTexts = violations.map((v) => v.text).filter((t) => this.scriptState.lastAnalysisViolationTexts.includes(t));
12060
12105
  this.scriptState.lastAnalysisViolationTexts = violations.map((v) => v.text);
12061
12106
  const lintError = formatLintError(violations);
@@ -12133,6 +12178,14 @@ FIX: derive every such value from the data itself \u2014 \`data.length\`, a real
12133
12178
  ${lines}
12134
12179
 
12135
12180
  FIX: build the title/description with a template literal that reads the value from \`data\`/\`datasets\` at render time (e.g. \` \\\`Total: \\\${fmt.money(datasets.summary[0].total)}\\\` \`), never a number typed directly into the string.`;
12181
+ }
12182
+ const hardcodedEntityNames = findHardcodedEntityNames(scriptBody, this.resolvedEntities.map((e) => e.displayName));
12183
+ if (hardcodedEntityNames.length > 0) {
12184
+ const lines = hardcodedEntityNames.map((v) => ` \u2022 "${v.text}" \u2014 ${v.reason}`).join("\n");
12185
+ return `write_script rejected: the script hardcodes ${hardcodedEntityNames.length} resolved entity name(s) as fixed text:
12186
+ ${lines}
12187
+
12188
+ FIX: read the name from \`data\`/\`datasets\` at render time, or filter/join on the resolved id (\`params.<name>Id\`) instead of the name string.`;
12136
12189
  }
12137
12190
  const draft = await this.scriptStore.saveDraft({
12138
12191
  recipeId: this.scriptState.recipeId ?? void 0,
@@ -13338,6 +13391,22 @@ function stripNamedExport(body, fnName) {
13338
13391
  }
13339
13392
  return body.slice(0, start);
13340
13393
  }
13394
+ function extractNamedExport(body, fnName) {
13395
+ const start = body.search(new RegExp(`export\\s+(?:async\\s+)?function\\s+${fnName}\\b`));
13396
+ if (start === -1) return null;
13397
+ const open = body.indexOf("{", start);
13398
+ if (open === -1) return null;
13399
+ let depth = 0;
13400
+ for (let i = open; i < body.length; i++) {
13401
+ const c = body[i];
13402
+ if (c === "{") depth++;
13403
+ else if (c === "}") {
13404
+ depth--;
13405
+ if (depth === 0) return body.slice(start, i + 1);
13406
+ }
13407
+ }
13408
+ return null;
13409
+ }
13341
13410
  function normalizeScriptBody(scriptBody) {
13342
13411
  let i = 0;
13343
13412
  const n = scriptBody.length;
@@ -13437,6 +13506,37 @@ var ScriptStore = class {
13437
13506
  return null;
13438
13507
  }
13439
13508
  }
13509
+ /**
13510
+ * Fetch one recipe by id, pinned to a SPECIFIC past version rather than
13511
+ * whatever is live today. Needed to render a past turn's conversation
13512
+ * history accurately after the script has since been edited — get() alone
13513
+ * always returns the CURRENT body, which may no longer be what that turn
13514
+ * actually ran.
13515
+ *
13516
+ * Returns null (never a silently-wrong version) when the requested version
13517
+ * isn't the current one and its archived body can't be found — e.g. the
13518
+ * archive was cleaned up after the whole recipe was later deleted.
13519
+ */
13520
+ async getVersion(id, version) {
13521
+ if (!this.store) return null;
13522
+ try {
13523
+ const row = await this.store.getById(id);
13524
+ if (!row) return null;
13525
+ if (version === row.version) {
13526
+ return this.rowToRecipe(row, this.readBody(row.fileBase));
13527
+ }
13528
+ const archivePath = path6.join(this.archiveDir, `${row.fileBase}.v${version}.ts`);
13529
+ if (!fs6.existsSync(archivePath)) {
13530
+ logger.warn(`[ScriptStore] getVersion(${id}, v${version}) \u2014 archived body not found, recipe is now at v${row.version}`);
13531
+ return null;
13532
+ }
13533
+ const body = fs6.readFileSync(archivePath, "utf-8");
13534
+ return this.rowToRecipe(row, body);
13535
+ } catch (err) {
13536
+ logger.warn(`[ScriptStore] getVersion(${id}, v${version}) failed: ${err}`);
13537
+ return null;
13538
+ }
13539
+ }
13440
13540
  // ============================================
13441
13541
  // Write
13442
13542
  // ============================================
@@ -13476,7 +13576,20 @@ var ScriptStore = class {
13476
13576
  const reuse = existing && existing.status === "draft" && existing.turnId === input.turnId;
13477
13577
  const id = reuse ? existing.id : `script_${Date.now()}_${randomBytes(3).toString("hex")}`;
13478
13578
  const fileBase = reuse ? existing.fileBase : input.parentId ? `${this.toSlug(input.name)}-${id.slice(-6)}` : await this.computeFileBase(input.name, id);
13479
- const normalized = normalizeScriptBody(input.scriptBody);
13579
+ let normalized = normalizeScriptBody(input.scriptBody);
13580
+ if (reuse) {
13581
+ const existingBody = this.readBody(fileBase);
13582
+ if (existingBody) {
13583
+ for (const fnName of ["getComponents", "getAnalysis"]) {
13584
+ if (new RegExp(`export\\s+(?:async\\s+)?function\\s+${fnName}\\b`).test(normalized)) continue;
13585
+ const carried = extractNamedExport(existingBody, fnName);
13586
+ if (carried) normalized = `${normalized.trimEnd()}
13587
+
13588
+ ${carried.trim()}
13589
+ `;
13590
+ }
13591
+ }
13592
+ }
13480
13593
  const bodyHash = this.hash(normalized);
13481
13594
  this.writeBody(fileBase, normalized);
13482
13595
  const recipe = {
@@ -17726,6 +17839,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${scriptMatch
17726
17839
  // ("use mode instead") can edit THIS script.
17727
17840
  scriptBinding: {
17728
17841
  recipeId: scriptMatch.recipe.id,
17842
+ version: scriptMatch.recipe.version,
17729
17843
  params: scriptMatch.extractedParams || {},
17730
17844
  name: scriptMatch.recipe.name,
17731
17845
  columns: scriptResult.executedQueries?.[0]?.data?.[0] ? Object.keys(scriptResult.executedQueries[0].data[0]) : void 0
@@ -18153,6 +18267,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${carriedRequ
18153
18267
  if (committed) {
18154
18268
  scriptBinding = {
18155
18269
  recipeId: committed.id,
18270
+ version: committed.version,
18156
18271
  params: {},
18157
18272
  name: committed.name,
18158
18273
  columns: authored.executedQueries?.[0]?.data?.[0] ? Object.keys(authored.executedQueries[0].data[0]) : void 0
@@ -18210,6 +18325,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${carriedRequ
18210
18325
  }
18211
18326
  scriptBinding = {
18212
18327
  recipeId: authored.recipeId,
18328
+ version: promoted?.version,
18213
18329
  params: {},
18214
18330
  name: authored.name,
18215
18331
  columns: authored.executedQueries?.[0]?.data?.[0] ? Object.keys(authored.executedQueries[0].data[0]) : void 0
@@ -18528,7 +18644,7 @@ var CONTEXT_CONFIG = {
18528
18644
  * Set to 0 to disable conversation history
18529
18645
  * Higher values provide more context but may increase token usage
18530
18646
  */
18531
- MAX_CONVERSATION_CONTEXT_BLOCKS: 3
18647
+ MAX_CONVERSATION_CONTEXT_BLOCKS: 2
18532
18648
  };
18533
18649
 
18534
18650
  // src/handlers/user-prompt-request.ts
@@ -18626,7 +18742,12 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
18626
18742
  }
18627
18743
  const requestStartTime = Date.now();
18628
18744
  logger.info(`Starting user prompt request with ${components.length} components`);
18629
- const conversationHistory = thread.getConversationContext(CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS, existingUiBlockId);
18745
+ const conversationScriptStore = new ScriptStore({ collections, projectId: process.env.SUPERATOM_PROJECT_ID || "" });
18746
+ const conversationHistory = await thread.getConversationContext(
18747
+ CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS,
18748
+ existingUiBlockId,
18749
+ async (recipeId, version) => (await conversationScriptStore.getVersion(recipeId, version))?.scriptBody ?? null
18750
+ );
18630
18751
  const MAX_EDIT_CANDIDATES = 5;
18631
18752
  let activeScriptBindings = thread.getScriptBindings(MAX_EDIT_CANDIDATES, existingUiBlockId);
18632
18753
  if (activeScriptBindings.length === 0 && collections?.["conversation-history"]?.["threadScriptBindings"]) {
@@ -19274,7 +19395,7 @@ async function handleActionsRequest(data, sendMessage, anthropicApiKey, groqApiK
19274
19395
  logger.debug(`[ACTIONS_REQ ${id}] Component: ${component?.name || "unknown"} (${component?.type || "unknown"})`);
19275
19396
  logger.debug(`[ACTIONS_REQ ${id}] Component data available: ${componentData ? "yes" : "no"}`);
19276
19397
  logger.debug(`[ACTIONS_REQ ${id}] Extracting conversation history (max ${CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS} blocks)`);
19277
- const conversationHistory = thread.getConversationContext(CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS, uiBlockId);
19398
+ const conversationHistory = await thread.getConversationContext(CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS, uiBlockId);
19278
19399
  const historyLineCount = conversationHistory.split("\n").filter((l) => l.trim()).length;
19279
19400
  logger.info(`[ACTIONS_REQ ${id}] Conversation history extracted: ${historyLineCount} lines`);
19280
19401
  logger.debug(`[ACTIONS_REQ ${id}] Conversation history preview:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superatomai/sdk-node",
3
- "version": "0.0.50-dsp",
3
+ "version": "0.0.51-dsp",
4
4
  "description": "Node.js TypeScript SDK for Superatom",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",