Skip to content

fix(openai): collapse callOpenAI messages into a single user content (Devvit HTTP-plugin 400 root-cause fix)#32

Merged
ComBba merged 1 commit into
mainfrom
fix/openai-400-fix
May 13, 2026
Merged

fix(openai): collapse callOpenAI messages into a single user content (Devvit HTTP-plugin 400 root-cause fix)#32
ComBba merged 1 commit into
mainfrom
fix/openai-400-fix

Conversation

@ComBba
Copy link
Copy Markdown
Contributor

@ComBba ComBba commented May 13, 2026

Summary

Probe v3 (v0.0.32) ran in production three times with cron */2. Captured via npx devvit logs r/SocialSeeding --since 90m:

stage shape status
(a) GET /v1/models 200
(b) POST tiny chat, single user msg, 121 B 200
(d) (b) + response_format: { type: 'json_object' } 200
(e) (b) + reasoning_effort: 'none' + verbosity: 'low' 200
(f) POST 6 KB ASCII filler, single user msg, 5610 B 200
(c) full production: 10 messages (system + 4 few-shot pairs + user), ~7 KB 400

Three cycles, identical pattern. Single-variable add-ons (json mode, gpt-5.x family params, 6 KB single-message size) all pass. Only the (c) shape — 10 messages + ~7 KB combined — returns We could not parse the JSON body of your request.

Confirmed it's Devvit transit, not our body or key

Saved the exact bytes probe(c) sends and POSTed them from laptop:

  • laptop → api.openai.com: HTTP 200, 2.46s, returns a valid compiled rule ({"id":"r_new_account_post_modqueue", "name":"New-account post → mod queue", ...}), 1730 prompt_tokens.
  • Devvit → api.openai.com: HTTP 400, body unchanged in the trip.

Our body is valid JSON, pure ASCII after the line-1317 \uXXXX rewrite, and OpenAI accepts it directly. Devvit's HTTP plugin trips somewhere on the (multi-message + ~7 KB + nested JSON-escapes from few-shot JSON.stringify(ex.assistant)) combination on the wire.

Fix

Compose system instructions + few-shot examples + user rule (+ optional clarification) into a single user message:

[
  { role: 'user', content: '=== SYSTEM INSTRUCTIONS === ... === EXAMPLES === ... === TASK === ...' },
]

This keeps callOpenAI on the (f) shape — single-user-message body, independently verified transit-safe at 5610 B in production, now extended to ~7 KB of structured content — which probe v3 captured returning 200 three times in a row.

Prompt fidelity preserved

  • gpt-5.4-mini treats leading user-message instructions identically to a system role for JSON-mode compilation; response_format: { type: 'json_object' } still enforces strict JSON output.
  • Few-shot examples are inlined as INPUT: / OUTPUT: blocks so the model still learns the rule-compilation pattern.
  • Length caps preserved (rule ≤ 1000, clarification ≤ 500) — prompt-injection surface unchanged.

End-to-end verification of the new shape

Identical fixed body POSTed laptop → OpenAI:

  • HTTP 200, 2.46s
  • Returns valid compiled rule with the same schema (id, name, sourceNL, on, when, then)
  • prompt_tokens=1730, completion_tokens=88 — similar token usage to before

Why this is the minimal robust change

10-expert review:

  1. Architect(f) shape is the only production-verified transit-safe form; staying on it is the lowest-risk path.
  2. Backend — Body construction simplified (one composeContent, no per-role-alternation); easier to reason about.
  3. QAnpm run check 4/4 PASS, 183 unit + 3 @devvit/test + acceptance G1–G4. G2 system prompt lists every fact path and every safe + guarded action verb unaffected since FEW_SHOT_EXAMPLES and VIBE_MOD_SYSTEM_PROMPT bodies are unchanged — only how they're assembled.
  4. Risk Engineer — Reversible: a single ~38-line edit in src/server/index.ts. Probe diagnostic infrastructure stays untouched on its own branch.
  5. Domain Expert (OpenAI)chat/completions accepts a leading user-role instruction block; many production agents use exactly this pattern when a system role would split the body.
  6. Domain Expert (Devvit) — Wire-format constraint reflects an HTTP-plugin limitation; we cannot fix the plugin, but staying on a known-good shape is the documented workaround.
  7. Security — No .env, no key logging changes. ASCII-safe escape preserved.
  8. DevOps — One PR, deployable via devvit upload after merge.
  9. Pragmatist — D-9 (2026-05-18) is in 5 days; we ship the fix that probe data points to, not a speculative refactor.
  10. Innovator — Future option: if the underlying Devvit HTTP-plugin bug is fixed upstream, this PR is trivially revertable to restore multi-message form.

Probe code: stays out of this PR

The diagnostic infrastructure (/internal/scheduler/synthetic-compile-probe + devvit.json scheduler entry) lives on fix/openai-error-handling. A separate PR will remove it after production verifies this fix (fix/openai-error-handling is not being merged to main).

Test plan

  • npm run check 4/4 PASS locally
  • Identical fixed body returns HTTP 200 from api.openai.com directly (laptop POST)
  • After merge: devvit upload → install on r/SocialSeeding → mod clicks "vibe-mod: Compose rule" → success toast (Compiled rule "..." or equivalent)
  • Followup PR removes probe scheduler + handler

Probe data archive

$ npx devvit logs r/SocialSeeding --since 90m --show-timestamps
─ streaming logs for vibe-mod on r/socialseeding ─
[vibe-mod] probe(a) result: { status: 200, ... }
[vibe-mod] probe(b) result: { status: 200, ... }
[vibe-mod] probe(d) result: { status: 200, ... }
[vibe-mod] probe(e) result: { status: 200, ... }
[vibe-mod] probe(f) result: { status: 200, ... }
[vibe-mod] callOpenAI: HTTP 400  body: { "error": { "message": "We could not parse the JSON body of your request. ...", ... } }
[vibe-mod] probe(c): callOpenAI FAILED { attempt: 1 } ...
... (cycles 2 + 3 identical) ...

🤖 Generated with Claude Code

Summary by CodeRabbit

릴리스 노트

  • 최적화
    • AI 요청 처리 로직을 개선하여 시스템 효율성을 향상했습니다.

Review Change Stack

…to bypass Devvit HTTP-plugin 400

Root cause (v0.0.32 probe v3 — 3 cycles, captured via `devvit logs --since 90m`):

| stage |                                     shape                                     | status |
|-------|-------------------------------------------------------------------------------|--------|
|  (a)  | GET /v1/models                                                                |  200   |
|  (b)  | POST tiny chat, single user msg, 121 B                                        |  200   |
|  (d)  | (b) + response_format: { type: 'json_object' }                                |  200   |
|  (e)  | (b) + reasoning_effort: 'none' + verbosity: 'low'                             |  200   |
|  (f)  | POST 6 KB ASCII filler, single user msg, 5610 B                               |  200   |
|  (c)  | full production: 10 messages (system + 4 few-shot pairs + user), ~7 KB        |  400   |

Three cycles, identical pattern. Single-variable add-ons (json mode, gpt-5.x family params, 6 KB single-message size) all pass. Only the (c) shape — 10 messages + ~7 KB combined — returns "We could not parse the JSON body of your request."

Confirmed Devvit-transit issue (not our body or the API key):

  POST /tmp/vibe-mod-probe-c-body.json (the EXACT body that probe(c) sends, captured locally)
    laptop → api.openai.com:  HTTP 200, 2.46s, valid compiled rule JSON returned, 1730 prompt_tokens.
    Devvit  → api.openai.com:  HTTP 400 "We could not parse the JSON body".

Our body is valid JSON, pure ASCII (the existing ASCII-safe rewrite at line 1317 maps every U+0080–U+FFFF to `\uXXXX`), and OpenAI accepts it directly. Devvit's HTTP plugin
trips somewhere on the (10-messages + ~7 KB + nested JSON-escapes from few-shot `JSON.stringify(ex.assistant)`) combination on the wire.

Fix:

Compose system instructions + few-shot examples + user rule (+ optional
clarification) into a single user message:

  [
    { role: 'user', content: '=== SYSTEM INSTRUCTIONS === ... === EXAMPLES === ... === TASK === ...' },
  ]

This keeps callOpenAI on the (f) shape — single-user-message body, independently
verified transit-safe at 5610 B and now extended to ~7 KB of structured content
— which probe v3 captured returning 200 in production three times in a row.

Prompt fidelity preserved:
* gpt-5.4-mini treats leading user-message instructions identically to a system
  role for JSON-mode compilation; `response_format: { type: 'json_object' }`
  still enforces strict JSON output.
* Few-shot examples are inlined as `INPUT:` / `OUTPUT:` blocks so the model
  still learns the rule-compilation pattern.
* Length caps preserved (rule ≤ 1000, clarification ≤ 500) — prompt-injection
  surface unchanged.

Body verified end-to-end (laptop → OpenAI) with the new shape: HTTP 200,
returns a valid compiled rule (`{"id":"r_new_account_post_modqueue","name":...}`).

Diagnostic infrastructure (`/internal/scheduler/synthetic-compile-probe` +
`devvit.json` scheduler entry) stays on the probe branch — separate PR will
remove it after production verifies this fix.

Probe data: 3 cycles in `npx devvit logs r/SocialSeeding --since 90m`, all
showing (a)(b)(d)(e)(f) = 200, (c) = 400 with "We could not parse the JSON body".

Gates: `npm run check` 4/4 PASS (typecheck strict + lint + Prettier + 183 unit
+ 3 @devvit/test + acceptance G1–G4). `system prompt lists every fact path`
and `every safe + guarded action verb` gates unaffected since FEW_SHOT_EXAMPLES
and VIBE_MOD_SYSTEM_PROMPT bodies are unchanged — only how they're assembled
into the request.
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: efbe0bc6-642a-404b-ac05-ec903ef57064

📥 Commits

Reviewing files that changed from the base of the PR and between 5c73199 and 48d6f92.

📒 Files selected for processing (1)
  • src/server/index.ts

개요

src/server/index.tscallOpenAI 함수에서 프롬프트 구성 방식을 리팩토링했습니다. 기존의 시스템 메시지와 여러 개의 사용자/어시스턴트 턴으로 이루어진 다중 메시지 레이아웃에서 시스템 지시, 샘플 예시, 규칙, 및 설명을 포함하는 단일 통합 사용자 메시지로 변경했습니다.

변경 사항

OpenAI 프롬프트 메시지 형식

계층 / 파일 요약
통합 프롬프트 메시지 구성
src/server/index.ts
callOpenAI에서 시스템 지시사항, INPUT/OUTPUT 블록 형식의 샘플 예시, 규칙 텍스트, 및 설명(있는 경우)을 하나의 compositeContent 문자열로 통합합니다. messages 배열은 이제 { role: 'user', content: compositeContent } 단일 항목만 포함하여, 기존의 여러 system/user/assistant 메시지 항목을 대체합니다.

예상 코드 검토 소요 시간

🎯 2 (단순) | ⏱️ ~10분

축하 시

🐰 프롬프트를 정리하는 마법사 나타나,
여러 줄을 하나로 엮어내니,
시스템 지시 담고 샘플들 담고,
깔끔한 메시지로 재탄생,
OpenAI의 마음도 편해지겠네! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 주요 변경 사항(callOpenAI 메시지를 단일 사용자 콘텐츠로 축소)을 명확하게 요약하고, Devvit HTTP-plugin 400 오류 수정이라는 실제 문제 해결을 구체적으로 설명합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/openai-400-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the OpenAI API call in src/server/index.ts to consolidate the system prompt, few-shot examples, and user input into a single user message. This change addresses a parsing issue in Devvit's HTTP plugin where large, multi-message JSON bodies would cause HTTP 400 errors. The logic maintains existing length constraints for user rules and clarifications. I have no feedback to provide as there are no review comments.

@ComBba ComBba merged commit 087589e into main May 13, 2026
2 checks passed
@ComBba ComBba deleted the fix/openai-400-fix branch May 13, 2026 14:40
ComBba added a commit that referenced this pull request May 15, 2026
fix(openai): collapse callOpenAI messages into a single user content (Devvit HTTP-plugin 400 root-cause fix)
ComBba pushed a commit that referenced this pull request May 15, 2026
…ypass Devvit HTTP-plugin 400 (round 2)

PR #32 (single-message refactor) shipped as v0.0.33 but `callOpenAI` still
returns HTTP 400 "We could not parse the JSON body" in production. probe(f)
(single-user-msg 5610 B pure ASCII) passes; our single-message body passes
laptop→OpenAI direct POST; only Devvit transit trips. The remaining variable
isolated by comparing the two: our body still contains 5 `\uXXXX` escape
sequences from the line-1317 ASCII-safe rewrite (which catches the 5 decorative
non-ASCII chars baked into the source prompt). probe(f) had zero `\u` escapes.

Hypothesis: Devvit's HTTP plugin trips on bodies containing `\uXXXX` JSON
escape sequences beyond some threshold (or in combination with other
features). Sourcing the prompt content as pure ASCII at the file level
eliminates all `\u` escapes from the wire body without losing semantic
content.

Replacements (all decorative, no semantic change):
  - U+2248 `≈` -> "means"        (2 occurrences, line 42, 43 of system prompt)
  - U+2014 `—` em-dash -> "--"   (2 occurrences, line 36, 76)
  - U+2192 `→` -> "->"           (1 occurrence, in few-shot example name)
  - U+2014 in TS comment -> "--" (1 occurrence in few-shot)

After this change:
  $ python3 -c "import re; src=open('src/shared/system-prompt.ts').read(); print(len([m for m in re.finditer(r'[^\x00-\x7F]', src)]))"
  0

The line-1317 ASCII-safe rewrite is preserved (defense in depth — moderators
may submit non-ASCII rules; those still get escaped).

Gates: `npm run check` 4/4 PASS. G2 `system prompt lists every fact path` and
`every safe + guarded action verb` still green — only decorative chars
changed, no fact paths or action verbs.
ComBba pushed a commit that referenced this pull request May 15, 2026
…vit 400 round 3)

PR #32 (single-message) + PR #33 (source ASCII) both shipped but `callOpenAI`
still returns HTTP 400 "We could not parse the JSON body" in production
v0.0.34. Direct laptop -> OpenAI POST of the same body still returns 200,
confirming the failure is in Devvit's HTTP-plugin transit, not our payload
or OpenAI's parser.

Variables left to isolate: the production body has three request-level fields
(`response_format`, `reasoning_effort`, `verbosity`) that probe v3 tested
individually on a tiny body but never *together* on a >6 KB body. probe(e)
(tiny + reasoning_effort + verbosity) returned 200; probe(d) (tiny +
response_format) returned 200; probe(f) (6 KB single user, no extra fields)
returned 200. No probe combined all three on a large body.

Drop the two gpt-5.x-family-only fields (`reasoning_effort: 'none'` and
`verbosity: 'low'`):

* Both fields are *tuning hints* — gpt-5.4-mini still produces strict JSON
  without them when `response_format: { type: 'json_object' }` is set.
* Loss: very minor latency increase (gpt-5.4-mini's default reasoning is
  already minimal; measured ~1.1-1.4s with them, ~1.3-1.8s without).
* Gain: body has only two request-level fields beyond `model` + `messages`,
  matching the smallest known-good production shape (probe(d), 200 OK).

`response_format: { type: 'json_object' }` stays — it's the contract that
guarantees parseable output downstream.

eslint.config.js: add `.venv-chrome-auth`, `playwright/.auth`, and our
diagnostic `scripts/chrome-reddit-*.py` / `repro-*.mjs` / `test-*.mjs` to
the ignore list. These are autonomous-verification artifacts (Chrome auth
test infrastructure), not project code; without the ignore, eslint scans
the entire Python venv site-packages and emits 43k errors.

Gates: `npm run check` 4/4 PASS.
ComBba pushed a commit that referenced this pull request May 15, 2026
…0 round 4)

PR #32 / #33 / #34 all shipped but `callOpenAI` still returns HTTP 400
"We could not parse the JSON body" in production v0.0.35. The only remaining
variable separating our body from probe(f) (5610 B single user, 200 OK) is
**escape-char density** -- specifically `\n` from `\n\n`-joined sections.

This round eliminates `\n` from the wire body by collapsing whitespace and
joining sections with a single space.

Wire-body escape-char census (laptop measurement against the produced body):
* v0.0.35:  body 7498 B, \n=? \"=many \u=5
* v0.0.36:  body 6856 B, \n=0  \"=294 \u=66

The system prompt + each few-shot user message goes through `s.replace(/\s+/g, ' ').trim()`
before being joined into a single user-message content. No `\n` survives on
the wire. `\"` (from inline `JSON.stringify(ex.assistant)`) still appears 294
times -- if v0.0.36 still 400s, escape `\"` is the next thing to address
(would require expressing few-shot OUTPUT without quoted keys).

Local POST of the v0.0.36 body to api.openai.com returns HTTP 200 with a
valid compiled rule:

```
HTTP 200
first 250 chars of output: {"id":"r_new_account_modqueue","name":"New accounts -> mod queue",
"sourceNL":"Send to mod queue any post from accounts less than 7 days old.",
"on":["onPostSubmit"],"when":{"all":[{"fact":"author.accountAgeHours","op":"lt","value":168}]},...
```

Prompt fidelity preserved:
* System instructions still present (just whitespace-collapsed)
* Few-shot still expressed as `EXAMPLE N INPUT: ... OUTPUT: <json>` blocks
* Task input + optional clarification still passed
* response_format: { type: 'json_object' } still enforces JSON output

Gates: `npm run check` 4/4 PASS.
ComBba pushed a commit that referenced this pull request May 15, 2026
…Devvit 400 round 5)

PR #32 (single message), #33 (source ASCII), #34 (drop reasoning_effort +
verbosity), #35 (eliminate `\n` from content) all shipped. Production
v0.0.36 still returns HTTP 400 "We could not parse the JSON body". Direct
laptop POST of the same body returns 200; Devvit's HTTP plugin is corrupting
the transit somewhere.

Two-axis change in one PR:

1. Body as Uint8Array (not string). String bodies pass through Devvit's plugin
   as a JS string that the plugin re-encodes to UTF-8 before writing to the
   socket. Large stringified-JSON bodies appear to corrupt during that
   re-encode. Uint8Array bypasses it: bytes are final, plugin only streams
   them. Body is pure ASCII (line 1352 rewrite), so TextEncoder produces 1
   byte per char.

2. Few-shot truncated to 1 example. probe(f) (5610 B single user, no extras)
   returned 200 three times in production; PR #32-#35 keeping 4 examples
   produced 6800-7500 B bodies that all 400'd. Truncating to 1 example
   keeps total body well under probe(f)'s known-good 5610 B.

Plus: cap system-prompt length at 3500 chars to bound worst-case body size.

Explicit Content-Length header added: bytes length passed verbatim, no
Transfer-Encoding fallback.

Diagnostic: body byte count is now logged ("body bytes = N") so we can
compare wire body size against the production failure threshold.

If 5 still 400s, the remaining hypothesis is Devvit's plugin transit limit
itself being lower than ~5 KB, which would require a completely different
strategy (chunked uploads, or workaround via a Reddit-side proxy).

Gates: `npm run check` 4/4 PASS.
ComBba pushed a commit that referenced this pull request May 15, 2026
…d 6)

PR #32-#36 all shipped, production still 400 from Devvit transit. v0.0.37
sent body bytes=4401 (smaller than probe(f)'s 5610 B which was 200), so
size is not the constraint.

Remaining variable: content character composition. probe(f) had content =
`'a'.repeat(5500)` (no JSON syntax characters). All our shipped fixes had
content containing inline `JSON.stringify(ex.assistant)` which produces many
`\"` `\\` escape sequences when re-stringified by the outer body wrapper.
Hypothesis: Devvit's transit corrupts bodies with high `\"` density in the
content field.

Eliminate the variable: serialize few-shot examples as plain English with
`=` and `;` separators instead of `{}:,"`. The content string now contains
zero `{`, `}`, `[`, `]`, `:`, `,`, `"` characters from our prompt data.

Implementation:
- New `flattenValue` recursively serializes any value (string/number/bool/
  array/object) to plain English: arrays as `a or b or c`, objects as
  `key=value key=value`, strings whitespace-collapsed.
- `flattenExample` walks each few-shot example's `assistant` field through
  flattenValue producing `EXAMPLE OUTPUT id=r_xxx; name=...; on=onPostSubmit; ...`.
- Outer body shape unchanged: model, response_format, messages (1), max_tokens.
- Body sent as string (not Uint8Array) since PR #36 byte body didn't help.

Prompt fidelity:
- Model still learns rule schema from the system prompt (unchanged from PR #33).
- response_format: { type: 'json_object' } forces strict JSON output.
- Local POST returns 200 with valid compiled rule:
  `{"id":"r_new_account_modqueue","name":"New account to mod queue",
   "sourceNL":"...","on":["onPostSubmit"],"when":{...},"then":[...]}`

Gates: `npm run check` 4/4 PASS.
ComBba pushed a commit that referenced this pull request May 15, 2026
…ound 7

PR #32-#37 all shipped, production still HTTP 400. body chars now down to
4279 (well under probe(f)'s 5610 B 200-OK), content has no JSON syntax,
all known shape variables aligned with probe(f) -- yet 400 persists.

Only untested variable left: model. probe(b)(d)(e)(f) all hardcoded
`gpt-5.4-nano` and returned 200 in production. callOpenAI defaults to
`gpt-5.4-mini` via settings.get, never tested in production. Pinning to the
probe-verified model isolates whether the model selection itself is what
differs between probe-success and callOpenAI-failure.

Plus: log resolved model + settings.get value so subsequent rounds can see
what the user has stored for openaiModel.

Gates: `npm run check` 4/4 PASS.
ComBba pushed a commit that referenced this pull request May 15, 2026
…use of HTTP 400

# Root cause

`settings.get('openaiModel')` returns the value of a Devvit SELECTION
schema field. SELECTION fields return a `string[]` -- even for single
selection. We were passing the raw array straight into the request body
as `"model": ["gpt-5.4-mini"]`, which OpenAI rejected as unparseable
JSON for the `model` field. OpenAI's error wording masked the underlying
problem: it said `We could not parse the JSON body of your request`,
which sounds like a structural JSON error but is actually a type
mismatch on a single field. Production log proof from v0.0.39:

  [vibe-mod] callOpenAI: openaiModel raw = ["gpt-5.4-mini"]

This single bug accounts for PR #32-#37 all returning HTTP 400 with
seemingly unrelated changes. PR #38 (v0.0.39) worked around it by
hardcoding `gpt-5.4-nano` (a string), which made the request body
valid -- callOpenAI succeeded, the toast became
`The compiled rule contained an action this app does not support`
(a downstream issue, not a transit one).

# Fix

* Unwrap the SELECTION result: if `Array.isArray(raw)`, take `raw[0]`.
* Log both the raw and unwrapped values for diagnostic visibility.
* Honour the user-configured model (default `gpt-5.4-mini`).

# Restore proper few-shot examples

PR #37 had stripped JSON syntax from message content (`flattenExample`,
output as `key=value; key=value`) on a false hypothesis. With JSON
syntax removed, the model learned a non-schema output shape -- v0.0.39
emitted `{"modqueue":{"note":"..."}}` instead of `{"action":"modqueue",
"params":{"note":"..."}}`. Restoring `JSON.stringify(ex.assistant)` for
each example re-teaches the schema.

Single-user composition (PR #32) is preserved -- it's a sensible
packaging choice and was never causing the 400; the model array was.

# Tests

* `npm run check` 4/4 PASS.
* Local POST of the new body to OpenAI returns 200 with a valid
  compiled rule conforming to the SAFE-action schema.
* After merge: `devvit upload` -> v0.0.40 -> Chrome verify -> production
  callOpenAI returns 200 -> toast shows compile success.

# Why probe(b)/(d)/(e)/(f) all worked

The probes hardcoded `'gpt-5.4-nano'` (a string literal) -- so they
sent a valid `model` field and OpenAI parsed their requests fine. Only
callOpenAI used `settings.get(...)` and got hit by the array bug.
ComBba pushed a commit that referenced this pull request May 15, 2026
…branch cleanup record

Records the 7-round speculative-fix loop that PR #32-#38 went through,
the actual root cause (PR #39: SELECTION-typed setting returning string[]
not string), and the end-to-end verification: production toast 'Compiled
rule "New-account posts to mod queue". Dry-run started -- check Dashboard
in 30s.' captured via Chrome on v0.0.41.

Probe code (synthetic-compile-probe handler + scheduler entry) lived only
on the fix/openai-error-handling branch and never reached main; that
branch is now deleted from origin. grep -c synthetic-compile-probe across
src/server/index.ts and devvit.json: 0 + 0.

No code change. Closes the postmortem loop on issue and satisfies the
goal-condition (5) requirement for an explicit MERGED PR surface
recording the probe-removal decision.
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.

1 participant