release: v2.23.0 through v2.26.0 - #29
Merged
Merged
Conversation
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
force-pushed
the
release/2.23.0-through-2.26.0
branch
from
August 24, 2026 11:17
a4a95f2 to
0f5292d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
incident_moderation_enabledon every detection method)batch()request/response shape fixes (it was rejected by the API outright), all twelve batch analysis types,continuationToken/resetConversationon the unified detection endpoints,SupportDatatypes,createVerificationSession()field fixestrajectory_risk,trajectory,severity_series),analyze()'sincident_moderation_enabledforwarding fixverdictOnlynow always keepsrationale(the field a moderator reads to triage an incident) and dropsaction_detail/evidenceinstead, on every endpoint that has fast mode. Companion toTuteliq/apiPR #109. Also fixes a real bug found while auditing this:includeEvidence: falsewas silently dropped and never reached the API.Full detail for each version is in
CHANGELOG.md, already written per-version.Test plan
npx tsc --noEmitclean at every commit (verified individually, not just at HEAD)npx vitest rungreen at every commit: 77 → 93 → 102 → 107 tests as each version's tests were addeddiff -rq) — this is a pure history reconstruction, not new functional work beyond 2.26.0verdictOnly/includeEvidencechanges live-verified against the API (see Tuteliq/api PR #109's test plan) — bullying/unsafe/grooming keeprationaleand dropaction_detailin fast mode; romance-scam/social-engineering/app-fraud keeprationaleand dropevidencein fast mode; explicitincludeEvidence/include_evidencestill wins in both directions