Skip to content

1.9.79: a hosted call refuses answers with more JSON values than it decodes - #281

Merged
tracking202 merged 4 commits into
1.9.79from
1.9.79-mcp-value-guard
Oct 28, 2026
Merged

tracking202 merged 4 commits into
1.9.79from
1.9.79-mcp-value-guard

Conversation

@tracking202

@tracking202 tracking202 commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Why

The hosted MCP connector runs the p202 CLI built for js/wasm, several engines to an isolate. The hosted relay holds each call to 1 MiB of the install's answers, but an engine's memory follows the number of JSON values it decodes, not the bytes it reads. One MiB of {"a":0} grew an engine to 83 MiB, and 136.5 MiB on report_timeseries, which decodes its answer twice. Three such engines end the isolate, and every other call running in it fails too.

What changes

The engine now counts values before anything decodes them, and refuses past a bound.

  • One reader for every answer. api.readAnswer is the only place the client reads an answer's body. That covers data answers, error answers, and the capabilities and version answers. TestAnswerBodiesAreReadOnlyByReadAnswer holds the package to this: any other io.ReadAll or .Body read fails it.
  • The count covers the whole call. Under api.LimitAnswerValues, every answer of the call counts toward one bound, because a command that reads every page keeps each page's rows. crosstab and a list with its stats both make several requests.
    • Once the bound is passed, that answer and every later one are refused, and the call sends nothing more (a command that carries on past a failed step, such as a delete of several ids, changes nothing further).
    • A refused answer is never handed on.
    • An error answer keeps its status. Only its message goes unread, so a 500 still reads as a server error.
    • A write's refusal says the install accepted the write before its answer was refused (CLAUDE.md [Core] Update PHP version check for PHP 8 #13). The hint is one of three: look before calling the tool again when a change was made, "nothing that changes data was sent" for a write tool refused on a read, and how to ask for less, with what the tool takes, for a read.
    • A command that answers without a read it can do without (whoami's user record) succeeds with a stderr note saying part of the answer is missing and why.
  • Malformed bodies are counted, not trusted. The counter does not stop at a syntax error. A body the relay cut short, or one that is not JSON, is counted to its end. A number ends where JSON's grammar ends it, and a literal counts from its first letter. For any bytes at all, the count is never less than what json.Unmarshal, a Decoder loop or Decoder.Token builds (FuzzJSONValuesOverBoundsEveryDecoder). For a JSON document, it is exactly the values the document holds.
  • The refusal says how to ask for less. runMCPCall sets the bound for a hosted run. The refusal is a validation error (exit 1) whose hint says how to ask for less and lists what the tool takes. It reads like the hosted all refusal.
  • A call's arguments are bounded too: 1,000 values. Arguments are decoded as well, and each item of a list argument costs its command far more to parse than an answer value costs to decode. The largest tool takes 29 arguments, and the one list argument is capped at 100 items by its command; 1,000 values keep an engine within a few MiB of idle.
  • Unchanged:
    • p202 mcp on your own computer: the bounds are set only in a hosted run.
    • p202 search decodes no answer and reads no install.

The bound: 80,000, from measurement

All numbers below are the wasm engine's linear memory in MiB, measured in Node under the hosted relay's 1 MiB read budget. Repeats vary by 0.5–1.5.

Real answers. Compact JSON as the API sends it, per MiB:

Rows Values per MiB
Breakdown rows of zero traffic 78,500
Report rows 62,600
Click rows (every optional join empty) 57,500
Campaigns 31,400

At 60,000, 1 MiB of report rows, zero rows or zero-traffic hours was refused on 36 of 37 read tools. Some tools read more than they print, such as crosstab and a list with its stats, and those do read that much. At 80,000 and at 100,000, none of these real shapes was refused on any tool.

Worst shapes just under each bound. Each shape was swept from 40k to 99.9k values, with repeats. The worst seen at or below each bound:

Tool family ≤ 60k ≤ 80k ≤ 100k
render path (lists, gets, reports; campaign_list) 31.0 37.8 44.0
breakdown family (report_winners, crosstab) 31.0 38.0 42.5
report_timeseries (decodes twice) 43.5 54.6 64.0
  • 80,000 is the lowest round bound that no real answer within the budget reaches.
  • 100,000 costs 6–10 MiB more and admits no real answer that 80,000 refuses.
  • report_timeseries stays the highest family until its second decode goes. That needs printing without a tree and streaming the timeseries tail, which is a separate change.

Before and after, 1 MiB answers. All 37 hosted read tools were run against every shape. These are the tools the earlier measurements listed. R marks the new refusal; x3 and x1 mark exits the tool already had on that shape, unchanged.

tool campaign rows report rows zero rows click rows {"a":0} [0,…] "" values nested 40 keys {"a":{}}
campaign_list 26.0 → 26.0 31.0 → 30.0 28.5 → 28.5 25.5 → 25.0 83.2 → 17.0 R 56.0 → 17.0 R 45.0 → 17.0 R 53.5 → 17.0 R 38.0 → 17.0 R 80.2 → 17.0 R
click_list 25.5 → 26.0 31.5 → 31.5 27.5 → 28.5 26.5 → 25.5 84.6 → 17.0 R 56.5 → 17.0 R 45.5 → 17.0 R 52.5 → 17.0 R 38.8 → 17.0 R 84.0 → 17.0 R
report_breakdown 26.0 → 26.0 31.0 → 31.0 28.5 → 28.5 27.0 → 26.5 83.1 → 17.2 R 56.0 → 17.0 R 43.5 → 17.2 R 51.0 → 17.0 R 38.2 → 17.2 R 84.0 → 17.2 R
report_timeseries 25.5 → 25.5 31.0 → 31.0 32.5 → 30.5 29.5 → 28.0 136.5 → 17.0 R 59.5 → 17.0 R 53.5 → 17.0 R 53.1 → 17.0 R 47.5 → 17.0 R 130.0 → 17.0 R
report_winners 19.0 → 19.0 24.5 → 24.5 21.5 → 21.5 20.0 → 20.5 79.5 → 17.0 R 37.5 x1 → 17.0 R 41.0 → 17.0 R 47.5 → 17.0 R 31.5 → 17.0 R 81.0 → 17.0 R
report_crosstab 19.5 x3 → 19.5 x3 23.5 x3 → 24.0 x3 22.0 x3 → 22.0 x3 20.5 x3 → 20.5 x3 76.7 x3 → 17.0 R 39.0 x1 → 17.0 R 40.0 x3 → 17.0 R 45.5 x3 → 17.0 R 31.5 x3 → 17.0 R 78.0 x3 → 17.0 R
forecast 19.0 x1 → 19.0 x1 23.5 x1 → 24.5 x1 21.5 x1 → 21.5 x1 23.5 x1 → 23.5 x1 71.5 x1 → 17.0 R 165.5 x1 → 17.0 R 38.0 x1 → 17.0 R 45.0 x1 → 17.0 R 31.0 x1 → 17.0 R 73.2 x1 → 17.0 R

Across all 37 tools:

  • Real shapes: within each tool's own repeat noise, and none refused.
  • The six synthetic shapes: from 31–165.5 MiB down to 17.0–17.2 MiB, refused on 36 of 37 tools.
  • whoami: its one data read, the user record, is optional. It answers with user_name_unavailable and the reason.

Tests

  • Counter (internal/api/answer_values_test.go):
    • Nesting, strings holding brackets, commas, colons and escapes, every number form, empty containers, repeated members.
    • 17 malformed bodies: cut mid-name, mid-number and mid-escape; a stray byte ahead; an HTML page ahead; no commas; values run together; NDJSON; unmatched brackets; strings where keys belong; raw control bytes; invalid UTF-8.
    • The fuzz target above, run locally for 5 minutes (3.5M inputs) with no failure.
  • Client (answer_values_client_test.go):
    • An answer past the bound is refused and not handed on.
    • Answers count together and the refusal latches.
    • An error answer keeps its status, with its body undecoded.
    • A write's refusal says it was accepted.
    • The capabilities and version answers are counted.
    • The single-reader structural test.
  • Through the real mcp call path (cmd/mcp_hosted_values_test.go):
    • A hosted call against 1 MiB of {"a":0} is refused, prints nothing, and allocates 4.4 MB. A native call on the same answer prints all 130,000 rows and allocates 74 MB.
    • Two answers under the bound but over it together are refused (with_stats). Natively the same call succeeds.
    • Every hosted read tool, answered past the bound, is refused with nothing printed.
    • A write tool's refusal says to look first.
    • Arguments past 1,000 values are refused before any request. At 1,000 the command reads them, and natively the command does.
  • The engine itself (TestHostedEngineBoundsWhatACallDecodes, js/wasm) runs through run(), the relay's entry, with the host's fetch replaced. The js/wasm job runs it, along with the counter tests and the client-wiring tests (TestBound*, TestAnswerBodiesAreReadOnlyByReadAnswer).
  • Planted defects. 25 were planted one at a time, each restored and checked with cmp. Every one was caught by a named test, none by a build error. They were:
    • no bound set
    • per answer instead of per call
    • no latch
    • an over-bound answer handed on
    • a body read with io.ReadAll
    • capabilities not counted
    • the counter stopping at a stray byte
    • literals and numbers swallowing what follows
    • two wrong member-name rules
    • a refused body handed on
    • the chain check stopping early
    • no argument bound
    • a call that keeps sending after the bound, and capabilities asked for after it
    • a bulk loop's error losing the refusal's hint
    • an accepted write not recorded, a preview read as a write, either write hint swapped, and the partial-answer note dropped
    • the command's wrapper kept
    • the write wording dropped, and the write hint dropped

What ran, and what did not

Verification ladder (.claude/skills/p202-verify/scripts/verify.sh --changed), scope lines verbatim:

  actionlint PASS
  go         PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.27)
  golangci   PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.27)
  patterns   SKIP  no new or modified .php files still exist, so no lines were examined; nothing is uncommitted, so pass --since <ref> to select on a change that is already committed
  syntax     PASS

Also run:

Not run: a live install, an agent eval, or anything on the hosted relay or Cloudflare. In Node, under the relay's engine I/O, a refused call leaves the engine with exit 1 and the CLI's usual JSON error on stderr, the shape of every other tool error; the relay's own result shaping was not run.

https://claude.ai/code/session_01NdpvkiQugB24uxzUoVGxZC

…ecodes

The hosted relay holds each call to 1 MiB of the install's answers, and an
engine's memory follows the JSON values it decodes, not the bytes: 1 MiB of
{"a":0} grew a js/wasm engine to 83 MiB (136.5 on report_timeseries, which
decodes its answer twice), and three such engines end the isolate they
share. A hosted run now counts values before anything decodes them:

- api.readAnswer is the one reader of an answer's body in the client (data
  answers, error answers, the capabilities and version answers), held there
  by TestAnswerBodiesAreReadOnlyByReadAnswer. Under LimitAnswerValues it
  counts every answer of the call together, since a command that reads
  every page keeps each page's rows, and once the count passes the bound it
  refuses that answer and every later one. A refused answer is never handed
  on; an error answer keeps its status; a write's refusal says the install
  accepted the write first.
- The count does not stop at a syntax error: a body the relay cut short, or
  one that is not JSON, is counted to its end, and the count is never less
  than what json.Unmarshal, a Decoder loop or Decoder.Token build from any
  bytes (FuzzJSONValuesOverBoundsEveryDecoder; 3.5M inputs locally).
- runMCPCall sets the bound for a hosted run (80,000 values) and refuses it
  as a validation error that says how to ask for less and what the tool
  takes. It also bounds a call's arguments at 1,000 values: each item of a
  list argument becomes a flag, and 131,000 landing_page_code offers in
  256 KiB grew an engine to 588 MiB.
- `p202 mcp` on your own computer is unchanged.

80,000 is measured on the wasm engine. Compact real answers within 1 MiB
hold at most about 78,500 values (breakdown rows of zero traffic; report
rows 62,600, click rows 57,500, campaigns 31,400): 60,000 refused 1 MiB of
report rows on 36 of the 37 read tools. Under 80,000 the worst shapes left
an engine at 38 MiB, 54.6 on report_timeseries; 100,000 left 44 and 64 and
admits no real answer 80,000 refuses.

The new tests run in the js/wasm job too (TestHostedEngineBoundsWhatACallDecodes
through the engine's own entry).

Claude-Session: https://claude.ai/code/session_01NdpvkiQugB24uxzUoVGxZC
Copilot AI balanced review requested due to automatic review settings October 27, 2026 10:18
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T11:01:13.533577Z 4f6025d New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@github-actions

Copy link
Copy Markdown
Contributor

Code review: CLEAN

For cf487c4: 0 issue(s). The review is encrypted for the maintainers in the review-findings artifact of this run.

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Adversarial review: NON-BLOCKING

For 4f6025d: 3 finding(s). The report is encrypted for the maintainers in the review-findings artifact of this run.

Rewritten on every push; the check fails on BLOCKING.

Review finding on #281: the bound withheld answers but not requests. A
command that carries on past a failed step kept sending: campaign_delete
with several ids deleted every remaining id and refused each answer. Once a
call's answers pass the bound, sendRefusal now stops the client sending
anything more (data requests, the capabilities and version reads), with the
refusal naming the request that was not sent. The answer that passed the
bound still says a write was accepted before it was refused.

This also closes the two cases that hung on a later answer: a small error
answer after the latch losing its message and category, and a fallback taken
on "capability not supported" after the capabilities answer was refused.
Neither can now send a request.

TestHostedCallSendsNothingAfterTheBound runs the delete loop through
`mcp call`; the value-bound tests reset the flags they parse when they end.

Claude-Session: https://claude.ai/code/session_01NdpvkiQugB24uxzUoVGxZC
@tracking202

Copy link
Copy Markdown
Owner Author

Answers to the adversarial review of cf487c4 (run 40487413165):

F3, requests were still sent after the bound latched: fixed in fed894f.

  • What changed:
    • Once a call's answers pass the bound, the client sends nothing more. That covers data requests and the capabilities and version reads (sendRefusal).
    • The refusal names the request it did not send.
  • Evidence:
    • TestHostedCallSendsNothingAfterTheBound runs campaign_delete with ids: "1,2,3" through mcp call, against an install answering past the bound.
    • Now one DELETE reaches the install; before this change, three did. Stderr says DELETE campaigns/2 was not sent and DELETE campaigns/3 was not sent.
  • I planted the defect (the send check removed): TestBoundRefusesAnAnswerBeforeItIsDecoded, TestBoundCoversTheCapabilitiesAndVersionAnswers and TestHostedCallSendsNothingAfterTheBound fail.

F2, a refused error answer loses its message and the 402 licence category: the latch case is gone with F3.

  • After the bound passes, nothing is sent, so no later error answer can arrive.
  • What remains is an error answer that itself passes the bound. It keeps:
    • its status;
    • its category by status;
    • a message saying the body was not read (TestBoundKeepsTheStatusOfAnErrorAnswer).
  • An unlicensed install refuses every request, so its licence refusal is the call's first answer, which is read in full.
  • No further change.

F1, SupportsCapability answers false when capabilities were refused: no caller on a hosted tool acts on that false. I grepped every caller of SupportsCapability, Capability and CapabilitiesError:

  • requireFeature refuses on CapabilitiesError().
  • setup_code.go:56 (landing_page_code) takes its fallback only when CapabilitiesError() == nil.
  • enum_flags.go:143 reads value lists to validate flags, and sends nothing.
  • sync.go, shell.go and diff.go are not hosted tools.

With F3's fix, a refused capabilities answer also leaves the call able to send nothing. After a refused version answer, /api/versions is the only request the install sees (TestBoundCoversTheCapabilitiesAndVersionAnswers).

Q4, body readers outside package api.

  • In cmd/, only campaign_check_urls.go, conversion_verify.go and verify.go read an HTTP body.
  • Their commands are none of the MCP tools: campaign check-urls, conversion simulate, and tracker/rotator test, check and trace.
  • TestHostedReadToolsRefuseAnswersPastTheValueBound runs every hosted read tool through mcp call against an install answering past the bound. Each one is refused with nothing printed, which a tool decoding by another path would not be.

Q8, test placement.

  • TestBound* and TestAnswerBodies* use httptest listeners, which js/wasm cannot open.
  • The js job runs the counter, its fuzz seeds, and TestHostedEngineBoundsWhatACallDecodes through run(), with the host's fetch replaced.

All 19 planted defects are caught by a named test. The verification ladder after fed894f, with go and golangci run under go1.25.14:

  go         PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.27)
  golangci   PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.27)
  patterns   SKIP  no new or modified .php files still exist, so no lines were examined; nothing is uncommitted, so pass --since <ref> to select on a change that is already committed
  syntax     PASS

The claude-code-review report for cf487c4 said its agents had not finished, so it reviewed nothing; that run is being re-run.

…ook first

Second review of #281:

- A delete of several ids catches each id's error and answers with its own
  partial failure, which does not wrap the refusal, so the call lost the
  hint to look before deleting again. runMCPCall now gives a hosted call's
  error the refusal's hint whenever the call's answers passed the bound
  (api.AnswerBoundPassed); the error keeps its own category.
- The client-wiring tests (TestBound*, TestAnswerBodiesAreReadOnlyByReadAnswer)
  answered through httptest, which js/wasm cannot listen on, so the js job
  did not run them. They now answer through http.DefaultTransport, which
  the client sends through on both platforms, and the js job runs them.
- TestHostedRefusalWrappedByTheCommandIsStillTheRefusal holds the
  normalisation of a refusal a command wraps in another category (a staged
  write's capability check reports it as a network error).

Claude-Session: https://claude.ai/code/session_01NdpvkiQugB24uxzUoVGxZC
@tracking202

Copy link
Copy Markdown
Owner Author

Answers to the second adversarial review, of fed894f (run 40489025995):

2. A bulk loop's partial failure lost the "look first" hint: fixed in 774b394.

  • What changed: runMCPCall gives a hosted call's error the refusal's hint whenever the call's answers passed the bound (api.AnswerBoundPassed), even when the command answered with its own error.
  • The error keeps its own category. A delete of several ids still exits 5, because some ids did fail.
  • TestHostedCallSendsNothingAfterTheBound now asserts exit 5 and the hint Look with the matching get or list tool before calling campaign_delete again.
  • I planted the defect (the new branch removed) and that test fails.

3. The client-wiring tests did not run in the js/wasm job: fixed in 774b394.

  • Cause: they answered through httptest, which js/wasm cannot listen on.
  • What changed: they now answer through http.DefaultTransport. The client sends through it on both platforms; in the js build it goes through the hosted redirect wrapper first.
  • The js job's -run now includes TestBound|TestAnswerBodiesAreReadOnlyByReadAnswer.
  • Under go1.25.14 for js/wasm, all 13 internal/api tests in that filter pass, the four TestBound* among them.

1. A string argument a command splits, such as a delete's ids: not a decode cost, so out of this PR.

  • {"ids":"1,2,…"} is one value. parseIDList (cmd/crud.go:771) split 256 KiB into 37,447 ids and allocated 4.9 MB (measured).
  • What it adds is requests: one DELETE per id. That predates this PR, and it is bounded by the relay's subrequest limits, not by the engine's memory.
  • I logged it for a separate decision on capping ids per call.

Also added: TestHostedRefusalWrappedByTheCommandIsStillTheRefusal.

  • A staged write's capability check wraps a refused capabilities answer in a network error. The call is still reported as the refusal: validation, exit 1, the refusal's hint, and no write sent.
  • Before this test, nothing held that normalisation. Its plant had gone unnoticed once the hint branch above was added.

All 22 planted defects are caught by a named test. The verification ladder after 774b394:

  actionlint PASS
  go         PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.27)
  golangci   PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.27)
  patterns   SKIP  no new or modified .php files still exist, so no lines were examined; nothing is uncommitted, so pass --since <ref> to select on a change that is already committed
  syntax     PASS

…al answer says why

Third review of #281:

- The hint told every write tool the install may have made the change, also
  when nothing was sent (a staged write whose capability read was refused).
  The client now records when the refused answer was to a request that
  changes data (not a dry run, not a read sent as POST), and the call's hint
  is one of three: look before calling the tool again (a change was made),
  nothing that changes data was sent (a write tool refused on a read), or
  how to ask for less and what the tool takes (a read).
- A command that answers without a read it can do without (whoami's user
  record) exited 0 with nothing saying the bound was why. The call now adds a
  note on stderr, which a host shows beside the answer, naming the bound and
  the next step.

Claude-Session: https://claude.ai/code/session_01NdpvkiQugB24uxzUoVGxZC
@tracking202

Copy link
Copy Markdown
Owner Author

Answers to the third adversarial review, of 774b394 (run 40490721003):

4. A write tool was told the change may have been made even when nothing was sent: fixed in 4f6025d.

  • What changed: the client records when the refused answer was to a request that changes data. A dry run and a read sent as POST do not count.
  • The hint is now one of three:
    • "look with the matching get or list tool before calling it again": a change was made.
    • "Nothing that changes data was sent": a write tool refused on a read, such as the staged capability check in TestHostedRefusalWrappedByTheCommandIsStillTheRefusal, which now asserts it.
    • how to ask for less and what the tool takes: a read tool.
  • TestBoundSaysAWriteWasAcceptedBeforeItsAnswerWasRefused also holds a DELETE preview and a POST to goals/validate to "not a write".
  • I planted each branch out, and the named tests fail: 4 plants.

1. A command that swallows the refusal exits 0 with nothing saying why: fixed in 4f6025d.

  • What changed: when a hosted call succeeds after its answers passed the bound, it adds a stderr note. The note says part of the answer is missing and why, and gives the next step.
  • whoami (an optional user record) is the case in the tree. TestHostedReadToolsRefuseAnswersPastTheValueBound now asserts the note for it, and a plant that drops the note fails that test.

2. Refusals cached for the client's life: no change needed.

  • A Client is built per command, by api.NewFromConfig() in each command's RunE (161 call sites, no package-level client).
  • The hosted engine runs one command per instance: hosted_js.go, hostedArgsRefusal.
  • The bound is also reset at the start of each mcp call (LimitAnswerValues).

3. Download counts a file as JSON: not reachable from a hosted tool.

  • The only caller is attribution export download (cmd/attribution_export.go:201), and no MCP tool maps to it.
  • With no bound set (every native run), countAnswer returns at once, so the strict size check is unchanged there.

Q9, the relay as a reader of the new error.

  • In Node, with the hosted relay's engine I/O, a refused call leaves the engine with exit 1 and the CLI's JSON error envelope on stderr. Every other tool error has the same shape. That was measured on all 37 read tools.
  • The relay's own result shaping was not run. The description now says so.

25 planted defects are caught, each by a named test. The verification ladder after 4f6025d:

  go         PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.27)
  golangci   PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.27)
  patterns   SKIP  no new or modified .php files still exist, so no lines were examined; nothing is uncommitted, so pass --since <ref> to select on a change that is already committed
  syntax     PASS

@tracking202
tracking202 merged commit 265789d into 1.9.79 Oct 28, 2026
12 checks passed
tracking202 pushed a commit that referenced this pull request Oct 28, 2026
#280's traffic source and category update and delete tools sit beside
the text ad writes in the catalogue; text_ad_list takes #279's hosted
omission of all. The catalogue budget goes to 88,000 characters: #280's
tools and the text ad writes together come to about 87,800. The docs
keep #281's hosted read bounds, then the text ad reconnect note; the
changelogs keep 1.9.79's lines and put the text ad lines after them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
tracking202 pushed a commit that referenced this pull request Oct 28, 2026
…lue bound

When an update sets the type or a link, text_ad_update reads the stored
ad before it writes. Under #281's bound an answer past it there stops the
call before the PUT, and the refusal says nothing that changes data was
sent; the test holds the call to that one GET.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
tracking202 pushed a commit that referenced this pull request Oct 28, 2026
…start IPN branch

GET /users/{id}/preferences now serves every integration secret as its
stored state (#270), and the PayKickstart IPN keys are one more:
paykickstart_ipn_secret_keys is on ServedPreferences::SECRET, answered
{"stored": true}, and the count this branch served in its place
(paykickstart_ipn_keys_stored, PayKickstartKeys::withoutKeys()) is gone.
GET /system/integrations keys_stored still says how many. The
PreferenceSecretsAreNotServedIntegrationTest round trip stores the keys
with the other secrets.

Conflicts: changelogs (1.9.79's lines, then this branch's), the
preferences docs and the OpenAPI schemas (1.9.79's, with the keys added
as a StoredSecret), and the controller (1.9.79's).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
tracking202 pushed a commit that referenced this pull request Oct 28, 2026
…or_get answers

#281 holds a hosted call's arguments to 1,000 JSON values, because each
item of a list argument becomes a flag of its own. rotator_set_rules takes
its rules as one JSON document, written to its command as one flag, and has
no per-rule tool to fall back on: at 1,000 values a hosted call could send
back about 30 rules of one criterion and two destinations, where rotator_get
answers about 100 under the result bound. A tool that takes a JSON document
is held to 5,000 values instead; at that bound the call allocated 5.5 MB, the
list arguments at theirs 5.1. Past it the refusal says what to leave out and
which command sends a larger set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
tracking202 pushed a commit that referenced this pull request Oct 28, 2026
…JVZoo branch

#268 now dates a notification's row by the provider's own time, held to the
click for a sale and to the sale for a refund, and brings 1.9.79's #270
(ServedPreferences: GET preferences serves each secret as {"stored": ...}),
#280, #281 and #282.

Conflicts: both changelogs keep the base's lines with the JVZoo line after
them; the OpenAPI preferences update takes #270's oneOf shape, with the
JVZoo key's rule in its string branch; the eval cases keep #268's admin-005
notes and add admin-006 after it. jvzoo_ipn_secret_key is already on
ServedPreferences::SECRET, and JVZoo adds no other column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
tracking202 pushed a commit that referenced this pull request Oct 28, 2026
…79's #270, #280, #281, #282) into the ClickBank branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr

# Conflicts:
#	202-config/changelogs.json
#	changelogs.txt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants