Skip to content

Phase 1.5 — production hardening + 0.2.0 release#23

Merged
adamy merged 14 commits into
mainfrom
feature/phase1-5
Jun 12, 2026
Merged

Phase 1.5 — production hardening + 0.2.0 release#23
adamy merged 14 commits into
mainfrom
feature/phase1-5

Conversation

@adamy

@adamy adamy commented Jun 12, 2026

Copy link
Copy Markdown
Owner

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

  • Five-dimension rate limiting (decision 008): concurrency (queue), per-minute (delay), per-session cap, per-IP/hour, daily token budget. Each disabled at 0.
  • End-to-end token accountingILlmChatClient now reports usage; tokens feed the daily budget and the audit trail (message + escalated events), including the escalation and off-topic streaming paths.
  • Audit log — opt-in NDJSON, one file per session bucketed by UTC date; pluggable IAuditLogger.
  • Session summary compression — folds old turns into a rolling LLM summary to bound long-conversation token cost.
  • Topic / off-topic classification folded into the answer JSON (no extra model call).
  • botwire-js npm SDK — zero-DOM JS/TS client (chat, SSE streaming, sessions); the embedded widget is rebuilt on top of it.
  • Widget enhancements — conversation starters, reset button, off-topic/blocked handling, data-lang.
  • Session self-heal — stale token transparently rebuilt + message resent.

Breaking change

  • ILlmChatClient.ChatAsync returns LlmChatResult (text + tokens) instead of string; ChatStreamingAsync gains an Action<int>? onUsage callback. Custom implementations must update; no change needed for the built-in OpenAI client.

Release prep (0.2.0)

  • Version bumped to 0.2.0 (.NET + botwire-js).
  • Dedicated image-free NUGET.md for all three packages; broadened package tags.
  • docs/release-notes/v0.2.0.md for the GitHub Release body.
  • Release workflow now publishes botwire-js to npm via Trusted Publishing (OIDC), gated on the .NET release and idempotent on re-runs.

botwire-js@0.2.0 is already live on npm (manual bootstrap publish + Trusted Publisher configured for future CI releases).

Verification

  • Release build: 0 warnings.
  • Tests: Core 251, AspNetCore 27, Email 22, npm 29 — all green. (One live-LLM integration test is model-nondeterministic and unrelated; it fails the same way on a clean tree.)

After merge

Tag v0.2.0 on main and 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

adamy and others added 14 commits June 11, 2026 18:48
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>
@adamy adamy merged commit 64705d5 into main Jun 12, 2026
2 checks passed
@adamy adamy deleted the feature/phase1-5 branch June 24, 2026 08:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant