Skip to content

Fifteen console types already disagree with the Worker — a date that never renders, a history that is always empty, no error anywhere #617

Closed

Description

@serge-ivo

Fifteen console types disagree with the Worker today — silent, because the fields are optional on both sides

A subset of #616 that is already wrong in production, not merely unguarded.

Two confirmed by name:

console expects Worker actually sends Effect
KnowledgeDoc.createdAt addedAt the date never renders
Credential.history recoveryHistory the history is always empty

Both fields are optional on both sides, so TypeScript is satisfied, nothing throws, nothing logs, and the value is simply undefined forever. That is the worst failure mode in this class: a feature that looks implemented and quietly does nothing.

Thirteen more were counted in the same sweep and not individually named.

Why this is separate from #616

#616 is the systemic fix — derive or compare all 125, ratchet the rest. That is real work with a real cost.

These fifteen are live defects with a user-visible effect right now, and each is a one-line rename. They should not wait on the architecture. Fixing them first also gives #616 a clean baseline: a guard introduced over a surface with fifteen known-bad rows starts life with an exemption list, which is how a ratchet becomes permanent.

Acceptance criteria

  1. All fifteen enumerated with file:line on both sides — the console's expectation and the Worker's producer.
  2. Each resolved by correcting the consumer, unless the Worker's name is genuinely wrong, in which case say so — renaming a producer field is an API change and needs to be stated as one.
  3. For each, note what the user was not seeing. A silently-empty history and a missing date have different weights, and the report should say which mattered.
  4. A test per fix that fails on the old name.
  5. Fixed before 90 of 125 console API-response types are hand-copied with no guard, and 15 already disagree with the Worker #616's guard lands, so that guard starts from zero known divergences.

Verified vs inferred

Activity

  1. serge-ivo commented on Aug 15, 2026

    @serge-ivo
    ContributorAuthor

    Done in 8aa63b7ea9a2e9e6c56c4e0e2b2ba9c50b8ba1e5.

    AC1 — the enumeration, and it is seventeen, not fifteen

    Denominator: 42 console-side API-response type declarations inspected, 17 diverged. The issue's caveat was right — only 4 of the 17 are nominal renames; 13 are structural, and one of those must not be renamed at all.

    # console (file:line) field Worker producer (file:line) producer sends class
    1 lib/types.ts:183 KnowledgeDoc.createdAt workers/api/src/agent-types.ts:80, written agent-do-knowledge.ts:80,:245 addedAt nominal
    2 lib/types.ts:182 KnowledgeDoc.type agent-types.ts:74-82 nothing — provenance is source + sourceUrl structural
    3 lib/types.ts:217 Credential.history lib/credentials.ts:30, mapped :107 recoveryHistory (col recovery_history) nominal
    4 lib/types.ts:56 Instance.capabilities.boardColumns routes/instances.ts:315 stripped before reply structural
    5 lib/types.ts:58 Instance.capabilities.settingsSchema routes/instances.ts:315 stripped before reply structural
    6 lib/types.ts:227 Notification.instanceId routes/notifications.ts:14 (SELECT *) col is agent_id, holding an agent id structural
    7 lib/types.ts:235 Notification.createdAt same created_at nominal — already mitigated
    8 lib/types.ts:247 Notification.data same no such column, ever structural
    9 lib/types.ts:226 Notification.read: boolean migrations/0006:9 INTEGER → 0/1 structural
    10 lib/types.ts:158 RuntimeTask.needs_human: boolean packages/browser-runner/src/types.ts:14 it is a TaskStatus value, not a field structural
    11 lib/types.ts:159 RuntimeTask.handoff_reason — nothing, anywhere structural
    12 lib/types.ts:160 RuntimeTask.handoff_field — nothing, anywhere structural
    13 lib/types.ts:153 RuntimeTask.result browser-runner/src/types.ts:44 output + error structural
    14 lib/types.ts:166 RuntimeEvent.timestamp (required) browser-runner/src/types.ts:57 createdAt only structural
    15 lib/types.ts:14 Agent.config: object routes/agents.ts:137 (SELECT *) a JSON string; /:id omits it entirely structural
    16 lib/types.ts:13 Agent.creator_login agents.ts:137, :267 not a column — only a CASE … as alias in the browse query structural
    17 tabs/IndexingTab.tsx:43 InstanceTrigger.action (4 members) lib/trigger-types.ts:34 7 members structural

    AC3 — what the user was not seeing, weighted

    One had a visible symptom. #1: the doc viewer's "added on" stamp (tabs/KnowledgeTab.tsx:506) has never rendered for anyone. tabs/IndexingTab.tsx:188 had already hit this and worked around it locally with doc as KnowledgeDoc & { addedAt?: string } — which is the evidence that the declaration, not the wire, was wrong.

    One was load-bearing and worse than a missing field. #12: RunDetail.tsx:405 read
    task?.handoff_field || detail.replace(/^Needs your input…/i,"")… || "your answer".
    The left operand can never win, so the label on the box a user fills during a handoff has always been scraped out of prose, or degraded to the literal words "your answer". Filed as #621.

    Two were dead optimisations. #4/#5: routes/instances.ts:315 strips both fields deliberately (measured: ~83 KB for 28 instances), and my/instances is the only endpoint any consumer of Instance calls. SettingsTab.tsx:58 calls its prop "the fast path" — for an optimisation that has never once fired. The board first-paints GENERIC_COLUMNS; settings first-paints empty. Both self-heal on the next fetch, which is why nobody reported them. Filed as #623.

    One corrects this issue's premise. #3 is described here as "the history is always empty". It is not — no component reads either name; the credential card renders domain and username only (KnowledgeTab.tsx:737-738). The cost so far is zero. It is fixed because the wrong name is what the next person to build that UI would reach for.

    One must NOT be renamed — AC2's escape hatch, used once. #6: matching the producer would give navigate(/instances/<agent_id>), a broken route rather than a fixed one. The branch was deleted, and the gap it was hiding (a row with no url is unclickable) is filed as #622.

    The rest cost nothing yet and were type-lies waiting to be believed. #14 is the sharpest: timestamp was declared required directly above a comment admitting it "is often absent". It is always absent — nothing emits it anywhere — so the declaration promised every future reader a string that never arrives. #17 is notable for a different reason: the console's other copy of that union (TriggersSection.tsx:53) had all seven, so the console disagreed with itself as well as with the Worker. Both now point at one declaration.

    AC2 — every fix is on the consumer

    No producer was renamed. The Worker's names were correct in all 17 cases; #4/#5 are a deliberate server decision with a measurement attached, and #6 is the case where the producer's name is right and still must not be adopted. So no API change is required or made.

    AC4 — a test per fix, watched red

    store/console/src/lib/types.test.ts. It is types, not assertions, because this defect class is invisible at runtime by construction — optional on both sides, so nothing throws and nothing logs — and a fixture-driven test is written from the same wrong belief as the code, so it agrees with the bug. Exclude<keyof Console, keyof Producer> extends never compares the declarations instead; store/console/tsconfig.json has include: ["src"], so CI's tsc --noEmit compiles it.

    Watched red, each one, before the fix went back:

    • restoring createdAt/type → types.test.ts(48,7): Type 'true' is not assignable to type 'never', plus three real read sites
    • restoring handoff_field/needs_human → the same at the RuntimeTask assertion
    • dropping run_pipeline from the union → the guard plus three live comparison sites in TriggersSection.tsx, which is what proves the dedup actually connected
    • re-adding instanceId / history → compile error from the exhaustive Record<keyof T, true>; and if they are also named there, the runtime arm fails with `instanceId` is declared on Notification but the table has no such column

    Two producers could not be reached from the console project and that is recorded in the file rather than worked around: lib/trigger-types.ts transitively drags Env and the Worker globals in, and the D1-shaped responses have no TypeScript declaration at all (SELECT *). Those are pinned by name against the migration column list — weaker, and labelled as such.

    AC5 — #616 now starts from zero

    No known divergence remains in the 42 inspected. #616 should begin with an empty exemption list, which was the point.

    Follow-ups filed rather than absorbed: #621 (structured handoff field), #622 (unclickable notification), #623 (the now-unfed props), #625 (RunDetail treats "expired" as finished — the #611 defect, in the console).

    Verified: pnpm typecheck (10 projects), 531 test files / 9254 tests, biome, all 9 script gates, docs:drift, console vite build, and pnpm test:e2e — 413 passed across chromium + webkit, including the 320px/390px WebKit mobile arms.

    Not verified: the seven remaining nominal-vs-structural judgements were made by reading the producer, not by observing a live response body for each. The four with a user-visible symptom (#1, #4, #5, #12) were traced to their render site.

  2. serge-ivo commented on Aug 15, 2026

    @serge-ivo
    ContributorAuthor

    CI green on 8aa63b7e, host deploy green, and the fix verified in the live console bundle: handoff_field is gone (0 occurrences) and addedAt is present (2). Closing.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions