Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions docs/clawhub-search-intelligence.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,10 @@ snapshot. Search evidence still has its separate completed-week window and query
privacy threshold. Missing adoption stays unavailable, not zero. Hermit neither
re-ranks the report nor publishes Featured selections.

The compact Carbon layout retains every proposed ID, slot, selection basis,
30d/7d install count and rationale, metadata freshness, plus all pending reservations.
Weekly search totals, source counts, coverage and bounded company-opportunity,
official-gap and mover rows remain separate sections. The complete
wire payload remains capped at 30,000 bytes. Reports that fit use one message;
longer reports are packed deterministically into bounded parts, without dropping
selection rows or reasons. Each part uses at most 4000 text characters and 40
components, with a dashboard link for full search links and removal details.
The complete wire payload remains capped at 30,000 bytes, including all Featured
evidence even when Discord does not display it. Pre-cutoff v4 reports retain the
compact linked-name/30-day-install formatter, pending-slot warnings and full-report
button. Each part uses at most 4000 text characters and 40 components.

V4 freezes the complete digest hash and ordered rendered-part hashes at the same
origin/week key. Each part reuses the existing message receipt, nonce and
Expand All @@ -104,6 +100,31 @@ partially delivered week. Frozen v1/v2/v3 payloads retain their original validat
rendering, hashes and single-message receipts. Deploy receiver support before
sending v4. Read-only dry runs must not call the delivery endpoint.

### Discord Featured presentation cutover

For v4 weeks ending **2026-11-02T00:00:00Z or later**, new delivery claims show
both catalogs' weekly search totals, source counts, coverage, classification,
company opportunities, official gaps and movers. Featured recommendations move
to the separate advisory review workflow; the Discord formatter never changes
Featured membership. Strict full-payload validation, privacy thresholds and
trusted-origin checks still apply. Bounded Carbon pages retain every supplied
search row; oversized search links open the full report instead.

The original full digest still owns the hash, origin/week key and part nonces.
An existing incomplete cutoff-week receipt can resume the unchanged legacy
formatter only when the original payload hash **and ordered legacy part hashes**
match. The atomic delivery owner chooses the winning manifest; it never replaces
an existing claim. Changed payloads and unknown incomplete manifests still
conflict, and uncertain sends still require reconciliation. No manual proof that
the cutoff week is unclaimed is needed.

Pre-cutoff producer-frozen reports use the byte-identical old formatter even
when first received after cutover. V1/v2/v3 behavior is unchanged. The cutoff is
an immutable UTC week identity, independent of request time or environment
settings. A rollback must retain this selector and both formatters for the
retained receipt lifetime; deploying a formatter-only historical revision after
new claims exist can conflict with their manifests.

## Delivery state and failure semantics

No migration is needed. The existing D1 `keyValue` primary key stores one receipt
Expand Down
5 changes: 5 additions & 0 deletions src/clawhubSearchIntelligence/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import { deliverWeeklyDigest } from "./delivery.js"

import { parseDigest, type Digest, type Row } from "./contract.js"
import { parseEvidenceDigest, renderEvidenceDigest } from "./evidence.js"
import { renderMonthlyDigest, renderMonthlySearchDigest } from "./monthly.js"

const apiPath = "/api/clawhub-search-intelligence/weekly"
// Frozen week identity, not request time or mutable config, selects presentation.
const featuredCutoverWeekEnd = Date.UTC(2026, 10, 2)
// Bound bytes while consuming the stream, not after allocating an arbitrary body.
const readBody = async (request: Request): Promise<unknown> => {
const reader = request.body?.getReader()
Expand Down Expand Up @@ -191,6 +194,8 @@ export const handleSearchIntelligenceApiRequest = async (
if (!digest)
return json({ error: "Invalid search intelligence payload" }, 400)
try {
if (digest.kind === "search_intelligence_weekly_v4" && digest.weekEnd >= featuredCutoverWeekEnd)
return await deliverWeeklyDigest(client, digest, renderMonthlySearchDigest(digest), renderMonthlyDigest(digest))
return await deliverWeeklyDigest(
client,
digest,
Expand Down
17 changes: 14 additions & 3 deletions src/clawhubSearchIntelligence/delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ const fingerprint = (body: MessageBody, value: string): MessageBody => ({
export const deliverWeeklyDigest = async (
client: Client,
digest: DigestIdentity,
rendered: MessageBody | MessageBody[]
rendered: MessageBody | MessageBody[],
legacyRendered?: MessageBody[]
): Promise<Response> => {
const key = `clawhub-search-weekly:${new URL(digest.dashboardUrl).origin}:${digest.weekStart}`
const payloadHash = await hash(canonical(digest))
Expand Down Expand Up @@ -174,8 +175,18 @@ export const deliverWeeklyDigest = async (
// A completed receipt acknowledges the same report across presentation changes.
// Incomplete deliveries stay bound to their original parts to prevent duplicates.
if (state.status === "sent") return success()
if (canonical(state.partHashes) !== canonical(partHashes))
return response({ error: "Weekly payload conflict" }, 409)
if (canonical(state.partHashes) !== canonical(partHashes)) {
// An old writer may already own the cutoff week. Only the SAME payload and
// exact original manifest can select legacy output; never replace its claim.
if (!legacyRendered)
return response({ error: "Weekly payload conflict" }, 409)
const legacyPartHashes = await Promise.all(
legacyRendered.map(body => hash(canonical(body)))
)
if (canonical(state.partHashes) !== canonical(legacyPartHashes))
return response({ error: "Weekly payload conflict" }, 409)
rendered = legacyRendered
}
for (let index = 0; index < rendered.length; index++) {
const result = await deliverMessage(
client,
Expand Down
68 changes: 68 additions & 0 deletions src/clawhubSearchIntelligence/monthly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,71 @@ export const renderMonthlyDigest = (digest: MonthlyDigest) => {
})
)
}

// Keep the legacy formatter above unchanged: frozen part hashes depend on it.
export const renderMonthlySearchDigest = (digest: MonthlyDigest) => {
const dashboardUrl =
digest.dashboardUrl.length <= 512
? digest.dashboardUrl
: new URL(
`/management?view=search-insights&endDay=${digest.weekEnd}`,
digest.dashboardUrl
).href
const brief = (value: string) =>
safe(value.length > 80 ? `${value.slice(0, 79)}…` : value)
const rowText = (row: Catalog["movers"][number]) =>
`[${brief(row.query)}](${link(row.searchUrl.length <= 512 ? row.searchUrl : dashboardUrl)}) (${row.scope}) · ${row.searches} searches · ${row.officialGaps} gaps · previous ${row.previousSearches}`
const preview = ["localhost", "127.0.0.1", "[::1]"].includes(
new URL(digest.dashboardUrl).hostname
)
return ([
["Plugins", digest.catalogs.plugins],
["Skills", digest.catalogs.skills]
] as const).flatMap(([name, catalog]) => {
const { coverage } = catalog
const time = (value: number | null) => value === null ? "unknown" : day(value)
const incomplete = coverage.dataThrough === null ||
coverage.dataThrough < digest.weekEnd ||
coverage.collectionStartedAt === null ||
coverage.collectionStartedAt > digest.weekStart || coverage.gapStart !== null
const header = [
`### ${preview ? "LOCAL PREVIEW · " : ""}ClawHub weekly search intelligence · ${name}`,
`${day(digest.weekStart)} – ${day(digest.weekEnd)} (UTC, end exclusive)`,
`${catalog.totalSearches} searches · Web ${catalog.sourceCounts.clawhubWeb} · Control UI ${catalog.sourceCounts.openclawControlUi}`,
`Data through ${time(coverage.dataThrough)}; collection started ${time(coverage.collectionStartedAt)}.${incomplete ? " Incomplete collection history." : ""}`,
...(coverage.gapStart !== null && coverage.gapEnd !== null
? [`Collection gap ${day(coverage.gapStart)} – ${day(coverage.gapEnd)} UTC.`] : []),
`Classification ${catalog.classificationStatus}; search metadata ${catalog.currentMetadataStatus}.`
].join("\n")
const sections = [
["company opportunities", catalog.companyOpportunities.map(row =>
`${rowText(row)}${row.companyProductName ? ` · ${brief(row.companyProductName)}` : ""} · ${Math.round(row.confidence * 100)}% classifier confidence`)],
["official gaps", catalog.officialGaps.map(rowText)],
["movers", catalog.movers.map(rowText)]
] as const
const pages: string[] = []
let page = ""
for (const [title, rows] of sections) {
const heading = `**${name} ${title}**`
for (const row of [heading, ...(rows.length ? rows : ["None qualified."])]) {
// Whole bounded rows only; reserve space for metadata and receipt fingerprint.
if (page.length + row.length + 1 > 2600) {
pages.push(page)
page = `${heading} (continued)`
}
page += `\n${row}`
}
}
pages.push(page)
return pages.map(page => serializePayload({
components: [new Container([
new TextDisplay(header),
new TextDisplay(page),
new TextDisplay("Company classification is advisory. At least 3 searches required: either week for movers, current week for other rows. Long links open the full report." +
(digest.truncated ? " Input capped; more evidence on the dashboard." : "")),
new Row([new DashboardLink(dashboardUrl)])
])],
allowedMentions: { parse: [] }
}))
})
}
Loading
Loading