Skip to content

sync: develop → develop-auto (pre-promotion sync for skillsets #980)#981

Merged
chronoai-shining merged 44 commits into
develop-autofrom
develop
Jun 9, 2026
Merged

sync: develop → develop-auto (pre-promotion sync for skillsets #980)#981
chronoai-shining merged 44 commits into
develop-autofrom
develop

Conversation

@chronoai-shining

Copy link
Copy Markdown
Collaborator

Routine sync of develop into develop-auto so the skillsets promotion PR #980 (develop-auto → develop) becomes up-to-date and mergeable under develop's strict status-check rule. Brings develop-only work (Ornn Assistant #970/#975) into develop-auto. No conflicts (develop-auto↔develop is MERGEABLE). Merge as a MERGE COMMIT (not squash).

chronoai-shining and others added 30 commits June 8, 2026 15:51
chore: promote /auto code-quality run → develop (restore CI parity)
Structural data for the landing lifecycle ring: the eight agent-facing
stages in sequence (id, order, line icon, accent voice, CTA route), plus
stroke line icons. Human copy lives in i18n and node geometry derives from
`order`, so this stays a pure, decoupled data module.
Agent-first copy for the eight lifecycle stages (name / title / body /
endpoint hint / CTA) plus the hub strings, under `landing.lifecycle`.
EN and ZH mirror each other key-for-key.
Forge-style ring styling + motion: comet orbit, comet-synced travelling
node pulse, activation ripple ping, flowing connector, counter-rotating
inner ring. Re-pins the consumed tokens to a fixed dark context (the hero
video is dark in BOTH themes) and adds a localized scrim for legibility.
All motion is gated by prefers-reduced-motion.
Interactive ring overlaid on the looping hero video. Eight stages around a
hairline circle; hover / focus / tap reveals a centre detail card (agent-API
hint + CTA) and closes on leave / blur / tap-outside. The layer is
pointer-events-none except the nodes and active card, so the video and the
CTA plate stay interactive. Self-assembling entrance (ring draws, nodes
stagger in, comet fades up). Mounted in HeroVideo. Colocated test covers the
disclosure behaviour (open on hover/focus/tap, switch, tap-outside close).
Move the dynamic announcement popup from a centred, page-dimming modal to a
non-blocking top-right notification card so it no longer covers the hero /
lifecycle ring. Drops the full-screen backdrop (pointer-events-none wrapper +
pointer-events-auto card); closes via the X button or Escape. Slides in from
the right. Drops the now-inaccurate aria-modal.
Re-anchor the hardcoded launch popup to the top-right (non-blocking, no
page-dimming backdrop) and compact it: tighter section spacing, smaller offer
numerals, smaller padding, and a shorter max-height (82vh -> 64vh) so it no
longer spans the viewport. Launch content is unchanged; overflow scrolls
within the card.
Minor bump: landing skill-lifecycle ring + top-right non-blocking popups.
A collapsed top-right 'headline pill' that expands on hover/focus/tap into a
dismissable stack. Aggregates the hardcoded launch announcement (pinned) with
all dynamic announcements from usePublicAnnouncements(), newest first. Each
item persists its dismissal in localStorage; the banner renders nothing once
everything is dismissed. Solid card surfaces so it stays legible over the dark
hero video and on light/dark app pages alike. Open-only on click so the
emulated mouseenter on tap can't slam it shut on touch.
…949)

Mount AnnouncementBanner in AnalyticsRoot so it appears on every page (not
just the landing). Remove the centred AnnouncementPopup + LaunchCelebrationPopup
from LandingPage and delete them — the banner subsumes both: the hardcoded
launch content moves into the banner's condensed launch card, and dynamic
announcements render as banner items. Net effect: one global, dismissable
announcement surface instead of two landing-only modals.
…e-ring

feat(web): skill-lifecycle orbital ring on the landing hero
Promotes 9 verified bug fixes from develop-auto:
#632 ZIP-bomb caps · #757 CodeQL modulo-bias+ReDoS · #766 playground abort
quota · #608 chat-completion tool_calls · #750 version-delete GUID routing ·
#751 notification dropdown refetch · #940 deleted-skill 404 · #941 My-Skills
count refresh · #752 timestamp i18n.

Cumulative suite green on the merged tree (ornn-api 1656 pass, ornn-web 337
pass, typechecks clean); each PR was CI-green at merge and acceptance criteria
re-verified. #803/#805 (NyxID identity verification) intentionally NOT included
— escalated to #948 for a human/NyxID trust-model decision.
…velop

chore: promote verified bug-fix run (9 fixes) → develop
Dated release-notes file for the v0.11.0 develop → main promotion: 9 bug
fixes + the #949 announcement-banner / skill-lifecycle-ring web features
+ the code-quality run (technical). Empty changeset so check-changeset
passes on this docs-only PR.

Co-authored-by: chronoai-shining <shiningwn@outlook.com>
release: promote develop → main (v0.11.0)
Co-authored-by: ornn-release-bot[bot] <283908294+ornn-release-bot[bot]@users.noreply.github.com>
chore: sync main → develop after v0.11.0
Add the type layer for the Ornn Assistant chatbot's data path. The
assistant reuses the Playground SSE transport but speaks its own event
contract, so the strings are deliberately distinct (`chat_text_delta`
vs the Playground's `text-delta`) to keep the two streams from ever
cross-wiring.

The event union is a Zod discriminated union over
chat_start / chat_text_delta / chat_error / chat_finish / keepalive —
no `as any`. `parseAssistantEvent` validates each parsed SSE payload and
returns null for anything malformed / off-contract, so a drifting
backend surfaces as a dropped event rather than an untyped object
reaching React. `usage` on chat_finish is forward-compatible: known
fields optional, unknown keys stripped.

`AssistantMessage` is a structural subset of `PlaygroundMessage`
(id/role/content) so the existing ChatMessage renderer can paint
assistant turns without adaptation.
Zustand store for the assistant conversation, streaming state, the live
answer buffer, and the slide-in panel's open/closed state. Session-only
(NO persist) exactly like playgroundStore — the assistant is a transient
helper, not a saved thread, so conversation + panel state reset on
reload.

clearMessages resets the conversation but intentionally leaves panel
visibility untouched (clearing a chat shouldn't close the panel the
user is looking at). finalizeAssistantMessage is a no-op on an empty
buffer so an aborted-before-first-token turn doesn't push a blank
bubble.
POST /api/v1/assistant/chat streaming client, mirroring
playgroundStreamApi: canonical Bearer auth via the auth store's
ensureFreshToken, AbortController cancel, shared parseSseChunk. Every
parsed data: payload is validated through parseAssistantEvent before
dispatch; unrecognized events are debug-logged and skipped rather than
forwarded untyped.

Non-OK responses are decoded as RFC 7807 problem+json (per
docs/CONVENTIONS.md) — detail/code are lifted into a synthetic
chat_error event so the UI has one uniform failure channel regardless
of whether the failure was transport-level or a streamed error.
Drives the send → stream → display loop. Reuses the playground
typewriter pacer: chat_text_delta events fill a pending buffer that a
fixed-cadence pacer drains char-by-char, with adaptive catch-up so the
visible text stays within ~1s of what's been received. Terminal events
(chat_finish / chat_error / abort) flush the buffer immediately.

Errors surface on BOTH the store (inline panel display) and a toast, so
a user who scrolled away still sees the failure. keepalive heartbeats
are ignored. The in-flight stream and pacer interval are torn down on
unmount.
Mirror the backend LlmProviderModel contract (ornn-api settings
llmProviders/types.ts) on the web client: add enabledForAssistant +
defaultForAssistant to LlmProviderModel, and the matching optional
fields to ModelFlagsPatchInput so the admin drawer can PATCH the
assistant surface. The backend PATCH schema already accepts both as
optional booleans and the repository defaults them to false, so existing
provider docs deserialize unchanged.
Add a fourth surface column (Assistant) to the per-provider model
manager, mirroring Playground and Skill-Gen: an enable Toggle plus a
default Radio per model row, each firing the existing optimistic PATCH
with enabledForAssistant / defaultForAssistant. Switched the table off
table-fixed so four columns share width gracefully, and bumped the
archived-section colSpan to 4.

Column header copy stays inline English to match its hardcoded
Playground / Skill-Gen siblings in this admin-only drawer; the
user-facing assistant strings are fully i18n'd in the widget.
The per-provider row summary counted playground- and skillGen-enabled
models; add the assistant-enabled count so admins can see at a glance
how many models back the Ornn Assistant surface. Extends the
modelCounts i18n string (en + zh) with the new {{assistant}} slot.
… section (#970)

Adds the third LLM surface (after playground/skillGen) that backs the
Ornn Assistant repo-aware Q&A chatbot.

- LlmProviderModel gains per-surface flags enabledForAssistant /
  defaultForAssistant; SurfaceKey gains 'Assistant'. Reads of pre-#970
  provider docs default both to false so an existing model never
  auto-routes to the assistant until an admin opts it in.
- Surface union widened to 'playground'|'skillGen'|'assistant'. The
  surface-field helpers and the at-most-one-default-per-surface
  invariant are now table-driven (SURFACE_KEY + ALL_SURFACES) so a
  future surface is a one-line addition, not scattered ternaries.
- New 'assistant' settings section (defaultProviderId, defaultModelId,
  sseKeepAliveMs, defaultMonthlyQuota) mirroring playground/skillGen,
  registered in the section registry and exposed via
  SettingsService.getAssistant().

Surfaces stay independent: flipping the assistant default leaves the
playground/skillGen defaults untouched (covered by new unit tests for
resolver precedence + the cross-provider single-default invariant).
…rs (#970)

- GET /me/models accepts surface=assistant; resolution-error labels are
  table-driven so the assistant surface emits a coherent
  MODEL_UNAVAILABLE message instead of borrowing skill-generation's.
- bootstrap resolveLlmProviderForSurface / resolveSurfaceDefaults now
  resolve the 'assistant' surface from the new settings section. The
  shared NyxLlmClient is reused unchanged.
…#970)

Carried over from the ProviderEditDrawer test template; the models-drawer
test never references ReactNode. Removing it clears the
@typescript-eslint/no-unused-vars lint error.
A floating bottom-right launcher opens a corner chat panel that streams
repo-aware answers about Ornn. Reuses the Playground chat primitives
(ChatMessage / ChatInput) and the assistant data layer (useAssistantChat
+ useAssistantStore).

Forge Workshop language (docs/DESIGN.md): semantic tokens only,
letterpress impression shadows (cta-letterpress / card-impression),
press-DOWN launcher hover, Framer Motion panel reveal that collapses
under prefers-reduced-motion. a11y: ESC + backdrop close, focus moves
into the composer on open and returns to the launcher on close,
focus-visible ember rings, labelled controls, a status-role thinking
indicator. Empty state suggests the three example questions
("What is Ornn?", "How is Ornn different?", "Find a skill that does X");
clicking one fills the composer for editing before send.

All strings via react-i18next (en + zh). Session-scoped — no persistence.
Render <AssistantWidget /> from RootLayout, gated on useIsAuthenticated
so it only exists for signed-in users. The widget portals its own
launcher + panel to document.body, so mounting it here (rather than
inside <main>, which is overflow-hidden) keeps it clear of the layout
clip and above page content while staying below toasts.
chronoai-shining and others added 14 commits June 9, 2026 14:46
…oader (#970)

Introduces the reusable KB primitives that ground the Ornn Assistant's
answers in the repo's own knowledge:

- tokens.ts: deterministic chars/4 token estimate + budget clamp +
  ASSISTANT_KB_TOKEN_BUDGET env resolution (default 18k). Model-agnostic
  on purpose — an exact tokenizer for one model is meaningless for
  another; determinism beats precision here.
- distiller.ts: the KbDistiller contract + DeterministicKbDistiller v1
  (markdown section extraction → per-source cap → titled concat → global
  budget clamp). The interface is the documented hook for a future
  LlmKbDistiller ("big model reads the repo at build time") — same
  contract, model-driven summarization swapped in underneath.
- loader.ts: cached, fail-soft runtime loader that reads the committed
  digest artifact, strips its provenance header, and defensively clamps
  to budget. A failed read degrades to empty grounding, never a crash.
- sources.ts: priority-ordered, capped source manifest (docs only, never
  code/secrets) consumed at build time.

Pure + deterministic by construction (no clocks/RNG/network). Unit tests
cover budget enforcement, section extraction, caching, and fail-soft.
Adds the build pipeline that turns the curated source manifest into the
runtime grounding artifact:

- scripts/build-assistant-kb.ts reads the manifest docs from the repo
  root, runs the DeterministicKbDistiller, and writes the committed
  digest.generated.md. Output carries an HTML-comment provenance header
  (stripped by the loader, never fed to the model) and NO timestamp, so
  re-running on unchanged inputs is byte-identical (clean diffs, stable
  CI). Missing source docs are skipped with a warning, not a failure.
- digest.generated.md: the committed artifact (~12.4k tokens, 6 sources)
  — README overview, CLAUDE positioning, ARCHITECTURE, the HTTP agent
  manual, API conventions, and a design overview slice.
- digest.artifact.test.ts guards the shipped artifact: non-empty, within
  budget, header hidden, Ornn-grounded, no secret-shaped content.
- package.json: `bun run build:assistant-kb` to regenerate.

Regenerate the artifact whenever the manifest or source docs change.
#970)

The Ornn Assistant is a billed LLM surface, so quota must reserve +
charge against it. Widen the quota `Surface` type to include 'assistant'
and resolve its default allotment from `assistant.defaultMonthlyQuota`.

Kept surgical to avoid frontend/admin ripple:
- `SURFACES` (admin-grant + redemption-code enums, quota-snapshot UI)
  stays ["playground","skillGen"]. The assistant isn't admin-grantable
  or redeemable in v1 — its allotment is the section default only.
- Introduced `GrantableSurface = SURFACES[number]`; grant/bulkGrant +
  the redemption grant entry use it, so they stay assignable to the
  notification layer's narrow surface type. The QuotaSnapshot shape is
  unchanged.
- `QuotaDefaults.defaultAssistantMonthly` is optional so existing
  resolver mocks keep compiling; the production resolver supplies it.
  Absent → 0 allotment (fail-closed) until wired.
…ce (#970)

The pure (HTTP-free) core of the Ornn Assistant — a non-agentic Q&A
pipeline:

- retrieval.ts: ScopedSkillRetriever runs visibility-scoped keyword
  search (scope "mixed") and enforces canReadSkill again at the
  projection layer, emitting RetrievedSkill — a SAFE projection (name,
  description, tags, category, createdOn, createdBy person-id). Listing
  fields explicitly (never spreading the doc) is the data-safety
  boundary: no createdByEmail/DisplayName, storageKey, skillHash,
  sharedWith* or any PII can leak.
- contextAssembler.ts: ASSISTANT_SYSTEM_PROMPT (grounded-Q&A persona,
  no fabrication, no cross-user data) + curated KB + scoped skills, all
  in one leading developer message; conversation turns follow verbatim.
- chatService.ts: emits chat_start → text deltas → chat_finish for ONE
  streamed completion with NO tools (the structural guarantee that the
  assistant can never trigger an execution loop). Retrieval failure is
  non-fatal (KB-only still answers); stream error → chat_error with a
  catalog code; best-effort usage on finish.

Tests pin the data-safety projection (incl. a projection-layer drop of
an unreadable skill), the no-tools guarantee, fail-soft retrieval, and
the event sequence.
Mounts the assistant endpoint following the playground reference +
CONVENTIONS pipeline: nyxidAuth → rateLimit(30/min) → validateBody →
resolveModel(assistant) → buildActorContext → quota reserve(assistant)
→ SSE stream → chargeOnCompletion. Model resolution + quota reserve run
before the stream so a misconfig/cap-hit returns clean RFC 7807 JSON;
in-stream failures surface as a chat_error event.

SSE frames carry both the native `event:` line and a JSON `data:` line
whose type matches (CONVENTIONS §6.3): chat_start / chat_text_delta /
chat_error / chat_finish + keepalive comment frames. Quota is reconciled
in a finally that always runs (commit on success / billed-then-aborted,
release on pre-token system error).

bootstrap wires wireAssistant over the shared NyxLlmClient + SkillRepo +
QuotaService, resolving the assistant surface defaults + sseKeepAliveMs
from settings, and warms the KB cache at boot. Route mounted under
/api/v1. Integration tests cover framing, auth/validation/model/quota
gates, and — mandatory — that a private skill + PII never reach the
streamed context through the real pipeline.
Adds assistantChatRequestBodySchema + assistantChatEventSchema and the
`POST /api/v1/assistant/chat` path (SSE response, 400/401/429/503 error
responses, "Assistant" tag) so the generated openapi.json documents the
new endpoint and its event contract.
Single minor changeset (fixed-versioned ornn-api + ornn-web) describing
the Ornn Assistant feature. Required by the check-changeset CI gate.
The Ornn Assistant endpoint POST /v1/assistant/chat ships without any new
contract surface to document, but its SSE event set and error behaviour
were undocumented. Two precise additions:

- CONVENTIONS.md §6.2: add the assistant row to the SSE endpoint mapping
  table — events chat_start, chat_text_delta, chat_error, chat_finish
  (the tool/file events from playground/chat do not apply here).
- ERRORS.md: note under upstream_unavailable that /assistant/chat
  introduces NO new error codes. Pre-stream failures reuse
  validation_error (400), authentication_required (401), rate_limited
  (429), and MODEL_NOT_ENABLED / MODEL_NOT_FOUND (400, only when an
  explicit modelId is supplied). In-stream LLM failure surfaces as an
  SSE chat_error event with code upstream_unavailable and no chat_finish.

Verified against ornn-api/src/domains/assistant/{routes,chatService}.ts
and the shared throwModelResolutionError helper.
Quality-review nit: the frontend assistantUsageSchema used
promptTokens/completionTokens while the backend AssistantUsage contract
(ornn-api domains/assistant/types.ts) and the OpenAPI spec emit
inputTokens/outputTokens/totalTokens. Rename the two drifted fields so a
populated chat_finish.usage validates instead of being silently stripped.
totalTokens was already correct.
Two quality-review a11y nits on the assistant panel:

- The panel declares role=dialog / aria-modal=true but had no focus
  trap, so Tab / Shift+Tab escaped to the backdrop'd page behind it. Add
  a trap scoped to the dialog via onKeyDown (currentTarget — no ref
  threading) that wraps focus at the first/last focusable. The existing
  focus-move-into-composer-on-open and restore-to-launcher-on-close
  behavior is unchanged.
- The header close / clear IconButtons were h-8 w-8 (32px), under the
  44px mobile touch-target guideline in docs/DESIGN.md. Bump the hit
  area to h-11 w-11 (44px); the launcher and suggestion rows already
  met it.
Security review finding #1 (LOW): the architecture + conventions KB
sources distilled whole docs, so internal infra surfaced in the digest
any authenticated user can elicit — env-var catalogs, internal proxy
header names (X-NyxID-*/X-Ornn-Caller-*), telemetry/observability
internals, and the user-directory section. No secret VALUES, but
needless internal-recon surface.

Add a `headings` allow-list to both sources (mirroring README/DESIGN):
- architecture → Project Overview, External Services, Skill Format only
  (drops the PostHog/telemetry, caller-type-detection, env-var config,
  and user-directory sections).
- conventions → §1 Response/error, §2 URL structure, §3 HTTP semantics,
  §4 Query params, §6 SSE only (drops §5 Auth's internal X-NyxID-* note
  and §7–§12 deprecation/caching/observability/architecture internals).

Regenerated digest is clean of every internal-recon marker (grep ✓) and
still carries the user-relevant grounding; ~10.8k tokens, deterministic.
The grounding digest (digest.generated.md) is a committed build artifact
distilled from the repo docs. If a source doc or the source manifest
changes but the digest isn't regenerated, the assistant silently ships
stale grounding.

Add an `assistant-kb-freshness` CI job that rebuilds the digest with
`bun run build:assistant-kb` and `git diff --exit-code`s the artifact —
the build is deterministic (no timestamp/RNG), so a clean tree means the
committed digest is in sync. A stale digest fails the PR with a clear
"re-run build:assistant-kb and commit" annotation.
Quality review flagged that the SSE client had only indirect coverage
via the hook. Add a focused transport test that mocks fetch + the auth
store and exercises the four load-bearing behaviors:

  - POSTs to /api/v1/assistant/chat with the Bearer header and the
    {messages, modelId} JSON body
  - parses chat_* frames off the stream via the real sseParser, in order
    (incl. a frame split across two chunk boundaries → remainder buffering)
  - maps a non-OK RFC 7807 problem+json response to a synthetic
    chat_error carrying detail + code
  - wires the AbortController so handle.abort() cancels the request and
    the AbortError path stays silent (no spurious chat_error)

The sseParser and event schema are the real modules, so this also guards
the parse → validate → dispatch seam end to end.
…hatbot

[Feature] Ornn Assistant — repo-aware Q&A chatbot (API + web widget)
@chronoai-shining chronoai-shining merged commit cfb8629 into develop-auto Jun 9, 2026
17 of 19 checks passed
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.

2 participants