Skip to content

fix: Report an empty search-actors page past the last result as such - #1568

Merged
jirispilka merged 4 commits into
masterfrom
fix/search-actors-exhausted-page
Oct 15, 2026
Merged

jirispilka merged 4 commits into
masterfrom
fix/search-actors-exhausted-page

Conversation

@jirispilka

@jirispilka jirispilka commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Closes #1276

Review page with before/after examples for this PR and the five related ones (#1563–#1567): https://claude.ai/artifact/WDRj1nwPnMTP9ktGyg3z3i

Reported by @lokicik. This follows the implementation spec in the maintainer comment on the issue.

search-actors returned the no-match guidance ("Keywords can include … A platform-only query such as "TikTok" …") for any empty page, including a page past the last result of a query that matched. With an empty query that advice cannot help, and it reads as if nothing matched.

What changed

buildNoActorsFoundInstructions(), shared by search-actors and search-actors-widget, now gets the offset. With offset > 0 it returns one sentence and no keyword advice; at offset 0 the text is unchanged. No new response fields; the output schema is unchanged. This is a tool-text change to structuredContent.instructions and the text content.

search-actors with keywords: "", limit: 1, offset: 1000000:

Before: No Actors were found for the search query "".
        Keywords can include the named platform and/or data type (e.g. "TikTok posts").
        A platform-only query such as "TikTok" is an option if more specific results do not fit.
Now:    No Actors past offset 1000000 for the search query "".

Notes for reviewers (human-written)

Proof it works

mcpc with the Claude connector tool set on this branch:

search-actors call Result
keywords: "", limit: 1, offset: 0 1 Actor
keywords: "", limit: 1, offset: 1000000 No Actors past offset 1000000 for the search query "".
keywords: "instagram scraper", limit: 5, offset: 5000 No Actors past offset 5000 for the search query "instagram scraper".
keywords: "zzqqxxnotarealactorkeyword123", offset: 0 the three-line no-match guidance, unchanged

The text content equals structuredContent.instructions, and structuredContent keeps its fields (actors: [], query, count: 0, instructions). The same offset: 1000000 call on a build with master's text returns the three-line guidance shown under "Before".

Unit tests (failing before the fix): an empty page at offset 1 returns the new sentence in structuredContent and the text content, for both search-actors and search-actors-widget. The existing empty-first-page tests keep the three-line text.

Implemented with Claude Code: code, tests, mcpc probe and this description. Reviewed by a separate Claude Code review pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_0149C4bPjve1UHWcxTtBsPXC


Generated by Claude Code

search-actors and search-actors-widget returned the no-match keyword
guidance for any empty page, including a page past the last result of
a query that matched. With offset > 0 the text is now "No Actors past
offset <offset> for the search query "<keywords>"." The offset 0 text
and the response fields are unchanged.

Closes #1276

Claude-Session: https://claude.ai/code/session_0149C4bPjve1UHWcxTtBsPXC
jirispilka added a commit that referenced this pull request Oct 14, 2026
…#1565)

## Why

Closes #1561

Review page with before/after examples for this PR and the five related
ones (#1563, #1564, #1566–#1568):
https://claude.ai/artifact/WDRj1nwPnMTP9ktGyg3z3i

Tool errors with an HTTP status that has no specific hint (400, 404,
405, 409, …) ended with "Verify the tool name and input parameters.",
though the tool name was right and the input valid. A missing run, for
example, read as if the call itself was wrong.

## What changed

The generic hint is now appended only when the error has no HTTP status.
With a status, the 401, 403, 429 and 5xx hints stay; any other status
adds nothing, since the text already carries the API's message and error
type. The run-limit and cannot-publish branches are unchanged.

`abort-actor-run` with a nonexistent `runId` (404):

```
Before: Error calling tool "abort-actor-run": Actor run was not found (API error type: record-not-found). Verify the tool name and input parameters.
Now:    Error calling tool "abort-actor-run": Actor run was not found (API error type: record-not-found).
```

An error without a status (a network failure, a plain `Error`) still
ends with "Verify the tool name and input parameters.". This is a
tool-text change to the error text every tool returns; no eval case
references the removed hint.

## Notes for reviewers (human-written)

## Proof it works

mcpc with the Claude connector tool set, this branch against a build
with master's `src/utils/mcp.ts`:

| Call | master | this branch |
|---|---|---|
| `abort-actor-run runId:="doesNotExist123"` | `… Actor run was not
found (API error type: record-not-found). Verify the tool name and input
parameters.` | `… Actor run was not found (API error type:
record-not-found).` |
| `get-actor-run runId:="doesNotExist123"` | `Run with ID
'doesNotExist123' not found.` | unchanged (the tool words its own
not-found error) |
| `get-key-value-store-record keyValueStoreId:="doesNotExist123"
recordKey:="INPUT"` | `Key-value store 'doesNotExist123' not found.` |
unchanged (own error) |

Unit tests (the 400 and 404 cases failing before the fix):
`getToolCallErrorUserText()` for 400 and 404 (no hint), 401, 403, 429
and 500 (hint kept) and an error without a status (generic hint kept),
and a `tools/call` of a tool that throws a 404.

Implemented with Claude Code: code, tests, mcpc probe and this
description. Reviewed by a separate Claude Code review pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_0149C4bPjve1UHWcxTtBsPXC

---
_Generated by [Claude
Code](https://claude.ai/code/session_0149C4bPjve1UHWcxTtBsPXC)_

Co-authored-by: Claude <noreply@anthropic.com>
jirispilka added a commit that referenced this pull request Oct 14, 2026
)

## Why

Closes apify/ai-team#334

Review page with before/after examples for this PR and the five related
ones (#1564–#1568): https://claude.ai/artifact/WDRj1nwPnMTP9ktGyg3z3i

`resources/read` and `apify-api-read` returned the account's proxy
password from `/v2/users/me` in plain text. `resources/read` is served
in every session.

## What changed

The JSON redaction that both paths share now covers the `password` key
as well as `urlSigningSecretKey`: the value becomes `[REDACTED]`, the
key stays. The function is renamed from `redactUrlSigningSecretKey` to
`redactJsonSecrets`.

`apify-api-read` with `path: /v2/users/me`, or `resources/read` on the
same URL:

```
Before: "proxy": { "password": "<the proxy password>", "groups": [...] }
Now:    "proxy": { "password": "[REDACTED]", "groups": [...] }
```

Any `password` string value in a JSON body is redacted, for example in
dataset items or a JSON key-value store record, and also in
`apify-api-write` responses, which go through the same function. The key
match is exact (`"password"`, case-sensitive). Text, XML and binary
bodies are unchanged, as for `urlSigningSecretKey`. The response shape,
including `structuredContent`, stays the same; only those values change.
The function is not exported through `internals.js`.

## Notes for reviewers (human-written)

## Proof it works

mcpc on a local build (`--tools=apify-api-read`), checking the field
with jq without printing the body:

| Check | Result |
|---|---|
| `resources-read https://api.apify.com/v2/users/me`:
`.data.proxy.password == "[REDACTED]"` | `true` |
| same: `.data.proxy` still has `password`, `groups` is an array |
`true`, `"array"` |
| same: a proxy password appears anywhere in the text | `false` |
| `tools-call apify-api-read path:="/v2/users/me"`: `structuredContent`
and `content[0].text` password `== "[REDACTED]"` | `true`, `true` |
| same: strings holding a proxy password anywhere in the result | `0` |
| same: `isError`, `statusCode` | `false`, `200` |

New unit tests (failing before the fix) cover `redactJsonSecrets()`,
`readApiResource()` and `apify-api-read` on a `/v2/users/me`-shaped body
with a fake value.

Implemented with Claude Code: code, tests, mcpc probe and this
description. Reviewed by a separate Claude Code review pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_0149C4bPjve1UHWcxTtBsPXC

---
_Generated by [Claude
Code](https://claude.ai/code/session_0149C4bPjve1UHWcxTtBsPXC)_

Co-authored-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jirispilka
jirispilka marked this pull request as ready for review October 14, 2026 09:17
@apify-service-account apify-service-account added the tested Temporary label used only programatically for some analytics. label Oct 14, 2026
@apify-service-account apify-service-account added the t-ai Issues owned by the AI team. label Oct 14, 2026

@vojtechj-apify vojtechj-apify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, one nit.

Comment thread src/tools/actors/search_actors.ts Outdated
@jirispilka
jirispilka merged commit 6456db7 into master Oct 15, 2026
16 checks passed
@jirispilka
jirispilka deleted the fix/search-actors-exhausted-page branch October 15, 2026 10:32
jirispilka added a commit that referenced this pull request Oct 15, 2026
…ts (#1567)

## Why

Closes apify/ai-team#335

Review page with before/after examples for this PR and the five related
ones (#1563–#1566, #1568):
https://claude.ai/artifact/WDRj1nwPnMTP9ktGyg3z3i

`get-dataset-list` and `get-key-value-store-list` default to
`unnamed=false`, so they list only named storages, while Actor runs
write to unnamed ones. The response did not say the list was limited,
and an empty page pointed at inspecting an entry of the empty list.
Nothing served said how long unnamed storages are kept.

## What changed

Summary with `unnamed=false` (the default) says "named"; with
`unnamed=true` it is unchanged. On an empty page with no next page,
`nextStep` states what `unnamed=true` adds instead of pointing at the
empty list. Pagination hints and the hint on a nonempty last page are
unchanged; tool descriptions and defaults are unchanged.

| `get-dataset-list` call (test account: 11 named, 855 unnamed datasets)
| Before | Now |
|---|---|---|
| defaults | `Listed 10 of 11 datasets.` | `Listed 10 of 11 named
datasets.` |
| `unnamed=true` | `Listed 10 of 855 datasets.` | unchanged |
| empty page, defaults | `Listed 0 of 11 datasets.` / `Use get-dataset
with a datasetId from the list to inspect a dataset.` | `Listed 0 of 11
named datasets.` / `unnamed=true includes the unnamed datasets that
Actor runs create.` |
| empty page, `unnamed=true` | `Listed 0 of 855 datasets.` / `Use
get-dataset with a datasetId from the list to inspect a dataset.` |
`Listed 0 of 855 datasets.` / `No follow-up step.` |

`get-key-value-store-list` does the same with "key-value stores". The
server instructions' `## Storage types` section gets one bullet:

```
- Unnamed storages are deleted after the account's data-retention period; named storages are kept until deleted.
```

The new text names no tool and asks for no call. This is a tool-text
change to `structuredContent.summary`/`nextStep` of both list tools and
to the server instructions.

## Notes for reviewers (human-written)

## Proof it works

mcpc with the Claude connector tool set; counts checked with `apify
datasets ls` / `apify key-value-stores ls` (with and without
`--unnamed`). The table above is this probe: "Before" on a build with
master's storage code, "Now" on this branch. Empty pages use an offset
past the end, so no storage was created or deleted.

`get-key-value-store-list` on the same account (18 named, 789 unnamed
stores):

| Call | `summary` | `nextStep` |
|---|---|---|
| defaults | `Listed 10 of 18 named key-value stores.` | `Call
get-key-value-store-list again with offset=10 to fetch the next page.` |
| `unnamed=true` | `Listed 10 of 789 key-value stores.` | `Call
get-key-value-store-list again with offset=10 to fetch the next page.` |
| `offset=100000` | `Listed 0 of 18 named key-value stores.` |
`unnamed=true includes the unnamed key-value stores that Actor runs
create.` |
| `offset=100000`, `unnamed=true` | `Listed 0 of 789 key-value stores.`
| `No follow-up step.` |

`content[1].text` is `summary` + newline + `nextStep`, and the retention
bullet is in the served instructions.

Unit tests: the `named` summary on default pages, both empty-page
`nextStep` variants (including an offset past the end) with schema
validation against `storageListOutputSchema`, and the retention bullet
in `## Storage types` fail before the fix. Guards keep the
`unnamed=true` summary and the inspect hint on a nonempty last page
unchanged.

Implemented with Claude Code: code, tests, mcpc probe and this
description. Reviewed by a separate Claude Code review pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_0149C4bPjve1UHWcxTtBsPXC

---
_Generated by [Claude
Code](https://claude.ai/code/session_0149C4bPjve1UHWcxTtBsPXC)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
jirispilka added a commit that referenced this pull request Oct 15, 2026
## Why

Closes #1560

Review page with before/after examples for this PR and the five related
ones (#1563–#1565, #1567, #1568):
https://claude.ai/artifact/WDRj1nwPnMTP9ktGyg3z3i

Aborting a run that had already finished returns it unchanged, and
`abort-actor-run` reported it like any status read ("SUCCEEDED in 68s.
…"), with no word that the abort did nothing. An already-ABORTED run
looked the same as an abort that took effect.

## What changed

`abort-actor-run` reads the run before aborting. When the status is
SUCCEEDED, FAILED, TIMED-OUT or ABORTED, it sends no abort and `summary`
starts with "Nothing was aborted: the run had already finished." Any
other status is aborted as before, and the returned ABORTING or ABORTED
text is unchanged. `nextStep` and the output schema are unchanged. This
is a tool-text change to `structuredContent.summary` and the narrative
text.

`abort-actor-run` on an ABORTED run:

```
Before: ABORTED after 9.612s. Actor status: "Finished! Total 1 requests: 1 succeeded, 0 failed".
Now:    Nothing was aborted: the run had already finished. ABORTED after 9.612s. Actor status: "Finished! Total 1 requests: 1 succeeded, 0 failed".
```

An unknown `runId` now returns the text `get-actor-run` uses, from the
read instead of the abort:

```
Before: Error calling tool "abort-actor-run": Actor run was not found (API error type: record-not-found).
Now:    Run with ID 'doesNotExist123' not found.
```

Aborting a running run now takes two API calls (read, then abort); a
finished run takes one, as before. If a run finishes between the read
and the abort, the response shows its final status without the prefix.
`structuredContent` keeps its shape; only the `summary` of a finished
run and the not-found text change, which matters if the hosted server's
contract tests pin either.

## Differs from #1560

The issue proposed checking the status the abort call returns, prefixing
only SUCCEEDED, FAILED and TIMED-OUT, and keeping today's text for
ABORTING and ABORTED. This PR does three things differently:

- **Reads the run before the abort.** The abort response for a run that
was already ABORTED looks the same as one for a run this call just
aborted, so only a read beforehand can tell them apart. The cost is one
extra API call per abort of an unfinished run.
- **Prefixes already-ABORTED runs too.** Nothing was aborted for them
either.
- **Changes the not-found text.** It now comes from the read, so it
matches `get-actor-run`.

## Notes for reviewers (human-written)

## Proof it works

mcpc with the Claude connector tool set, `abort-actor-run` on finished
runs from the account (`get-actor-run` before and after shows the same
status and `finishedAt`; the calls changed nothing):

| Run | Status | `structuredContent.summary` |
|---|---|---|
| `Nz8MuAIqzMUiusK4A` | SUCCEEDED | Nothing was aborted: the run had
already finished. SUCCEEDED in 68.317s. Actor status: "Finished! Scraped
2 pages, 0 failed". |
| `SCbQwsYRhX8vmarD3` | FAILED | Nothing was aborted: the run had
already finished. FAILED after 96.95s. |
| `O3KXkXfnQfLp8X13n` | ABORTED | Nothing was aborted: the run had
already finished. ABORTED after 9.612s. Actor status: "Finished! Total 1
requests: 1 succeeded, 0 failed". |
| `b08qGCehNYxW69XI4` | ABORTED | Nothing was aborted: the run had
already finished. ABORTED after 12.799s. Actor status: "Starting the
crawler". |
| `doesNotExist123` | – | `isError: true`, `Run with ID
'doesNotExist123' not found.` |

`content[1].text` starts with the same summary, and `content[0]` equals
`structuredContent`. The probe started no run, so aborting a running run
is covered by unit tests only.

Unit tests (failing before the fix): for SUCCEEDED, FAILED, TIMED-OUT
and ABORTED runs no abort is sent and the summary starts with the
sentence, `content[0]` mirrors `structuredContent`, and the response
validates against `actorRunOutputSchema`; an unknown run returns the
not-found error without an abort. READY, RUNNING, TIMING-OUT and
ABORTING runs are aborted once, and an ABORTING or ABORTED response
keeps its summary.

Implemented with Claude Code: code, tests, mcpc probe and this
description. Reviewed by a separate Claude Code review pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_0149C4bPjve1UHWcxTtBsPXC

---
_Generated by [Claude
Code](https://claude.ai/code/session_0149C4bPjve1UHWcxTtBsPXC)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
jirispilka added a commit that referenced this pull request Oct 16, 2026
…1600)

## Why

Closes #1593

The Store search API pages before it filters, so a page at a later
offset can be empty while later offsets still return Actors. Since
#1568, `search-actors` and `search-actors-widget` described such a page
as `No Actors past offset N…`, which says the results ended.

## What changed

Empty page at `offset > 0`:

```
Before: No Actors past offset 8 for the search query "czech real estate".
Now:    No Actors returned at offset 8 for the search query "czech real estate".
```

The `offset: 0` keyword guidance is unchanged. The search helper still
returns items only; no extra API call, paging logic or retry text. The
text is a tool-text contract change: it is
`structuredContent.instructions` and the text content of
`search-actors`, and the text content of `search-actors-widget`. The
widget's `structuredContent` keys are unchanged.

## Notes for reviewers (human-written)

## Proof it works

Tests first: the default and widget response tests failed against the
old wording, then passed after the fix. They cover offset-zero guidance
and empty later pages (offset 8 for the default response, offset 1 for
the widget). Duplicate helper-only tests were removed; all cases are
mocked.

mcpc against a local build of this branch (`PORT=3003 pnpm start`), the
Claude connector selection (`?tools=…&client=claude`, 21 tools; with
`&ui=true`, 23 tools), protocol `2026-07-28`. Live Store, `limit: 1`:

| Call | Result |
|---|---|
| `search-actors` `linkedin jobs`, offset 3 |
`cheap_scraper/linkedin-job-scraper` |
| `search-actors` `linkedin jobs`, offset 4 | `No Actors returned at
offset 4 for the search query "linkedin jobs".` |
| `search-actors` `linkedin jobs`, offset 5 |
`valig/linkedin-jobs-scraper` |
| `search-actors` `czech real estate`, offset 7 |
`parseforge/sreality-cz-scraper` |
| `search-actors` `czech real estate`, offset 8 | `No Actors returned at
offset 8 for the search query "czech real estate".` |
| `search-actors` `czech real estate`, offset 9 |
`solidcode/reality-idnes-cz-scraper` |
| `search-actors-widget` (Apps mode) `linkedin jobs`, offset 4 | same
sentence; `structuredContent` keys `actors`, `count`, `query`,
`widgetActors` (`[]`) |
| `search-actors-widget` (Apps mode) `czech real estate`, offset 8 |
same sentence, same keys |
| `search-actors` (Apps mode) `czech real estate`, offset 8 | same
sentence in text and `structuredContent.instructions` |
| `search-actors` `zzqqxxnotarealactorkeyword123`, offset 0 | the
three-line keyword guidance, unchanged |

The same offsets on protocol `2025-11-25`, default and Apps mode, give
the same results.

On a build of current master (same selection), `linkedin jobs` offset 4
and `czech real estate` offset 8 return `No Actors past offset …`.

Implemented with Claude Code (code, tests, mcpc probes, this
description). A separate Claude Code review pass found no issues.

Test/comment cleanup assisted by Codex; all five required checks pass
after the trim.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_011QiQRhqm3wKyv6KFLncAqG

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-ai Issues owned by the AI team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: search-actors treats an exhausted page as no matching Actors

4 participants