@recursiv/sdk 0.7.89 → 0.7.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -100,7 +100,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
100
100
  | `r.communities` | `create`, `join`, `search`, `members` |
101
101
  | `r.chat` | `send`, `dm`, `createGroup` (`encrypted: true` for an end-to-end encrypted group, 0.7.59), `messages` (cursor paging), `acceptRequest`, `declineRequest`, `leave`, `deleteConversation`; a private group's admin: `addMembers`, `removeMember`, `updateGroup` (0.7.59); photos and voice notes: `uploadAttachment`, `signAttachments` (private chat storage, 0.7.82) |
102
102
  | `r.tags` | `create`, `list` |
103
- | `r.signals` | `batch` (up to 100 engagement signals, such as views, dwell and hides, for the member's own ranking; `keepalive` for a page-leave flush), `record` (one signal) (0.7.71) |
103
+ | `r.signals` | `batch` (up to 100 engagement signals, such as views, dwell and hides, for the member's own ranking; `keepalive` for a page-leave flush), `record` (one signal) (0.7.71); `clearViewingHistory` ("Clear my viewing history"); typed `SignalMetadata`, an id per event so a retry is stored once, and `duplicates`/`limited`/`rejected` in the answer (0.7.93) |
104
104
  | `r.privacy` | `capabilities` (the platform privacy facts the deployed API's build enforces, and its commit); pair with `@recursiv/sdk/privacy` (`PRIVACY_FACTS`, `isFactLive`, `liveFacts`) (unreleased) |
105
105
  | `r.curator` | `today` (World Today; a world story can carry `story_key` and `discussion`, its count across networks), `storyPosts` / `memberStoryPosts` (one page of a story's posts, keyless or under the member's key), `storyPostPages` (walk every page), `forYou`, `run`, `refreshIfStale`, `seedDeck` |
106
106
  | `r.uploads` | `uploadMedia` — upload post/chat bytes end to end |
package/dist/index.d.ts CHANGED
@@ -225,8 +225,8 @@ export { FEED_TOP_NETWORKS, FeedsResource } from './resources/feeds.js';
225
225
  export type { FeedStoriesParams, FeedStoriesResponse, FeedStory, FeedTopExplain, FeedTopItem, FeedTopMeta, FeedTopNetwork, FeedTopNetworkCard, FeedTopNetworkEntry, FeedTopNetworkMap, FeedTopParams, FeedTopResponse, FeedTopTarget, PublicFeedTopMeta, PublicFeedTopParams, PublicFeedTopResponse, } from './resources/feeds.js';
226
226
  export type { BookmarkCollection, BookmarkCollectionInput, BookmarkCollectionKind, BookmarkCollectionPatch, BookmarkCollectionsList, BookmarkCollectionTarget, BookmarkCollectionVisibility, BookmarkedPost, BookmarkInput, BookmarkItem, BookmarkKey, BookmarkKeyRow, BookmarkKeysPage, BookmarkKind, BookmarkMergeItem, BookmarkMergeResult, BookmarkProtocol, BookmarkRequestOptions, BookmarksPage, FiledBookmark, RemovedBookmarkCollection, SavedBookmark, UnfiledBookmark, UnsavedBookmark, } from './resources/bookmarks.js';
227
227
  export { AccountsResource } from './resources/accounts.js';
228
- export { SIGNAL_BATCH_MAX, SignalsResource } from './resources/signals.js';
229
- export type { SignalBatchResult, SignalEventInput, SignalEventType, SignalNotRecordedReason, SignalRecordResult, SignalSendOptions, } from './resources/signals.js';
228
+ export { SIGNAL_BATCH_MAX, SignalsResource, newClientEventId } from './resources/signals.js';
229
+ export type { ClearViewingHistoryResult, SignalBatchResult, SignalEventInput, SignalEventType, SignalExit, SignalMetadata, SignalNotRecordedReason, SignalRecordResult, SignalRejection, SignalSendOptions, SignalSurface, } from './resources/signals.js';
230
230
  export { SEALED_ALLOWANCE_REMAINING_HEADER, SEALED_REFUSAL_HEADER, SealedResource, SealedTransportError, } from './resources/sealed.js';
231
231
  export type { SealedAllowance, SealedAttestationInput, SealedConfig, SealedModel, SealedPlan, SealedPurpose, SealedQuotaDetails, SealedSendInput, SealedTarget, SealedTargetKind, SealedTransportErrorCode, } from './resources/sealed.js';
232
232
  export type { SiblingAccount, SiblingAccountsResponse, SwitchAccountResult, } from './resources/accounts.js';
package/dist/index.js CHANGED
@@ -279,7 +279,7 @@ export { SEARCH_SECTIONS, SearchResource } from './resources/search.js';
279
279
  export { BOOKMARK_MERGE_MAX_ITEMS, BookmarkCollectionsResource, BookmarksResource } from './resources/bookmarks.js';
280
280
  export { FEED_TOP_NETWORKS, FeedsResource } from './resources/feeds.js';
281
281
  export { AccountsResource } from './resources/accounts.js';
282
- export { SIGNAL_BATCH_MAX, SignalsResource } from './resources/signals.js';
282
+ export { SIGNAL_BATCH_MAX, SignalsResource, newClientEventId } from './resources/signals.js';
283
283
  export { SEALED_ALLOWANCE_REMAINING_HEADER, SEALED_REFUSAL_HEADER, SealedResource, SealedTransportError, } from './resources/sealed.js';
284
284
  export { OrganizationSettingsResource } from './resources/organization-settings.js';
285
285
  export { ProjectSettingsResource } from './resources/project-settings.js';
@@ -6,24 +6,117 @@ import type { SingleResponse } from '../types.js';
6
6
  * one, so send them in a request of their own if the API may be older.
7
7
  */
8
8
  export type SignalEventType = 'view' | 'click' | 'dwell' | 'react_up' | 'react_down' | 'save' | 'hide' | 'share' | 'mute_source' | 'open_chat' | 'ask_agent' | 'more_like' | 'unhide';
9
- /** One engagement signal. An open-network post has no Minds id: name it in `metadata` (e.g. `{ ref }`) instead. */
9
+ /**
10
+ * Where a signal happened: each one a public place. In the Minds app the server lists these (and Discover's older
11
+ * `discover_featured`, `discover_hot`, `discover_rediscover`) and refuses any other name, `other` included, with a 400
12
+ * (`private_surface`). Another app's well-formed name the server does not list is stored as `other`, so a new surface
13
+ * still lands there; add it here (and to the server's SIGNAL_SURFACES) to keep it apart. There is no `group`: a group
14
+ * can be an encrypted chat, which never sends a signal.
15
+ */
16
+ export type SignalSurface = 'home_feed' | 'discovery' | 'discovery_story' | 'discovery_module' | 'discover_category' | 'discover_for_you' | 'postcard' | 'story' | 'swipe' | 'watch' | 'clip_strip' | 'discover_videos' | 'story_cluster' | 'headline_gallery' | 'post_clip' | 'search_videos' | 'profile' | 'search' | 'post' | 'other';
17
+ /** How a watch, or a card's time on screen, ended. */
18
+ export type SignalExit = 'swipe' | 'leave' | 'hide' | 'tap';
19
+ /**
20
+ * A signal's metadata: the only keys the server keeps (0.7.93, the ingest contract). A key not listed here is dropped.
21
+ * A listed key with a value of the wrong type or past its limit refuses that event (named in `rejections`, never
22
+ * stored); `null` or `''` reads as absent. Times are milliseconds.
23
+ */
24
+ export interface SignalMetadata {
25
+ /**
26
+ * Where it happened (up to 64 characters). In the Minds app one of SignalSurface's names other than `other`;
27
+ * elsewhere an unlisted name is stored as `other`.
28
+ */
29
+ surface?: SignalSurface | (string & {});
30
+ /** Which feed of the surface, e.g. `all`, `category`, `for-you:world` (up to 64). */
31
+ feed?: string;
32
+ /** The block of the feed, e.g. `top` (up to 32). */
33
+ block?: string;
34
+ /** The 0-based row in the feed (0 to 100,000). */
35
+ position?: number;
36
+ /** The place within a module that holds several (0 to 10,000). */
37
+ slot?: number;
38
+ /** A category feed's category (up to 64). */
39
+ category?: string;
40
+ /**
41
+ * An item with no Minds post id: an open-network post `<protocol>/<native id>` (native id up to 2,048 characters, no
42
+ * whitespace), a story lead `cluster:<64 hex>` or a World Today story `today:<uuid>`. Stored in an indexed column.
43
+ */
44
+ ref?: string;
45
+ /** How long the clip played (0 to one hour). Held to `duration_ms` x (`loops` + 1) and to the wall clock. */
46
+ watch_ms?: number;
47
+ /** The clip's length (0 to six hours). */
48
+ duration_ms?: number;
49
+ completed?: boolean;
50
+ /** Times the clip looped (0 to 1,000). */
51
+ loops?: number;
52
+ sound_on?: boolean;
53
+ /** How it ended. A watch under a second that ended in a `swipe` is stored as a skip. */
54
+ exit?: SignalExit;
55
+ /** Cards on screen at once in a grid (1 to 200). Readers divide a grid card's dwell by it; it never divides watch time. */
56
+ visible_count?: number;
57
+ autoplay?: boolean;
58
+ /**
59
+ * The client's id for this event (a letter or digit, then up to 63 of `A-Za-z0-9_.:-`): an event sent again with the
60
+ * same id is stored once. `batch` and `record` add one to an event that has none.
61
+ */
62
+ client_event_id?: string;
63
+ /** The event's place in the client's session (0 to 2^31 - 1). */
64
+ session_seq?: number;
65
+ /** How the action was taken, e.g. `less_like_this`, `module_hide` (up to 64). */
66
+ via?: string;
67
+ /** What the action came from, e.g. `postcard_bookmark` (up to 64). */
68
+ source?: string;
69
+ /** How the item was shown, e.g. `video` (up to 32). */
70
+ presentation?: string;
71
+ }
72
+ /** One engagement signal. An open-network post has no Minds id: name it by `metadata.ref` instead. */
10
73
  export interface SignalEventInput {
11
74
  event_type: SignalEventType;
12
75
  /** The Minds post's id (a UUID). */
13
76
  post_id?: string;
14
- /** Milliseconds on screen, for `dwell` (0 to one hour). */
77
+ /** Milliseconds on screen, for `dwell` (0 to one hour; a card's counts up to five minutes). */
15
78
  dwell_ms?: number;
16
- metadata?: Record<string, unknown>;
79
+ metadata?: SignalMetadata;
17
80
  }
18
81
  /**
19
82
  * Why nothing was stored, on a 200: `no_project_scope` (the key is not bound to an app, so there is no app database
20
- * for the signal) or `project_db_unavailable` (the app database did not take it; signals are best-effort).
83
+ * for the signal), `project_db_unavailable` (the app database did not take it; signals are best-effort),
84
+ * `viewing_off` (the member turned off "Use what I watch and read to personalize", so a dwell or open is not kept),
85
+ * `rate_limited` (past the member's events a minute) or `duplicate` (`record` only: an event with this
86
+ * `client_event_id` is already stored).
21
87
  */
22
- export type SignalNotRecordedReason = 'no_project_scope' | 'project_db_unavailable';
88
+ export type SignalNotRecordedReason = 'no_project_scope' | 'project_db_unavailable' | 'viewing_off' | 'rate_limited' | 'duplicate';
89
+ /** Why the server refused one event of a batch: a metadata value of the wrong type or past its limit. */
90
+ export interface SignalRejection {
91
+ /** The event's index in the batch. */
92
+ index: number;
93
+ reason: 'invalid_metadata' | 'invalid_ref' | 'invalid_client_event_id';
94
+ /** The metadata key. */
95
+ key: string;
96
+ }
23
97
  export interface SignalBatchResult {
24
- /** Events stored. */
98
+ /** Events stored by this call. */
25
99
  recorded: number;
100
+ /**
101
+ * Events the server did not keep because the member's viewing personalization is off: dwell and opens. Views and
102
+ * explicit choices are always kept. Absent when none were left out.
103
+ */
104
+ withheld?: number;
26
105
  reason?: SignalNotRecordedReason;
106
+ /** Events already stored (the same `client_event_id`, in this batch or an earlier one): stored once. */
107
+ duplicates?: number;
108
+ /** Events past the member's rate (600 a minute by default): not stored. */
109
+ limited?: number;
110
+ /** Events refused by the contract: not stored. The rest of the batch is. */
111
+ rejected?: number;
112
+ /** The first ten refused events, and why. */
113
+ rejections?: SignalRejection[];
114
+ }
115
+ /** What "Clear my viewing history" did: the member's views, dwell, Watch time and opens in this app are gone. */
116
+ export interface ClearViewingHistoryResult {
117
+ cleared: true;
118
+ /** Rows deleted. */
119
+ deleted: number;
27
120
  }
28
121
  export interface SignalRecordResult {
29
122
  recorded: boolean;
@@ -44,15 +137,45 @@ export declare const SIGNAL_BATCH_MAX = 100;
44
137
  * Engagement signals (views, dwell, hides, "More like this") for the member's own ranking, under the member's
45
138
  * app-bound key (`agents:write`). They land in the app's database and feed For You. Best-effort: a signal the server
46
139
  * cannot store still answers 200 with `recorded` 0 or false and a `reason`.
140
+ *
141
+ * Only from a public place, never a private message: a `metadata` key or placement (`surface`, `feed`, `source`,
142
+ * `context`, `route`...) that names a chat, DM, inbox, encrypted or Sealed conversation, by any spelling (`chatList`,
143
+ * `dmThread`, `/messages/1`), is refused with a 400 (`private_surface`), and nothing in that request is stored. An app
144
+ * names its own public places (`{ source: 'discover' }`). In the Minds app the placements (`surface`, `feed`, `source`,
145
+ * `via`, `layoutSource`) must be ones the server lists (its feeds, Discover, Watch, stories, the pager and post cards),
146
+ * and any other key that names a place (`context`) is refused there too.
147
+ *
148
+ * That check reads the metadata as sent. The server then keeps only SignalMetadata's keys, each within its limit; a
149
+ * batch names any event it refused and stores the rest. `record` answers 400 (`invalid_signal`) for an event it refuses.
47
150
  */
48
151
  export declare class SignalsResource {
49
152
  private client;
153
+ /**
154
+ * The id each event object was sent with. An event sent again (a retry of the same request, or the app sending the
155
+ * same object after a failed flush) carries the same id, so the server stores it once. Weak: an event the app has let
156
+ * go is not held here.
157
+ */
158
+ private readonly eventIds;
50
159
  constructor(client: HttpClient);
51
160
  /**
52
161
  * Store up to SIGNAL_BATCH_MAX events in one request (POST /signals/batch). The server reads a post's newest hide or
53
162
  * unhide, so send a batch only after the one before it has landed when order matters. An empty array stores nothing.
163
+ * Each event without a `metadata.client_event_id` is sent with one (the same one each time that object is sent).
54
164
  */
55
165
  batch(events: SignalEventInput[], options?: SignalSendOptions): Promise<SingleResponse<SignalBatchResult>>;
56
166
  /** Store one event (POST /signals/post). Prefer `batch`; this is for an API that predates it (404 there). */
57
167
  record(event: SignalEventInput, options?: SignalSendOptions): Promise<SingleResponse<SignalRecordResult>>;
168
+ /**
169
+ * "Clear my viewing history" (DELETE /signals/history): the member's views, dwell, Watch time and opens in this app
170
+ * are deleted, and what was built from them is rebuilt. Their explicit choices (hides, "More like this", saves,
171
+ * shares, votes) stay. A delete that could not be done is an error (503), never a success.
172
+ */
173
+ clearViewingHistory(): Promise<SingleResponse<ClearViewingHistoryResult>>;
174
+ /** The event as sent: the caller's object is never changed. */
175
+ private withEventId;
58
176
  }
177
+ /**
178
+ * A fresh event id the server's grammar takes: a random UUID where the platform has one, otherwise the time and about
179
+ * 104 random bits (React Native's Hermes has no crypto.randomUUID). Unique per member is all it needs to be.
180
+ */
181
+ export declare function newClientEventId(): string;
@@ -4,23 +4,79 @@ export const SIGNAL_BATCH_MAX = 100;
4
4
  * Engagement signals (views, dwell, hides, "More like this") for the member's own ranking, under the member's
5
5
  * app-bound key (`agents:write`). They land in the app's database and feed For You. Best-effort: a signal the server
6
6
  * cannot store still answers 200 with `recorded` 0 or false and a `reason`.
7
+ *
8
+ * Only from a public place, never a private message: a `metadata` key or placement (`surface`, `feed`, `source`,
9
+ * `context`, `route`...) that names a chat, DM, inbox, encrypted or Sealed conversation, by any spelling (`chatList`,
10
+ * `dmThread`, `/messages/1`), is refused with a 400 (`private_surface`), and nothing in that request is stored. An app
11
+ * names its own public places (`{ source: 'discover' }`). In the Minds app the placements (`surface`, `feed`, `source`,
12
+ * `via`, `layoutSource`) must be ones the server lists (its feeds, Discover, Watch, stories, the pager and post cards),
13
+ * and any other key that names a place (`context`) is refused there too.
14
+ *
15
+ * That check reads the metadata as sent. The server then keeps only SignalMetadata's keys, each within its limit; a
16
+ * batch names any event it refused and stores the rest. `record` answers 400 (`invalid_signal`) for an event it refuses.
7
17
  */
8
18
  export class SignalsResource {
9
19
  client;
20
+ /**
21
+ * The id each event object was sent with. An event sent again (a retry of the same request, or the app sending the
22
+ * same object after a failed flush) carries the same id, so the server stores it once. Weak: an event the app has let
23
+ * go is not held here.
24
+ */
25
+ eventIds = new WeakMap();
10
26
  constructor(client) {
11
27
  this.client = client;
12
28
  }
13
29
  /**
14
30
  * Store up to SIGNAL_BATCH_MAX events in one request (POST /signals/batch). The server reads a post's newest hide or
15
31
  * unhide, so send a batch only after the one before it has landed when order matters. An empty array stores nothing.
32
+ * Each event without a `metadata.client_event_id` is sent with one (the same one each time that object is sent).
16
33
  */
17
34
  batch(events, options) {
18
- return this.client.post('/signals/batch', { events }, sendOptions(options));
35
+ return this.client.post('/signals/batch', { events: events.map((event) => this.withEventId(event)) }, sendOptions(options));
19
36
  }
20
37
  /** Store one event (POST /signals/post). Prefer `batch`; this is for an API that predates it (404 there). */
21
38
  record(event, options) {
22
- return this.client.post('/signals/post', event, sendOptions(options));
39
+ return this.client.post('/signals/post', this.withEventId(event), sendOptions(options));
40
+ }
41
+ /**
42
+ * "Clear my viewing history" (DELETE /signals/history): the member's views, dwell, Watch time and opens in this app
43
+ * are deleted, and what was built from them is rebuilt. Their explicit choices (hides, "More like this", saves,
44
+ * shares, votes) stay. A delete that could not be done is an error (503), never a success.
45
+ */
46
+ clearViewingHistory() {
47
+ return this.client.delete('/signals/history');
48
+ }
49
+ /** The event as sent: the caller's object is never changed. */
50
+ withEventId(event) {
51
+ if (!event || typeof event !== 'object')
52
+ return event;
53
+ const given = event.metadata?.client_event_id;
54
+ if (typeof given === 'string' && given)
55
+ return event;
56
+ let id = this.eventIds.get(event);
57
+ if (!id) {
58
+ id = newClientEventId();
59
+ this.eventIds.set(event, id);
60
+ }
61
+ return { ...event, metadata: { ...event.metadata, client_event_id: id } };
62
+ }
63
+ }
64
+ /**
65
+ * A fresh event id the server's grammar takes: a random UUID where the platform has one, otherwise the time and about
66
+ * 104 random bits (React Native's Hermes has no crypto.randomUUID). Unique per member is all it needs to be.
67
+ */
68
+ export function newClientEventId() {
69
+ const cryptoApi = globalThis.crypto;
70
+ if (typeof cryptoApi?.randomUUID === 'function') {
71
+ try {
72
+ return cryptoApi.randomUUID();
73
+ }
74
+ catch {
75
+ // A non-secure context can refuse it: fall through.
76
+ }
23
77
  }
78
+ const random = () => Math.floor(Math.random() * 2 ** 52).toString(36).padStart(11, '0');
79
+ return `${Date.now().toString(36)}-${random()}${random()}`;
24
80
  }
25
81
  function sendOptions(options) {
26
82
  if (!options)
package/dist/types.d.ts CHANGED
@@ -1651,6 +1651,12 @@ export interface ContentPreferences {
1651
1651
  * that predate it: treat absent as off.
1652
1652
  */
1653
1653
  sealed_adult?: boolean;
1654
+ /**
1655
+ * "Use what I watch and read to personalize": on by default. Off, the server stores only views (so a post the member
1656
+ * saw is not served again) and their explicit choices, and no ranking reads what they watched or read. Absent on
1657
+ * servers that predate it: treat absent as on.
1658
+ */
1659
+ personalize_from_viewing?: boolean;
1654
1660
  }
1655
1661
  export interface UpdateNotificationChannelsInput {
1656
1662
  /** Master switch: sets in-app on every kind. */
@@ -1698,6 +1704,8 @@ export interface UpdateContentPreferencesInput {
1698
1704
  * accepted.
1699
1705
  */
1700
1706
  sealed_adult?: boolean;
1707
+ /** "Use what I watch and read to personalize". `false` is the member's opt-out; `true` turns it back on. */
1708
+ personalize_from_viewing?: boolean;
1701
1709
  }
1702
1710
  export interface Session {
1703
1711
  id: string;
package/llm.md CHANGED
@@ -826,7 +826,7 @@ Methods: `list`, `get`, `create`, `delete`
826
826
  ## Platform Resources
827
827
 
828
828
  - `r.settings` — User preferences, sessions, password changes, account deletion. `revokeOtherSessions({ keep_session_token? })` signs out every other device and keeps this key (and the proven session); `revokeAllSessions()` also revokes this key. Accounts with no password confirm `requestEmailChange({ new_email, email_code })` with a code from `requestConfirmationCode('email_change')`. With a project-bound key, `getPreferences()` includes app-scoped `content`; persist changes with `updateContent({ show_nsfw?, autoplay_video?, ai_enabled?, default_feed?, sealed_adult? })`; `content.default_feed_chosen` is true once the member set `default_feed` themselves (absent on older APIs). `privacy.age_is_adult` is true only when the member answered the age question 18 or older (false for 13-17 and for no answer; only their own preferences carry it). `content.sealed_adult` is the Sealed adult-content setting: it reads true only while it is on and the member is a known adult, and `updateContent({ sealed_adult: true })` from anyone else throws `AuthorizationError` with code `sealed_adult_needs_adult` and saves nothing in that request; `sealed_adult: false` is always accepted. Both are absent on older APIs (treat as false). Notifications (0.7.72): `getPreferences()` returns `notifications.channels`, each kind (`post_mention`, `post_reply`, `post_repost`, `post_reaction`, `follow`, `message`, and the operator kinds) with `{ inApp, push, email }`; `updateNotifications({ channels: { post_reaction: { push: false } } })` changes that kind only, while `in_app`/`push`/`email` stay master switches over every kind. `disableAccount({ password })` (0.7.71) deactivates the account (hidden, nothing deleted; signing back in restores it), asked once; an API that does not serve deactivation answers 404 `NotFoundError`, so keep the control hidden until yours does.
829
- - `r.signals` (0.7.71) — The member's engagement signals for their own ranking (For You), under their app-bound key (`agents:write`): `batch(events, { keepalive?, retry? })` sends up to `SIGNAL_BATCH_MAX` (100) `{ event_type, post_id?, dwell_ms?, metadata? }` to POST /signals/batch; `record(event)` sends one to POST /signals/post (for an API before the batch route, which answers that one 404). `event_type`: `view`, `click`, `dwell`, `react_up`, `react_down`, `save`, `hide`, `share`, `mute_source`, `open_chat`, `ask_agent`, `more_like`, `unhide`. Best-effort: a signal the server cannot store answers 200 with `recorded` 0 (or false) and a `reason` (`no_project_scope`, `project_db_unavailable`). `keepalive: true` is for a flush as the page is hidden or unloads: the request is on the wire before the call returns and is asked once.
829
+ - `r.signals` (0.7.71) — The member's engagement signals for their own ranking (For You), under their app-bound key (`agents:write`): `batch(events, { keepalive?, retry? })` sends up to `SIGNAL_BATCH_MAX` (100) `{ event_type, post_id?, dwell_ms?, metadata? }` to POST /signals/batch; `record(event)` sends one to POST /signals/post (for an API before the batch route, which answers that one 404). `event_type`: `view`, `click`, `dwell`, `react_up`, `react_down`, `save`, `hide`, `share`, `mute_source`, `open_chat`, `ask_agent`, `more_like`, `unhide`. Best-effort: a signal the server cannot store answers 200 with `recorded` 0 (or false) and a `reason` (`no_project_scope`, `project_db_unavailable`, `viewing_off`, `rate_limited`, or for `record` `duplicate`). `keepalive: true` is for a flush as the page is hidden or unloads: the request is on the wire before the call returns and is asked once. Only from a public place, never a private message: a `metadata` key or placement (`surface`, `feed`, `source`, `context`, `route`...) naming a chat, DM, inbox, encrypted or Sealed conversation by any spelling (`chatList`, `dmThread`, `/messages/1`) answers 400 (`private_surface`) and nothing in that request is stored; this is read on the metadata as sent, before any key is dropped. An app names its own public places (`{ source: 'discover' }`). In the Minds app a `surface` must be one of `SignalSurface`'s names other than `other` (each a public place the server lists), a `feed`, `source`, `via` or `layoutSource` one the server lists (its feeds, Discover, Watch, stories, the pager and post cards), and any other key naming a place (`context`) is refused there too. With the member's "Use what I watch and read to personalize" off (`settings.updateContent({ personalize_from_viewing: false })`, on by default), only views and explicit choices are stored and `withheld` counts the dwell and opens left out. `clearViewingHistory()` (DELETE /signals/history) is "Clear my viewing history": the member's views, dwell, Watch time and opens in this app are deleted; their hides, "More like this", saves, shares and votes stay. Ingest contract (0.7.93): `metadata` is `SignalMetadata`; the server keeps only its keys (`surface`, `feed`, `block`, `position`, `slot`, `category`, `ref`, `watch_ms`, `duration_ms`, `completed`, `loops`, `sound_on`, `exit` (`swipe`/`leave`/`hide`/`tap`), `visible_count`, `autoplay`, `client_event_id`, `session_seq`, `via`, `source`, `presentation`) and drops the rest; a listed key of the wrong type or past its limit refuses that event (batch: `rejected` + the first ten `rejections` `{ index, reason, key }`, the rest stored; record: 400 `invalid_signal`). `batch`/`record` add a `client_event_id` to each event without one (the same id each time that object is sent), so a retry is stored once (`duplicates`). An open-network item is named by `metadata.ref` (`<protocol>/<native id>`, `cluster:<id>`, `today:<uuid>`). Watch time is held to `duration_ms` x (`loops` + 1) and to the wall clock, a card's dwell to 5 min; a watch under 1 s ending in `exit: 'swipe'` is stored as a skip. Past 600 events a minute per member the rest are `limited` (record: `reason: 'rate_limited'`).
830
830
 
831
831
  ```typescript
832
832
  await r.signals.batch([{ event_type: 'view', post_id: post.id }, { event_type: 'dwell', post_id: post.id, dwell_ms: 4200 }])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.89",
3
+ "version": "0.7.93",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",