Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .agents/skills/add-column-type/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ Registering the *type* is compiler-enforced. Registering its *metadata* is not,
- [ ] `migrateCellsTo` / `migrateCellsFrom` added if the stored bytes change
- [ ] New metadata keys added to `TYPE_SPECIFIC_COLUMN_KEYS` + `FOREIGN_METADATA_VERB`
- [ ] Unit tests for `coerce` / `isCompatibleWith` round-trips, verified to fail without the code
- [ ] Docs row added to `apps/docs/content/docs/en/tables/index.mdx`
- [ ] Docs row added to `apps/docs/content/docs/tables/index.mdx`

## Final Validation (Required)

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/add-integration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ bun run deployment-config:check
bun run docs:check
```

This creates `apps/docs/content/docs/en/integrations/{service}.mdx` — one page per service carrying the block's Actions and, if it has one, its Triggers section. Never hand-edit generated pages; the only editable region is the `{/* MANUAL-CONTENT */}` block (see `scripts/README.md`).
This creates `apps/docs/content/docs/integrations/{service}.mdx` — one page per service carrying the block's Actions and, if it has one, its Triggers section. Never hand-edit generated pages; the only editable region is the `{/* MANUAL-CONTENT */}` block (see `scripts/README.md`).

The docs generator refreshes `packages/deployment-config/src/integrations.json`, and the deployment
config generator projects service-account provider IDs from that catalog plus the canonical OAuth
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/add-model/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ If the entry has `capabilities.thinking` or `capabilities.reasoningEffort`, it a

- **Anthropic-family (`anthropic`, `azure-anthropic`) thinking models MUST declare `capabilities.thinking.streamed`** (`'full' | 'summary' | 'none'`). Verify against Anthropic's current thinking-display and streaming docs: visible thinking returned by the API is summarized, including when Sim opts models whose default display is `omitted` into `display: 'summarized'` on agent-events runs, so current Claude thinking models use `'summary'`. Use `'full'` only if future official API docs explicitly guarantee raw thinking deltas. `bun run agent-stream-docs:check` (CI) fails if the field is missing.
- Other families usually omit the field and inherit the provider default in `getThinkingStreamVisibility` (Gemini/OpenAI → summaries; Bedrock/Meta → none; OpenAI-compatible vendors with documented reasoning fields → full deltas). Set it explicitly only when the model deviates from its family.
- After inserting the entry, run `bun run agent-stream-docs:generate` and commit the regenerated `apps/docs/content/docs/en/workflows/blocks/agent.mdx` — CI diffs it.
- After inserting the entry, run `bun run agent-stream-docs:generate` and commit the regenerated `apps/docs/content/docs/workflows/blocks/agent.mdx` — CI diffs it.
- Include the `streamed` value (with its source URL) in the verification report when set.

### Wrong family entirely?
Expand Down
113 changes: 98 additions & 15 deletions .github/actions/docker-build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ inputs:
required: true
max-cache-size-mb:
description: >-
Layer cache to retain after the post-job prune, in MB. Must stay above one
Layer cache to retain after this action prunes, in MB. Must stay above one
build's working set (base + dependency layers + RUN --mount=type=cache
dirs) or every build evicts what the next one needs. Falls back to the
small-image default below when empty.
small-image default in the prune step when empty — the fallback lives there
rather than here because callers pass this from a matrix field, and an unset
matrix key arrives as the empty string, which counts as "provided" and would
bypass an input `default:` entirely.
required: false

# Registry logins must precede this action. provenance/sbom stay off: attestation
Expand All @@ -49,24 +52,16 @@ runs:
PLATFORMS: ${{ inputs.platforms }}
run: echo "value=${GITHUB_REPOSITORY##*/}/${FILE#./}/${PLATFORMS//\//-}" >> "$GITHUB_OUTPUT"

# max-cache-size-mb is what bounds the disk: BuildKit's default GC is
# time-based only (layers unused for 8 days), and setup-docker-builder skips
# pruning altogether when the value is empty. On a repo that builds this
# often nothing ever ages out, so the disks grew without limit —
# app.Dockerfile/linux-amd64 reached 351 GB inside a day, and realtime, whose
# image is under 300 MB, sat at 249 GB. Sticky disks bill at ~$0.51/GB-month,
# so that was real money for layers no build would ever read again.
#
# The fallback is here rather than an input `default:` because callers pass
# this from a matrix field, and an unset matrix key arrives as the empty
# string — which counts as "provided", so a `default:` would never apply and
# a row that forgot the field would silently go back to unbounded growth.
# This action does NOT bound the disk — see the prune step below. BuildKit's
# own GC is time-based only (layers unused for 8 days), and these disks are
# mounted many times a day, so nothing ever ages out: app.Dockerfile/linux-amd64
# reached 351 GB inside a day of being created, and realtime, whose image is
# under 300 MB, sat at 249 GB. Sticky disks bill at ~$0.51/GB-month.
- name: Set up Blacksmith builder
if: inputs.provider == '' || inputs.provider == 'blacksmith'
uses: useblacksmith/setup-docker-builder@a5256a73e30f09e37e3eceb8ca36043d17621d24 # v2
with:
cache-key: ${{ steps.cache-key.outputs.value }}
max-cache-size-mb: ${{ inputs.max-cache-size-mb || '25600' }}

- name: Build and push (Blacksmith)
if: inputs.provider == '' || inputs.provider == 'blacksmith'
Expand All @@ -80,6 +75,94 @@ runs:
provenance: false
sbom: false

# Bound the layer cache ourselves. setup-docker-builder v1 took a
# max-cache-size-mb input and pruned in its own post step, but the v2 rewrite
# dropped it — and GitHub only WARNS on an unknown composite input, so passing
# it to v2 silently did nothing for a day while the app disk sat at 200+ GB.
#
# This is v1's command verbatim (its dist/index.js pruneBuildkitCache), against
# the fixed address v2 itself uses for `buildctl du` and `debug workers`:
# sudo buildctl --addr tcp://127.0.0.1:1234 prune --all --keep-storage <MB>
#
# Note buildctl's --all is NOT `docker buildx prune --all`. Here it means
# "include internal/frontend references" (cache/manager.go: without it, records
# typed internal or frontend, and any ref shared with an external source, are
# skipped). It does not wipe the cache, and --keep-storage still caps what is
# retained -- it maps straight onto the modern MaxUsedSpace field, so it is the
# buildctl spelling of --max-used-space rather than a deprecated alias.
# `RUN --mount=type=cache` dirs are typed exec.cachemount and are reclaimed
# either way; --all is here because it is what v1 used and it prunes strictly
# more. Runs before the builder's post step, which is what commits the disk.
#
# Warn rather than fail: a cache that is too large is not worth failing a
# deploy over. The du either side is what makes a silent no-op visible — the
# failure mode that hid the v2 input regression in the first place.
- name: Prune the layer cache
if: (inputs.provider == '' || inputs.provider == 'blacksmith') && !cancelled()
shell: bash
env:
KEEP_MB: ${{ inputs.max-cache-size-mb || '25600' }}
run: |
addr='tcp://127.0.0.1:1234'

# A zero or non-numeric value is NOT a no-op. buildctl parses
# --keep-storage as a float, and BuildKit's cache manager treats
# keepBytes==0 as "no cap" (`gcMode := opt.keepBytes != 0`), pruning
# everything eligible rather than trimming to a limit. A typo such as
# '40GB' — valid in turbo.json, but this flag is a bare MB number — would
# silently empty the cache and make every later build cold, costing far
# more than the storage it saves. Refuse instead.
if ! [[ "$KEEP_MB" =~ ^[1-9][0-9]*$ ]]; then
echo "::warning::max-cache-size-mb must be a positive whole number of MB, got '${KEEP_MB}' — skipping prune rather than risk wiping the cache"
exit 0
fi

# Print the whole Total line rather than picking a column: buildctl's du
# table is whitespace-aligned and its layout is not a stable contract.
#
# The trailing `|| true` is load-bearing. Composite steps run under
# `bash -e -o pipefail`, where `cur="$(total)"` takes the substitution's
# exit status, so a failing du would abort the step and fail the build --
# `echo "$(total)"` survives but the assignment in the settle loop does
# not. buildctl exiting non-zero here is entirely plausible: deleting a
# sticky disk out from under a running job makes buildkitd panic inside
# DiskUsage, and grep also exits 1 whenever the table has no Total line.
# Cache hygiene must never be able to fail a deploy.
total() { sudo buildctl --addr "$addr" du 2>/dev/null | grep -iE '^total:' | tr -s ' \t' ' ' || true; }
echo "before prune -> $(total)"

if sudo buildctl --addr "$addr" prune --all --keep-storage "$KEEP_MB"; then
# buildctl prune returns BEFORE buildkitd has finished deleting
# (moby/buildkit#1198). The builder's post step then SIGTERMs buildkitd
# and SIGKILLs it after 30s (shutdownBuildkitd: `const a=3e4`); on
# SIGKILL it sets sigkillUsed and SKIPS the sticky disk commit, throwing
# away this run's cache and risking a corrupt bbolt metadata DB. So wait
# for du to stop moving before handing back. Bounded — this is hygiene,
# not correctness, and the steady-state trim settles almost at once.
prev=''; stable=0
for _ in $(seq 1 60); do
cur="$(total)"
Comment thread
waleedlatif1 marked this conversation as resolved.
# An empty reading means du FAILED, never that the cache is empty:
# buildctl prints its `Total:` line unconditionally (cmd/buildctl
# diskusage.go), so an empty cache still reports `Total: 0B`. Without
# the -n guard the initial prev='' matched two empty readings and the
# loop exited after ~2s -- precisely when du is failing and the prune
# is most likely still deleting. Treat it as unstable and wait out the
# bound instead.
if [ -n "$cur" ] && [ "$cur" = "$prev" ]; then
stable=$((stable + 1))
[ "$stable" -ge 2 ] && break
else
stable=0
fi
prev="$cur"
sleep 2
done
echo "after prune -> $(total) (keep-storage ${KEEP_MB} MB)"
else
echo "::warning::Layer cache prune failed; this sticky disk is unbounded for this run"
fi

- name: Set up Docker Buildx
if: inputs.provider != '' && inputs.provider != 'blacksmith'
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ jobs:
with:
filters: |
docs:
- 'apps/docs/content/docs/en/**'
- 'apps/docs/content/docs/**'
- 'apps/sim/scripts/process-docs.ts'
- 'apps/sim/lib/chunkers/**'

Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ start-collector.sh

## Helm Chart Tests
helm/sim/test
i18n.cache

## Claude Code
.claude/launch.json
Expand Down
107 changes: 105 additions & 2 deletions apps/desktop/src/main/browser-agent/cdp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,8 @@ describe('browser-agent screenshot capture', () => {
expect(shot).toEqual({
dataUrl: `data:image/jpeg;base64,${Buffer.from('resized').toString('base64')}`,
scale: 0.5,
viewport: { width: 2048, height: 1024 },
imageSize: { width: 1024, height: 512 },
})
})

Expand All @@ -558,14 +560,115 @@ describe('browser-agent screenshot capture', () => {

const image = vi.mocked(nativeImage.createFromBuffer).mock.results[0].value
expect(image.resize).not.toHaveBeenCalled()
expect(shot).toEqual({ dataUrl: 'data:image/jpeg;base64,c2lt', scale: 0.5 })
expect(shot).toEqual({
dataUrl: 'data:image/jpeg;base64,c2lt',
scale: 0.5,
viewport: { width: 2048, height: 1024 },
imageSize: { width: 1024, height: 512 },
})
})

it('returns the raw capture when the image cannot be decoded', async () => {
const { contents } = captureFixture(null)

const shot = await captureScreenshot(contents)

expect(shot).toEqual({ dataUrl: 'data:image/jpeg;base64,c2lt', scale: 0.5 })
expect(shot).toEqual({
dataUrl: 'data:image/jpeg;base64,c2lt',
scale: 0.5,
viewport: { width: 2048, height: 1024 },
imageSize: null,
})
})

it('does not expose deprecated device-pixel metrics as a CSS viewport', async () => {
const { contents } = captureFixture({ width: 1024, height: 512 })
vi.mocked(contents.debugger.sendCommand).mockImplementation((method: string) => {
if (method === 'Page.getLayoutMetrics') {
return Promise.resolve({ layoutViewport: { clientWidth: 2048, clientHeight: 1024 } })
}
if (method === 'Page.captureScreenshot') return Promise.resolve({ data: 'c2lt' })
return Promise.resolve(undefined)
})

const shot = await captureScreenshot(contents)

expect(shot.viewport).toBeNull()
expect(shot.imageSize).toEqual({ width: 1024, height: 512 })
})

it('accepts stable finite scroll offsets around the capture', async () => {
const { contents } = captureFixture({ width: 1024, height: 512 })
vi.mocked(contents.debugger.sendCommand).mockImplementation((method: string) => {
if (method === 'Page.getLayoutMetrics') {
return Promise.resolve({
cssLayoutViewport: {
clientWidth: 2048,
clientHeight: 1024,
pageX: 12,
pageY: 34,
},
})
}
if (method === 'Page.captureScreenshot') return Promise.resolve({ data: 'c2lt' })
return Promise.resolve(undefined)
})

await expect(captureScreenshot(contents)).resolves.toMatchObject({
viewport: { width: 2048, height: 1024 },
imageSize: { width: 1024, height: 512 },
})
})

it.each([
[
'dimensions',
{ cssLayoutViewport: { clientWidth: 2048, clientHeight: 1024 } },
{ cssLayoutViewport: { clientWidth: 1024, clientHeight: 512 } },
],
[
'metric units',
{ cssLayoutViewport: { clientWidth: 2048, clientHeight: 1024 } },
{ layoutViewport: { clientWidth: 2048, clientHeight: 1024 } },
],
[
'horizontal scroll offset',
{ cssLayoutViewport: { clientWidth: 2048, clientHeight: 1024, pageX: 0, pageY: 20 } },
{ cssLayoutViewport: { clientWidth: 2048, clientHeight: 1024, pageX: 10, pageY: 20 } },
],
[
'vertical scroll offset',
{ cssLayoutViewport: { clientWidth: 2048, clientHeight: 1024, pageX: 10, pageY: 20 } },
{ cssLayoutViewport: { clientWidth: 2048, clientHeight: 1024, pageX: 10, pageY: 30 } },
],
[
'offset validity',
{ cssLayoutViewport: { clientWidth: 2048, clientHeight: 1024, pageX: 0, pageY: 0 } },
{
cssLayoutViewport: {
clientWidth: 2048,
clientHeight: 1024,
pageX: 0,
pageY: Number.NaN,
},
},
],
['availability', {}, {}],
])(
'rejects a capture when viewport %s change during CDP capture',
async (_label, before, after) => {
const { contents } = captureFixture({ width: 1024, height: 512 })
let metricsRead = 0
vi.mocked(contents.debugger.sendCommand).mockImplementation((method: string) => {
if (method === 'Page.getLayoutMetrics') {
metricsRead++
return Promise.resolve(metricsRead === 1 ? before : after)
}
if (method === 'Page.captureScreenshot') return Promise.resolve({ data: 'c2lt' })
return Promise.resolve(undefined)
})

await expect(captureScreenshot(contents)).rejects.toThrow(/viewport changed/)
}
)
})
Loading
Loading