Skip to content

release: v2.23.0 through v2.26.0 - #29

Merged
safenestdev merged 4 commits into
mainfrom
release/2.23.0-through-2.26.0
Aug 24, 2026
Merged

release: v2.23.0 through v2.26.0#29
safenestdev merged 4 commits into
mainfrom
release/2.23.0-through-2.26.0

Conversation

@safenestdev

Copy link
Copy Markdown
Contributor

Summary

Four releases' worth of work that had accumulated locally without ever being committed (last pushed commit was 2.22.0). Split into one commit per version, matching CHANGELOG.md, which was itself already written but uncommitted:

  • 2.23.0 — per-call incident logging control (incident_moderation_enabled on every detection method)
  • 2.24.0batch() request/response shape fixes (it was rejected by the API outright), all twelve batch analysis types, continuationToken/resetConversation on the unified detection endpoints, SupportData types, createVerificationSession() field fixes
  • 2.25.0 — conversation-level risk (trajectory_risk, trajectory, severity_series), analyze()'s incident_moderation_enabled forwarding fix
  • 2.26.0verdictOnly now always keeps rationale (the field a moderator reads to triage an incident) and drops action_detail/evidence instead, on every endpoint that has fast mode. Companion to Tuteliq/api PR #109. Also fixes a real bug found while auditing this: includeEvidence: false was silently dropped and never reached the API.

Full detail for each version is in CHANGELOG.md, already written per-version.

Test plan

  • npx tsc --noEmit clean at every commit (verified individually, not just at HEAD)
  • npx vitest run green at every commit: 77 → 93 → 102 → 107 tests as each version's tests were added
  • Final working tree verified byte-identical to the pre-split uncommitted state (diff -rq) — this is a pure history reconstruction, not new functional work beyond 2.26.0
  • 2.26.0's verdictOnly/includeEvidence changes live-verified against the API (see Tuteliq/api PR #109's test plan) — bullying/unsafe/grooming keep rationale and drop action_detail in fast mode; romance-scam/social-engineering/app-fraud keep rationale and drop evidence in fast mode; explicit includeEvidence/include_evidence still wins in both directions

Every detection method now accepts an optional
incident_moderation_enabled (via the shared TrackingFields). It
overrides the account-level incident-logging setting for that single
request: true forces the incident to be persisted, false suppresses
persistence, and omitting it defers to the account default (which
itself defaults to enabled). Useful for suppressing logging on test
traffic or opting specific calls in or out. false is passed through
correctly, not treated as "unset".
…new types

batch() sent a request shape the API has never accepted. POST
/api/v1/batch/analyze requires each item to be { id, type, data }; the
SDK sent { type, text, context, external_id }, so every batch call
was rejected with body/items/0 must have required property 'id'.
Three separate mismatches: the missing id, text/messages sitting on
the item instead of inside data, and parallel nested under an options
object the route never reads — so parallel: false was silently
ignored even had the rest been valid. Batch analysis did not work
through the SDK, or through the MCP server that calls it.

Items now carry an optional id; one is generated positionally
(item-0, item-1, …) when not supplied. id addresses an item within
the request and is echoed on its result — it is not external_id,
which is your own record's identifier and is still returned alongside
it.

batch() also returned a result shape that did not match its own type:
the API keys results by id and reports timing as
summary.processingTimeMs; BatchAnalyzeResult declares results[].index
and a top-level processing_time_ms. Both were undefined at runtime.
The response is now mapped back: positional index restored by id,
external_id re-attached from the request, processing_time_ms and
summary.total_credits_used populated.

createVerificationSession() discarded recommended_image_width and
verification_mode. The API returns both; the SDK projected the
response down to four fields. recommended_image_width is the capture
width at which document small print (document number, issuing
authority, issue date) survives OCR. expires_at is also now correctly
typed as number (epoch milliseconds), which is what the API sends.

Added: all twelve batch analysis types (previously only bullying,
unsafe, emotions and grooming); batch emotions items take messages
(sender/text) rather than grooming's sender_role/text;
continuationToken/resetConversation on the unified detection
endpoints (detectCoerciveControl, detectVulnerabilityExploitation,
detectDistressSignals); SupportData/SupportHelpline/SupportResponseGuide
types on BullyingResult, GroomingResult, UnsafeResult and
DetectionResult.

Changed: DetectionResult.rationale is now optional. verdictOnly: true
suppresses rationale generation server-side, so the field was already
absent at runtime while the type promised a string.
… severity_series)

risk_score has only ever scored the message in the current request. An
external reviewer fed a six-turn bullying escalation and watched the
scores go 5, 10, 65, 5, 75, 5 — the final "see you tomorrow :)", sent
immediately after two flagged messages, came back described as a
positive social interaction. Correct per message; useless for a child
who had just been excluded. Slow-burn exclusion cannot be seen one
message at a time.

The API now returns a conversation-level view alongside the
continuation token, typed here on BullyingResult, GroomingResult and
DetectionResult — the same three result types that carry
continuation_token:

- trajectory_risk (0-1) — risk for the conversation rather than for
  the turn. Anchored on the highest severity seen so far, decaying
  slowly across benign turns and never falling below the current
  turn, so a friendly message straight after an escalation does not
  reset it. On the reviewer's conversation it reads 0.74 where
  risk_score reads 0.10.
- trajectory — rising | stable | declining | none, exported as the
  ConversationTrajectory type.
- severity_series — per-turn severity, oldest first: the evidence
  behind the other two, so the number can be shown rather than
  asserted.

All three are optional and absent on the first turn of a fresh
conversation, where they would only restate risk_score. They require
a continuationToken to be threaded through the conversation; without
one, every call is a first turn. Branch on the higher of risk_score
and trajectory_risk, not on risk_score alone.

Fixed: analyze() accepted incident_moderation_enabled, dropped it, and
reported it as applied. The flag lives on the shared TrackingFields,
so analyze() accepted it — but it was never forwarded to the
detectBullying / detectUnsafe calls the method fans out to, while
being copied verbatim into the returned result. A caller passing
false to suppress incident persistence got incidents persisted by
both sub-calls and a response claiming otherwise. It is now forwarded
to both, and declared on AnalyzeResult instead of being an untyped
extra field.

Note: analyze() still cannot report a trajectory: it accepts no
continuationToken, so every call is a fresh first turn and its
combined risk_score remains the maximum of the per-message scores.
Where a sub-result does carry conversation state it is preserved in
full under result.bullying. For multi-turn work call detectBullying
directly.
…e fields instead

Two accounts on this SDK found every persisted bullying incident
summary reading as the same flat generic string, because rationale —
the one field a moderator reads to triage an incident — was the field
fast mode cut server-side on detectBullying / detectUnsafe.
action_detail (the secondary, comparably-sized moderator-guidance
field) was left unconditional, backwards from what fast mode should
prioritise. detectGrooming already had this right; bullying and
unsafe are now consistent with it. GroomingResult.rationale is
correspondingly now typed as required (string, not string?) — it was
always unconditional at runtime, the type just hadn't caught up.
BullyingResult.rationale / UnsafeResult.rationale stay optional at
the type level as a defensive measure (a malformed LLM response could
still theoretically omit it), but are documented as always generated.
Requires the API deployed on or after 2026-08-24; against an older
deployment, verdictOnly continues to omit rationale as before.

verdictOnly now also implies includeEvidence: false on the
fraud/safety-extended endpoints, unless includeEvidence is set
explicitly. Previously verdictOnly had no effect at all on
detectRomanceScam, detectSocialEngineering, detectAppFraud,
detectMuleRecruitment, detectGamblingHarm, detectCoerciveControl,
detectVulnerabilityExploitation, detectRadicalisation,
detectDistressSignals, detectTFGBV and detectSyntheticContent — the
full evidence[] array, including quoted excerpts from the input, was
always returned regardless. evidence is this endpoint family's
equivalent of action_detail: the expensive, skippable field.
rationale was already unconditional here and is untouched. Pass
includeEvidence: true alongside verdictOnly: true if you want fast
mode's other savings without losing evidence. Requires the API
deployed on or after 2026-08-24; against an older deployment,
verdictOnly continues to have no effect on evidence.

Fixed: includeEvidence: false was silently dropped and never reached
the API. buildDetectionBody only forwarded includeEvidence when it
was truthy (if (input.includeEvidence) ...), so an explicit
includeEvidence: false — the caller's choice to exclude evidence
entirely — was indistinguishable from not setting it at all, and the
server's default (true) applied instead. Found auditing the
verdictOnly change above. Now forwards true and false alike, and
omits the field only when the caller truly didn't set it (letting the
server apply its own default, including the new verdictOnly inference
above).
@safenestdev
safenestdev force-pushed the release/2.23.0-through-2.26.0 branch from a4a95f2 to 0f5292d Compare August 24, 2026 11:17
@safenestdev
safenestdev merged commit 24bc9ef into main Aug 24, 2026
6 checks passed
@safenestdev
safenestdev deleted the release/2.23.0-through-2.26.0 branch August 24, 2026 13:05
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