fix: serialize paginated session searches
This commit is contained in:
@@ -9,7 +9,7 @@ Workspace-authorized model tools over `ctx.sessionQuery`. The package depends on
|
||||
| `maxSearchResults` | `100` | Maximum authorized non-self hits collected across internal provider pages |
|
||||
| `searchTimeoutMs` | `30000` | Cooperative deadline attached to both full-text search tools |
|
||||
|
||||
The caller comes exclusively from `ToolExecution.exec.agent`. Cross-session access requires exact equality between the target and caller session `cwd` values; a caller without `cwd` can inspect only itself. Search never exposes provider cursors, offsets, page sizes, or a model-controlled limit. Timestamps at the tool boundary require an explicit `Z` or numeric offset and become inclusive epoch-millisecond filters.
|
||||
The caller comes exclusively from `ToolExecution.exec.agent`. Cross-session access requires exact equality between the target and caller session `cwd` values; a caller without `cwd` can inspect only itself. Search never exposes provider cursors, offsets, page sizes, or a model-controlled limit. Because one search consumes generation-bound provider cursors internally, both search tools execute exclusively with sibling tool calls; the three exact trace/read tools opt into parallel execution. Timestamps at the tool boundary require an explicit `Z` or numeric offset and become inclusive epoch-millisecond filters.
|
||||
|
||||
`session_search` always omits the caller session. A current-session `session_event_search` stops immediately before the step that invoked it, so the active assistant output and logged tool call cannot match themselves. Direct targets are authorized before trace, event, or title reads. Lineage output replaces unauthorized ancestor and descendant boundaries with markers that contain no hidden session id.
|
||||
|
||||
|
||||
@@ -211,7 +211,6 @@ export function apply(ctx: Context, config: Config): void {
|
||||
parameters: SESSION_SEARCH_PARAMETERS,
|
||||
output: TEXT_OUTPUT,
|
||||
timeoutMs: resolved.searchTimeoutMs,
|
||||
isConcurrencySafe: () => true,
|
||||
execute: (args, exec) => executeSessionSearch(ctx, args, exec, resolved.maxSearchResults),
|
||||
presentCall: presentSessionSearchCall,
|
||||
}))
|
||||
@@ -222,7 +221,6 @@ export function apply(ctx: Context, config: Config): void {
|
||||
parameters: EVENT_SEARCH_PARAMETERS,
|
||||
output: TEXT_OUTPUT,
|
||||
timeoutMs: resolved.searchTimeoutMs,
|
||||
isConcurrencySafe: () => true,
|
||||
execute: (args, exec) => executeEventSearch(ctx, args, exec, resolved.maxSearchResults),
|
||||
presentCall: presentEventSearchCall,
|
||||
}))
|
||||
|
||||
@@ -248,15 +248,13 @@ describe('registration and schemas', () => {
|
||||
expect(sessionSchema?.parameters).not.toHaveProperty('properties.cwd')
|
||||
expect(mounted.ctx.tools.get('session_search')?.timeoutMs).toBe(1234)
|
||||
expect(mounted.ctx.tools.get('session_trace')?.timeoutMs).toBeUndefined()
|
||||
const safeArgs: Record<string, unknown> = {
|
||||
session_search: { query: 'q' },
|
||||
session_event_search: { query: 'q' },
|
||||
const parallelArgs: Record<string, unknown> = {
|
||||
session_trace: {},
|
||||
session_event_trace: { seq: 0 },
|
||||
session_event_read: { seq: 0 },
|
||||
}
|
||||
for (const name of names) {
|
||||
expect(mounted.ctx.tools.get(name)?.isConcurrencySafe?.(safeArgs[name])).toBe(true)
|
||||
for (const [name, args] of Object.entries(parallelArgs)) {
|
||||
expect(mounted.ctx.tools.get(name)?.isConcurrencySafe?.(args)).toBe(true)
|
||||
}
|
||||
expect(mounted.ctx.tools.get('session_search')?.output.render({}, 'rendered'))
|
||||
.toEqual([{ type: 'text', text: 'rendered' }])
|
||||
@@ -287,6 +285,27 @@ describe('registration and schemas', () => {
|
||||
.not.toContain('tool:session-query')
|
||||
})
|
||||
|
||||
it('keeps generation-bound searches exclusive while exact observations remain parallel', async () => {
|
||||
const mounted = await mount()
|
||||
const classifications = [
|
||||
['session_search', { query: 'q' }, 'exclusive'],
|
||||
['session_event_search', { query: 'q' }, 'exclusive'],
|
||||
['session_trace', {}, 'parallel'],
|
||||
['session_event_trace', { seq: 0 }, 'parallel'],
|
||||
['session_event_read', { seq: 0 }, 'parallel'],
|
||||
] as const
|
||||
|
||||
for (const [name, args, kind] of classifications) {
|
||||
expect(mounted.ctx.tools.executionMode({
|
||||
name,
|
||||
arguments: args,
|
||||
callId: CallId(`mode-${name}`),
|
||||
signal: new AbortController().signal,
|
||||
agent: fakeAgent(mounted.caller),
|
||||
})).toEqual({ kind })
|
||||
}
|
||||
})
|
||||
|
||||
it('fails invalid direct config before registering anything', async () => {
|
||||
const mounted = await mount()
|
||||
for (const maxSearchResults of [0, 1.5, Number.NaN]) {
|
||||
|
||||
Reference in New Issue
Block a user