Skip to content

Release - #284

Open
pirate-bot wants to merge 20 commits into
mainfrom
development
Open

pirate-bot wants to merge 20 commits into
mainfrom
development

Conversation

@pirate-bot

@pirate-bot pirate-bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Linked issues

This release will close the following issues once merged:

Public changelog

  • Fixed Knowledge Base posts failing to add on hosts with URL fopen disabled.
  • Fixed the chatbot missing phone numbers and emails shown only as links.
  • Fixed the chatbot finding knowledge base answers by exact words.
  • Fixed chat losing knowledge while Qdrant migration runs or fails.
  • Fixed the chatbot resolving tomorrow and events to the correct date.
  • Fixed chat errors on sites using retired OpenAI models
  • Fixed large Knowledge Base imports failing on OpenAI rate limits.
  • Fixed follow-up questions searching old chat history instead of the new question.
  • Fixed WPBakery Raw HTML pages syncing as encoded text.
  • Updated dependencies

pirate-bot and others added 3 commits October 22, 2026 12:38
…ash on it (#278)

* fix(kb): load the tokenizer vocabulary through the HTTP API, never crash on it

The tiktoken library fetches its vocabulary with fopen() on a remote
URI and needs a writable temp directory. On a host with allow_url_fopen
off or a locked temp directory it threw a RuntimeException that nothing
caught, so every self-hosted add crashed the REST request (#241).

A new Vocab_Loader streams the download with wp_remote_get() into the
temp directory, or uploads when that is not writable, under the file
name the library uses so an existing cache is reused. Streaming is the
fast path: buffering the 1.7 MB body through the HTTP API took over a
minute where streaming took seconds. With nowhere to write it buffers
in memory as a last resort. Downloads are checksummed, written beside
the final name and renamed, so a reader never sees a partial file.

ingest_document() now reports a tokenizer failure as an error on the
row instead of letting the exception escape. The encoder is built once
per request.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(tokenizer): trim page context by characters when the vocabulary is unavailable

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(page-context): describe the character-trim fallback

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(kb): keep a tokenizer failure on the row and retry it from the cron

QA on the PR build found the "could not be tokenized" reason only in the
snackbar: the row showed nothing once it faded, and nothing after a reload.
Development has since started recording every ingest error against the
post (#276), so after the rebase the reason stays on the row and counts
toward Needs attention. The update cron now records it as a retry rather
than as something the admin must fix, since the post stays queued.

The #275 encoder guard in pinned_chunks() dropped the chunk that needed a
trim; with trim_to_limit() falling back to a character cut, the chunk is
kept trimmed and the guard goes. Its test now blocks the download the way
the tokenizer tests do, because an unusable temp directory alone no longer
stops the vocabulary from loading.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(tokenizer): remember a failed vocabulary load for the rest of the request

With ingest no longer crashing on it, update_posts() went on to the next
post and tried the download again, up to five times a run; a slow CDN
could block the cron for one timeout per post. The first failure is now
kept and rethrown for the request.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* style(tokenizer): satisfy PHPCS on the remembered-failure change

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@pirate-bot

pirate-bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor Author

🌍 i18n String Review Report

📊 Summary

Category Count
➕ Added 35
➖ Removed 14
🔄 Changed 0
Total 49
➕ Added Strings (35) - Click to expand
String Location Words Suggested Match
Hyve Connect refused the request (%s). Synci... inc/DB_Table.php:1892 13 Can't reach Hyve Connect right now. We'll keep retrying.
Hyve Connect could not be reached. Syncing res... inc/DB_Table.php:1896 13 Can't reach Hyve Connect right now.
The content source could not be found. inc/Ingestion_Queue.php:118 7 This content is not in the knowledge base.
This source is already being indexed. Wait for... inc/Ingestion_Queue.php:122 14 A sync is already in progress.
The indexed content could not be saved. inc/Ingestion_Queue.php:146 7 Indexing failed: %s
The embedding response was incomplete. inc/Ingestion_Queue.php:480 5 Unexpected response from the hosted AI.
The staged content is missing. Re-add this co... inc/Ingestion_Queue.php:519 8 Fix the reported issue and re-add this content.
BF Sale - %s off inc/Main.php:1204 5 Black Friday Sale - %s off
OpenAI did not return a moderation result. Thi... inc/OpenAI.php:1487 13 The hosted AI did not return a result.
Qdrant reported an error (%s). Hyve will ret... src/backend/screens/Integra... 20 Can't reach Hyve Connect right now. We'll keep retrying.
Disconnect locally src/backend/screens/Integra... 2 Disconnect
Checking %1$d of %2$d src/backend/screens/Knowled... 4 Adding %1$s of %2$s
Checking %1$d of %2$d, waiting for the API limit src/backend/screens/Knowled... 9 Adding %1$s of %2$s
Checking %1$d of %2$d, retrying after an error src/backend/screens/Knowled... 8 Adding %1$s of %2$s
Embedding %1$d of %2$d src/backend/screens/Knowled... 4 Adding %1$s of %2$s
Embedding %1$d of %2$d, waiting for the API limit src/backend/screens/Knowled... 9 Adding %1$s of %2$s
keyword %s src/backend/screens/Message... 2 Unsupported setting: %s
Query (rewritten) src/backend/screens/Message... 2 Query
The exact text used to search the knowledge bas... src/backend/screens/Message... 26 The exact text used to search the knowledge base: the question blended with recent turns of the conversation, so follow-ups keep their topic.
The exact text used to search the knowledge base. src/backend/screens/Message... 9 The exact text used to search the knowledge base: the question blended with recent turns of the conversation, so follow-ups keep their topic.
Content is queued for indexing. src/backend/utils.js:189 5 The source is waiting to be processed.
The content could not be tokenized: %s inc/DB_Table.php:933 7 No close match
Hyve Connect rejected this site's credentials.... inc/DB_Table.php:1888 10 No close match
The Qdrant index could not be saved. inc/Ingestion_Queue.php:580 7 No close match
The Qdrant index could not be published. inc/Ingestion_Queue.php:598 7 No close match
The old Qdrant index could not be replaced. inc/Ingestion_Queue.php:617 8 No close match
The replacement index could not be saved. inc/Ingestion_Queue.php:630 7 No close match
The previous index could not be removed. inc/Ingestion_Queue.php:634 7 No close match
The query could not be rewritten. inc/OpenAI.php:750 6 No close match
Qdrant did not confirm the upload. inc/Qdrant_API.php:994 6 No close match
Sync paused src/backend/screens/Connect... 2 No close match
Sync paused. src/backend/screens/Connect... 2 No close match
Could not disconnect from Qdrant. You can disc... src/backend/screens/Integra... 33 No close match
Embedding %1$d of %2$d, retrying after an error src/backend/screens/Knowled... 8 No close match
Knowledge base content sent to the model as con... src/backend/screens/Message... 60 No close match
Total 355
➖ Removed Strings (14) - Click to expand
String Location
GPT-5 src/backend/screens/AI.js:91
Reasoning model with strong quality at a lower ... src/backend/screens/AI.js:93
GPT-4.1 src/backend/screens/AI.js:99
Capable and proven, great for detailed answers. src/backend/screens/AI.js:101
GPT-4.1 mini src/backend/screens/AI.js:107
Faster and cheaper than GPT-4.1, a solid all... src/backend/screens/AI.js:109
GPT-4.1 nano src/backend/screens/AI.js:115
Ultra-fast and very low cost, best for lightwe... src/backend/screens/AI.js:117
GPT-4o src/backend/screens/AI.js:123
Smart, cost-effective general-purpose model. src/backend/screens/AI.js:125
GPT-4o mini src/backend/screens/AI.js:131
Fastest and most affordable, best for most chats. src/backend/screens/AI.js:133
Knowledge base content sent to the model as con... src/backend/screens/Message...
The exact text used to search the knowledge bas... src/backend/screens/Message...

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Plugin build for b758731 is ready 🛎️!

dependabot Bot and others added 17 commits October 28, 2026 02:01
Bumps [codeinwp/themeisle-sdk](https://github.com/Codeinwp/themeisle-sdk) from 3.3.67 to 3.3.68.
- [Release notes](https://github.com/Codeinwp/themeisle-sdk/releases)
- [Commits](Codeinwp/themeisle-sdk@v3.3.67...v3.3.68)

---
updated-dependencies:
- dependency-name: codeinwp/themeisle-sdk
  dependency-version: 3.3.68
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
)

* fix(kb): keep tel: and mailto: link targets in knowledge base text

A contact page often holds its phone number and email only as link
targets: `<a href="tel:+441274998877">Call us</a>`. `html_to_text()`
strips every tag, so the number never reached the knowledge base and the
bot could not answer "what is your phone number?" from a page that shows
one. Both modes are affected, since self-hosted and Connect ingest both
go through the same extractor.

The target of a `tel:` or `mailto:` link is now folded into the anchor
text before the strip, as "Call us (+441274998877)", unless the text
already shows it. Phone numbers are compared on digits from the end so
national and international forms match, a `+`-prefixed different number
is not taken for the target, `;ext=` becomes " ext. N", and an icon-only
link stores the bare value. No other scheme is folded, so navigation
URLs stay out of chunks. An unclosed contact link is bounded by the next
anchor and cannot take the rest of the page with it.

Connect reconcile hashes the extracted text, so already-synced pages
with a contact link re-upload once after this ships.

Fixes Codeinwp/hyve#350

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): extract page context like the knowledge base

Page awareness turned a scraped or rendered page into text with a plain
tag strip, predating the structured extractor the knowledge base uses.
A contact page not in the index lost its tel:/mailto: targets there,
and tables and headings fused as they once did on ingest. Both
page-context paths now go through Tokenizer::html_to_text().

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(connect): let the vocabulary download through the sync test's HTTP mock

The contact-link sync test answered every request with the canned SSE
body, so on a runner with no cached vocabulary the tokenizer's download
got that body, failed its checksum, and the failure was remembered for
the rest of the run. The mock now answers only the Connect workflow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(kb): address contact-link review findings

- A quoted tel:/mailto: target may hold spaces ("tel:01274 998877", as
  some widgets print a phone field raw); only an unquoted one ends at
  whitespace.
- The shown-number check tries each contiguous span of digit groups in
  a number-like run, so "01274 998877 (24 hours)" and "Mon-Fri 9-5
  01274 998877" count as showing the number. A run with its own "+"
  country code is read from its start and must carry the whole linked
  number, so "+44 1274 998877" shows tel:01274998877 while
  "+1 274 998877" does not show a UK target.
- An icon-only link's bare value is padded with spaces so it does not
  fuse with the text or link after it.
- Connect reconcile rehashes an unedited post that was synced by an
  older extractor: the synced marker now records the extractor version
  (Tokenizer::EXTRACTOR_VERSION), the manifest reuses a stored hash only
  when that matches, and a post whose text is unchanged adopts the
  current version so later reconciles reuse its hash again. Without
  this, a contact page synced before the fold kept its old text on the
  platform until edited.
- Tests: page-context coverage for the loop-page path, and the
  linear-time test now ends with a closed link that must be folded, so
  a regex that gives up fails it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(kb): a coded label must be the same number as a coded tel: target

"+49 441274998877" ends with the digits of tel:+441274998877, so the
suffix compare took it for the target and dropped the fold. When both
the label and the target carry a "+" country code they now have to be
the same number. A label with a code may still stand for a national
target that has none. The span loop is capped at 32 digit groups per
run so a table of numbers inside one anchor cannot stall extraction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(kb): read a 00 dialling prefix as a country code

Only "+" marked a number as carrying its country code, so
tel:00441274998877 with the label "+49 441274998877" (or the reverse
pairing) was taken for the same number by the suffix compare and the
target was dropped. A leading "00" now counts the same as "+" on both
sides, so two coded numbers must match exactly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(kb): say a 00 prefix counts as a country code in same_number()

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…eval (#283)

* feat(chat): fuse a keyword search with the vector search for KB retrieval

A question with a term the embedding does not carry (a product code, a
name, a rare word) scored below the floor and the chunk never reached the
model. Retrieval now runs two passes: the cosine scan or Qdrant search as
before, and a keyword pass over the chunk text through a FULLTEXT index on
the local table (prefix-matched terms, substring matching for scripts
without word boundaries). Each list keeps the matches within its own
relative cutoff; the two are merged by reciprocal rank with a title bonus
and then fitted to the token budget. The keyword pass runs on the local
table in Qdrant mode too, and alone when the embeddings call fails, which
no longer fails the turn. The trace records both scores per chunk.

Weights and the candidate cap are filterable through
`hyve_hybrid_retrieval`. The table version moves to 1.3.0 so dbDelta adds
the index on upgrade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): keep the question first in the keyword query, run it in the ability, show keyword hits

Review findings. The keyword pass reads ten terms, so a long thread could
crowd the current question's words out; the message now leads the keyword
query. The test-retrieval ability runs the keyword pass too, carries on
keyword-only when embeddings fail and says so with `embedding_failed`,
and its chunks carry `lexical`. The Messages debug panel labels a chunk
only the keyword search found. Equal scores order by row id. The
retrieval tests commit their cleanup so seeds and options no longer leak
into other classes, and new tests cover the upgrade index, the parser
edges, the fusion filter and the source order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(messages): show the keyword score next to the vector score, keep the upgrade test's version committed

A chunk both passes found showed only its vector score, so the debug panel
could not tell it from a vector-only hit; it now reads "0.50 · keyword 1.20".
The upgrade test's ALTERs commit implicitly, which left the downgraded
1.2.0 version behind after rollback; the upgrade is redone and committed
in `finally`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(e2e): cover the dual and keyword-only source scores in the debug ledger

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): keep combining marks in keyword query terms, match marked words as substrings

Bengali, Hindi and other abugida questions such as "মূল্য কত" produced no
keyword terms: the term splitter dropped combining marks (vowel signs,
viramas), breaking every word into fragments shorter than the FULLTEXT
minimum. Keep marks in the words, and also match marked words as
substrings, since InnoDB FULLTEXT can tokenize at marks inside a word.

Covers the QA findings on Codeinwp/hyve#323.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): reach precomposed content from decomposed accented queries

Keeping combining marks in keyword terms broke decomposed input: a
Vietnamese keyboard sends "giá" as "gia" + U+0301, and that term matches
neither NFC content in FULLTEXT nor as a substring. A Latin word carrying
marks now also adds its mark-free form, after the whole words so it never
displaces one under the ten-term cap. Other scripts keep their marks.

The long-input test now checks that a later marked word survives a long
unmarked one, without a timing assertion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(chat): cover the three-character guard on the mark-free keyword term

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): match short Arabic, Hebrew and Hangul words when nothing reaches the index

"سن من" has two two-letter words, below InnoDB's three-character FULLTEXT
minimum, and Arabic script was not substring-matched, so the keyword pass
found nothing for a chunk holding the phrase verbatim. Two-letter words in
those scripts are content words; they are now matched as substrings, but
only when no word in the question reaches the index: index scores scale
with the corpus while a substring hit does not, so a preposition such as
"در" would otherwise outrank and evict a real indexed match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(chat): cover the five-word cap and deduplication of short keyword words

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): match single-syllable Korean words when nothing reaches the index

"내 집" is two one-syllable words, so the two-letter rule left the keyword
pass empty. A Hangul syllable is a whole morpheme; accept one or two of
them, still only when no word reaches the index. Single Arabic and Hebrew
letters stay out: they are particles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): exclude lone jamo from short Hangul queries

* fix(chat): retain exact keyword hits through rank fusion

* fix(chat): link keyword-only sources only at the best query coverage

With the keyword list no longer cut by the score ratio, every included
keyword-only post became a source link, so a question matching one common
word in many chunks linked them all. A keyword-only post now links only
when its chunk matches as many question words as the best keyword
candidate: indexed words at a word start, segments as substrings, accents
folded. The score-ratio and similarity-threshold docblocks now say they
apply to vector matches only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): ignore index stopwords when counting source-link coverage

The FULLTEXT index never matches its default stopwords, so a chunk full of
"what", "is" and "the" must not out-cover the answer for the source link.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ns or fails (#285)

* fix(qdrant): keep the knowledge base answering while the migration runs or fails

Qdrant was marked active before the asynchronous migration ran, and chat
then searched Qdrant only. A migration that failed, returned an unconfirmed
upload, or never ran because WP-Cron was blocked left every unmigrated chunk
unreachable, so a previously indexed knowledge base looked empty.

The local table keeps every vector even after a row is marked Qdrant-backed,
so retrieval now uses the local vectors while any processed chunk is still
unmigrated, and falls back to them when the Qdrant search itself fails.

The migration treats an unconfirmed upload as a failure, retries a transient
failure after five minutes, ends after a terminal error that deactivated
Qdrant, and only migrates processed rows. A batch holds a lock so a second
cron run cannot upload the same rows alongside it, and migration points use
a stable id derived from the row so a retry after a lost response overwrites
instead of duplicating. The status endpoint reports the remaining count from
the table and re-arms a lost cron event; the chat path does the same, so a
site with blocked WP-Cron recovers from visitor traffic alone.

The Qdrant client throws a bare Error on some connection failures, which the
catch (\Exception) blocks never saw; every catch now takes Throwable and
reports a readable message. Disconnecting Qdrant also clears its stored
error, and the Integrations card shows the last error while migrating.

Fixes #265

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(qdrant): reuse the existing connection error string

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(qdrant): address migration recovery review findings

* fix(qdrant): allow local disconnect and bound HTTP requests

* chore: remove PR verification artifacts

* fix(qdrant): state the lock's real guarantee and prove an overlap is harmless

On a host without cURL, Guzzle's stream handler bounds each read, not the
whole request, so an upload with a dripping response can outlive the
migration lock TTL and the next run can take the lock while the first is
still uploading. That overlap cannot duplicate knowledge: migration points
carry a stable id derived from the row, so the repeat upsert overwrites the
same points. The lock docblock now says so instead of promising exclusivity,
and a test ages the lock past the TTL from inside the upload and asserts both
runs sent identical point ids with every row migrated once.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(qdrant): do not count chunks without a vector as migrated

A Connect import can insert processed rows with an empty embedding. The
migration skipped them but still marked them as Qdrant-backed, so the
progress shown in the Integrations panel counted chunks that were never
uploaded. Rows with no vector are now excluded from the unmigrated query
and count, so they are never pending and never counted as moved, and only
rows actually uploaded are marked as Qdrant. A row holding an unreadable
vector is normalised to the empty form so it is dropped rather than retried
forever.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: pirate-bot <58979018+pirate-bot@users.noreply.github.com>
)

The model had no clock, so it guessed today's date, resolved "tomorrow"
to the wrong day and asked visitors for the date. The base prompt also
let it write its own apology in English to non-English visitors,
describe retrieved excerpts as site navigation, assert that the site
lacks something retrieval missed, confirm visitor claims, promise
bookings it cannot make, and omit URLs that were in the context.

apply_system_prompt() now appends a date line after the base prompt,
spelled out as well as ISO because gpt-5.4-nano compares "25 October"
against "28 October 2026" reliably but not against "2026-10-28". It
goes after the static text so prompt caching keeps a shared prefix.

The base prompt gains rules for dates, for what the model cannot know
and cannot do, verbatim numbers, links from the context, the visitor's
language, and worked examples for the cases small models got wrong.
The "I apologize, but I couldn't understand" example is gone:
unparseable input returns an empty response with success:false and the
site's default_message shows. Connect chat settings send the site
timezone so the platform dates its prompt the same way.

Partial answers and "I can't do that in this chat, here is the contact"
replies carry success:true so the visitor sees them; they drop out of
the unanswered-question analytics as a result.

Refs Codeinwp/hyve#327.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Bumps [@wordpress/e2e-test-utils-playwright](https://github.com/WordPress/gutenberg/tree/HEAD/packages/e2e-test-utils-playwright) from 2.1.0 to 3.1.0.
- [Release notes](https://github.com/WordPress/gutenberg/releases)
- [Changelog](https://github.com/WordPress/gutenberg/blob/trunk/packages/e2e-test-utils-playwright/CHANGELOG.md)
- [Commits](https://github.com/WordPress/gutenberg/commits/@wordpress/e2e-test-utils-playwright@3.1.0/packages/e2e-test-utils-playwright)

---
updated-dependencies:
- dependency-name: "@wordpress/e2e-test-utils-playwright"
  dependency-version: 3.1.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
OpenAI shut down gpt-4.1-nano on 23 October 2026, and has scheduled gpt-5 (11 December 2026) and the gpt-4o / gpt-4.1 families (14 January 2027). This is the fallback.
Bumps [codeinwp/themeisle-sdk](https://github.com/Codeinwp/themeisle-sdk) from 3.3.68 to 3.3.69.
- [Release notes](https://github.com/Codeinwp/themeisle-sdk/releases)
- [Commits](Codeinwp/themeisle-sdk@v3.3.68...v3.3.69)

---
updated-dependencies:
- dependency-name: codeinwp/themeisle-sdk
  dependency-version: 3.3.69
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [codeinwp/themeisle-sdk](https://github.com/Codeinwp/themeisle-sdk) from 3.3.69 to 3.3.70.
- [Release notes](https://github.com/Codeinwp/themeisle-sdk/releases)
- [Commits](Codeinwp/themeisle-sdk@v3.3.69...v3.3.70)

---
updated-dependencies:
- dependency-name: codeinwp/themeisle-sdk
  dependency-version: 3.3.70
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [codeinwp/themeisle-sdk](https://github.com/Codeinwp/themeisle-sdk) from 3.3.70 to 3.3.71.
- [Release notes](https://github.com/Codeinwp/themeisle-sdk/releases)
- [Commits](Codeinwp/themeisle-sdk@v3.3.70...v3.3.71)

---
updated-dependencies:
- dependency-name: codeinwp/themeisle-sdk
  dependency-version: 3.3.71
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(kb): resume large imports after rate limits

* fix(kb): surface rate-limit details and cover the import queue's edges

- Show the account's x-ratelimit limit, remaining and reset values when
  an import gives up, so the admin knows what to raise or wait for.
- Record a visible error on a document created by a failed enqueue
  instead of leaving a hidden post behind.
- Clear the stored Qdrant warning once a staged publish or prune
  succeeds.
- Share one polling hook between Indexed content and Needs Attention.
- Make the update-cron test fail without the active-job filter, and add
  tests for a different document during a job, Needs Attention scope,
  the Qdrant warning, and the progress chip and queued notice in the
  browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(kb): recover every import, hide unpruned Qdrant points, name sync 429 limits

- The watchdog pages through all active jobs instead of the newest 100,
  so a lost event on an older source is always rescheduled.
- When an import gives up pruning the previous Qdrant generation after
  a day, search answers only from the new generation and the watchdog
  keeps retrying the prune. Deleting a source's points clears the mark.
- Short documents indexed in the request now record the endpoint, model,
  HTTP status and token limit headers on failure, through the same
  OpenAI::describe_error() helper the queue uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(kb): keep a pending Qdrant prune from touching newer imports

- The watchdog no longer retries a pending prune while an import owns
  the source; it takes the source lease and re-checks before deleting.
  Retrying around an older generation would have deleted the newer
  import's points.
- When a new generation is revealed while an older prune is still
  pending, search switches to the new generation.
- Stale generations are excluded inside the Qdrant query, so the result
  limit only counts visible points. Pending prunes live in one option,
  so search needs no per-hit lookup.
- A later error response no longer ends a cooldown that an earlier 429
  started; only a success does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(kb): make pending Qdrant prunes safe under concurrency

- Store each source's pending prune as its own post meta row instead of
  one shared option, so requests for different sources cannot overwrite
  each other. Search still reads them in one query.
- Re-read a source's marker under its lease before retrying the prune,
  so an import that finished in the meantime is never pruned around the
  generation it replaced.
- Move the marker to the new generation before recording the reveal, so
  a run that stops in between never leaves search naming a removed
  generation.
- Remove the watchdog's page cap; the scan reads until a short page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(qdrant): drop pending prune markers when Qdrant is deactivated

A marker left behind would hide, then prune, the points a later
migration uploads without a generation, leaving the source with rows
marked processed but nothing in Qdrant.

The job-scan test now fakes 102 pages through posts_pre_query, so it
fails with the old 100-page cap, and the test-only page-size parameter
is gone. A new test covers a marker cleared while the watchdog prunes
another source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(qdrant): never write a prune marker once Qdrant is deactivated

An import still waiting on a prune when another request deactivates
Qdrant could write a marker afterwards. On reconnection it would hide,
then prune, the migrated points. set_current_generation() now reads the
Qdrant status from the database and refuses to write while inactive,
and connecting Qdrant clears any marker left from an earlier
connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(qdrant): tie prune markers to the Qdrant connection they belong to

A run waiting on a prune while Qdrant was deactivated and connected
again could still write its marker for the new connection, and re-saving
settings on a live connection cleared valid markers.

Each connection now gets an ID when it is made from an inactive state.
An import pins that ID when its upload starts, markers store it, and a
marker is only written or read for the current connection. Re-running
init() on a live connection keeps the ID and the markers. Fresh
connections and deactivation still remove leftover marker rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(qdrant): upload again when Qdrant is reconnected mid-publish

An import waiting to retry its prune had already uploaded and revealed
its generation, but only on the connection it started on. After a
reconnection to a new target, the retry went straight to the prune and
deleted the source's migrated points, leaving search with nothing.

When the pinned connection no longer matches, publish() now restarts the
Qdrant upload on the current connection before revealing or pruning.
A new test also covers a marker stored for another connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(kb): only blame the API limit when an import waits on it

QA found the progress chip saying "waiting for the API limit" after a
Qdrant upload error. Any retryable failure schedules a retry, so the
chip blamed the rate limit for errors that had nothing to do with it.

The job now records why it waits, and the public summary carries it as
`wait`: `rate_limit` for a 429 or a learned budget, `retry` for any
other retryable error. The chip says "retrying after an error" for the
latter. The summary field is additive, so Hyve Pro is unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(kb): build the import progress label from one lookup

The six chip labels differed only in their string, so they now live in
one table keyed by phase and wait reason, with a single sprintf call.
Each string stays a literal __() call with its translators comment, and
make-pot still extracts all six with their comments. The valid-sprintf
rule cannot follow the lookup, so that one line carries a reasoned
suppression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(chat): rewrite follow-ups into standalone retrieval queries

Retrieval used to embed the visitor's last six turns joined with the new
message. Follow-ups kept their topic, but a new question was searched
mostly as the old ones, so retrieval stuck to earlier topics.

A follow-up is now rewritten by one small completion (gpt-5.4-nano,
filterable via hyve_retrieval_rewrite_model) into a standalone question,
and only that is embedded. A first message is searched as sent. A long
message keeps its own wording ahead of the rewrite so a condensing
rewrite cannot drop its details, and the keyword pass always starts with
the visitor's own words. A failed rewrite searches the message as sent.

The debug trace records the searched query and whether it was rewritten,
on the self-hosted path and from the platform's job_complete on Connect.

Refs Codeinwp/hyve#324

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(chat): keep long rewrites whole and cover the rewrite trace

A short follow-up can expand past 300 characters with the subject of a
long earlier question. The rewrite was cut there and, being short, was
searched alone, so the tail of the question was lost. The rewrite cap is
now 800 characters, and a separate 300-character threshold decides when
a long message keeps its own wording ahead of the rewrite. Behaviour at
300 and 301 characters is unchanged. This mirrors the platform's
RetrievalQuery::MAX_CHARS and LONG_CHARS.

The Connect trace docblock now names the searched query, the Connect
debug test fails without the change, and an e2e case covers the
"Query (rewritten)" label.

Refs Codeinwp/hyve#324

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(chat): reject a rewrite cut off by the token limit

OpenAI marks a completion that hit max_completion_tokens with
finish_reason "length". Its first line can stop before the subject the
rewrite was meant to add, and it was then searched alone. Such a rewrite
now fails like any other, so the visitor's message is searched as sent.

Refs Codeinwp/hyve#324

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(chat): retry the rewrite with the chat model when the key cannot use it

A key or project barred from the rewrite model answered every follow-up
with model_not_found or permission_denied. The next successful call
cleared the saved error, so the site silently searched raw follow-ups
with no dashboard notice. Retry once with the site's chat model, which
every turn already uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(chat): share the self-hosted OpenAI test setup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(chat): keep the resolved subject of a long follow-up's rewrite

A follow-up over 800 characters with its pronoun at the end comes back
as the whole message with the subject resolved at the end. The flat
800-character cap cut that subject off, and the flat 400-token limit
cut longer or non-Latin rewrites short. Both now grow with the message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… batches forever (#292)

* fix(connect): send WPBakery raw HTML as text and stop retrying failed batches forever

When WPBakery's shortcodes are not registered in the request that
extracts a page, [vc_raw_html] blocks reach the knowledge base as a
base64 blob that the embeddings provider rejects. The sync treated that
as a temporary error and resent the same batch on every cron run.

Tokenizer::html_to_text() now decodes [vc_raw_html] blocks and drops
[vc_raw_js] before shortcode markers are removed; EXTRACTOR_VERSION is
bumped so reconcile rehashes unedited posts once.

connect_run_sync() classifies batch errors: per-document 422s fail only
the named sources, auth and site-setup faults pause the job, provider
outages pause after a bounded retry budget, and other failures mark the
batch's sources with _hyve_processing_error so the job moves on. The
Connect and Knowledge Base screens show why a sync is paused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(connect): take the batch order from the query in the 422 test

Posts created in the same second have no stable order, so the rejected
document index pointed at a different post on the CI database.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(connect): keep concurrent edits pending, resume a paused sync by cron and show the pause reason

An edit saved by another request during a failing upload only reached the
database, so the stale meta cache let the failure write it off. Each source
now carries an edit mark (save time plus re-index flag) re-read past the
cache. A paused sync schedules a cron event at its resume time instead of
waiting for an admin load, the live mode is read through the settings
defaults, and both admin screens name the pause and its reason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(connect): lift the re-index flag for the batch and re-arm an unanswered pause

A flag set before the batch was built describes content the batch already
carries, yet it shielded the source from a terminal failure, and a re-save
in the same second was indistinguishable from it. The batch now lifts the
flag on its sources and restores it wherever a source stays pending, so a
flag found after the request can only mean an edit made meanwhile. A paused
job whose cron check finds the platform still down schedules another wait.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(connect): lift the flag before reading content and settle edited-page failures

The batch lifted a source's re-index flag after rendering it, so an edit
landing while another source rendered was lost. The lift now precedes the
read, in the batch and in the update cron, and a flag found afterwards is a
newer edit that keeps the source pending. The update cron stands down while
syncing is paused, pauses the site on credential, setup and outage failures
instead of re-sending an edited page every minute, and fails a refused page
once. A timed pause check only re-arms its cron event, never rewrites the
status, and re-arms when the plan is full as well as when the platform is
down.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(connect): preserve retry budgets across delayed syncs

* refactor(connect): drop the per-save edit marker from the update cron

The marker was written on every save to guard a rare overlap of two
update-cron runs. The flag claim and the re-check after a failed upload
already cover an edit made during an upload; the remaining overlap now
re-sends the page once at most. The retry counter stays in post meta,
written only while a page keeps failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(connect): retry temporary failures on edited pages and never requeue a removed page

A rate limit or maintenance answer on an edited synced page paused the whole
site at once; it now gets the batch path's bounded retries first. A removal
finishing during the settling probe had its flag written back, so the next
cron run re-uploaded the removed page: the flag is now restored only while
the page is still in the knowledge base, and in Connect mode the update cron
only selects pages that are. The failure handler's docblock lists its
outcomes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(connect): keep an outage's spent retries across a resume

While uploads keep failing but the stats route answers, every resume
gave an edited page, and the batch job, a fresh retry budget, so a
lasting outage was re-sent five times per pause cycle forever. The spent
budget now survives the pause: after a resume one upload probes the
outage and the job pauses again if it persists. A stored upload or a new
edit clears the count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(connect): probe an outage after a resume without spending other retries

Carrying the spent retry count across an outage pause made a single 500
after the resume fail an edited page for good, and the resume set the
count in a second write a running sync could race. A resume after an
outage now seeds a probe marker in the one status write that makes the
job runnable. While probing, only another rate-limit or maintenance
answer pauses at once; server errors and timeouts keep their retries.
The first stored upload, from a batch or an edited page, ends the probe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(connect): bound the outage probe and end it on any other answer

The probe only ended on a stored upload, so a resume that sent nothing or
got another answer left it armed, and one 503 much later paused the whole
site without retries. The probe is now a deadline one pause wait ahead,
ends on any upload answer other than another rate limit or maintenance
answer, and ends when the resumed update run finds nothing to send.
Ending it rereads the stored status past the request cache and changes
only the probe, so a pause another cron process stored survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(connect): say a transport failure also ends the outage probe

The comments said the probe ends because the upload route answered, but
a timeout or refused connection ends it too, with nothing answering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(connect): show a Sync paused chip on the Connect screen while the sync is paused

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(connect): show Sync paused over Offline when an outage pause also fails the stats call

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Bumps [codeinwp/themeisle-sdk](https://github.com/Codeinwp/themeisle-sdk) from 3.3.71 to 3.3.72.
- [Release notes](https://github.com/Codeinwp/themeisle-sdk/releases)
- [Commits](Codeinwp/themeisle-sdk@v3.3.71...v3.3.72)

---
updated-dependencies:
- dependency-name: codeinwp/themeisle-sdk
  dependency-version: 3.3.72
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
The "Docs" link (header bar and dashboard help card) and the Qdrant "Learn
more" link used old HelpScout URLs (/article/2009-…, /article/2066-…). They
redirect to the Hyve docs, but the redirect drops the query string, and the
links had no UTM parameters anyway. Both now point at the current docs pages
through tsdk_utmify().

Part of Codeinwp/docs-site#1596.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix: point the docs links to the current Hyve docs pages with UTMs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants