Phase 1.5 — production hardening + 0.2.0 release#23
Merged
Conversation
Server returns stable status "InvalidSession" (was "Blocked") on the invalid-token 400 so clients detect it without matching English text. Widget clears the stored token, creates a fresh session, and resends the message once — app-pool recycles no longer surface an error. - vitest + happy-dom test harness for the widget (first JS tests) - CI: run widget tests; trigger on feature/phase1-5 - README: reverse-proxy / Cloudflare deployment section BREAKING CHANGE: status field on the invalid-token 400 response changed from "Blocked" to "InvalidSession". Refs task 21. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 21: widget session self-healing on InvalidSession
Split ConversationSession.History into FullHistory (untrimmed, used only for ticket generation) and SendHistory (sent to the answer LLM). SummaryCompressor folds the oldest messages into a one-line summary system message once SendHistory grows past twice SummaryInterval, keeping the latest SummaryInterval messages — bounding token cost on long conversations without losing context for tickets. - BotWireOptions.SummaryInterval (default 20; 0 disables) - AnswerProvider/triage read SendHistory; TicketGenerator reads FullHistory - InMemoryConversationStore caps SendHistory only; FullHistory kept intact - logs an Information line each time compression fires - unit tests for compressor (threshold, interval=0, fold, prior-summary), FullHistory preservation, and AnswerProvider send-path Refs task 22. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Write-only business/compliance event sink, separate from ILogger<T> app logging. NullAuditLogger is the default (zero overhead); AddJsonAuditLog(path) writes append-only NDJSON, one JSON object per line, opened FileShare.Read for live tailing with concurrent writes serialised so lines never interleave. Wired into BotWireChatService and the stream endpoint: - message (user + assistant, with latency) - guard_blocked (length / pii / prompt_injection) - rate_limited (per-IP limiter) - escalated (NeedHuman + ticket created) - error (provider/stream failure, logged then rethrown) provider_failover is defined and serialisation-tested but not yet emitted — it has no caller until the Failover client (task 26). Token/provider fields on assistant messages arrive with task 27. Enabled in the BasicEmail sample. Refs task 29. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When auto-triage ran after FailOpenEscalateThreshold consecutive fail-open turns and decided no escalation was needed, the counter stayed at/above the threshold, so triage re-ran on every subsequent turn. The continuing turn now resets the counter (FailedOpen = false) in both the non-streaming and streaming paths, so triage only fires again after a fresh streak of fail-opens. Refs task 21. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t, assistant content
- JsonFileAuditLogger writes one NDJSON file per session under a date
folder ({root}/{yyyyMMdd}/{sessionId}.ndjson); AddJsonAuditLog takes
a root directory. Session ids are sanitised for file names; events
with no session bucket to "no-session".
- Non-ASCII text (e.g. 中文) is stored as readable UTF-8 instead of
\uXXXX, in both the audit file and the SSE chat stream.
- Assistant replies now record their content (was role/latency only).
- Sample sets Console.OutputEncoding = UTF8 so non-ASCII logs render.
Refs task 29.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The model now replies with a JSON object {offtopic, action, message}
via response_format json_object, instead of a first-line ANSWER/ESCALATE
control word that the model frequently failed to emit. Folds the topic
classifier into the same call (offtopic field).
Key fixes behind this:
- Send-history stores the raw JSON envelope, not plain display text.
The model imitates its own prior turns, so plain-text history taught
it to drop the format and reply with bare/whitespace text on later
turns (root cause of blank-answer bug). FullHistory keeps readable
text for ticket generation.
- Empty/invalid replies retry up to MaxAnswerAttempts (default 3) within
a turn before escalating to a human, replacing the fail-open triage.
- JsonAnswerStreamReader streams the message token-by-token, waiting for
offtopic/action before emitting and holding leading whitespace.
- Configurable ChatProvider.Temperature (default 0.2) for consistent
grounded answers.
- Audit log records the full raw JSON response on assistant messages.
Removes ResponseControl and FailOpenEscalateThreshold.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BotWireClient (Layer 1): framework-agnostic, zero-DOM client for session init, chat, and SSE streaming. Self-heals a stale token once (same recovery the widget did), injectable fetch for testing, X-BotWire-Key support. Public stream events use clean names (delta/collect_contact/escalated/blocked/done), mapped from the server wire events. Widget refactored to consume BotWireClient — removes the duplicated initSession/openStream/isInvalidSession/post and the manual SSE loop (-65 net lines). sessionStorage stays in the widget; the token is bridged via get/setSessionToken. HTTP errors now surface the host errorMessage, matching the old !resp.ok branch. Package set up for dual ESM+CJS publish (exports map, .d.ts via tsc), version 0.1.0, AGPL-3.0-or-later. Rebuilds the embedded widget bundle (BotWire.AspNetCore/botwire.js, 14.4kb / 4.7KB gzip). Refs Task 31. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four UX features on the botwire-widget Web Component, all opt-in via data-* and inert when unset: - data-starters="a|b|c": starter chips on an empty conversation; click fills the input and sends; hidden after the first message; restored on reset. - Reset button in the header (data-reset="false" hides it, data-reset-confirm ="false" skips the dialog): clears the message list and session, re-inits, and aborts any in-flight stream so it cannot write into the fresh session. - data-lang (en/zh-CN/ja): built-in UI translations via a t() lookup; a host data-* attribute still wins, and en / no data-lang is unchanged. - Off-topic blocked events now render as a normal assistant bubble (its own bubble, never overwriting streamed text), with a data-offtopic-message override. Streaming now uses an AbortController so reset/supersede cancels the turn; the finally only mutates shared state for the active stream. Build: BuildBotWireWidget tracks all npm/botwire-js/src/**/*.ts (the widget imports the SDK) and runs BeforeTargets="Build;CoreCompile" so the rebuilt botwire.js is embedded in the same build instead of one build behind. Refs Task 32. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…k 27) Implements the rate-limiting design (decision 008) with five independently configurable dimensions, each disabled when set to 0: - MaxConcurrentSessions — queue/wait for a slot (never reject) - MaxMessagesPerMinute — per-session delay (never reject) - MaxMessagesPerSession — prompt user to start a new conversation - MaxSessionsPerIpPerHour — reject new sessions over the cap - DailyTokenBudget — degraded response when the daily budget is spent Counters are in-memory and per-process (reset on restart); durable, cross-instance budgeting via Redis is deferred to Phase 3 and documented. To feed the token budget and audit trail with real usage, ILlmChatClient now surfaces token counts: ChatAsync returns LlmChatResult (text + tokens) and the streaming overload reports usage via an onUsage callback, falling back to a char-based estimate when the provider omits usage. Tokens flow through AnswerResult.TokensUsed and a new internal BotEvent.Usage event so every path — including escalation (which emits no Done event) and off-topic streamed replies — records its usage on the assistant or escalated audit entry. TicketGenerator now returns its summarisation token count as well. Wires RateLimitOptions onto BotWireOptions.RateLimiting and registers the limiter in DI. Adds unit tests for all five dimensions plus service-level gate and token-accounting tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…otes - Bump version to 0.2.0 (.NET + botwire-js npm package) - Add a concise, image-free NUGET.md and point all three packages at it (the root README has screenshots + relative links that don't render on NuGet) - Broaden PackageTags for discoverability (ai, rag, openai, llm, gpt, sse, self-hosted, support-bot, helpdesk, ...) - Add docs/release-notes/v0.2.0.md for the GitHub Release body Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an npm job, gated on the .NET release succeeding, that verifies the tag matches package.json, builds (SDK + widget), tests, and publishes with provenance. Authenticates via the NPM_TOKEN repo secret. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… token npm steers CI/CD to Trusted Publishing. Drop NPM_TOKEN; authenticate via OIDC (id-token: write already set), matching the NuGet Trusted Publishing approach. Upgrade npm in the job since OIDC publishing needs npm >= 11.5.1, newer than Node 22 bundles. Provenance is attached automatically under trusted publishing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
npm publish has no --skip-duplicate, so a re-run or a manually bootstrapped first publish would fail the job. Skip publishing when the version already exists on the registry. Co-Authored-By: Claude Opus 4.8 <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.
Phase 1.5 of BotWire: production-hardening features, a framework-agnostic JS SDK, and the 0.2.0 release prep. All Phase 1.5 tasks are delivered.
Highlights
0.ILlmChatClientnow reports usage; tokens feed the daily budget and the audit trail (message+escalatedevents), including the escalation and off-topic streaming paths.IAuditLogger.botwire-jsnpm SDK — zero-DOM JS/TS client (chat, SSE streaming, sessions); the embedded widget is rebuilt on top of it.data-lang.Breaking change
ILlmChatClient.ChatAsyncreturnsLlmChatResult(text + tokens) instead ofstring;ChatStreamingAsyncgains anAction<int>? onUsagecallback. Custom implementations must update; no change needed for the built-in OpenAI client.Release prep (0.2.0)
0.2.0(.NET +botwire-js).NUGET.mdfor all three packages; broadened package tags.docs/release-notes/v0.2.0.mdfor the GitHub Release body.botwire-jsto npm via Trusted Publishing (OIDC), gated on the .NET release and idempotent on re-runs.botwire-js@0.2.0is already live on npm (manual bootstrap publish + Trusted Publisher configured for future CI releases).Verification
After merge
Tag
v0.2.0onmainand push → CI packs + publishes to NuGet (OIDC) and creates the GitHub Release. The npm job will skip 0.2.0 (already published) and handle 0.3.0+ automatically.🤖 Generated with Claude Code