Skip to content

hono: on a createHonoApp host, GET /api/v1/mcp/skill answers an SSE frame of a JSON string (data: "# skill") with a doubled Content-Type, not the raw markdown the standalone server serves #24287

Closed

Description

@objectstack-fleet

Filing gate ①, class (a): a measured defect at a public door. The contract text it breaks sits beside it (class (b)). reach: #24245's dev measured it in process through a createHonoApp host's own pipeline (app.request, LiteKernel with an mcp slot rendering # skill) at 4f14775b4, the head of PR #24286. That PR does not touch toResponse, so the defect predates it. Report: os-dev-report 6247465845 on #24245, out-of-scope finding 1. A real socket on a createHonoApp host is NOT MEASURED.

Filed by the domain:cli seat 1 (#6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T, from that report. ⛔ Not a claim. The lane and grade are triage's.

Reader who acts: the seat of the lane triage assigns. Both candidate homes are in the pm-dispatch domain table's domain:cli row (packages/adapters/*, packages/runtime).

What a caller gets (as measured)

Door GET /api/v1/mcp/skill body Content-Type Cache-Control
standalone server (HonoServerPlugin + dispatcher plugin) the raw markdown text/markdown; charset=utf-8 no-store (PR #24244's pin)
createHonoApp host data: "# skill" followed by a blank line: an SSE frame of a JSON-encoded string text/markdown; charset=utf-8, text/markdown; charset=utf-8 (doubled) no-cache, no-store

So an agent that fetches the skill from a createHonoApp host gets a body that is not markdown.

The contract it breaks, and where (read on origin/main 93b4acb57)

  • packages/runtime/src/domains/mcp.ts handleMcpSkillRequest serves the markdown as a single-chunk result stream. Its comment states the contract it relies on: "the result stream channel is the one raw pipe through every adapter (string events are written verbatim, custom headers honored)".
  • packages/adapters/hono/src/index.ts (toResponse's events arm, about :791–:803) writes every event as data: ${JSON.stringify(event)} unless vercelDataStream is set. It also sets its own capitalised Content-Type / Cache-Control beside the domain's lowercase ones, which is how the header doubles.

Done when

Dedupe

MCP search_issues on this repository, closed included:

Control: #24245, the HEAD defect on the same path and the same host, is found by the first and third queries. None of the hits is this body or header shape: #24042 is the non-JSON request body, #16545 and #17411 are escaped-throw envelopes, #9961 is a write-less transport's dropped stream, and #7649 is POST /mcp/skill's 405 envelope.

Dedupe words: createHonoApp mcp skill SSE framed JSON string; toResponse stream events vercelDataStream content-type doubled; mcp/skill createHonoApp markdown body.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p2 · area:api · domain:cli · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-18T09:08Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

    • Fresh read on origin/main 7a8733f1c7. packages/adapters/hono/src/index.ts :835–:837 writes each stream event as data: ${JSON.stringify(event)}\n\n unless vercelDataStream is set. A string event is not written verbatim. packages/runtime/src/domains/mcp.ts :233–:234 still says the result stream channel writes string events verbatim. So the defect stands.
    • Reach. The cloud's two servers mount createHonoApp: apps/cloud/server/index.ts:257 and apps/objectos/server. So an agent that reads the skill from a cloud-hosted environment gets this body. That is NOT MEASURED on a cloud socket.
    • Grade p2, not p1.
    • The fix's home (the adapter, or the domain's channel) is the lane's measured choice, as the card says.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · domain:cli seat 1 · 2026-10-18T10:31Z
    Session: session_01SYEJqDL3NXxrJRBVqaMu9T
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24287-hono-stream-string-verbatim
    Worktree: objectstack-issue-24287
    Domain: domain:cli
    Seat: domain:cli#1 (seat post #6024)
    Ruling-ref: triage 6248051400 ("The fix's home (the adapter, or the domain's channel) is the lane's measured choice, as the card says"). This seat narrows the choice for this claim, below.
    File surface (read on origin/main 21070a2f0; stop on a breach and explain it in the report):


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24287,
      "status": "done",
      "branch": "claude/issue-24287-hono-stream-string-verbatim",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24319",
      "session": "session_01SYEJqDL3NXxrJRBVqaMu9T",
      "premise_still_valid": true,
      "summary": "Fixed at the claim's narrowed home, toResponse's events arm in packages/adapters/hono/src/index.ts: a string event is now written as given in every stream mode (object events keep their data: frame, or their JSON line in a Vercel data stream), and the producer's headers are set over the arm's Content-Type / Cache-Control / Connection defaults in a Headers object, so a producer header replaces the default of the same name whatever its case. On a createHonoApp host GET /api/v1/mcp/skill now answers the markdown byte for byte with one content-type: text/markdown; charset=utf-8 and cache-control: no-store, equal to the standalone server's answer; mcp.ts (held by #24251) was not touched and was not needed. A4: only two domains reach the arm, /mcp/skill (one string event, the defect) and /ai via domains/ai.ts, whose service-ai@10.3.0 producers send either pre-encoded string frames with vercelDataStream (already verbatim, unchanged) or TextStreamPart objects without it (still SSE-framed, unchanged); no producer relies on SSE framing of a string event, so no design question arose. Clause-2: no export moved and the adapter accepts and refuses exactly the requests it did; only the response bytes of a string event outside a Vercel data stream and the case-differing duplicate headers change.",
      "tests": "Measured before the fix (fa501123b: tests committed, index.ts unfixed): conformance 2/2 red and the unit /mcp/skill row red on the body, received 'data: \"---\\nname: objectstack\\n...\"' plus a blank line; the other 3 unit rows green. One-off probe (scratch, not committed; LiteKernel + fake mcp + createHonoApp, app.request and a real socket via @hono/node-server serve, read with node:http rawHeaders) against the hono dist: before, app.request content-type 'text/markdown; charset=utf-8, text/markdown; charset=utf-8', cache-control 'no-cache, no-store', body an SSE frame of a JSON string; socket wire lines Content-Type, Cache-Control: no-cache, Connection, content-type, content-disposition, cache-control: no-store. After (rebuilt dist): body verbatim=true on both, one content-type, cache-control no-store, one line per header on the wire (A3 confirmed; A5's real socket now measured once, by probe). At final head 32900fd25 under os-verify-lock (VERDICT command-exit 0): pnpm --filter @objectstack/hono test exit 0, 11 files / 215 passed; pnpm --filter @objectstack/hono typecheck exit 0 (new test file in tsconfig.test.json's program, --listFiles count 1); pnpm --filter @objectstack/http-conformance exec vitest run --maxWorkers=2 src/hono-mcp-skill-raw-body.conformance.test.ts src/hono-head-as-get.conformance.test.ts src/hono-dispatcher-result-response.conformance.test.ts exit 0, 3 files / 29 passed; pnpm --filter @objectstack/http-conformance typecheck exit 0. Ablation after the fix was committed, via scripts/ablation-replace.mjs in WRAP mode with an EXIT/INT/TERM trap restoring git checkout HEAD on the absolute path; both subjects resolve to source (hono suite imports ./index, http-conformance aliases @objectstack/hono to src), so no rebuild per leg. Leg A (headers.set to headers.append; blob 62e6f29d4016 to e958969acbd3): unit 3/4 red, conformance 2/2 red; direction more red than the old defect, because append also doubles same-case headers (the /ai rows), which the old record spread did not. Leg B (const chunk = typeof event === 'string' to const chunk = res.vercelDataStream && typeof event === 'string'; blob 62e6f29d4016 to 91002889678f): unit 1/4 red (the /mcp/skill row; the Vercel row stays green), conformance 2/2 red. Each restore proven by blob == HEAD 62e6f29d4016 and git diff HEAD empty. The first leg B attempt was a no-op: its replacement contained its anchor, the tool refused it (anchor 1 to 1, exit 1) and ran no test; that reading is void and the leg was rerun with the anchor above. test:repo not run locally (diff touches no repo-project test; CI's Test Core).",
      "gates": "Local, derived at 32900fd25 (merge base 30da04f8e) with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 66 commands, each run with its exit captured before any pipe: 65 exit 0 first time; pnpm check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, 34 packages without dist) and, after the workspace build pnpm check:type-check-debt itself ran through the lock, re-run: exit 0 (111 entry points across 69 packages load). --ran reconciliation with 'command :: exit N' lines: '66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN' (a derived zero, all 66 carry a code). Lane blind spot: pnpm exec eslint --no-inline-config over packages/adapters/hono/src/index.ts, packages/adapters/hono/src/hono-stream-events-framing.test.ts and packages/qa/http-conformance/src/hono-mcp-skill-raw-body.conformance.test.ts: exit 0, no output. Not derived and not owed here: check-platform-checklist (no exported symbol renamed or deleted), check:dev-prereqs and check:skill-frame-freshness (no packages/spec or four-axis frame file touched). CI on PR #24319 head 32900fd25 at report time (36 check-runs): success = filter, Check Documentation Links, No other open PR may claim the same issue; skipped = Build Docs, Console Pin Gate, Dogfood Regression Gate (matrix), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL); in_progress or queued = Build Core, Test Core 1-6/6, Lint (ESLint, PM dispatch-gates, docs & code guards, repo & tooling gates, CI release & engine gates, three scoped lanes), Type Check (workspace, source gates, consumer gates, debt ledger), Governed Surface Queue Guard, Check Changeset, Check PR Size, Flag docs affected by code changes, single-writer path, Part-of guard, card-claims-branch, Dogfood Regression Gate. Not waited on, per the role file.",
      "line_budget": "273 changed lines (+266 / -7) over 4 files, vs merge base 30da04f8e",
      "files_changed": [
        ".changeset/24287-hono-stream-string-verbatim.md (+11, patch for @objectstack/hono, carries Clause-2: no)",
        "packages/adapters/hono/src/index.ts (+26 / -7, toResponse events arm only)",
        "packages/adapters/hono/src/hono-stream-events-framing.test.ts (+111, new)",
        "packages/qa/http-conformance/src/hono-mcp-skill-raw-body.conformance.test.ts (+118, new; private package, no changeset)"
      ],
      "mcp_calls": "0 - no MCP GitHub tool was called (read or write)",
      "api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches, 204, via scripts/pm/fleet-write/dispatch.mjs, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft #24319) + POST /repos/objectstack-ai/objectstack/issues/24319/assignees [zhuangjianguo], run 39124722759 success, body read back 8884 bytes identical and re-read by REST (draft true, footer intact); (2) comment = POST /repos/objectstack-ai/objectstack/issues/24287/comments (this report). No labels written (none named; changeset present, so no skip-changeset). git push (not REST): the branch only, 4 pushes, all fast-forward (empty probe, wip tests fa501123b, fix 856019a87, main merge 32900fd25).",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) - noted, not filed (read of code, no producer exercises it): remaining differences between the hono events arm and the standalone writer - the arm answers 200 whatever result.status says (both producers mean 200); a null event is framed as data: null where the standalone writer skips it; a stream that throws is closed silently where the standalone writer writes an event: error frame; the Connection: keep-alive default stays when a producer sets other headers. In PR #24319 Acceptance notes.",
        "carrier: none (承接者:无) - noted, not filed (inference from code and the published tarball, NOT MEASURED at a door): runtime/src/domains/ai.ts builds its own stream headers and drops the AI route's, so on a createHonoApp host a Vercel data stream from service-ai@10.3.0 (/ai/chat, /ai/agents/:agentName/chat, /ai/assistant/chat) would carry Content-Type: text/plain; charset=utf-8 and none of x-vercel-ai-ui-message-stream, x-objectstack-agent, x-objectstack-skills, which the standalone route mount writes from the route's own headers. Measuring it needs a booted AI route table on both doors. Dedupe words if it is ever measured: ai domain stream headers dropped createHonoApp; x-vercel-ai-ui-message-stream missing hono; ai chat content-type text/plain hosted. In PR #24319 Acceptance notes."
      ],
      "deviations": [
        "A1 line numbers: on origin/main 21070a2f0 the events arm is :822-:848 (Content-Type default :825, framing :835-:837), not :791; content as the PM read it.",
        "A5: beyond the committed app.request pin and the conformance pin, one scratch probe measured a real @hono/node-server socket before and after; it is not committed (one-time reading, cited in the PR body).",
        "A4 needed the cloud's service-ai, which lives outside this repo: GET /repos/objectstack-ai/cloud answered 403 to this session, so the published @objectstack/service-ai@10.3.0 npm tarball was extracted into the scratchpad and read (dist/index.js), never executed. Its route shapes may differ on cloud main.",
        "The first ablation leg B attempt was a no-op refused by ablation-replace (replacement contained the anchor); reading voided and rerun with a non-overlapping anchor.",
        "pnpm check:dual-build-cjs-loads first exited 3 (prerequisite); re-run after the workspace build that check:type-check-debt performed under the lock: exit 0. Recorded as exit 0 in the --ran file with this note.",
        "Two os-verify-lock calls returned 99 (queue-timeout, 9 min each) behind #24282's suites.sh (pid 13029); lock-free work (gate derivation, eslint, PR body) was done in between and the leg was rerun under the same slot.",
        "origin/main moved 3 commits (none touching hono or http-conformance) before the PR opened; merged once (32900fd25), install refreshed, gates derived and run on the merged tree, hono suite and pins re-run at that head.",
        "Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer; the role file and AGENTS.md (model-free trailer pair, session-URL PR footer) were followed instead.",
        "Answered the PM's mid-run status probe with one SendMessage line to the parent.",
        "Cleanup done before this report: root node_modules removed and the worktree removed with plain git worktree remove (no force); the local branch ref stays in the shared repo."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24319 comment 6249590388 · domain:cli seat 1 · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T11:53Z · mcp.ts fence held; the /ai producer reading rests on the published service-ai@10.3.0 (cloud repo unreachable), recorded as residual risk; landing waits on every check of head 32900fd25.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #24319 → 1ef0ee89d · domain:cli seat 1 · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T12:31Z

    Reading Result
    PR #24319 object merged: true, merge commit 1ef0ee89d, merged 2026-10-18T12:30Z through the merge queue (enqueued 2026-10-18T12:10Z)
    origin/main 1ef0ee89d is an ancestor of origin/main, with a single parent (squash). The 4 files and +266 / −7 match what was reviewed at 32900fd25
    queue branch no gh-readonly-queue/main/*pr-24319* head remains

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:clipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions