Repository navigation
Conversation
The writer was failing on long-form (~2000 word) blog runs with "Unterminated string in JSON at position 4590" because the model hit its per-response max_tokens budget while emitting one giant JSON-escaped string for `bodyMarkdown`. The Claude Agent SDK doesn't expose max_tokens on its Options type, so we can't bump the cap from the outside. Fix: change BlogDraftSchema's `bodyMarkdown` to accept either a single string OR an array of section markdown strings, with a Zod transform that joins paragraphs with a blank line. The downstream contract (`bodyMarkdown: string`) is unchanged — the transform runs inside runtime validation, so DB storage, dashboard rendering, GEO-Editor, Formatter, and the post-approval dispatcher all keep their existing shape. Also updates the writer prompt to encourage the array form for blog-html runs (one entry per H2 section) — keeps the worst single JSON string under ~3KB per entry instead of the post-as-one-blob 13KB that was truncating. Provider-agnostic: works under Anthropic and Ollama Cloud. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptoms: 2,000-word blog runs ended in workflow state "done" with no
actual draft produced. Root cause: writer hit `single-task invocation
exceeded 300s` mid-generation, GEO-Editor + Formatter cascade-skipped,
and pipeline-reporter + slack-digest still ran (triggerRule
"all_done"), which made the run appear successful.
Two changes:
- SINGLE_TIMEOUT_MS: 300_000 → 600_000. Sonnet 4.6 on a deep-technical
2K-word draft routinely lands at 4–6 min wall-clock; 300s clipped
legitimate completions. 600s gives long-form generation real room
while still failing fast on a genuinely hung model.
- Per-persona model pin (PERSONA_MODEL_PINS): writer now hard-pinned
to "claude-sonnet-4-6" regardless of `getModelForTier(tier)` and
regardless of `GMAESTRO_LLM_PROVIDER`. Flipping the global provider
env to ollama no longer silently downgrades the writer to Kimi
K2.6, which can't produce coherent 2K-word drafts.
Caveat: the SDK still reads global ANTHROPIC_BASE_URL, so this pin
assumes the user is on `GMAESTRO_LLM_PROVIDER=anthropic` (current
setup). A future change to env.ts can preserve Anthropic credentials
for pinned-model calls when the global provider is ollama.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The BlogDraft approval card's "Send via" picker only listed Reddit,
because PROVIDERS_BY_ARTIFACT.BlogDraft only had a reddit entry. An
internal blog post intended for the company's own static-site repo
had no in-app path to dispatch.
Adds a github entry as the FIRST provider in BlogDraft (so it wins
the auto-default when both reddit and github are connected). It
mirrors the existing ChannelVariant.github 2-step flow (commit file
→ open PR), pulling content directly from the BlogDraft's
bodyMarkdown / title / slug. Repo + branch + path defaults target
the Anvil marketing site; override per-draft via
proposed_action.metadata.{repo, branch, path, prTitle, prBody}.
Verified: approvals page picker now shows "GitHub PR" + "Reddit",
GitHub is auto-selected, approve button reads "Approve & send via
GitHub PR".
Note: this conflicts with a WIP variant of providers.ts (a
different worktree session had set BlogDraft: [] and routed via
Formatter+ChannelVariant). Coordinate with that session before
merging if both designs land at the same time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conductor and Manager nodes were stuck rendering "RUNNING" forever on
workflows that had clearly terminated, because aggregate() returns
"running" if ANY child is still "running" — and at least one child
specialist routinely lacks a persona_completed event:
1. The persona failed at exec/parse and the workflow function recorded
the failure to workflow_nodes but never emitted a persona_completed
event to the activity bus (e.g. writer timing out at 600s).
2. Skip-cascade dropped the started→completed pair for a mid-chain
node whose upstream errored.
Adds a sweep to deriveNodeStatuses: when workflow_done arrives, flip
any remaining "running" nodes to "done". The detailed per-node error
state is still surfaced via workflow_nodes / the popover; the DAG
overview just needed to reflect "this workflow is finished."
Verified on two real runs:
- 99eb6c9a (Reddit thread): conductor + content-mgr were "running"
because geo-editor never finished. Now: all "done" except the
correctly-skipped Formatter.
- f4276704 (deep blog): conductor + content-mgr were "running"
because writer timed out at 600s. Now: all "done" except the
correctly-skipped GEO-Editor + Formatter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the writer failing on long-form (~2000 word) blog runs with
Unterminated string in JSON at position 4590. Root cause: the model hit its per-response max_tokens budget while emitting one giant JSON-escaped string forbodyMarkdown. The Claude Agent SDK doesn't exposemax_tokenson itsOptionstype, so we can't bump the cap from the outside.Fix: change
BlogDraftSchema.bodyMarkdownto accept either a single string OR an array of section markdown strings, with a Zod transform that joins paragraphs with a blank line.The downstream contract (
bodyMarkdown: string) is unchanged — the transform runs inside runtime validation, so DB storage, dashboard rendering, GEO-Editor, Formatter, and the post-approval dispatcher all keep their existing shape. Inferred TypeScript output type stays asstring.Writer prompt updated to encourage the array form for
blog-htmlruns (one entry per H2 section). Keeps the worst single JSON string under ~3KB per entry instead of the post-as-one-blob ~13KB that was truncating.Provider-agnostic: works under Anthropic and Ollama Cloud.
Test plan
pnpm typecheckcleanWrite a deep technical blog post (~2,000 words) for the company at https://composio.dev/...) and confirm the writer produces a validBlogDraftend-to-end without the JSON-truncation erroroutreach_drafts/blog_draftstable still storesbodyMarkdownas a single text column (transform already joined)Touch points
lib/shared/schemas.ts—BlogDraftSchema.bodyMarkdownnowstring | string[]with join-on-transformlib/personas/prompts/writer.md— adds guidance + example showing the array formNotes
This PR contains ONLY the body-chunking fix. There's separate uncommitted WIP in this worktree (dashboard refactors, new content personas, etc.) which I left alone — that's another session's work.
🤖 Generated with Claude Code