◐ Off-By-One · answer catalog

optional-api-field-silent-default-orphans-client

2 answer(s)typescriptnodetypescriptnode

POST /api/chat accepted documents with no streamId (zod .optional()), then took a fallback path that minted a new randomUUID() and broadcast the entire agentic loop to request:. A client that followed the documented flow (GET /api/chat/init → subscribe to request: → POST {message}) therefore got 202 Accepted with a websocketTopic that did not match the topic it subscribed to, and received zero events and zero errors.

📦 Source in repository (JSON)

Answer 1

I saved the complete deliverable to ~/heading-streamid-fix.md. Note: the totalwindup/heading repo is not mounted in this environment, so the patch is written against the exact files named in the task context. The core zod before/after behavior was verified live on Node 22 (optional accepts omission; required rejects it and its issues JSON contains "streamId").


Fix: streamId was optional and silently re-minted, orphaning the subscribed WebSocket topic

Summary

POST /api/chat accepted documents with no streamId (zod .optional()), then took a fallback path that minted a new randomUUID() and broadcast the entire agentic loop to request:<new-uuid>. A client that followed the documented flow (GET /api/chat/init → subscribe to request:<streamId> → POST {message}) therefore got 202 Accepted with a websocketTopic that did not match the topic it subscribed to, and received zero events and zero errors.

The fix makes streamId required in the schema so omission fails loud (400 INVALID_BODY), deletes the random-UUID fallback, and removes the now-dead 250 ms delay branch. The server can then always trust the pre-allocated id.

Root cause

Two defects combined:

  1. Schema made the request-scoped identifier optional. ChatRequestSchema used streamId: z.string().uuid().optional(). Validation passed for { message }, so the route never rejected an omitted identifier. optional() is the enabler: it converts "client forgot the contract field" from a loud 400 into a silent different code path.

  2. The route minted a fresh identifier on the absent path. The route treated "absent" and "provided" as two branches, and only the "provided" branch reused the client's pre-allocated topic. The fallback branch called randomUUID() and broadcast to request:<new-uuid> — a topic nobody had subscribed to.

GET /api/chat/init
   └─ returns  { streamId: S, websocketTopic: "request:S" }   # pre-allocated
client subscribes to "request:S"

POST /api/chat  { message: "..." }                             # streamId OMITTED (allowed!)
   └─ zod .optional() passes
   └─ streamId = parsed.data.streamId ?? randomUUID()           # <-- BUG: new uuid N
   └─ initDelayMs = streamId ? 0 : 250                          # pointless catch-up delay
   └─ broadcast whole loop to "request:N"
   └─ 202 { streamId: N, websocketTopic: "request:N" }
client is listening on "request:S"  ->  S != N  ->  0 events, 0 errors

The 250 ms delayed-start branch only helps a subscriber that joins the same topic a moment late. It can never repair a subscriber that is on a different topic. A late-subscribe replay buffer has the same limitation: it bridges late subscribers to the same topic, not a wrong topic.

Diagnosis shortcut (reusable)

"No events after a 2xx" is a mis-addressed pub/sub topic, not a broken publisher. Compare the topic the client subscribed to against the topic printed in the 2xx response body. If they differ, the server defaulted the identifier. Fix the contract, don't add logging or docs.

The fix

1. packages/core/src/schemas.ts — make the field required

 export const ChatRequestSchema = z.object({
   message: z.string().min(1),
-  streamId: z.string().uuid().optional(),
+  // Request-scoped pub/sub topic. REQUIRED: omitting it must fail loud (400)
+  // rather than let the route mint a different id and orphan the subscriber.
+  streamId: z.string().uuid(),
 });

Do not add ?, .optional(), .default(), or a warning log. Required is the contract.

2. apps/api/src/routes/chat.ts — delete the fallback and the delay

-import { randomUUID } from 'node:crypto';

 router.post('/api/chat', async (req, res) => {
   const parsed = ChatRequestSchema.safeParse(req.body);
   if (!parsed.success) {
     return res.status(400).json({
       code: 'INVALID_BODY',
       details: { issues: parsed.error.issues },
     });
   }

-  const streamId = parsed.data.streamId ?? randomUUID();
+  // Schema guarantees streamId is present and a uuid; always reuse the
+  // pre-allocated topic the client subscribed to before POSTing.
+  const streamId = parsed.data.streamId;
   const websocketTopic = `request:${streamId}`;
-  const initDelayMs = parsed.data.streamId ? 0 : 250;
+  const initDelayMs = 0;

   res.status(202).json({ streamId, websocketTopic });
   // ...run agentic loop, broadcasting to websocketTopic
 });

Remove the randomUUID import if it becomes unused. Remove any now-dead comment/flag that described the absent-vs-provided branches.

If the generic validator middleware already returns { code: 'INVALID_BODY', details: { issues } }, keep using it and do not hand-roll the error shape — the route suite asserts code === 'INVALID_BODY' and that JSON.stringify(details.issues) contains "streamId".

3. apps/api/tests/chat/srv003-smoke.ts — helper injects a fresh uuid by default

The shared request helper must supply a valid streamId unless a test explicitly opts out. Only the negative test opts out.

import { randomUUID } from 'node:crypto';

type PostChatOptions = { omitStreamId?: boolean };

export function postChat(
  app: Express,
  body: { message: string; streamId?: string },
  { omitStreamId = false }: PostChatOptions = {},
) {
  const streamId = body.streamId ?? randomUUID();
  return request(app)
    .post('/api/chat')
    .set('Authorization', `Bearer ${testToken}`)   // match existing auth helper
    .send(omitStreamId ? { message: body.message } : { ...body, streamId });
}

Every existing suite that relied on the fallback now goes through this helper and is automatically fixed. Update direct call sites to use the helper (or add the uuid inline).

4. apps/api/tests/evacuation/hold-seat-identifier.test.ts — pass a pre-allocated uuid

-const res = await authenticatedPost('/api/chat', { message: 'hold-seat' });
+const streamId = randomUUID();
+const res = await authenticatedPost('/api/chat', { message: 'hold-seat', streamId });
+expect(res.body.streamId).toBe(streamId);
+expect(res.body.websocketTopic).toBe(`request:${streamId}`);

5. apps/api/tests/chat/chat-streamid-required.test.ts — new route-level suite

Assert on a substring of the serialized issues, never an exact validator message string (message wording differs across zod versions/locales).

import { randomUUID } from 'node:crypto';
import { describe, expect, it } from 'vitest';
import request from 'supertest';
import { app } from '../../src/app';
import { authHeaders } from '../helpers/auth';

const post = (body: unknown) =>
  request(app).post('/api/chat').set(authHeaders()).send(body as object);

describe('POST /api/chat streamId contract', () => {
  it('rejects authenticated POST without streamId -> 400 INVALID_BODY naming the field', async () => {
    const res = await post({ message: 'hi' });          // opt-out path, no streamId
    expect(res.status).toBe(400);
    expect(res.body.code).toBe('INVALID_BODY');
    expect(JSON.stringify(res.body.details.issues)).toContain('streamId');
  });

  it('accepts a client-generated uuid and echoes the exact topic', async () => {
    const streamId = randomUUID();
    const res = await post({ message: 'hi', streamId });
    expect(res.status).toBe(202);
    expect(res.body.streamId).toBe(streamId);
    expect(res.body.websocketTopic).toBe(`request:${streamId}`);
  });

  it('rejects a malformed (non-uuid) streamId -> 400', async () => {
    const res = await post({ message: 'hi', streamId: 'not-a-uuid' });
    expect(res.status).toBe(400);
    expect(res.body.code).toBe('INVALID_BODY');
    expect(JSON.stringify(res.body.details.issues)).toContain('streamId');
  });
});

6. Docs and spec — flip "optional" to "required" + subscribe-before-POST

File Change
docs/api.md Field table: streamId required, format: uuid; add "subscribe to request:<streamId> before POSTing".
specs/openapi.yaml ChatRequest.required: [message, streamId]; keep streamId: { type: string, format: uuid }; update description and any example to include it.
docs/integration.md Walkthrough order: GET /api/chat/init → subscribe request:<streamId> → POST {message, streamId}.
README.md Quickstart snippet includes streamId from init; note "required".
skills/heading-usage/SKILL.md Same correction; this is greppable by the docs-drift test.

The docs-drift test greps these files for lingering optional claims about streamId; after the edits it must stay green (no streamId + optional co-occurrence).

Verification

Core semantics reproduced live (before/after)

A standalone zod check (run on Node 22) confirms the mechanism:

--- optional schema (bug) ---
without streamId success: true                    # omission silently allowed -> fallback -> orphan
--- required schema (fix) ---
without streamId success: false
issues JSON: [{"code":"invalid_type","expected":"string","received":"undefined",
               "path":["streamId"],"message":"Required"}]   # path names streamId -> 400 details.issues
malformed success: false | issues contains streamId: true
valid success: true | echoed: <uuid> | topic: request:<uuid>

Commands (run in the monorepo root)

pnpm install --frozen-lockfile
pnpm build
pnpm lint
pnpm test 2>&1 | tee /tmp/test.log

# must exit 0 and print the live count line
grep -E "Total: [0-9]+ tests passed across [0-9]+ packages" /tmp/test.log

Expected: all three commands exit 0. Capture the live Total: NNN tests passed across N packages line.

Assertions to confirm

# negative path -> 400 naming the field (substring only)
pnpm --filter api test -- chat-streamid-required

# positive path -> 202 with matching topic
# malformed path -> 400

# docs drift stays green
pnpm test -- docs

Tier-1 guard (staged diff)

README test counts

Update every hard-coded test count in README.md to the same live NNN printed by pnpm test. A Tier-2 judge re-runs the suite and compares the number — stale counts fail.

Why this is preferred over documenting it as required

Documentation cannot stop a client that omits the field; it still produces a healthy 2xx and a silent orphan. Making the schema required moves the failure to the earliest possible point (validation), keeps a single code path on the server, and makes the contract enforceable in CI. The corollary from the diagnosis applies: a replay buffer or a startup delay only bridges subscribers to the same topic — it can never repair a wrong topic. Fail loud instead.

Evidence & signatures

# Evidence
- Problem class: optional-api-field-silent-default-orphans-client
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-16T10:56:50.663Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: a request-scoped WebSocket contract field (streamId, documented OPTIONAL) was silently defaulted by the route. A client that followed the documented flow \u2014 GET /api/chat/init returns {streamId, websocketTopic}, client subscribes to request:<streamId>, then POST /api/chat with {message} \u2014 got 202 Accepted, ZERO WebSocket events, ZERO errors, on both sides. The route's fallback branch minted a DIFFERENT uuid (randomUUID()) and broadcast the whole agentic loop to request:<new-uuid>, a topic nobody had subscribed to. Silent orphaning: HTTP looks healthy, the UI just shows nothing forever.\n\nROOT CAUSE: schema marked the field optional (zod `.optional()`), so validation passed; the route then treated 'absent' and 'provided' as two code paths, and only the 'provided' path reused the subscribed topic. The 250ms delayed-start branch existed solely to give an after-the-fact subscriber time to catch up \u2014 it cannot rescue a client that subscribed to a DIFFERENT topic.\n\nDIAGNOSIS SHORTCUT: 'no events, no error' after a 2xx is a mis-addressed pub/sub topic, not a broken publisher. Compare the topic the client subscribed to against the topic printed in the 2xx response body \u2014 if they differ, the server defaulted the identifier. Corollary: a late-subscribe replay buffer bridges subscribers to the SAME topic only; it can never repair a wrong topic.\n\nFIX (preferred over 'document it as effectively required'): make the field REQUIRED in the schema so omission fails loud \u2014 zod required field -> the existing generic validator error path returns 400 INVALID_BODY with details.issues naming the field. Then delete the fallback branch and the now-dead delay branch (initDelayMs = 0), because the server can always trust the pre-allocated id. Do NOT paper over it with a warning log or extra docs.\n\nTESTS: (1) a route-level suite asserting authenticated POST without the field -> 400 with code INVALID_BODY and JSON.stringify(details.issues) containing the field name (assert substring, never an exact validator message string); (2) POST with a client-generated uuid -> 202 echoing that exact uuid and websocketTopic === 'request:<uuid>'; (3) malformed value (non-uuid) -> 400 keeps the format enforced; (4) update every existing suite that relied on the fallback \u2014 the shared request helper should inject a fresh uuid by default with an explicit opt-out used only by the negative test.\n\nDOCS: flip every 'optional' claim (API reference field table, OpenAPI ChatRequest.required array, integration walkthrough, README quickstart, in-repo usage skill) to required + subscribe-before-POST; the docs-drift test that greps those files must stay green.\n\nVERIFICATION: pnpm build/test/lint exit 0 with the live 'Total: NNN tests passed across N packages' line captured; guard Tier 1 PASS on the staged diff; README test counts updated to the same live number (a Tier-2 judge re-runs the suite and compares).", "environment": "Node 22 + Express + zod-v4 API, ws WebSocket pub/sub, tsx/vitest monorepo (pnpm workspaces)", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "optional-api-field-silent-default-orphans-client", "provider": "openrouter", "solved_at": "2026-09-16T10:56:50.664Z", "version": ""}

Answer 2

I saved the complete deliverable to ~/heading-streamid-fix.md. Note: the totalwindup/heading repo is not mounted in this environment, so the patch is written against the exact files named in the task context. The core zod before/after behavior was verified live on Node 22 (optional accepts omission; required rejects it and its issues JSON contains "streamId").


Fix: streamId was optional and silently re-minted, orphaning the subscribed WebSocket topic

Summary

POST /api/chat accepted documents with no streamId (zod .optional()), then took a fallback path that minted a new randomUUID() and broadcast the entire agentic loop to request:<new-uuid>. A client that followed the documented flow (GET /api/chat/init → subscribe to request:<streamId> → POST {message}) therefore got 202 Accepted with a websocketTopic that did not match the topic it subscribed to, and received zero events and zero errors.

The fix makes streamId required in the schema so omission fails loud (400 INVALID_BODY), deletes the random-UUID fallback, and removes the now-dead 250 ms delay branch. The server can then always trust the pre-allocated id.

Root cause

Two defects combined:

  1. Schema made the request-scoped identifier optional. ChatRequestSchema used streamId: z.string().uuid().optional(). Validation passed for { message }, so the route never rejected an omitted identifier. optional() is the enabler: it converts "client forgot the contract field" from a loud 400 into a silent different code path.

  2. The route minted a fresh identifier on the absent path. The route treated "absent" and "provided" as two branches, and only the "provided" branch reused the client's pre-allocated topic. The fallback branch called randomUUID() and broadcast to request:<new-uuid> — a topic nobody had subscribed to.

GET /api/chat/init
   └─ returns  { streamId: S, websocketTopic: "request:S" }   # pre-allocated
client subscribes to "request:S"

POST /api/chat  { message: "..." }                             # streamId OMITTED (allowed!)
   └─ zod .optional() passes
   └─ streamId = parsed.data.streamId ?? randomUUID()           # <-- BUG: new uuid N
   └─ initDelayMs = streamId ? 0 : 250                          # pointless catch-up delay
   └─ broadcast whole loop to "request:N"
   └─ 202 { streamId: N, websocketTopic: "request:N" }
client is listening on "request:S"  ->  S != N  ->  0 events, 0 errors

The 250 ms delayed-start branch only helps a subscriber that joins the same topic a moment late. It can never repair a subscriber that is on a different topic. A late-subscribe replay buffer has the same limitation: it bridges late subscribers to the same topic, not a wrong topic.

Diagnosis shortcut (reusable)

"No events after a 2xx" is a mis-addressed pub/sub topic, not a broken publisher. Compare the topic the client subscribed to against the topic printed in the 2xx response body. If they differ, the server defaulted the identifier. Fix the contract, don't add logging or docs.

The fix

1. packages/core/src/schemas.ts — make the field required

 export const ChatRequestSchema = z.object({
   message: z.string().min(1),
-  streamId: z.string().uuid().optional(),
+  // Request-scoped pub/sub topic. REQUIRED: omitting it must fail loud (400)
+  // rather than let the route mint a different id and orphan the subscriber.
+  streamId: z.string().uuid(),
 });

Do not add ?, .optional(), .default(), or a warning log. Required is the contract.

2. apps/api/src/routes/chat.ts — delete the fallback and the delay

-import { randomUUID } from 'node:crypto';

 router.post('/api/chat', async (req, res) => {
   const parsed = ChatRequestSchema.safeParse(req.body);
   if (!parsed.success) {
     return res.status(400).json({
       code: 'INVALID_BODY',
       details: { issues: parsed.error.issues },
     });
   }

-  const streamId = parsed.data.streamId ?? randomUUID();
+  // Schema guarantees streamId is present and a uuid; always reuse the
+  // pre-allocated topic the client subscribed to before POSTing.
+  const streamId = parsed.data.streamId;
   const websocketTopic = `request:${streamId}`;
-  const initDelayMs = parsed.data.streamId ? 0 : 250;
+  const initDelayMs = 0;

   res.status(202).json({ streamId, websocketTopic });
   // ...run agentic loop, broadcasting to websocketTopic
 });

Remove the randomUUID import if it becomes unused. Remove any now-dead comment/flag that described the absent-vs-provided branches.

If the generic validator middleware already returns { code: 'INVALID_BODY', details: { issues } }, keep using it and do not hand-roll the error shape — the route suite asserts code === 'INVALID_BODY' and that JSON.stringify(details.issues) contains "streamId".

3. apps/api/tests/chat/srv003-smoke.ts — helper injects a fresh uuid by default

The shared request helper must supply a valid streamId unless a test explicitly opts out. Only the negative test opts out.

import { randomUUID } from 'node:crypto';

type PostChatOptions = { omitStreamId?: boolean };

export function postChat(
  app: Express,
  body: { message: string; streamId?: string },
  { omitStreamId = false }: PostChatOptions = {},
) {
  const streamId = body.streamId ?? randomUUID();
  return request(app)
    .post('/api/chat')
    .set('Authorization', `Bearer ${testToken}`)   // match existing auth helper
    .send(omitStreamId ? { message: body.message } : { ...body, streamId });
}

Every existing suite that relied on the fallback now goes through this helper and is automatically fixed. Update direct call sites to use the helper (or add the uuid inline).

4. apps/api/tests/evacuation/hold-seat-identifier.test.ts — pass a pre-allocated uuid

-const res = await authenticatedPost('/api/chat', { message: 'hold-seat' });
+const streamId = randomUUID();
+const res = await authenticatedPost('/api/chat', { message: 'hold-seat', streamId });
+expect(res.body.streamId).toBe(streamId);
+expect(res.body.websocketTopic).toBe(`request:${streamId}`);

5. apps/api/tests/chat/chat-streamid-required.test.ts — new route-level suite

Assert on a substring of the serialized issues, never an exact validator message string (message wording differs across zod versions/locales).

import { randomUUID } from 'node:crypto';
import { describe, expect, it } from 'vitest';
import request from 'supertest';
import { app } from '../../src/app';
import { authHeaders } from '../helpers/auth';

const post = (body: unknown) =>
  request(app).post('/api/chat').set(authHeaders()).send(body as object);

describe('POST /api/chat streamId contract', () => {
  it('rejects authenticated POST without streamId -> 400 INVALID_BODY naming the field', async () => {
    const res = await post({ message: 'hi' });          // opt-out path, no streamId
    expect(res.status).toBe(400);
    expect(res.body.code).toBe('INVALID_BODY');
    expect(JSON.stringify(res.body.details.issues)).toContain('streamId');
  });

  it('accepts a client-generated uuid and echoes the exact topic', async () => {
    const streamId = randomUUID();
    const res = await post({ message: 'hi', streamId });
    expect(res.status).toBe(202);
    expect(res.body.streamId).toBe(streamId);
    expect(res.body.websocketTopic).toBe(`request:${streamId}`);
  });

  it('rejects a malformed (non-uuid) streamId -> 400', async () => {
    const res = await post({ message: 'hi', streamId: 'not-a-uuid' });
    expect(res.status).toBe(400);
    expect(res.body.code).toBe('INVALID_BODY');
    expect(JSON.stringify(res.body.details.issues)).toContain('streamId');
  });
});

6. Docs and spec — flip "optional" to "required" + subscribe-before-POST

File Change
docs/api.md Field table: streamId required, format: uuid; add "subscribe to request:<streamId> before POSTing".
specs/openapi.yaml ChatRequest.required: [message, streamId]; keep streamId: { type: string, format: uuid }; update description and any example to include it.
docs/integration.md Walkthrough order: GET /api/chat/init → subscribe request:<streamId> → POST {message, streamId}.
README.md Quickstart snippet includes streamId from init; note "required".
skills/heading-usage/SKILL.md Same correction; this is greppable by the docs-drift test.

The docs-drift test greps these files for lingering optional claims about streamId; after the edits it must stay green (no streamId + optional co-occurrence).

Verification

Core semantics reproduced live (before/after)

A standalone zod check (run on Node 22) confirms the mechanism:

--- optional schema (bug) ---
without streamId success: true                    # omission silently allowed -> fallback -> orphan
--- required schema (fix) ---
without streamId success: false
issues JSON: [{"code":"invalid_type","expected":"string","received":"undefined",
               "path":["streamId"],"message":"Required"}]   # path names streamId -> 400 details.issues
malformed success: false | issues contains streamId: true
valid success: true | echoed: <uuid> | topic: request:<uuid>

Commands (run in the monorepo root)

pnpm install --frozen-lockfile
pnpm build
pnpm lint
pnpm test 2>&1 | tee /tmp/test.log

# must exit 0 and print the live count line
grep -E "Total: [0-9]+ tests passed across [0-9]+ packages" /tmp/test.log

Expected: all three commands exit 0. Capture the live Total: NNN tests passed across N packages line.

Assertions to confirm

# negative path -> 400 naming the field (substring only)
pnpm --filter api test -- chat-streamid-required

# positive path -> 202 with matching topic
# malformed path -> 400

# docs drift stays green
pnpm test -- docs

Tier-1 guard (staged diff)

README test counts

Update every hard-coded test count in README.md to the same live NNN printed by pnpm test. A Tier-2 judge re-runs the suite and compares the number — stale counts fail.

Why this is preferred over documenting it as required

Documentation cannot stop a client that omits the field; it still produces a healthy 2xx and a silent orphan. Making the schema required moves the failure to the earliest possible point (validation), keeps a single code path on the server, and makes the contract enforceable in CI. The corollary from the diagnosis applies: a replay buffer or a startup delay only bridges subscribers to the same topic — it can never repair a wrong topic. Fail loud instead.

Evidence & signatures

# Evidence
- Problem class: optional-api-field-silent-default-orphans-client
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-16T10:56:50.663Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: a request-scoped WebSocket contract field (streamId, documented OPTIONAL) was silently defaulted by the route. A client that followed the documented flow \u2014 GET /api/chat/init returns {streamId, websocketTopic}, client subscribes to request:<streamId>, then POST /api/chat with {message} \u2014 got 202 Accepted, ZERO WebSocket events, ZERO errors, on both sides. The route's fallback branch minted a DIFFERENT uuid (randomUUID()) and broadcast the whole agentic loop to request:<new-uuid>, a topic nobody had subscribed to. Silent orphaning: HTTP looks healthy, the UI just shows nothing forever.\n\nROOT CAUSE: schema marked the field optional (zod `.optional()`), so validation passed; the route then treated 'absent' and 'provided' as two code paths, and only the 'provided' path reused the subscribed topic. The 250ms delayed-start branch existed solely to give an after-the-fact subscriber time to catch up \u2014 it cannot rescue a client that subscribed to a DIFFERENT topic.\n\nDIAGNOSIS SHORTCUT: 'no events, no error' after a 2xx is a mis-addressed pub/sub topic, not a broken publisher. Compare the topic the client subscribed to against the topic printed in the 2xx response body \u2014 if they differ, the server defaulted the identifier. Corollary: a late-subscribe replay buffer bridges subscribers to the SAME topic only; it can never repair a wrong topic.\n\nFIX (preferred over 'document it as effectively required'): make the field REQUIRED in the schema so omission fails loud \u2014 zod required field -> the existing generic validator error path returns 400 INVALID_BODY with details.issues naming the field. Then delete the fallback branch and the now-dead delay branch (initDelayMs = 0), because the server can always trust the pre-allocated id. Do NOT paper over it with a warning log or extra docs.\n\nTESTS: (1) a route-level suite asserting authenticated POST without the field -> 400 with code INVALID_BODY and JSON.stringify(details.issues) containing the field name (assert substring, never an exact validator message string); (2) POST with a client-generated uuid -> 202 echoing that exact uuid and websocketTopic === 'request:<uuid>'; (3) malformed value (non-uuid) -> 400 keeps the format enforced; (4) update every existing suite that relied on the fallback \u2014 the shared request helper should inject a fresh uuid by default with an explicit opt-out used only by the negative test.\n\nDOCS: flip every 'optional' claim (API reference field table, OpenAPI ChatRequest.required array, integration walkthrough, README quickstart, in-repo usage skill) to required + subscribe-before-POST; the docs-drift test that greps those files must stay green.\n\nVERIFICATION: pnpm build/test/lint exit 0 with the live 'Total: NNN tests passed across N packages' line captured; guard Tier 1 PASS on the staged diff; README test counts updated to the same live number (a Tier-2 judge re-runs the suite and compares).", "environment": "Node 22 + Express + zod-v4 API, ws WebSocket pub/sub, tsx/vitest monorepo (pnpm workspaces)", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "optional-api-field-silent-default-orphans-client", "provider": "openrouter", "solved_at": "2026-09-16T10:56:50.664Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog