openspec/changes/archive/2026-09-10-add-parakeet-api/reviews/2026-09-10-re-advise.md

Re-advise: add-parakeet-api

ADVISE: accept READER: agt-fable-5-1-arch-review SPAWN: /Users/dukejones/work/ClientProjects/AllSystemsGo/AICamera/.spawns/aicam-parakeet-shape-1789079938-41901-4c435cab

Rigor: architecture. Density: standard. Reader family: Claude (Fable 5.1). Author family: grok. Prior: reviews/2026-09-10-advise.md (send-back, five spec lines + one struck design clause). Amend under review: 4194d0f. Read order: packet Why → proposal.mdsteer.md → change spec specs/parakeet-asr/spec.md → prior review → daBOM src/lib/server/api/app.ts:657 (servers: [{ url: '/api/v1' }]) and root.ts:30-37 (well-known trio). Blind take written before design.md, tasks.md, and the sibling deltas were opened. No living parakeet-asr spec exists; capability is ADDED at fold.

Blind take (written before design.md / tasks.md)

  1. Pin, held. servers /api/v1 is now a SHALL in Well-known, and the first scenario checks it. The document is self-locating. This was the one line that turned “same bytes” into a contract; it is there.
  2. Pin, held. Sidecar has no network listener; Bun is the sole tailnet socket; the sidecar has no port scenario is ss/lsof-checkable. The Tailscale-bind requirement now covers both processes, not “the ASR process” singular.
  3. Pin, held. CPU-only is mechanical: CUDA_VISIBLE_DEVICES empty, CPUExecutionProvider, intra-op ≤ 4, and a scenario that runs a clip beside the 30 fps stack. The cost on this box is the CPU steal, and the spec now names it.
  4. Pin, held. GET /api/v1/health reports warm | cold | down, provider, model id, and is listed in the document. The 503 and the GPU-idle scenarios both have a cheap probe.
  5. Pin, held. Stream is NDJSON {text, t0, t1, final}, non-browser v1, listed in the document, last event final: true. One response type per operation, as createRoute needs.
  6. Refuse, held. No resample. “Wrong sample shape SHALL be 422” stands alone; the “or resample in the sidecar” escape is gone from the spec. Must confirm it is also struck from design, since the engine actor reads design.
  7. Refuse, held. 415 for non-audio content-type, 422 for declared-but-wrong shape. The two are separated as asked.
  8. Would pin, minor. Health SHOULD answer 200 in every sidecar state with the state in the body. A /health that returns 503 when the sidecar is cold is indistinguishable from Bun being down, which is the one thing the probe exists to separate. The spec does not say the status code; the hono actor will pick one. One clause.
  9. Would refine, minor, my own wording. “End-of-stream is connection close” came from my prior take and the author copied it. If the client closes the TCP connection the server cannot deliver final: true. The precise statement is: end-of-input is end of the request body (half-close); the server then emits the last event with final: true and closes the response. This is a stream-sibling implementation detail; the intent is unambiguous.
  10. Tradeoff, accepted. Raw PCM on POST /transcribe needs a declared content-type so the 415 rule is clean; audio/L16; rate=16000; channels=1 (RFC 2586) or application/octet-stream named in the document. “As declared in the spec” delegates this to the OpenAPI document, which is the contract. Fine, the document decides, not prose.

Compare against design.md, tasks.md, sibling deltas

Opened after the take: design.md, tasks.md, add-parakeet-{engine,hono,stream} spec deltas and tasks, sim/3090/serve.py bind_ip() (env → tailscale ip -4100.103.147.70).

Every send-back item landed in the spec, and design agrees with it.

Send-back itemSpec (folds)Design (does not fold)
1. Resample contradiction“Wrong sample shape SHALL be 422”, no escape“The sidecar does not resample or mix channels.” Struck.
2. Health opClip transcribe: GET /api/v1/health warm\|cold\|down, provider, model id, listedNot repeated; not needed.
3. Sidecar listenerTailscale bind: “SHALL NOT open a network listener … child on stdin/IPC. Bun SHALL be the sole tailnet socket” + sidecar has no port scenarioSame sentence. Engine tasks: “Bun child on stdin/IPC — no TCP/UDP listener”. The “Tailscale-adjacent unit or” is gone.
4. CPU env + threads + CPU scenarioCPU-only: CUDA_VISIBLE_DEVICES empty, intra-op ≤ 4, transcribe beside 30 fps plugins scenarioCUDA_VISIBLE_DEVICES=, providers=["CPUExecutionProvider"], four threads.
5. serversWell-known: “SHALL set servers to the /api/v1 base”; scenario checks itTable row: servers: [{ url: '/api/v1' }]. Matches daBOM app.ts:657.
note: NDJSONAudio stream pipe: NDJSON {text, t0, t1, final}, final: true last, non-browser v1NDJSON only; “v1 stream clients are non-browser”.
note: 415 vs 422Both codes, separatedBoth codes, separated.

The hono sibling’s scenario that accepted ”/transcribe or /api/v1/transcribe as declared” now reads ”servers includes /api/v1, and paths includes /transcribe and /health”. The ambiguity the take was about is closed across the DAG, not just in this change.

Steelman against the take: the take’s remaining items (8, 9, 10) are each one clause an implementer can get right from the spec’s intent, and none of them is a line that would fold wrong. Item 8 (health status code) is the strongest; a /health that 503s when cold does not break any stated scenario, it only weakens the probe. That is a hono-node test, not a shape send-back. Item 9 is my own phrase; the author should not be sent back for copying the reader. Item 10 is delegated to the OpenAPI document by design, which is the point of the change.

Notes, not blocking. Addressed to the sibling actors and the folder.

  • Health status code (hono). GET /api/v1/health SHOULD return 200 in every sidecar state with the state in the body. Reserve 503 for /transcribe. Otherwise “sidecar cold” and “Bun down” look the same to a probe. Worth one clause at fold under Clip transcribe.
  • Half-close, not connection close (stream). Read “end-of-stream is connection close” as: the client ends the request body; the server emits the last event with final: true, then closes the response. If the client tears down TCP first, no final can arrive and that is the client’s fault, not a failed scenario. Stream tasks still say “NDJSON or SSE {text, t0, t1}” and “chunked POST or WS”; the shape spec has decided both. NDJSON with final, and chunked POST, because OpenAPI 3.1 cannot describe a WebSocket and the spec says the stream operation SHALL appear in the well-known document. WS is refused by the shape spec, not by this note.
  • Workspace vs own lockfile (hono). Hono tasks say “Bun workspace asr/”. A root workspaces entry shares the root bun.lock with the Pages build, which is the graph the prior take refused to grow. Prefer asr/ with its own package.json and lockfile, no root workspace membership. The spec line “SHALL NOT be mounted inside the SvelteKit adapter-static app” is satisfied either way; this is about the lockfile, not the mount.
  • Raw PCM content-type (hono). Declare it in the document (audio/L16; rate=16000; channels=1 or application/octet-stream) so the 415 rule has a clean edge. Prose says “as declared in the spec”; the document is where it gets declared.
  • Bind resolution (hono). Default 100.103.147.70 in the spec is fine; resolve like serve.py bind_ip() (env → tailscale ip -4 → default) and never fall through to 0.0.0.0. Design already says never 0.0.0.0.
  • Fold order (folder). Unchanged from the prior review. This change carries the full capability; engine “Warm CPU sidecar”, hono “Well-known spec is served”, stream “Stream listed in well-known” are each a subset of a requirement here. Fold this change first; fold the siblings as scenarios under its requirements, not as parallel requirements.
  • ARCHITECTURE.md note is owed at fold per tasks; design.md is the draft. Fine.

Verdict. Accept. The five spec lines and the struck design clause from the send-back are all present, spec and design say the same thing, and the sibling deltas no longer contradict the shape. Nothing left here folds wrong. Remaining notes are sibling-node implementation clauses.