Merge branch 'staging' into big-nate/bus-1374-remove-dashboard-cache-if-a-metric-version-is-updated

This commit is contained in:
Nate Kelley 2025-07-14 14:30:16 -06:00
commit d96bb62a2e
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
20 changed files with 173 additions and 131 deletions

View File

@ -11,7 +11,7 @@
"build": "tsup",
"dev": "bun --watch src/index.ts",
"dev:build": "tsup --watch",
"lint": "biome check",
"lint": "biome check --write",
"start": "bun dist/index.js",
"test": "vitest run",
"test:coverage": "vitest --coverage",

View File

@ -44,9 +44,22 @@ export class SlackOAuthService {
'channels:history',
'channels:read',
'chat:write',
'im:write',
'im:read',
'im:history',
'mpim:read',
'mpim:history',
'mpim:write',
'chat:write.public',
'users:read',
'users:read.email',
'app_mentions:read',
'commands',
'groups:history',
'groups:write',
'files:write',
'files:read',
'reactions:write',
],
},
tokenStorage,

View File

@ -8,7 +8,7 @@
"deploy": "npx trigger.dev@v4-beta deploy",
"prebuild": "node scripts/validate-env.js",
"build": "echo 'No build step required but we run it to make sure env is loaded' && tsc --noEmit",
"lint": "biome check",
"lint": "biome check --write",
"test": "vitest run",
"test:watch": "vitest watch",
"test:coverage": "vitest run --coverage",

View File

@ -15,7 +15,8 @@
"noNonNullAssertion": "error",
"useImportType": "warn",
"useNodejsImportProtocol": "error",
"useConsistentArrayType": "error"
"useConsistentArrayType": "error",
"noUnusedTemplateLiteral": "off"
},
"suspicious": {
"noExplicitAny": "error",

View File

@ -18,7 +18,7 @@
"build:commonjs": "tsc --module commonjs --moduleResolution node",
"build:commonjs:watch": "npm run build:commonjs && tsc --module commonjs --moduleResolution node --watch",
"dev": "tsc --watch",
"lint": "biome check",
"lint": "biome check --write",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest",

View File

@ -19,7 +19,7 @@
"typecheck": "tsc --noEmit",
"dev": "tsc --watch",
"dev:mastra": "mastra dev --dir src",
"lint": "biome check",
"lint": "biome check --write",
"test": "vitest run",
"test:watch": "vitest watch",
"test:coverage": "vitest run --coverage",

View File

@ -258,6 +258,7 @@ ${params.sqlDialectGuidance}
- Only the following chart types are supported: table, line, bar, combo, pie/donut, number cards, and scatter plot. Other chart types are not supported.
- You cannot write Python code or perform advanced analyses such as forecasting or modeling.
- You cannot highlight or flag specific elements (e.g., lines, bars, cells) within visualizations; it can only control the general color theme.
- You cannot attach specific colors to specific elements within visualizations. Only general color themes are supported.
- Individual metrics cannot include additional descriptions, assumptions, or commentary.
- Dashboard layout constraints:
- Dashboards display collections of existing metrics referenced by their IDs.

View File

@ -509,6 +509,8 @@ ${params.sqlDialectGuidance}
- The system is read-only and cannot write to databases.
- Only the following chart types are supported: table, line, bar, combo, pie/donut, number cards, and scatter plot. Other chart types are not supported.
- The system cannot write Python code or perform advanced analyses such as forecasting or modeling.
- You cannot highlight or flag specific elements (e.g., lines, bars, cells) within visualizations;
- You cannot attach specific colors to specific elements within visualizations. Only general color themes are supported.
- Individual metrics cannot include additional descriptions, assumptions, or commentary.
- Dashboard layout constraints:
- Dashboards display collections of existing metrics referenced by their IDs.

View File

@ -95,8 +95,15 @@ function createDataMetadata(results: Record<string, unknown>[]): DataMetadata {
columnType = ColumnType.Timestamp;
simpleType = SimpleType.Date;
} else if (typeof firstValue === 'string') {
// Check if it looks like a date
if (!Number.isNaN(Date.parse(firstValue))) {
// Check if it's a numeric string first
if (!Number.isNaN(Number(firstValue))) {
columnType = Number.isInteger(Number(firstValue)) ? ColumnType.Int4 : ColumnType.Float8;
simpleType = SimpleType.Number;
} else if (
!Number.isNaN(Date.parse(firstValue)) &&
// Additional check to avoid parsing simple numbers as dates
(firstValue.includes('-') || firstValue.includes('/') || firstValue.includes(':'))
) {
columnType = ColumnType.Timestamp;
simpleType = SimpleType.Date;
} else {
@ -112,7 +119,13 @@ function createDataMetadata(results: Record<string, unknown>[]): DataMetadata {
if (values.length > 0) {
if (simpleType === SimpleType.Number) {
const numValues = values.filter((v) => typeof v === 'number') as number[];
const numValues = values
.map((v) => {
if (typeof v === 'number') return v;
if (typeof v === 'string' && !Number.isNaN(Number(v))) return Number(v);
return null;
})
.filter((v) => v !== null) as number[];
if (numValues.length > 0) {
minValue = Math.min(...numValues);
maxValue = Math.max(...numValues);

View File

@ -326,24 +326,25 @@ export function cleanupIncompleteToolCalls(messages: CoreMessage[]): CoreMessage
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
const toolCalls = msg.content.filter(
c => typeof c === 'object' && 'type' in c && c.type === 'tool-call'
(c) => typeof c === 'object' && 'type' in c && c.type === 'tool-call'
);
// Check if any tool calls lack results
const orphanedToolCalls = toolCalls.filter(toolCall => {
const orphanedToolCalls = toolCalls.filter((toolCall) => {
// Look ahead for matching tool result
for (let j = i + 1; j < cleaned.length; j++) {
const nextMsg = cleaned[j];
if (!nextMsg) continue; // Add guard for undefined
if (nextMsg.role === 'tool' && Array.isArray(nextMsg.content)) {
const hasResult = nextMsg.content.some(
c => typeof c === 'object' &&
'type' in c &&
c.type === 'tool-result' &&
'toolCallId' in c &&
'toolCallId' in toolCall &&
c.toolCallId === toolCall.toolCallId
(c) =>
typeof c === 'object' &&
'type' in c &&
c.type === 'tool-result' &&
'toolCallId' in c &&
'toolCallId' in toolCall &&
c.toolCallId === toolCall.toolCallId
);
if (hasResult) return false;
}
@ -353,11 +354,18 @@ export function cleanupIncompleteToolCalls(messages: CoreMessage[]): CoreMessage
if (orphanedToolCalls.length > 0) {
// Remove the entire assistant message with orphaned tool calls
console.info(`cleanupIncompleteToolCalls: Removing assistant message with ${orphanedToolCalls.length} orphaned tool calls`, {
messageIndex: i,
orphanedToolCallIds: orphanedToolCalls.map(tc => 'toolCallId' in tc ? tc.toolCallId : 'unknown'),
textContent: msg.content.filter(c => typeof c === 'object' && 'type' in c && c.type === 'text').map(c => 'text' in c ? c.text : ''),
});
console.info(
`cleanupIncompleteToolCalls: Removing assistant message with ${orphanedToolCalls.length} orphaned tool calls`,
{
messageIndex: i,
orphanedToolCallIds: orphanedToolCalls.map((tc) =>
'toolCallId' in tc ? tc.toolCallId : 'unknown'
),
textContent: msg.content
.filter((c) => typeof c === 'object' && 'type' in c && c.type === 'text')
.map((c) => ('text' in c ? c.text : '')),
}
);
cleaned.splice(i, 1);
break; // Only clean up the last problematic message
}

View File

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import type { CoreMessage } from 'ai';
import { describe, expect, it } from 'vitest';
import { cleanupIncompleteToolCalls } from '../../../src/utils/retry';
describe('cleanupIncompleteToolCalls', () => {
@ -7,15 +7,15 @@ describe('cleanupIncompleteToolCalls', () => {
const messages: CoreMessage[] = [
{
role: 'user',
content: 'Hello'
content: 'Hello',
},
{
role: 'assistant',
content: [
{ type: 'text', text: 'Let me help' },
{ type: 'tool-call', toolCallId: '123', toolName: 'getTodo', args: {} }
]
}
{ type: 'tool-call', toolCallId: '123', toolName: 'getTodo', args: {} },
],
},
// No tool result - orphaned
];
@ -28,16 +28,14 @@ describe('cleanupIncompleteToolCalls', () => {
const messages: CoreMessage[] = [
{
role: 'assistant',
content: [
{ type: 'tool-call', toolCallId: '123', toolName: 'getTodo', args: {} }
]
content: [{ type: 'tool-call', toolCallId: '123', toolName: 'getTodo', args: {} }],
},
{
role: 'tool',
content: [
{ type: 'tool-result', toolCallId: '123', toolName: 'getTodo', result: { todo: 'test' } }
]
}
{ type: 'tool-result', toolCallId: '123', toolName: 'getTodo', result: { todo: 'test' } },
],
},
];
const cleaned = cleanupIncompleteToolCalls(messages);
@ -50,10 +48,8 @@ describe('cleanupIncompleteToolCalls', () => {
const messages: CoreMessage[] = [
{
role: 'assistant',
content: [
{ type: 'tool-call', toolCallId: '123', toolName: 'getTodo', args: {} },
]
}
content: [{ type: 'tool-call', toolCallId: '123', toolName: 'getTodo', args: {} }],
},
// Missing result for toolCallId '123' - partially orphaned
];
@ -65,27 +61,30 @@ describe('cleanupIncompleteToolCalls', () => {
const messages: CoreMessage[] = [
{
role: 'assistant',
content: [
{ type: 'tool-call', toolCallId: '111', toolName: 'getTodo', args: {} }
]
content: [{ type: 'tool-call', toolCallId: '111', toolName: 'getTodo', args: {} }],
},
{
role: 'tool',
content: [
{ type: 'tool-result', toolCallId: '111', toolName: 'getTodo', result: { todo: 'first' } }
]
{
type: 'tool-result',
toolCallId: '111',
toolName: 'getTodo',
result: { todo: 'first' },
},
],
},
{
role: 'user',
content: 'Another request'
content: 'Another request',
},
{
role: 'assistant',
content: [
{ type: 'text', text: 'Processing...' },
{ type: 'tool-call', toolCallId: '222', toolName: 'createTodo', args: { title: 'new' } }
]
}
{ type: 'tool-call', toolCallId: '222', toolName: 'createTodo', args: { title: 'new' } },
],
},
// No result for '222' - orphaned
];
@ -100,12 +99,12 @@ describe('cleanupIncompleteToolCalls', () => {
const messages: CoreMessage[] = [
{
role: 'user',
content: 'Hello'
content: 'Hello',
},
{
role: 'assistant',
content: 'Hi there, how can I help?'
}
content: 'Hi there, how can I help?',
},
];
const cleaned = cleanupIncompleteToolCalls(messages);
@ -126,9 +125,9 @@ describe('cleanupIncompleteToolCalls', () => {
content: [
{ type: 'text', text: 'Let me search for that' },
{ type: 'tool-call', toolCallId: '789', toolName: 'search', args: { query: 'test' } },
{ type: 'text', text: 'Searching now...' }
]
}
{ type: 'text', text: 'Searching now...' },
],
},
// No tool result - orphaned
];
@ -141,15 +140,18 @@ describe('cleanupIncompleteToolCalls', () => {
{
role: 'tool',
content: [
{ type: 'tool-result', toolCallId: '999', toolName: 'getTodo', result: { error: 'Not found' } }
]
{
type: 'tool-result',
toolCallId: '999',
toolName: 'getTodo',
result: { error: 'Not found' },
},
],
},
{
role: 'assistant',
content: [
{ type: 'tool-call', toolCallId: '999', toolName: 'getTodo', args: {} }
]
}
content: [{ type: 'tool-call', toolCallId: '999', toolName: 'getTodo', args: {} }],
},
];
const cleaned = cleanupIncompleteToolCalls(messages);
@ -163,16 +165,26 @@ describe('cleanupIncompleteToolCalls', () => {
role: 'assistant',
content: [
{ type: 'tool-call', toolCallId: 'abc', toolName: 'getTodo', args: {} },
{ type: 'tool-call', toolCallId: 'def', toolName: 'createTodo', args: { title: 'new' } }
]
{ type: 'tool-call', toolCallId: 'def', toolName: 'createTodo', args: { title: 'new' } },
],
},
{
role: 'tool',
content: [
{ type: 'tool-result', toolCallId: 'abc', toolName: 'getTodo', result: { todo: 'existing' } },
{ type: 'tool-result', toolCallId: 'def', toolName: 'createTodo', result: { id: 1, title: 'new' } }
]
}
{
type: 'tool-result',
toolCallId: 'abc',
toolName: 'getTodo',
result: { todo: 'existing' },
},
{
type: 'tool-result',
toolCallId: 'def',
toolName: 'createTodo',
result: { id: 1, title: 'new' },
},
],
},
];
const cleaned = cleanupIncompleteToolCalls(messages);
@ -184,14 +196,12 @@ describe('cleanupIncompleteToolCalls', () => {
const messages: CoreMessage[] = [
{
role: 'assistant',
content: [
{ type: 'text', text: 'Here is your answer' }
]
}
content: [{ type: 'text', text: 'Here is your answer' }],
},
];
const cleaned = cleanupIncompleteToolCalls(messages);
expect(cleaned).toHaveLength(1); // No changes
expect(cleaned).toEqual(messages);
});
});
});

View File

@ -1,6 +1,6 @@
import { describe, it, expect, vi } from 'vitest';
import type { CoreMessage } from 'ai';
import { APICallError } from 'ai';
import { describe, expect, it, vi } from 'vitest';
import { detectRetryableError, handleRetryWithHealing } from '../../../src/utils/retry';
import type { RetryableError, WorkflowContext } from '../../../src/utils/retry';
@ -12,7 +12,7 @@ describe('529 error handling', () => {
responseHeaders: {},
responseBody: 'Server is overloaded, please try again',
url: 'https://api.example.com',
requestBodyValues: {}
requestBodyValues: {},
});
const result = detectRetryableError(error);
@ -27,36 +27,39 @@ describe('529 error handling', () => {
const messagesWithOrphan: CoreMessage[] = [
{
role: 'user',
content: 'Please analyze this data'
content: 'Please analyze this data',
},
{
role: 'assistant',
content: [
{ type: 'text', text: 'Let me analyze that for you' },
{ type: 'tool-call', toolCallId: 'tc-123', toolName: 'analyzeData', args: { data: 'test' } }
]
}
{
type: 'tool-call',
toolCallId: 'tc-123',
toolName: 'analyzeData',
args: { data: 'test' },
},
],
},
// No tool result - connection interrupted
];
const retryableError: RetryableError = {
type: 'overloaded-error',
originalError: new Error('529'),
healingMessage: { role: 'user', content: 'Server overloaded (529). Retrying after cleanup...' },
requiresMessageCleanup: true
healingMessage: {
role: 'user',
content: 'Server overloaded (529). Retrying after cleanup...',
},
requiresMessageCleanup: true,
};
const context: WorkflowContext = {
const context: WorkflowContext = {
currentStep: 'analyst',
availableTools: new Set(['analyzeData', 'createMetrics'])
availableTools: new Set(['analyzeData', 'createMetrics']),
};
const result = await handleRetryWithHealing(
retryableError,
messagesWithOrphan,
0,
context
);
const result = await handleRetryWithHealing(retryableError, messagesWithOrphan, 0, context);
expect(result.healedMessages).toHaveLength(1); // Only user message remains
expect(result.healedMessages[0]?.role).toBe('user');
@ -71,7 +74,7 @@ describe('529 error handling', () => {
responseHeaders: {},
responseBody: 'Overloaded',
url: 'https://api.example.com',
requestBodyValues: {}
requestBodyValues: {},
});
const error500 = new APICallError({
@ -80,7 +83,7 @@ describe('529 error handling', () => {
responseHeaders: {},
responseBody: 'Internal error',
url: 'https://api.example.com',
requestBodyValues: {}
requestBodyValues: {},
});
const result529 = detectRetryableError(error529);
@ -94,19 +97,17 @@ describe('529 error handling', () => {
});
it('should use longer backoff for 529 errors', async () => {
const messages: CoreMessage[] = [
{ role: 'user', content: 'Test' }
];
const messages: CoreMessage[] = [{ role: 'user', content: 'Test' }];
const retryableError: RetryableError = {
type: 'overloaded-error',
originalError: new Error('529'),
healingMessage: { role: 'user', content: 'Retrying...' }
healingMessage: { role: 'user', content: 'Retrying...' },
};
const context: WorkflowContext = {
const context: WorkflowContext = {
currentStep: 'analyst', // Changed from 'test' to valid value
availableTools: new Set()
availableTools: new Set(),
};
const result = await handleRetryWithHealing(
@ -122,41 +123,36 @@ describe('529 error handling', () => {
it('should log cleanup details', async () => {
const consoleSpy = vi.spyOn(console, 'info');
const messagesWithMultipleOrphans: CoreMessage[] = [
{
role: 'assistant',
content: [
{ type: 'tool-call', toolCallId: 'tc-1', toolName: 'tool1', args: {} },
{ type: 'tool-call', toolCallId: 'tc-2', toolName: 'tool2', args: {} }
]
}
{ type: 'tool-call', toolCallId: 'tc-2', toolName: 'tool2', args: {} },
],
},
];
const retryableError: RetryableError = {
type: 'overloaded-error',
originalError: new Error('529'),
healingMessage: { role: 'user', content: 'Retrying...' }
healingMessage: { role: 'user', content: 'Retrying...' },
};
const context: WorkflowContext = {
const context: WorkflowContext = {
currentStep: 'analyst',
availableTools: new Set()
availableTools: new Set(),
};
await handleRetryWithHealing(
retryableError,
messagesWithMultipleOrphans,
0,
context
);
await handleRetryWithHealing(retryableError, messagesWithMultipleOrphans, 0, context);
expect(consoleSpy).toHaveBeenCalledWith(
'analyst: Cleaned incomplete tool calls after 529 error',
expect.objectContaining({
originalCount: 1,
cleanedCount: 0,
removed: 1
removed: 1,
})
);
@ -167,37 +163,35 @@ describe('529 error handling', () => {
const completeMessages: CoreMessage[] = [
{
role: 'assistant',
content: [
{ type: 'tool-call', toolCallId: 'tc-123', toolName: 'getTodo', args: {} }
]
content: [{ type: 'tool-call', toolCallId: 'tc-123', toolName: 'getTodo', args: {} }],
},
{
role: 'tool',
content: [
{ type: 'tool-result', toolCallId: 'tc-123', toolName: 'getTodo', result: { todo: 'test' } }
]
}
{
type: 'tool-result',
toolCallId: 'tc-123',
toolName: 'getTodo',
result: { todo: 'test' },
},
],
},
];
const retryableError: RetryableError = {
type: 'overloaded-error',
originalError: new Error('529'),
healingMessage: { role: 'user', content: 'Retrying...' }
healingMessage: { role: 'user', content: 'Retrying...' },
};
const context: WorkflowContext = {
const context: WorkflowContext = {
currentStep: 'analyst',
availableTools: new Set()
availableTools: new Set(),
};
const result = await handleRetryWithHealing(
retryableError,
completeMessages,
0,
context
);
const result = await handleRetryWithHealing(retryableError, completeMessages, 0, context);
expect(result.healedMessages).toHaveLength(2); // No cleanup needed
expect(result.healedMessages).toEqual(completeMessages);
});
});
});

View File

@ -17,7 +17,7 @@
"build": "tsc",
"build:commonjs": "tsc --module commonjs --moduleResolution node",
"build:commonjs:watch": "npm run build:commonjs && tsc --module commonjs --moduleResolution node --watch",
"lint": "biome check",
"lint": "biome check --write",
"lint:fix": "biome check --write",
"test": "vitest run",
"test:coverage": "vitest run --coverage",

View File

@ -37,7 +37,7 @@
"db:studio": "drizzle-kit studio",
"db:init": "echo 'dev:init should be run from turbo.json' && true",
"dev": "echo 'Running db:init from turbo.json' && npm run db:init",
"lint": "biome check",
"lint": "biome check --write",
"stop": "pnpm run db:stop",
"test": "vitest run",
"test:coverage": "vitest run --coverage",

View File

@ -17,7 +17,7 @@
"test": "vitest run",
"test:watch": "vitest watch",
"test:coverage": "vitest run --coverage",
"lint": "biome check",
"lint": "biome check --write",
"lint:fix": "biome check --apply .",
"format": "biome format .",
"format:fix": "biome format --write ."

View File

@ -8,7 +8,7 @@
"prebuild": "tsx scripts/type-import-check.ts",
"build": "tsc --build",
"dev": "tsc --watch",
"lint": "biome check",
"lint": "biome check --write",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"

View File

@ -19,7 +19,7 @@
"build": "tsc",
"typecheck": "tsc --noEmit",
"dev": "tsc --watch",
"lint": "biome check",
"lint": "biome check --write",
"test": "vitest run",
"test:watch": "vitest watch",
"test:coverage": "vitest run --coverage",

View File

@ -23,7 +23,7 @@
"build": "tsc",
"build:commonjs": "tsc --module commonjs --moduleResolution node",
"build:commonjs:watch": "npm run build:commonjs && tsc --module commonjs --moduleResolution node --watch",
"lint": "biome check",
"lint": "biome check --write",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest watch",

View File

@ -17,7 +17,7 @@
"prebuild": "node scripts/validate-env.js",
"build": "tsc",
"typecheck": "tsc --noEmit",
"lint": "biome check",
"lint": "biome check --write",
"test": "vitest run",
"test:watch": "vitest watch",
"test:coverage": "vitest run --coverage"

View File

@ -6,7 +6,7 @@
"module": "src/index.ts",
"scripts": {
"build": "tsc --build",
"lint": "biome check",
"lint": "biome check --write",
"test": "vitest run",
"test:coverage": "vitest --coverage",
"test:ui": "vitest --ui",