Skip to content

Commit 4410335

Browse files
committed
Merge remote-tracking branch 'origin/staging' into wip/platform-org-wiring
2 parents 2b9f12a + 61843fe commit 4410335

23 files changed

Lines changed: 301 additions & 713 deletions

‎.github/workflows/publish-cli.yml‎

Lines changed: 0 additions & 107 deletions
This file was deleted.

‎.github/workflows/publish-sim-cli.yml‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ jobs:
106106
echo "tag=$TAG"
107107
} >> "$GITHUB_OUTPUT"
108108
109+
# `./embed` and the `#sim-cli/*` imports point at TypeScript source, which
110+
# only the monorepo's own Next.js app consumes (it transpiles the package).
111+
# The tarball ships `dist` alone, so published they would be dead entry
112+
# points; the smoke test below fails on any export that does not exist.
113+
- name: Drop source-only entry points
114+
working-directory: packages/sim-cli
115+
run: |
116+
jq 'del(.imports, .exports["./embed"])' package.json > "$RUNNER_TEMP/package.json"
117+
mv "$RUNNER_TEMP/package.json" package.json
118+
109119
# Bun packs because it rewrites `workspace:` ranges to real versions,
110120
# which `npm pack` would publish verbatim. The smoke test below and the
111121
# publish job both use this one tarball, so what was tested is what ships.
@@ -126,6 +136,20 @@ jobs:
126136
SMOKE_DIR="$(mktemp -d "$RUNNER_TEMP/sim-cli-smoke.XXXXXX")"
127137
tar -xzf "$RUNNER_TEMP/package.tgz" -C "$SMOKE_DIR"
128138
test "$("$SMOKE_DIR/package/dist/index.js" --version)" = "$VERSION"
139+
node -e '
140+
const fs = require("node:fs")
141+
const path = require("node:path")
142+
const root = path.join(process.argv[1], "package")
143+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"))
144+
const targets = Object.values(pkg.exports ?? {}).flatMap((entry) =>
145+
typeof entry === "string" ? [entry] : Object.values(entry)
146+
)
147+
const missing = targets.filter((target) => !fs.existsSync(path.join(root, target)))
148+
if (missing.length > 0) {
149+
console.error("Exports point at files the tarball does not contain: " + missing.join(", "))
150+
process.exit(1)
151+
}
152+
' "$SMOKE_DIR"
129153
130154
- name: Verify version is unpublished
131155
env:

‎apps/sim/lib/knowledge/mcp/server.protocol.test.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,27 @@ describe('Search MCP protocol', () => {
9393
expect(JSON.parse(searchContent.text)).toMatchObject({
9494
results: [{ documentId, queryIndexes: [0] }],
9595
})
96-
const read = await client.callTool({ name: 'read_document', arguments: { documentId } })
96+
const read = await client.callTool({
97+
name: 'read_document',
98+
arguments: { documentId, limit: 8 },
99+
})
97100
expect(read.isError).not.toBe(true)
101+
expect(mocks.liveRead.mock.lastCall?.[0].input).toMatchObject({
102+
limit: 8,
103+
scaledWindow: { documentsInCall: 1 },
104+
})
98105
expect(read.content).toEqual([
99106
{ type: 'text', text: expect.stringContaining('Read evidence') },
100107
])
108+
const batchRead = await client.callTool({
109+
name: 'read_document',
110+
arguments: { documentIds: [documentId, `${documentId}b`], limit: 8 },
111+
})
112+
expect(batchRead.isError).not.toBe(true)
113+
expect(mocks.liveRead.mock.lastCall?.[0].input).toMatchObject({
114+
limit: 8,
115+
scaledWindow: { documentsInCall: 2 },
116+
})
101117
const offsetless = await client.callTool({
102118
name: 'search',
103119
arguments: {

‎apps/sim/lib/knowledge/mcp/server.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ export function createKnowledgeMcpServer(context: KnowledgeMcpContext): McpServe
298298
...options,
299299
documentId: id,
300300
organizationId,
301+
scaledWindow: { documentsInCall: documentId ? 1 : batch.length },
301302
resultSecretRegistry: registry,
302303
signal,
303304
},

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,89 @@ describe('authorized live retrieval', () => {
11931193
})
11941194
).rejects.toBe('user_stop:test')
11951195
})
1196+
describe('read window', () => {
1197+
const content = 'x'.repeat(30_000)
1198+
async function readWindow(extra: Record<string, unknown>, text = content) {
1199+
const [result] = (await searchLiveKnowledge.execute({ principal, input })).results
1200+
if (!result) throw new Error('Expected the search fixture to return a result')
1201+
mocks.read.mockResolvedValue({ ...document, content: text })
1202+
const read = await readLiveDocument.execute({
1203+
principal,
1204+
input: {
1205+
workspaceId: 'workspace',
1206+
documentId: result.documentId,
1207+
limit: 3,
1208+
resultSecretRegistry: new ResolvedSecretTraceRegistry([]),
1209+
...extra,
1210+
},
1211+
})
1212+
const [chunk] = read.chunks
1213+
if (!chunk) throw new Error('Expected the read to return a window')
1214+
return {
1215+
length: chunk.content.length,
1216+
encoded: JSON.stringify(chunk.content).length - 2,
1217+
next: read.next,
1218+
}
1219+
}
1220+
1221+
const scaled = (documentsInCall: number) => ({ scaledWindow: { documentsInCall } })
1222+
1223+
it('returns a proportionally longer window when a scaling caller asks for more chunks', async () => {
1224+
const { length, next } = await readWindow({ limit: 8, ...scaled(1) })
1225+
expect(length).toBe(21_333)
1226+
expect(next).toEqual({ startChunkIndex: 0, startOffset: 21_333 })
1227+
})
1228+
1229+
it('continues a longer window from where the previous one ended', async () => {
1230+
const { length, next } = await readWindow({ limit: 8, ...scaled(1), startOffset: 21_333 })
1231+
expect(length).toBe(30_000 - 21_333)
1232+
expect(next).toBeNull()
1233+
})
1234+
1235+
it.each([
1236+
[5, 1, 13_333],
1237+
[8, 2, 20_000],
1238+
[8, 3, 13_333],
1239+
[8, 4, 10_000],
1240+
])(
1241+
'shares one call budget: limit %i across %i documents reads %i characters each',
1242+
async (limit, documentsInCall, expected) => {
1243+
expect((await readWindow({ limit, ...scaled(documentsInCall) })).length).toBe(expected)
1244+
}
1245+
)
1246+
1247+
it.each([
1248+
[1, 21_333],
1249+
[2, 20_000],
1250+
])(
1251+
'keeps a quote-heavy window within its share once JSON-encoded, across %i documents',
1252+
async (documentsInCall, share) => {
1253+
const quotes = '"'.repeat(30_000)
1254+
const { length, encoded, next } = await readWindow(
1255+
{ limit: 8, ...scaled(documentsInCall) },
1256+
quotes
1257+
)
1258+
expect(encoded).toBeLessThanOrEqual(share)
1259+
expect(length).toBeGreaterThanOrEqual(8_000)
1260+
expect(next).toEqual({ startChunkIndex: 0, startOffset: length })
1261+
}
1262+
)
1263+
1264+
it('keeps the standard window for quote-heavy text at the default limit', async () => {
1265+
const { length } = await readWindow({ limit: 3, ...scaled(1) }, '"'.repeat(30_000))
1266+
expect(length).toBe(8_000)
1267+
})
1268+
1269+
it.each([
1270+
['at the default limit', { limit: 3, ...scaled(1) }],
1271+
['below the default limit', { limit: 1, ...scaled(1) }],
1272+
['when the call budget is spread over five documents', { limit: 8, ...scaled(5) }],
1273+
['when the call budget is spread over ten documents', { limit: 8, ...scaled(10) }],
1274+
['for a caller that does not scale', { limit: 8 }],
1275+
])('keeps the standard window %s', async (_, extra) => {
1276+
expect((await readWindow(extra)).length).toBe(8_000)
1277+
})
1278+
})
11961279
it('refuses a chunk index other than zero before reading the document from its provider', async () => {
11971280
const [result] = (await searchLiveKnowledge.execute({ principal, input })).results
11981281
if (!result) throw new Error('Expected the search fixture to return a result')

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

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,45 @@ export const LIVE_NOT_STARTED_MESSAGE =
288288
export const LIVE_OVER_ACCOUNT_LIMIT_MESSAGE = `Not searched: one search covers at most ${MAX_ACCOUNTS} accounts. Search this source or account alone.`
289289
/** Previews center on the query's longest matching term, like indexed passages. */
290290
const PREVIEW_CHARACTERS = 1800
291-
/** Characters one read returns, the same page budget indexed document reads use. */
291+
/** Characters one read returns at the default limit, the page budget indexed document reads use. */
292292
const READ_WINDOW_CHARACTERS = 8000
293+
/** The read limit {@link READ_WINDOW_CHARACTERS} is sized for, which is the default limit. */
294+
const READ_WINDOW_LIMIT = 3
295+
/**
296+
* Characters the documents of one scaled read call share. Through four documents it keeps the
297+
* call's result, with about a thousand characters of metadata per document, under the
298+
* 50,000-character size at which MCP clients such as Claude Code save a tool result to a file
299+
* rather than show it. From five documents the standard page per document is the larger, so
300+
* those calls read exactly what they read without scaling.
301+
*/
302+
const SCALED_READ_CALL_CHARACTERS = 40_000
303+
304+
/**
305+
* Where one read's window ends. A live document is one chunk, so a caller that scales the window
306+
* gets a longer one when it asks for more chunks than the default, bounded by an even share of the
307+
* call's budget across the documents it reads. The share counts the window as the result encodes
308+
* it, since JSON escapes quotes, backslashes and line breaks, so a longer window is shortened until
309+
* its encoded text fits. The window is never shorter than the standard page, which is also what a
310+
* read at or below the default limit, or from any other caller, returns.
311+
*/
312+
function readWindowEnd(
313+
content: string,
314+
start: number,
315+
{ limit, scaledWindow }: Pick<LiveReadInput, 'limit' | 'scaledWindow'>
316+
): number {
317+
const standardEnd = Math.min(content.length, start + READ_WINDOW_CHARACTERS)
318+
if (!scaledWindow || limit <= READ_WINDOW_LIMIT) return standardEnd
319+
const scaled = Math.floor((READ_WINDOW_CHARACTERS * limit) / READ_WINDOW_LIMIT)
320+
const share = Math.floor(SCALED_READ_CALL_CHARACTERS / Math.max(1, scaledWindow.documentsInCall))
321+
const budget = Math.min(scaled, share)
322+
let end = Math.min(content.length, start + budget)
323+
for (let attempt = 0; attempt < 3 && end > standardEnd; attempt++) {
324+
const encoded = JSON.stringify(content.slice(start, end)).length - 2
325+
if (encoded <= budget) return end
326+
end = start + Math.floor(((end - start) * budget) / encoded)
327+
}
328+
return standardEnd
329+
}
293330

294331
/**
295332
* Verifies candidates in rank order. A revoked grant fails the whole account; any other
@@ -990,6 +1027,11 @@ export type LiveReadInput = ResourceOwner & {
9901027
documentId: string
9911028
filters?: WorkspaceSearchFilters
9921029
limit: number
1030+
/**
1031+
* Lengthens the window for a limit above the default, sharing one call's budget across the
1032+
* documents it reads, as {@link readWindowEnd} sizes it.
1033+
*/
1034+
scaledWindow?: { documentsInCall: number }
9931035
startChunkIndex?: number
9941036
startOffset?: number
9951037
resultSecretRegistry: ResolvedSecretTraceRegistry
@@ -1135,7 +1177,7 @@ export const readLiveDocument = defineAuthorizedKnowledgeUseCase({
11351177
'validation',
11361178
'This document changed. Read again from the beginning'
11371179
)
1138-
const end = Math.min(content.length, start + READ_WINDOW_CHARACTERS)
1180+
const end = readWindowEnd(content, start, input)
11391181
return {
11401182
documentId: input.documentId,
11411183
knowledgeBaseId: '',

‎biome.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@
171171
"**/*.spec.ts",
172172
"apps/desktop/e2e/**",
173173
"apps/sim/vitest.setup.ts",
174-
"packages/cli/**",
175174
"packages/sim-cli/**",
176175
"packages/sim-setup/**",
177176
"packages/db/script-migrations/**",

‎bun.lock‎

Lines changed: 0 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎knip.jsonc‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,6 @@
153153
"ignoreIssues": { "components/icons/icons.tsx": ["exports", "types", "duplicates"] }
154154
},
155155
"apps/pii": { "includeEntryExports": false },
156-
"packages/cli": { "includeEntryExports": false },
157156
"packages/db": {
158157
"includeEntryExports": false,
159158
"entry": [

0 commit comments

Comments
 (0)