Skip to content

Commit e47d22b

Browse files
committed
improvement(search): name Slack authors, read only when needed, and list by date without a bound
1 parent 4fd0b06 commit e47d22b

13 files changed

Lines changed: 265 additions & 60 deletions

File tree

‎apps/sim/lib/api/contracts/mothership-assistant-tools.ts‎

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export const workspaceSearchFiltersSchema = z.object({
127127
.enum(['relevance', 'newest', 'oldest'])
128128
.optional()
129129
.describe(
130-
'Live search ordering by relevance or the provider date used by startDate/endDate. Date sorting covers retrieved results; inspect partial coverage before claiming latest or earliest overall.'
130+
'Live search ordering by relevance or the provider date used by startDate/endDate. Date sorting covers retrieved results; inspect partial coverage before claiming latest or earliest overall. Without search terms or dates, newest or oldest lists items up to now.'
131131
),
132132
source: z
133133
.string()
@@ -169,7 +169,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
169169
.max(2000)
170170
.default('')
171171
.describe(
172-
'Search terms, without dates already supplied as filters. May be empty for a live date-bounded listing.'
172+
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.'
173173
),
174174
topK: z
175175
.number()
@@ -182,18 +182,19 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
182182
),
183183
})
184184
.superRefine((input, context) => {
185-
if (
186-
!input.query &&
187-
!input.nativeQueries?.some((query) => query.query) &&
188-
!input.startDate &&
189-
!input.endDate &&
190-
!input.modifiedAfter &&
191-
!input.modifiedBefore
185+
const bounded = Boolean(
186+
input.startDate ||
187+
input.endDate ||
188+
input.modifiedAfter ||
189+
input.modifiedBefore ||
190+
input.sortBy === 'newest' ||
191+
input.sortBy === 'oldest'
192192
)
193+
if (!input.query && !input.nativeQueries?.some((query) => query.query) && !bounded)
193194
context.addIssue({
194195
code: 'custom',
195196
path: ['query'],
196-
message: 'Supply search terms, a native query, or a date bound.',
197+
message: 'Supply search terms, a native query, a date bound, or sortBy newest or oldest.',
197198
})
198199
if (
199200
input.startDate &&
@@ -205,17 +206,11 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
205206
path: ['endDate'],
206207
message: 'endDate must be after startDate.',
207208
})
208-
if (
209-
input.nativeQueries?.some((query) => !query.query) &&
210-
!input.startDate &&
211-
!input.endDate &&
212-
!input.modifiedAfter &&
213-
!input.modifiedBefore
214-
)
209+
if (input.nativeQueries?.some((query) => !query.query) && !bounded)
215210
context.addIssue({
216211
code: 'custom',
217212
path: ['nativeQueries'],
218-
message: 'Empty native queries require a date bound.',
213+
message: 'Empty native queries require a date bound or sortBy newest or oldest.',
219214
})
220215
})
221216

‎apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts‎

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export const workspaceSearchFiltersSchema = z.object({
139139
.enum(['relevance', 'newest', 'oldest'])
140140
.optional()
141141
.describe(
142-
'Live search ordering by relevance or the provider date used by startDate/endDate. Date sorting covers retrieved results; inspect partial coverage before claiming latest or earliest overall.'
142+
'Live search ordering by relevance or the provider date used by startDate/endDate. Date sorting covers retrieved results; inspect partial coverage before claiming latest or earliest overall. Without search terms or dates, newest or oldest lists items up to now.'
143143
),
144144
source: z
145145
.string()
@@ -181,7 +181,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
181181
.max(2000)
182182
.default('')
183183
.describe(
184-
'Search terms, without dates already supplied as filters. May be empty for a live date-bounded listing.'
184+
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.'
185185
),
186186
topK: z
187187
.number()
@@ -194,18 +194,19 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
194194
),
195195
})
196196
.superRefine((input, context) => {
197-
if (
198-
!input.query &&
199-
!input.nativeQueries?.some((query) => query.query) &&
200-
!input.startDate &&
201-
!input.endDate &&
202-
!input.modifiedAfter &&
203-
!input.modifiedBefore
197+
const bounded = Boolean(
198+
input.startDate ||
199+
input.endDate ||
200+
input.modifiedAfter ||
201+
input.modifiedBefore ||
202+
input.sortBy === 'newest' ||
203+
input.sortBy === 'oldest'
204204
)
205+
if (!input.query && !input.nativeQueries?.some((query) => query.query) && !bounded)
205206
context.addIssue({
206207
code: 'custom',
207208
path: ['query'],
208-
message: 'Supply search terms, a native query, or a date bound.',
209+
message: 'Supply search terms, a native query, a date bound, or sortBy newest or oldest.',
209210
})
210211
if (
211212
input.startDate &&
@@ -217,17 +218,11 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
217218
path: ['endDate'],
218219
message: 'endDate must be after startDate.',
219220
})
220-
if (
221-
input.nativeQueries?.some((query) => !query.query) &&
222-
!input.startDate &&
223-
!input.endDate &&
224-
!input.modifiedAfter &&
225-
!input.modifiedBefore
226-
)
221+
if (input.nativeQueries?.some((query) => !query.query) && !bounded)
227222
context.addIssue({
228223
code: 'custom',
229224
path: ['nativeQueries'],
230-
message: 'Empty native queries require a date bound.',
225+
message: 'Empty native queries require a date bound or sortBy newest or oldest.',
231226
})
232227
})
233228

‎apps/sim/lib/mothership/generated/tool-catalog-v1.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6021,7 +6021,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
60216021
},
60226022
sortBy: {
60236023
description:
6024-
'Live search ordering by relevance or the provider date used by startDate/endDate. Date sorting covers retrieved results; inspect partial coverage before claiming latest or earliest overall.',
6024+
'Live search ordering by relevance or the provider date used by startDate/endDate. Date sorting covers retrieved results; inspect partial coverage before claiming latest or earliest overall. Without search terms or dates, newest or oldest lists items up to now.',
60256025
type: 'string',
60266026
enum: ['relevance', 'newest', 'oldest'],
60276027
},
@@ -6095,7 +6095,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
60956095
query: {
60966096
default: '',
60976097
description:
6098-
'Search terms, without dates already supplied as filters. May be empty for a live date-bounded listing.',
6098+
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.',
60996099
type: 'string',
61006100
maxLength: 2000,
61016101
},

‎apps/sim/lib/mothership/generated/tool-schemas-v1.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5948,7 +5948,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
59485948
},
59495949
sortBy: {
59505950
description:
5951-
'Live search ordering by relevance or the provider date used by startDate/endDate. Date sorting covers retrieved results; inspect partial coverage before claiming latest or earliest overall.',
5951+
'Live search ordering by relevance or the provider date used by startDate/endDate. Date sorting covers retrieved results; inspect partial coverage before claiming latest or earliest overall. Without search terms or dates, newest or oldest lists items up to now.',
59525952
type: 'string',
59535953
enum: ['relevance', 'newest', 'oldest'],
59545954
},
@@ -6052,7 +6052,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
60526052
query: {
60536053
default: '',
60546054
description:
6055-
'Search terms, without dates already supplied as filters. May be empty for a live date-bounded listing.',
6055+
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.',
60566056
type: 'string',
60576057
maxLength: 2000,
60586058
},

‎apps/sim/lib/mothership/tools/server/knowledge/workspace-search.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export const searchWorkspaceServerTool: BaseServerTool = {
106106
})
107107
return {
108108
success: true,
109-
message: `Found ${data.results.length} live results. Read documentIds for more content. ${CITATION_INSTRUCTION}`,
109+
message: `Found ${data.results.length} live results. Read a documentId when its result text is cut off or more context is needed. ${CITATION_INSTRUCTION}`,
110110
data: {
111111
...data,
112112
results: data.results.map((item) => ({

‎apps/sim/lib/sim-search/live/application.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,36 @@ describe('authorized live retrieval', () => {
255255
})
256256
expect(result.retrieval.status).toBe('partial')
257257
})
258+
it('lists newest first up to now when no terms or dates are given, and only then', async () => {
259+
vi.useFakeTimers({ now: new Date('2026-09-25T02:00:00Z'), toFake: ['Date'] })
260+
try {
261+
await searchLiveKnowledge.execute({
262+
principal,
263+
input: { ...input, query: '', filters: { sortBy: 'newest' } },
264+
})
265+
expect(mocks.search).toHaveBeenLastCalledWith(
266+
'google_drive',
267+
expect.anything(),
268+
expect.objectContaining({
269+
filters: { sortBy: 'newest', endDate: '2026-09-25T02:00:00.000Z' },
270+
})
271+
)
272+
await searchLiveKnowledge.execute({
273+
principal,
274+
input: { ...input, filters: { sortBy: 'newest' } },
275+
})
276+
expect(mocks.search).toHaveBeenLastCalledWith(
277+
'google_drive',
278+
expect.anything(),
279+
expect.objectContaining({ filters: { sortBy: 'newest' } })
280+
)
281+
await expect(
282+
searchLiveKnowledge.execute({ principal, input: { ...input, query: '' } })
283+
).rejects.toThrow('Invalid live search query')
284+
} finally {
285+
vi.useRealTimers()
286+
}
287+
})
258288
it('reports candidates that could not be verified and keeps the verified ones', async () => {
259289
mocks.search.mockResolvedValue({
260290
documents: [document, { ...document, id: 'other', url: 'https://docs.google.com/other' }],

‎apps/sim/lib/sim-search/live/application.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
matchesSourceDates,
4646
sourceDate,
4747
sourceDateType,
48+
withImpliedListingBound,
4849
} from '@/lib/sim-search/live/dates'
4950
import { NativeSearchError } from '@/lib/sim-search/live/http'
5051
import { joinMessages } from '@/lib/sim-search/live/pages'
@@ -293,6 +294,11 @@ export const searchLiveKnowledge = defineAuthorizedKnowledgeUseCase({
293294
const queries = input.nativeQueries
294295
? nativeSearchQueriesSchema.parse(input.nativeQueries)
295296
: undefined
297+
if (
298+
(!input.query.trim() && !queries?.some((query) => query.query)) ||
299+
queries?.some((query) => !query.query)
300+
)
301+
input = { ...input, filters: withImpliedListingBound(input.filters, new Date()) }
296302
if (
297303
(!input.query.trim() &&
298304
!hasDateBounds(input.filters) &&

‎apps/sim/lib/sim-search/live/dates.test.ts‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { searchWorkspaceInputSchema } from '@/lib/api/contracts/mothership-assis
44
import { intersectWorkspaceSearchFilters } from '@/lib/knowledge/search/filters'
55
import { searchAtlassian } from '@/lib/sim-search/live/atlassian'
66
import { searchCoda } from '@/lib/sim-search/live/coda'
7-
import { matchesSourceDates, sourceDate, sourceDateType } from '@/lib/sim-search/live/dates'
7+
import {
8+
matchesSourceDates,
9+
sourceDate,
10+
sourceDateType,
11+
withImpliedListingBound,
12+
} from '@/lib/sim-search/live/dates'
813
import { searchGitHub } from '@/lib/sim-search/live/github'
914
import { searchGitLab } from '@/lib/sim-search/live/gitlab'
1015
import { searchCalendar, searchDrive, searchGmail } from '@/lib/sim-search/live/google'
@@ -29,6 +34,28 @@ const doc: NativeDocument = {
2934
}
3035

3136
describe('generic live search dates', () => {
37+
it('lists newest or oldest first up to now when no terms or dates are given', () => {
38+
const now = new Date('2026-09-25T02:00:00Z')
39+
for (const sortBy of ['newest', 'oldest'] as const) {
40+
expect(searchWorkspaceInputSchema.safeParse({ sortBy }).success).toBe(true)
41+
expect(
42+
searchWorkspaceInputSchema.safeParse({
43+
sortBy,
44+
nativeQueries: [{ provider: 'slack', query: '', modifiers: 'in:<#D1>' }],
45+
}).success
46+
).toBe(true)
47+
expect(withImpliedListingBound({ sortBy }, now)).toEqual({
48+
sortBy,
49+
endDate: '2026-09-25T02:00:00.000Z',
50+
})
51+
}
52+
expect(searchWorkspaceInputSchema.safeParse({ sortBy: 'relevance' }).success).toBe(false)
53+
expect(withImpliedListingBound({ sortBy: 'relevance' }, now)).toEqual({ sortBy: 'relevance' })
54+
expect(withImpliedListingBound(undefined, now)).toBeUndefined()
55+
expect(
56+
withImpliedListingBound({ sortBy: 'newest', startDate: filters.startDate }, now)
57+
).toEqual({ sortBy: 'newest', startDate: filters.startDate })
58+
})
3259
it('accepts a date-only request and rejects invalid or unbounded listings', () => {
3360
expect(searchWorkspaceInputSchema.parse(filters)).toMatchObject({
3461
...filters,
@@ -38,7 +65,7 @@ describe('generic live search dates', () => {
3865
for (const value of [
3966
{},
4067
{ query: '' },
41-
{ sortBy: 'oldest' },
68+
{ sortBy: 'relevance' },
4269
{ startDate: 'today' },
4370
{ ...filters, endDate: filters.startDate },
4471
{ ...filters, endDate: '2026-09-21T00:00:00Z' },

‎apps/sim/lib/sim-search/live/dates.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ export function dateSortDirection(filters?: WorkspaceSearchFilters): 'asc' | 'de
3030
return filters.sortBy === 'oldest' ? 'asc' : 'desc'
3131
}
3232

33+
/**
34+
* A newest- or oldest-first listing without terms or dates lists items up to now, the default
35+
* Slack's conversations.history applies to an omitted `latest`. Providers need a bound to list
36+
* without search terms, so the implied one is made explicit and checked like any other.
37+
*/
38+
export function withImpliedListingBound(
39+
filters: WorkspaceSearchFilters | undefined,
40+
now: Date
41+
): WorkspaceSearchFilters | undefined {
42+
if (!dateSortDirection(filters) || hasDateBounds(filters)) return filters
43+
return { ...filters, endDate: now.toISOString() }
44+
}
45+
3346
/** Native bounds may be widened for provider precision; returned metadata is checked exactly. */
3447
export function nativeDateBounds(input: NativeSearchInput): { start?: string; end?: string } {
3548
const filters = input.filters

‎apps/sim/lib/sim-search/live/providers.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -567,14 +567,16 @@ describe('native search endpoints', () => {
567567
has_more: true,
568568
})
569569
.mockResolvedValueOnce({ ok: true, permalink: 'https://team.slack.com/archives/C1/p123456' })
570+
.mockResolvedValueOnce({ ok: true, user: { profile: { display_name: 'Sid' } } })
570571
const result = await readSlack(api, '123.456', 'C1')
571572
expect(api.json.mock.calls[0]).toEqual([
572573
'/api/conversations.replies',
573574
{ query: { channel: 'C1', ts: '123.456', limit: '100' } },
574575
])
575-
expect(result.content).toContain('Reply evidence')
576+
expect(api.json.mock.calls[2]).toEqual(['/api/users.info', { query: { user: 'U1' } }])
577+
expect(result.content).toContain('Sid: Reply evidence')
576578
expect(result.content).toContain('Thread continues')
577-
expect(api.json).toHaveBeenCalledTimes(2)
579+
expect(api.json).toHaveBeenCalledTimes(3)
578580
})
579581
it('applies Slack modifiers and date bounds without requiring term clauses', async () => {
580582
const api = client()

0 commit comments

Comments
 (0)