@botiverse/raft-sdk 1.0.0-alpha.3 → 1.0.0-alpha.5

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
@@ -58,6 +58,52 @@ export async function onStep(state: Stored) {
58
58
  await raft.messages.send(pending); // { target, content, idempotencyKey, seen: { upToSeq } }
59
59
  ```
60
60
 
61
+ ### Persisting state between tool calls (`state`)
62
+
63
+ If your runtime keeps nothing in memory between model steps, give the client a
64
+ store. The SDK loads it before the first operation and saves after each
65
+ successful operation that changed it; you implement two async methods.
66
+
67
+ ```ts
68
+ const raft = createRaft({ serverUrl, credential, state: store });
69
+
70
+ await raft.inbox.commit(); // the batch the previous call pulled is now processed
71
+ const batch = await raft.inbox.check(); // acknowledges it on the Server, returns the next batch
72
+ // … hand batch.data.messages to the model …
73
+ ```
74
+
75
+ - `inbox.check()` records the returned batch's cursor as **pending**; pulling
76
+ acknowledges nothing.
77
+ - `inbox.commit()` promotes the pending cursor to **committed** (also accepts
78
+ `{ cursor }`). The next `check()` sends it as `since`, which is what
79
+ acknowledges that batch. The SDK never commits on its own, so a call that
80
+ dies before `commit()` gets the same batch again.
81
+ - The seen frontier and held-send keys are saved too: resending the same
82
+ content to the same target after a hold reuses its idempotency key. After a
83
+ hold, `raft.frontier.recordHeld(held.data)` then `await raft.state.save()`.
84
+ - Saving is one attempt and never fails the operation; failures and stale
85
+ writes go to `onStateSaveError`. Losing the state is safe: at worst a batch
86
+ is delivered once more or a send is held once.
87
+
88
+ The state is one small versioned JSON value:
89
+ `{ schema: "raft-sdk-state.v1", version, cursor, pendingCursor, frontier, continuations }`.
90
+ `save(state, { expectedVersion })` receives the `version` this client loaded;
91
+ throw to reject a stale write, or ignore it if your store cannot compare.
92
+ An IndexedDB-style store with a synchronous transaction:
93
+
94
+ ```ts
95
+ const store: RaftStateStore = {
96
+ load: async () => (await db.get("inbox", "state")) ?? null,
97
+ save: async (state, { expectedVersion }) => {
98
+ await db.transaction("inbox", "readwrite", (tx) => {
99
+ const cur = tx.get("inbox", "state") as { version?: number } | undefined;
100
+ if (cur?.version !== expectedVersion) throw new Error("stale");
101
+ tx.put("inbox", state, "state");
102
+ });
103
+ },
104
+ };
105
+ ```
106
+
61
107
  A push notice is a content-free wake-up: verify it, then pull.
62
108
 
63
109
  ```ts
@@ -101,10 +147,13 @@ if (!signal.ok) return new Response(signal.message, { status: 401 });
101
147
  - `raft.messages.search / resolve / react / unreact` — find a specific
102
148
  message (previews neutralise `@handles` and `#channels`), resolve one id to
103
149
  its canonical form and reply target, add or remove a reaction.
104
- - `raft.attachments.upload({ target, filename, bytes })` — small-file multipart
105
- upload (the target is resolved to a channel id first); files at or above the
106
- Server's direct-upload threshold are refused with a next action pointing at
107
- the upload-session routes. `download`, `comments`.
150
+ - `raft.attachments.upload({ target, filename, bytes })` — multipart below the
151
+ Server's direct-upload threshold, an upload session (presigned PUT with
152
+ `fetch`, then complete) at or above it, exactly as the CLI chooses. The
153
+ target is resolved to a channel id first. `download`, `comments`.
154
+ - `raft.actions.prepare({ target, action })` — post an action card
155
+ (`channel:create`, `channel:add_member`, `agent:create`, integration cards)
156
+ for a human to confirm; the human who clicks it executes it.
108
157
  - `raft.mentions.pending / execute / deliveries` — @mentions you sent that
109
158
  reached nobody, the notify/add recovery, and per-target delivery outcomes.
110
159
  - `raft.manual.get / search` — the Raft Manual for Agents; both need a short
@@ -11044,13 +11044,17 @@ var SeenFrontier = class SeenFrontier {
11044
11044
  aliases = /* @__PURE__ */ new Map();
11045
11045
  static fromSnapshot(snapshot) {
11046
11046
  const frontier = new SeenFrontier();
11047
- if (!snapshot || snapshot.version !== 1) return frontier;
11047
+ frontier.absorb(snapshot);
11048
+ return frontier;
11049
+ }
11050
+ /** Merge a snapshot into this frontier by the same monotonic rules (never lowers a mark). */
11051
+ absorb(snapshot) {
11052
+ if (!snapshot || snapshot.version !== 1) return;
11053
+ for (const [alias, canonical] of Object.entries(snapshot.aliases ?? {})) this.recordAlias(alias, canonical);
11048
11054
  for (const [target, state] of Object.entries(snapshot.targets ?? {})) {
11049
- if (positiveInt(state.upTo)) frontier.recordUpTo(target, state.upTo);
11050
- if (Array.isArray(state.exact)) frontier.recordExact(target, state.exact);
11055
+ if (positiveInt(state.upTo)) this.recordUpTo(target, state.upTo);
11056
+ if (Array.isArray(state.exact)) this.recordExact(target, state.exact);
11051
11057
  }
11052
- for (const [alias, canonical] of Object.entries(snapshot.aliases ?? {})) frontier.recordAlias(alias, canonical);
11053
- return frontier;
11054
11058
  }
11055
11059
  /** Remember that the Server resolved `requested` to `canonical` (a thread spelling, a `~agent` suffix, …). */
11056
11060
  recordAlias(requested, canonical) {
@@ -11197,7 +11201,7 @@ async function checkInbox(client, request = {}, frontier) {
11197
11201
  * return value summarises the drain; the batch that ends the iteration is
11198
11202
  * acknowledged only by the caller's next pull (`summary.cursor`).
11199
11203
  */
11200
- async function* drainInbox(client, request = {}, frontier) {
11204
+ async function* drainInbox(client, request = {}, frontier, onPull) {
11201
11205
  const ack = request.ack ?? "cursor";
11202
11206
  let cursor = request.since ?? null;
11203
11207
  let ackMode = ack;
@@ -11220,6 +11224,7 @@ async function* drainInbox(client, request = {}, frontier) {
11220
11224
  error: round
11221
11225
  };
11222
11226
  const batch = round.data;
11227
+ await onPull?.(sent, batch);
11223
11228
  ackMode = batch.ackMode;
11224
11229
  stillUnread = batch.stillUnreadConversations;
11225
11230
  if (batch.messages.length > 0) {
@@ -11528,13 +11533,6 @@ async function sendMessage(client, request, frontier, internal = {}) {
11528
11533
  if (data.state === "sent") return sentOutcome(request.target, data);
11529
11534
  return failureOutcome(opError("INVALID_RESPONSE", { message: `Unexpected send state "${data.state}".` }));
11530
11535
  }
11531
- /** Reply where a received message came from. */
11532
- function replyTo(client, message, request, frontier) {
11533
- return sendMessage(client, {
11534
- ...request,
11535
- target: message.target
11536
- }, frontier);
11537
- }
11538
11536
  function taskTimestamps(t) {
11539
11537
  return `${t.createdAt ? ` created=${formatUtcTimestamp(t.createdAt)}` : ""}${t.updatedAt ? ` updated=${formatUtcTimestamp(t.updatedAt)}` : ""}`;
11540
11538
  }
@@ -12931,10 +12929,9 @@ function inferAttachmentMimeType(filename, bytes, explicit) {
12931
12929
  return sniffMimeType(bytes) ?? FILENAME_MIME_MAP[ext] ?? "application/octet-stream";
12932
12930
  }
12933
12931
  /**
12934
- * Small-file multipart upload (`POST /upload`). Files at or above the Server's
12935
- * direct-upload threshold need the upload-session routes (`routes.attachments.
12936
- * createUploadSession` …), which this operation reports as `UNAVAILABLE` with a
12937
- * next action rather than attempting silently.
12932
+ * Upload bytes into a conversation, choosing the path the CLI would: multipart
12933
+ * `POST /upload` below the Server's direct-upload threshold, an upload session
12934
+ * (create → PUT to a presigned URL → complete) at or above it.
12938
12935
  */
12939
12936
  async function uploadAttachment(client, transport, request) {
12940
12937
  if (!request.target?.trim() || !request.filename?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and a filename are required to upload." }));
@@ -12944,15 +12941,11 @@ async function uploadAttachment(client, transport, request) {
12944
12941
  const maxBytes = capability.ok ? capability.data.maxBytes : AGENT_ATTACHMENT_UPLOAD_FALLBACK_MAX_BYTES;
12945
12942
  const threshold = capability.ok && capability.data.directUploadEnabled ? capability.data.directUploadThresholdBytes : null;
12946
12943
  if (request.bytes.byteLength > maxBytes) return failureOutcome(opError("INVALID_REQUEST", { message: `Attachment is ${request.bytes.byteLength} bytes; the Server's maximum is ${maxBytes}.` }));
12947
- if (threshold !== null && request.bytes.byteLength >= threshold) return failureOutcome(opError("UNAVAILABLE", {
12948
- message: `Files of ${threshold} bytes or more use the direct-upload session routes, which this operation does not drive yet.`,
12949
- nextAction: "Use routes.attachments.createUploadSession / completeUploadSession, or upload a smaller file.",
12950
- retryable: false
12951
- }));
12952
12944
  const resolved = await client.channels.resolve({ target: request.target });
12953
12945
  if (!resolved.ok) return failureFromClientResult(resolved);
12954
12946
  const channelId = resolved.data.channelId;
12955
12947
  const mimeType = inferAttachmentMimeType(request.filename, request.bytes, request.mimeType);
12948
+ if (threshold !== null && request.bytes.byteLength >= threshold) return uploadThroughSession(client, transport, request, channelId, mimeType);
12956
12949
  const copy = new Uint8Array(request.bytes.byteLength);
12957
12950
  copy.set(request.bytes);
12958
12951
  const form = new FormData();
@@ -12992,20 +12985,22 @@ async function uploadAttachment(client, transport, request) {
12992
12985
  }
12993
12986
  const parsed = agentApiContract.attachmentUpload.response.body.safeParse(body);
12994
12987
  if (!parsed.success) return failureOutcome(opError("INVALID_RESPONSE"));
12995
- const data = parsed.data;
12988
+ return uploadedOutcome(request.target, parsed.data, mimeType);
12989
+ }
12990
+ function uploadedOutcome(target, data, mimeType) {
12996
12991
  return {
12997
12992
  ok: true,
12998
12993
  state: "uploaded",
12999
12994
  data: {
13000
12995
  ...data,
13001
- target: request.target,
12996
+ target,
13002
12997
  mimeType: data.mimeType ?? mimeType
13003
12998
  },
13004
12999
  next: {
13005
13000
  kind: "send_with_attachment",
13006
- command: `raft message send --target "${request.target}" --attachment-id ${data.id}`,
13001
+ command: `raft message send --target "${target}" --attachment-id ${data.id}`,
13007
13002
  args: {
13008
- target: request.target,
13003
+ target,
13009
13004
  attachmentIds: [data.id]
13010
13005
  },
13011
13006
  why: "The upload alone posts nothing; send a message that links the attachment id."
@@ -13013,6 +13008,69 @@ async function uploadAttachment(client, transport, request) {
13013
13008
  text: formatAgentAttachmentUploaded(data)
13014
13009
  };
13015
13010
  }
13011
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13012
+ /**
13013
+ * Direct upload for files at or above the Server's threshold, mirroring the
13014
+ * CLI: create a session, PUT the bytes to the presigned URL (fetch; one retry
13015
+ * when the object store may have committed), then complete, which the Server
13016
+ * verifies against the object store. A PUT that definitely failed cancels the
13017
+ * session; a PUT whose outcome is unknown leaves it for completion to verify.
13018
+ */
13019
+ async function uploadThroughSession(client, transport, request, channelId, mimeType) {
13020
+ const created = await client.attachments.createUploadSession({
13021
+ channelId,
13022
+ filename: request.filename,
13023
+ mimeType,
13024
+ sizeBytes: request.bytes.byteLength,
13025
+ clientRequestId: crypto.randomUUID()
13026
+ });
13027
+ if (!created.ok) return failureFromClientResult(created);
13028
+ const { uploadId, upload } = created.data;
13029
+ const body = new Uint8Array(request.bytes.byteLength);
13030
+ body.set(request.bytes);
13031
+ let definitelyFailed = null;
13032
+ for (let attempt = 0; attempt < 2; attempt += 1) {
13033
+ let response;
13034
+ try {
13035
+ response = await transport.fetch(upload.url, {
13036
+ method: upload.method ?? "PUT",
13037
+ headers: { ...upload.headers },
13038
+ body: body.buffer,
13039
+ redirect: "error"
13040
+ });
13041
+ } catch {
13042
+ if (attempt === 0) continue;
13043
+ break;
13044
+ }
13045
+ if (response.ok || response.status === 412) {
13046
+ definitelyFailed = null;
13047
+ break;
13048
+ }
13049
+ const mayExist = response.status === 408 || response.status === 429 || response.status >= 500;
13050
+ if (mayExist && attempt === 0) continue;
13051
+ if (!mayExist) definitelyFailed = response.status;
13052
+ break;
13053
+ }
13054
+ if (definitelyFailed !== null) {
13055
+ await client.attachments.cancelUploadSession({ uploadId }).catch(() => void 0);
13056
+ return failureOutcome(opError("HTTP_ERROR", {
13057
+ message: `Direct object upload failed with HTTP ${definitelyFailed}; the upload session was cancelled.`,
13058
+ status: definitelyFailed,
13059
+ nextAction: "Retry the upload; nothing was attached."
13060
+ }));
13061
+ }
13062
+ for (let attempt = 0; attempt < 3; attempt += 1) {
13063
+ const completed = await client.attachments.completeUploadSession({ uploadId });
13064
+ if (completed.ok) {
13065
+ const attachment = completed.data.attachment;
13066
+ return uploadedOutcome(request.target, attachment, mimeType);
13067
+ }
13068
+ const code = completed.error.kind === "http" ? completed.error.errorCode : null;
13069
+ if (!(code === "UPLOAD_OBJECT_NOT_FOUND" || code === "UPLOAD_VERIFICATION_IN_PROGRESS") || attempt === 2) return failureFromClientResult(completed);
13070
+ await wait(250 * (attempt + 1));
13071
+ }
13072
+ return failureOutcome(opError("UNAVAILABLE", { message: "Direct upload completion ended without a terminal response." }));
13073
+ }
13016
13074
  async function downloadAttachment(client, request) {
13017
13075
  if (!request.attachmentId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "An attachment id is required." }));
13018
13076
  const result = await client.attachments.download({ attachmentId: request.attachmentId });
@@ -13614,11 +13672,210 @@ async function searchManual(client, request) {
13614
13672
  };
13615
13673
  }
13616
13674
  //#endregion
13675
+ //#region ../shared/src/agentOps/state.ts
13676
+ const RAFT_STATE_SCHEMA = "raft-sdk-state.v1";
13677
+ const RAFT_STATE_CONTINUATIONS_PER_TARGET = 3;
13678
+ const RAFT_STATE_CONTINUATIONS_TOTAL = 20;
13679
+ function isNullableCursor(value) {
13680
+ return value === null || typeof value === "number" && Number.isInteger(value) && value >= 0;
13681
+ }
13682
+ /** Validate a loaded value; anything unrecognised is treated as an empty store (safe: see file header). */
13683
+ function parseRaftState(value) {
13684
+ if (!value || typeof value !== "object") return null;
13685
+ const v = value;
13686
+ if (v.schema !== "raft-sdk-state.v1") return null;
13687
+ if (typeof v.version !== "number" || !Number.isInteger(v.version) || v.version < 0) return null;
13688
+ if (!isNullableCursor(v.cursor ?? null) || !isNullableCursor(v.pendingCursor ?? null)) return null;
13689
+ const frontier = v.frontier;
13690
+ const continuations = Array.isArray(v.continuations) ? v.continuations.filter((c) => {
13691
+ const r = c;
13692
+ return Boolean(r) && typeof r.target === "string" && typeof r.idempotencyKey === "string" && typeof r.contentHash === "string" && typeof r.heldAt === "string";
13693
+ }) : [];
13694
+ return {
13695
+ schema: RAFT_STATE_SCHEMA,
13696
+ version: v.version,
13697
+ cursor: v.cursor ?? null,
13698
+ pendingCursor: v.pendingCursor ?? null,
13699
+ frontier: frontier && frontier.version === 1 ? frontier : {
13700
+ version: 1,
13701
+ targets: {},
13702
+ aliases: {}
13703
+ },
13704
+ continuations
13705
+ };
13706
+ }
13707
+ /** Hex SHA-256 identifying one logical message (WebCrypto; Workers-safe). */
13708
+ async function hashRaftSendContent(target, content, attachmentIds = []) {
13709
+ const bytes = new TextEncoder().encode(`${target}\n${[...attachmentIds].sort().join(",")}\n${content}`);
13710
+ return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
13711
+ }
13712
+ /**
13713
+ * One client's view of the persisted state: loads once, holds the working
13714
+ * copy, saves on request. Also used without a store (memory only), so the
13715
+ * pending/committed cursor flow behaves the same with or without persistence.
13716
+ */
13717
+ var RaftStateSession = class {
13718
+ store;
13719
+ onError;
13720
+ frontier;
13721
+ cursor = null;
13722
+ pendingCursor = null;
13723
+ continuations = [];
13724
+ loadedVersion = void 0;
13725
+ loading = null;
13726
+ dirty = false;
13727
+ constructor(store, frontier, onError) {
13728
+ this.store = store;
13729
+ this.onError = onError;
13730
+ this.frontier = frontier;
13731
+ }
13732
+ get persistent() {
13733
+ return this.store !== void 0;
13734
+ }
13735
+ /** The version loaded from (or last saved to) the store; `undefined` before the first save of an empty store. */
13736
+ get version() {
13737
+ return this.loadedVersion;
13738
+ }
13739
+ /** Load once. A failed or unreadable load starts from an empty state (reported, not thrown). */
13740
+ ensureLoaded() {
13741
+ if (!this.store) return Promise.resolve();
13742
+ this.loading ??= (async () => {
13743
+ let raw = null;
13744
+ try {
13745
+ raw = await this.store.load();
13746
+ } catch (error) {
13747
+ this.onError?.(error, {
13748
+ phase: "load",
13749
+ version: void 0
13750
+ });
13751
+ return;
13752
+ }
13753
+ const state = parseRaftState(raw);
13754
+ if (!state) return;
13755
+ this.loadedVersion = state.version;
13756
+ this.cursor = state.cursor;
13757
+ this.pendingCursor = state.pendingCursor;
13758
+ this.frontier.absorb(state.frontier);
13759
+ this.continuations = state.continuations ?? [];
13760
+ })();
13761
+ return this.loading;
13762
+ }
13763
+ markDirty() {
13764
+ this.dirty = true;
13765
+ }
13766
+ /** Record the cursor of a batch just returned. Acknowledges nothing. */
13767
+ setPending(cursor) {
13768
+ if (cursor === null || cursor === this.pendingCursor) return;
13769
+ this.pendingCursor = cursor;
13770
+ this.dirty = true;
13771
+ }
13772
+ /**
13773
+ * Promote a cursor to committed. With no argument, commits `pendingCursor`.
13774
+ * Returns the committed cursor, or null when there was nothing to commit.
13775
+ * Never moves the committed cursor backwards.
13776
+ */
13777
+ commit(cursor) {
13778
+ const next = cursor === void 0 ? this.pendingCursor : cursor;
13779
+ if (next === null || next === void 0) return null;
13780
+ if (this.cursor === null || next > this.cursor) this.cursor = next;
13781
+ if (this.pendingCursor !== null && this.pendingCursor <= this.cursor) this.pendingCursor = null;
13782
+ this.dirty = true;
13783
+ return this.cursor;
13784
+ }
13785
+ findContinuation(target, contentHash) {
13786
+ return this.continuations.find((c) => c.target === target && c.contentHash === contentHash);
13787
+ }
13788
+ rememberContinuation(entry) {
13789
+ this.continuations = this.continuations.filter((c) => !(c.target === entry.target && c.contentHash === entry.contentHash));
13790
+ this.continuations.push(entry);
13791
+ const perTarget = this.continuations.filter((c) => c.target === entry.target);
13792
+ for (const drop of perTarget.slice(0, Math.max(0, perTarget.length - 3))) this.continuations.splice(this.continuations.indexOf(drop), 1);
13793
+ if (this.continuations.length > 20) this.continuations.splice(0, this.continuations.length - 20);
13794
+ this.dirty = true;
13795
+ }
13796
+ forgetContinuation(target, contentHash) {
13797
+ const before = this.continuations.length;
13798
+ this.continuations = this.continuations.filter((c) => !(c.target === target && c.contentHash === contentHash));
13799
+ if (this.continuations.length !== before) this.dirty = true;
13800
+ }
13801
+ snapshot(nextVersion = (this.loadedVersion ?? 0) + 1) {
13802
+ return {
13803
+ schema: RAFT_STATE_SCHEMA,
13804
+ version: nextVersion,
13805
+ cursor: this.cursor,
13806
+ pendingCursor: this.pendingCursor,
13807
+ frontier: this.frontier.snapshot(),
13808
+ continuations: [...this.continuations]
13809
+ };
13810
+ }
13811
+ /**
13812
+ * Save if anything changed (or `force`). One attempt; a failure is reported
13813
+ * and swallowed. Returns whether the save succeeded (true when nothing to save).
13814
+ */
13815
+ async save(options = {}) {
13816
+ if (!this.store) return true;
13817
+ if (!this.dirty && !options.force) return true;
13818
+ const expectedVersion = this.loadedVersion;
13819
+ const state = this.snapshot();
13820
+ try {
13821
+ await this.store.save(state, { expectedVersion });
13822
+ } catch (error) {
13823
+ this.onError?.(error, {
13824
+ phase: "save",
13825
+ version: expectedVersion
13826
+ });
13827
+ return false;
13828
+ }
13829
+ this.loadedVersion = state.version;
13830
+ this.dirty = false;
13831
+ return true;
13832
+ }
13833
+ };
13834
+ //#endregion
13835
+ //#region ../shared/src/agentOps/actions.ts
13836
+ /** Text is the CLI's `raft action prepare` output. */
13837
+ function formatAgentActionCardPosted(target, messageId) {
13838
+ const shortId = messageId ? messageId.slice(0, 8) : null;
13839
+ return shortId ? `Action card posted to ${target} as message ${messageId} (short ${shortId}). The human can click the action verb to commit.\n` : `Action card posted to ${target}.\n`;
13840
+ }
13841
+ async function prepareActionCard(client, request) {
13842
+ if (!request?.target?.trim() || !request.action) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and an action are required to prepare a card." }));
13843
+ const result = await client.actions.prepare(request);
13844
+ if (!result.ok) return failureFromClientResult(result);
13845
+ const card = {
13846
+ target: request.target,
13847
+ messageId: result.data.messageId
13848
+ };
13849
+ return {
13850
+ ok: true,
13851
+ state: "prepared",
13852
+ data: card,
13853
+ next: {
13854
+ kind: "await_confirmation",
13855
+ command: `raft message read --target "${request.target}" --around ${card.messageId.slice(0, 8)}`,
13856
+ args: {
13857
+ target: request.target,
13858
+ messageId: card.messageId
13859
+ },
13860
+ why: "A human must click the card to commit it; its outcome arrives in your inbox."
13861
+ },
13862
+ text: formatAgentActionCardPosted(request.target, card.messageId)
13863
+ };
13864
+ }
13865
+ //#endregion
13617
13866
  //#region src/raft.ts
13618
13867
  function createRaft(options) {
13619
13868
  const serverUrl = requireServerUrl(options.serverUrl);
13620
13869
  const authorization = `Bearer ${requireAgentCredential(options.credential)}`;
13621
13870
  const frontier = SeenFrontier.fromSnapshot(options.frontier);
13871
+ const session = new RaftStateSession(options.state, frontier, options.onStateSaveError);
13872
+ /** Load before, run, then save if the operation succeeded and changed the state. */
13873
+ const withState = async (run) => {
13874
+ await session.ensureLoaded();
13875
+ const outcome = await run();
13876
+ if (outcome.ok) await session.save();
13877
+ return outcome;
13878
+ };
13622
13879
  const routes = createRaftRoutes({
13623
13880
  serverUrl,
13624
13881
  fetch: options.fetch,
@@ -13639,6 +13896,27 @@ function createRaft(options) {
13639
13896
  throttle: options.throttle,
13640
13897
  auth: { authorization }
13641
13898
  } });
13899
+ /**
13900
+ * Send through the state session: a resend of the same logical message
13901
+ * (same target, content, attachments) reuses the held send's idempotency key;
13902
+ * a hold is remembered; a successful send clears it.
13903
+ */
13904
+ const sendWithState = (request) => withState(async () => {
13905
+ const contentHash = await hashRaftSendContent(request.target, request.content ?? "", request.attachmentIds ?? []);
13906
+ const pending = request.idempotencyKey ? void 0 : session.findContinuation(request.target, contentHash);
13907
+ const outcome = await sendMessage(routes, pending ? {
13908
+ ...request,
13909
+ idempotencyKey: pending.idempotencyKey
13910
+ } : request, frontier);
13911
+ if (outcome.ok && outcome.state === "held") session.rememberContinuation({
13912
+ target: request.target,
13913
+ idempotencyKey: outcome.data.idempotencyKey,
13914
+ contentHash,
13915
+ heldAt: (/* @__PURE__ */ new Date()).toISOString()
13916
+ });
13917
+ else if (outcome.ok && outcome.state === "sent") session.forgetContinuation(request.target, contentHash);
13918
+ return outcome;
13919
+ });
13642
13920
  return {
13643
13921
  identity: { whoami: () => getRaftContext(routes) },
13644
13922
  wake: {
@@ -13650,14 +13928,53 @@ function createRaft(options) {
13650
13928
  }
13651
13929
  },
13652
13930
  inbox: {
13653
- check: (request) => checkInbox(inboxApi, request, frontier),
13654
- drain: (request) => drainInbox(inboxApi, request, frontier),
13931
+ check: (request = {}) => withState(async () => {
13932
+ const since = request.since ?? session.cursor ?? void 0;
13933
+ const outcome = await checkInbox(inboxApi, {
13934
+ ...request,
13935
+ ...since === void 0 ? {} : { since }
13936
+ }, frontier);
13937
+ if (outcome.ok) {
13938
+ session.markDirty();
13939
+ if (since !== void 0 && (request.ack ?? "cursor") === "cursor") session.commit(since);
13940
+ if (outcome.data.ackMode === "cursor" && outcome.data.messages.length > 0) session.setPending(outcome.data.cursor);
13941
+ }
13942
+ return outcome;
13943
+ }),
13944
+ commit: async (target) => {
13945
+ await session.ensureLoaded();
13946
+ const cursor = session.commit(target === void 0 ? void 0 : target.cursor);
13947
+ return {
13948
+ cursor,
13949
+ saved: cursor === null ? true : await session.save()
13950
+ };
13951
+ },
13952
+ drain: (request = {}) => (async function* () {
13953
+ await session.ensureLoaded();
13954
+ const since = request.since ?? session.cursor ?? void 0;
13955
+ return yield* drainInbox(inboxApi, {
13956
+ ...request,
13957
+ ...since === void 0 ? {} : { since }
13958
+ }, frontier, async (sent, batch) => {
13959
+ if (sent !== null) session.commit(sent);
13960
+ if (batch.ackMode === "cursor" && batch.messages.length > 0) session.setPending(batch.cursor);
13961
+ session.markDirty();
13962
+ await session.save();
13963
+ });
13964
+ })(),
13655
13965
  list: (request) => listInbox(routes, request)
13656
13966
  },
13657
13967
  messages: {
13658
- read: (request) => readHistory(routes, request, frontier),
13659
- send: (request) => sendMessage(routes, request, frontier),
13660
- reply: (message, request) => replyTo(routes, message, request, frontier),
13968
+ read: (request) => withState(async () => {
13969
+ const outcome = await readHistory(routes, request, frontier);
13970
+ if (outcome.ok) session.markDirty();
13971
+ return outcome;
13972
+ }),
13973
+ send: (request) => sendWithState(request),
13974
+ reply: (message, request) => sendWithState({
13975
+ ...request,
13976
+ target: message.target
13977
+ }),
13661
13978
  search: (request) => searchMessages(routes, request),
13662
13979
  resolve: (request) => resolveMessage(routes, request),
13663
13980
  react: (request) => reactToMessage(routes, request, "add"),
@@ -13678,6 +13995,7 @@ function createRaft(options) {
13678
13995
  execute: (request) => executeMentionAction(routes, request),
13679
13996
  deliveries: (request) => senderMentionDeliveries(routes, request)
13680
13997
  },
13998
+ actions: { prepare: (request) => prepareActionCard(routes, request) },
13681
13999
  manual: {
13682
14000
  get: (request) => getManualTopic(routes, request),
13683
14001
  search: (request) => searchManual(routes, request)
@@ -13711,6 +14029,15 @@ function createRaft(options) {
13711
14029
  update: (request) => updateProfile(routes, request)
13712
14030
  },
13713
14031
  frontier,
14032
+ state: {
14033
+ save: async () => {
14034
+ await session.ensureLoaded();
14035
+ session.markDirty();
14036
+ return session.save();
14037
+ },
14038
+ load: () => session.ensureLoaded(),
14039
+ snapshot: () => session.snapshot(session.version ?? 0)
14040
+ },
13714
14041
  routes
13715
14042
  };
13716
14043
  }
@@ -13718,6 +14045,9 @@ function createRaft(options) {
13718
14045
  exports.RAFT_INBOX_NOTICE_SCHEMA = RAFT_INBOX_NOTICE_SCHEMA;
13719
14046
  exports.RAFT_NOTICE_DELIVERY_ID_HEADER = RAFT_NOTICE_DELIVERY_ID_HEADER;
13720
14047
  exports.RAFT_NOTICE_SIGNATURE_HEADER = RAFT_NOTICE_SIGNATURE_HEADER;
14048
+ exports.RAFT_STATE_CONTINUATIONS_PER_TARGET = RAFT_STATE_CONTINUATIONS_PER_TARGET;
14049
+ exports.RAFT_STATE_CONTINUATIONS_TOTAL = RAFT_STATE_CONTINUATIONS_TOTAL;
14050
+ exports.RAFT_STATE_SCHEMA = RAFT_STATE_SCHEMA;
13721
14051
  exports.RaftCredentialError = RaftCredentialError;
13722
14052
  exports.RaftSdkConfigurationError = RaftSdkConfigurationError;
13723
14053
  exports.SeenFrontier = SeenFrontier;
@@ -13729,10 +14059,12 @@ exports.createRaftClientFromStore = createRaftClientFromStore;
13729
14059
  exports.createRaftRoutes = createRaftRoutes;
13730
14060
  exports.describeRaftRoute = describeRaftRoute;
13731
14061
  exports.hasAgentMessageIdentity = hasAgentMessageIdentity;
14062
+ exports.hashRaftSendContent = hashRaftSendContent;
13732
14063
  exports.inferAttachmentMimeType = inferAttachmentMimeType;
13733
14064
  exports.joinRaftChannelByTarget = joinRaftChannelByTarget;
13734
14065
  exports.listRaftRoutes = listRaftRoutes;
13735
14066
  exports.parseRaftRegularChannelTarget = parseRaftRegularChannelTarget;
14067
+ exports.parseRaftState = parseRaftState;
13736
14068
  exports.projectRaftMessage = projectRaftMessage;
13737
14069
  exports.projectRaftMessages = projectRaftMessages;
13738
14070
  exports.verifyInboxNotice = verifyInboxNotice;
package/dist/esm/index.js CHANGED
@@ -11043,13 +11043,17 @@ var SeenFrontier = class SeenFrontier {
11043
11043
  aliases = /* @__PURE__ */ new Map();
11044
11044
  static fromSnapshot(snapshot) {
11045
11045
  const frontier = new SeenFrontier();
11046
- if (!snapshot || snapshot.version !== 1) return frontier;
11046
+ frontier.absorb(snapshot);
11047
+ return frontier;
11048
+ }
11049
+ /** Merge a snapshot into this frontier by the same monotonic rules (never lowers a mark). */
11050
+ absorb(snapshot) {
11051
+ if (!snapshot || snapshot.version !== 1) return;
11052
+ for (const [alias, canonical] of Object.entries(snapshot.aliases ?? {})) this.recordAlias(alias, canonical);
11047
11053
  for (const [target, state] of Object.entries(snapshot.targets ?? {})) {
11048
- if (positiveInt(state.upTo)) frontier.recordUpTo(target, state.upTo);
11049
- if (Array.isArray(state.exact)) frontier.recordExact(target, state.exact);
11054
+ if (positiveInt(state.upTo)) this.recordUpTo(target, state.upTo);
11055
+ if (Array.isArray(state.exact)) this.recordExact(target, state.exact);
11050
11056
  }
11051
- for (const [alias, canonical] of Object.entries(snapshot.aliases ?? {})) frontier.recordAlias(alias, canonical);
11052
- return frontier;
11053
11057
  }
11054
11058
  /** Remember that the Server resolved `requested` to `canonical` (a thread spelling, a `~agent` suffix, …). */
11055
11059
  recordAlias(requested, canonical) {
@@ -11196,7 +11200,7 @@ async function checkInbox(client, request = {}, frontier) {
11196
11200
  * return value summarises the drain; the batch that ends the iteration is
11197
11201
  * acknowledged only by the caller's next pull (`summary.cursor`).
11198
11202
  */
11199
- async function* drainInbox(client, request = {}, frontier) {
11203
+ async function* drainInbox(client, request = {}, frontier, onPull) {
11200
11204
  const ack = request.ack ?? "cursor";
11201
11205
  let cursor = request.since ?? null;
11202
11206
  let ackMode = ack;
@@ -11219,6 +11223,7 @@ async function* drainInbox(client, request = {}, frontier) {
11219
11223
  error: round
11220
11224
  };
11221
11225
  const batch = round.data;
11226
+ await onPull?.(sent, batch);
11222
11227
  ackMode = batch.ackMode;
11223
11228
  stillUnread = batch.stillUnreadConversations;
11224
11229
  if (batch.messages.length > 0) {
@@ -11527,13 +11532,6 @@ async function sendMessage(client, request, frontier, internal = {}) {
11527
11532
  if (data.state === "sent") return sentOutcome(request.target, data);
11528
11533
  return failureOutcome(opError("INVALID_RESPONSE", { message: `Unexpected send state "${data.state}".` }));
11529
11534
  }
11530
- /** Reply where a received message came from. */
11531
- function replyTo(client, message, request, frontier) {
11532
- return sendMessage(client, {
11533
- ...request,
11534
- target: message.target
11535
- }, frontier);
11536
- }
11537
11535
  function taskTimestamps(t) {
11538
11536
  return `${t.createdAt ? ` created=${formatUtcTimestamp(t.createdAt)}` : ""}${t.updatedAt ? ` updated=${formatUtcTimestamp(t.updatedAt)}` : ""}`;
11539
11537
  }
@@ -12930,10 +12928,9 @@ function inferAttachmentMimeType(filename, bytes, explicit) {
12930
12928
  return sniffMimeType(bytes) ?? FILENAME_MIME_MAP[ext] ?? "application/octet-stream";
12931
12929
  }
12932
12930
  /**
12933
- * Small-file multipart upload (`POST /upload`). Files at or above the Server's
12934
- * direct-upload threshold need the upload-session routes (`routes.attachments.
12935
- * createUploadSession` …), which this operation reports as `UNAVAILABLE` with a
12936
- * next action rather than attempting silently.
12931
+ * Upload bytes into a conversation, choosing the path the CLI would: multipart
12932
+ * `POST /upload` below the Server's direct-upload threshold, an upload session
12933
+ * (create → PUT to a presigned URL → complete) at or above it.
12937
12934
  */
12938
12935
  async function uploadAttachment(client, transport, request) {
12939
12936
  if (!request.target?.trim() || !request.filename?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and a filename are required to upload." }));
@@ -12943,15 +12940,11 @@ async function uploadAttachment(client, transport, request) {
12943
12940
  const maxBytes = capability.ok ? capability.data.maxBytes : AGENT_ATTACHMENT_UPLOAD_FALLBACK_MAX_BYTES;
12944
12941
  const threshold = capability.ok && capability.data.directUploadEnabled ? capability.data.directUploadThresholdBytes : null;
12945
12942
  if (request.bytes.byteLength > maxBytes) return failureOutcome(opError("INVALID_REQUEST", { message: `Attachment is ${request.bytes.byteLength} bytes; the Server's maximum is ${maxBytes}.` }));
12946
- if (threshold !== null && request.bytes.byteLength >= threshold) return failureOutcome(opError("UNAVAILABLE", {
12947
- message: `Files of ${threshold} bytes or more use the direct-upload session routes, which this operation does not drive yet.`,
12948
- nextAction: "Use routes.attachments.createUploadSession / completeUploadSession, or upload a smaller file.",
12949
- retryable: false
12950
- }));
12951
12943
  const resolved = await client.channels.resolve({ target: request.target });
12952
12944
  if (!resolved.ok) return failureFromClientResult(resolved);
12953
12945
  const channelId = resolved.data.channelId;
12954
12946
  const mimeType = inferAttachmentMimeType(request.filename, request.bytes, request.mimeType);
12947
+ if (threshold !== null && request.bytes.byteLength >= threshold) return uploadThroughSession(client, transport, request, channelId, mimeType);
12955
12948
  const copy = new Uint8Array(request.bytes.byteLength);
12956
12949
  copy.set(request.bytes);
12957
12950
  const form = new FormData();
@@ -12991,20 +12984,22 @@ async function uploadAttachment(client, transport, request) {
12991
12984
  }
12992
12985
  const parsed = agentApiContract.attachmentUpload.response.body.safeParse(body);
12993
12986
  if (!parsed.success) return failureOutcome(opError("INVALID_RESPONSE"));
12994
- const data = parsed.data;
12987
+ return uploadedOutcome(request.target, parsed.data, mimeType);
12988
+ }
12989
+ function uploadedOutcome(target, data, mimeType) {
12995
12990
  return {
12996
12991
  ok: true,
12997
12992
  state: "uploaded",
12998
12993
  data: {
12999
12994
  ...data,
13000
- target: request.target,
12995
+ target,
13001
12996
  mimeType: data.mimeType ?? mimeType
13002
12997
  },
13003
12998
  next: {
13004
12999
  kind: "send_with_attachment",
13005
- command: `raft message send --target "${request.target}" --attachment-id ${data.id}`,
13000
+ command: `raft message send --target "${target}" --attachment-id ${data.id}`,
13006
13001
  args: {
13007
- target: request.target,
13002
+ target,
13008
13003
  attachmentIds: [data.id]
13009
13004
  },
13010
13005
  why: "The upload alone posts nothing; send a message that links the attachment id."
@@ -13012,6 +13007,69 @@ async function uploadAttachment(client, transport, request) {
13012
13007
  text: formatAgentAttachmentUploaded(data)
13013
13008
  };
13014
13009
  }
13010
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13011
+ /**
13012
+ * Direct upload for files at or above the Server's threshold, mirroring the
13013
+ * CLI: create a session, PUT the bytes to the presigned URL (fetch; one retry
13014
+ * when the object store may have committed), then complete, which the Server
13015
+ * verifies against the object store. A PUT that definitely failed cancels the
13016
+ * session; a PUT whose outcome is unknown leaves it for completion to verify.
13017
+ */
13018
+ async function uploadThroughSession(client, transport, request, channelId, mimeType) {
13019
+ const created = await client.attachments.createUploadSession({
13020
+ channelId,
13021
+ filename: request.filename,
13022
+ mimeType,
13023
+ sizeBytes: request.bytes.byteLength,
13024
+ clientRequestId: crypto.randomUUID()
13025
+ });
13026
+ if (!created.ok) return failureFromClientResult(created);
13027
+ const { uploadId, upload } = created.data;
13028
+ const body = new Uint8Array(request.bytes.byteLength);
13029
+ body.set(request.bytes);
13030
+ let definitelyFailed = null;
13031
+ for (let attempt = 0; attempt < 2; attempt += 1) {
13032
+ let response;
13033
+ try {
13034
+ response = await transport.fetch(upload.url, {
13035
+ method: upload.method ?? "PUT",
13036
+ headers: { ...upload.headers },
13037
+ body: body.buffer,
13038
+ redirect: "error"
13039
+ });
13040
+ } catch {
13041
+ if (attempt === 0) continue;
13042
+ break;
13043
+ }
13044
+ if (response.ok || response.status === 412) {
13045
+ definitelyFailed = null;
13046
+ break;
13047
+ }
13048
+ const mayExist = response.status === 408 || response.status === 429 || response.status >= 500;
13049
+ if (mayExist && attempt === 0) continue;
13050
+ if (!mayExist) definitelyFailed = response.status;
13051
+ break;
13052
+ }
13053
+ if (definitelyFailed !== null) {
13054
+ await client.attachments.cancelUploadSession({ uploadId }).catch(() => void 0);
13055
+ return failureOutcome(opError("HTTP_ERROR", {
13056
+ message: `Direct object upload failed with HTTP ${definitelyFailed}; the upload session was cancelled.`,
13057
+ status: definitelyFailed,
13058
+ nextAction: "Retry the upload; nothing was attached."
13059
+ }));
13060
+ }
13061
+ for (let attempt = 0; attempt < 3; attempt += 1) {
13062
+ const completed = await client.attachments.completeUploadSession({ uploadId });
13063
+ if (completed.ok) {
13064
+ const attachment = completed.data.attachment;
13065
+ return uploadedOutcome(request.target, attachment, mimeType);
13066
+ }
13067
+ const code = completed.error.kind === "http" ? completed.error.errorCode : null;
13068
+ if (!(code === "UPLOAD_OBJECT_NOT_FOUND" || code === "UPLOAD_VERIFICATION_IN_PROGRESS") || attempt === 2) return failureFromClientResult(completed);
13069
+ await wait(250 * (attempt + 1));
13070
+ }
13071
+ return failureOutcome(opError("UNAVAILABLE", { message: "Direct upload completion ended without a terminal response." }));
13072
+ }
13015
13073
  async function downloadAttachment(client, request) {
13016
13074
  if (!request.attachmentId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "An attachment id is required." }));
13017
13075
  const result = await client.attachments.download({ attachmentId: request.attachmentId });
@@ -13613,11 +13671,210 @@ async function searchManual(client, request) {
13613
13671
  };
13614
13672
  }
13615
13673
  //#endregion
13674
+ //#region ../shared/src/agentOps/state.ts
13675
+ const RAFT_STATE_SCHEMA = "raft-sdk-state.v1";
13676
+ const RAFT_STATE_CONTINUATIONS_PER_TARGET = 3;
13677
+ const RAFT_STATE_CONTINUATIONS_TOTAL = 20;
13678
+ function isNullableCursor(value) {
13679
+ return value === null || typeof value === "number" && Number.isInteger(value) && value >= 0;
13680
+ }
13681
+ /** Validate a loaded value; anything unrecognised is treated as an empty store (safe: see file header). */
13682
+ function parseRaftState(value) {
13683
+ if (!value || typeof value !== "object") return null;
13684
+ const v = value;
13685
+ if (v.schema !== "raft-sdk-state.v1") return null;
13686
+ if (typeof v.version !== "number" || !Number.isInteger(v.version) || v.version < 0) return null;
13687
+ if (!isNullableCursor(v.cursor ?? null) || !isNullableCursor(v.pendingCursor ?? null)) return null;
13688
+ const frontier = v.frontier;
13689
+ const continuations = Array.isArray(v.continuations) ? v.continuations.filter((c) => {
13690
+ const r = c;
13691
+ return Boolean(r) && typeof r.target === "string" && typeof r.idempotencyKey === "string" && typeof r.contentHash === "string" && typeof r.heldAt === "string";
13692
+ }) : [];
13693
+ return {
13694
+ schema: RAFT_STATE_SCHEMA,
13695
+ version: v.version,
13696
+ cursor: v.cursor ?? null,
13697
+ pendingCursor: v.pendingCursor ?? null,
13698
+ frontier: frontier && frontier.version === 1 ? frontier : {
13699
+ version: 1,
13700
+ targets: {},
13701
+ aliases: {}
13702
+ },
13703
+ continuations
13704
+ };
13705
+ }
13706
+ /** Hex SHA-256 identifying one logical message (WebCrypto; Workers-safe). */
13707
+ async function hashRaftSendContent(target, content, attachmentIds = []) {
13708
+ const bytes = new TextEncoder().encode(`${target}\n${[...attachmentIds].sort().join(",")}\n${content}`);
13709
+ return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
13710
+ }
13711
+ /**
13712
+ * One client's view of the persisted state: loads once, holds the working
13713
+ * copy, saves on request. Also used without a store (memory only), so the
13714
+ * pending/committed cursor flow behaves the same with or without persistence.
13715
+ */
13716
+ var RaftStateSession = class {
13717
+ store;
13718
+ onError;
13719
+ frontier;
13720
+ cursor = null;
13721
+ pendingCursor = null;
13722
+ continuations = [];
13723
+ loadedVersion = void 0;
13724
+ loading = null;
13725
+ dirty = false;
13726
+ constructor(store, frontier, onError) {
13727
+ this.store = store;
13728
+ this.onError = onError;
13729
+ this.frontier = frontier;
13730
+ }
13731
+ get persistent() {
13732
+ return this.store !== void 0;
13733
+ }
13734
+ /** The version loaded from (or last saved to) the store; `undefined` before the first save of an empty store. */
13735
+ get version() {
13736
+ return this.loadedVersion;
13737
+ }
13738
+ /** Load once. A failed or unreadable load starts from an empty state (reported, not thrown). */
13739
+ ensureLoaded() {
13740
+ if (!this.store) return Promise.resolve();
13741
+ this.loading ??= (async () => {
13742
+ let raw = null;
13743
+ try {
13744
+ raw = await this.store.load();
13745
+ } catch (error) {
13746
+ this.onError?.(error, {
13747
+ phase: "load",
13748
+ version: void 0
13749
+ });
13750
+ return;
13751
+ }
13752
+ const state = parseRaftState(raw);
13753
+ if (!state) return;
13754
+ this.loadedVersion = state.version;
13755
+ this.cursor = state.cursor;
13756
+ this.pendingCursor = state.pendingCursor;
13757
+ this.frontier.absorb(state.frontier);
13758
+ this.continuations = state.continuations ?? [];
13759
+ })();
13760
+ return this.loading;
13761
+ }
13762
+ markDirty() {
13763
+ this.dirty = true;
13764
+ }
13765
+ /** Record the cursor of a batch just returned. Acknowledges nothing. */
13766
+ setPending(cursor) {
13767
+ if (cursor === null || cursor === this.pendingCursor) return;
13768
+ this.pendingCursor = cursor;
13769
+ this.dirty = true;
13770
+ }
13771
+ /**
13772
+ * Promote a cursor to committed. With no argument, commits `pendingCursor`.
13773
+ * Returns the committed cursor, or null when there was nothing to commit.
13774
+ * Never moves the committed cursor backwards.
13775
+ */
13776
+ commit(cursor) {
13777
+ const next = cursor === void 0 ? this.pendingCursor : cursor;
13778
+ if (next === null || next === void 0) return null;
13779
+ if (this.cursor === null || next > this.cursor) this.cursor = next;
13780
+ if (this.pendingCursor !== null && this.pendingCursor <= this.cursor) this.pendingCursor = null;
13781
+ this.dirty = true;
13782
+ return this.cursor;
13783
+ }
13784
+ findContinuation(target, contentHash) {
13785
+ return this.continuations.find((c) => c.target === target && c.contentHash === contentHash);
13786
+ }
13787
+ rememberContinuation(entry) {
13788
+ this.continuations = this.continuations.filter((c) => !(c.target === entry.target && c.contentHash === entry.contentHash));
13789
+ this.continuations.push(entry);
13790
+ const perTarget = this.continuations.filter((c) => c.target === entry.target);
13791
+ for (const drop of perTarget.slice(0, Math.max(0, perTarget.length - 3))) this.continuations.splice(this.continuations.indexOf(drop), 1);
13792
+ if (this.continuations.length > 20) this.continuations.splice(0, this.continuations.length - 20);
13793
+ this.dirty = true;
13794
+ }
13795
+ forgetContinuation(target, contentHash) {
13796
+ const before = this.continuations.length;
13797
+ this.continuations = this.continuations.filter((c) => !(c.target === target && c.contentHash === contentHash));
13798
+ if (this.continuations.length !== before) this.dirty = true;
13799
+ }
13800
+ snapshot(nextVersion = (this.loadedVersion ?? 0) + 1) {
13801
+ return {
13802
+ schema: RAFT_STATE_SCHEMA,
13803
+ version: nextVersion,
13804
+ cursor: this.cursor,
13805
+ pendingCursor: this.pendingCursor,
13806
+ frontier: this.frontier.snapshot(),
13807
+ continuations: [...this.continuations]
13808
+ };
13809
+ }
13810
+ /**
13811
+ * Save if anything changed (or `force`). One attempt; a failure is reported
13812
+ * and swallowed. Returns whether the save succeeded (true when nothing to save).
13813
+ */
13814
+ async save(options = {}) {
13815
+ if (!this.store) return true;
13816
+ if (!this.dirty && !options.force) return true;
13817
+ const expectedVersion = this.loadedVersion;
13818
+ const state = this.snapshot();
13819
+ try {
13820
+ await this.store.save(state, { expectedVersion });
13821
+ } catch (error) {
13822
+ this.onError?.(error, {
13823
+ phase: "save",
13824
+ version: expectedVersion
13825
+ });
13826
+ return false;
13827
+ }
13828
+ this.loadedVersion = state.version;
13829
+ this.dirty = false;
13830
+ return true;
13831
+ }
13832
+ };
13833
+ //#endregion
13834
+ //#region ../shared/src/agentOps/actions.ts
13835
+ /** Text is the CLI's `raft action prepare` output. */
13836
+ function formatAgentActionCardPosted(target, messageId) {
13837
+ const shortId = messageId ? messageId.slice(0, 8) : null;
13838
+ return shortId ? `Action card posted to ${target} as message ${messageId} (short ${shortId}). The human can click the action verb to commit.\n` : `Action card posted to ${target}.\n`;
13839
+ }
13840
+ async function prepareActionCard(client, request) {
13841
+ if (!request?.target?.trim() || !request.action) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and an action are required to prepare a card." }));
13842
+ const result = await client.actions.prepare(request);
13843
+ if (!result.ok) return failureFromClientResult(result);
13844
+ const card = {
13845
+ target: request.target,
13846
+ messageId: result.data.messageId
13847
+ };
13848
+ return {
13849
+ ok: true,
13850
+ state: "prepared",
13851
+ data: card,
13852
+ next: {
13853
+ kind: "await_confirmation",
13854
+ command: `raft message read --target "${request.target}" --around ${card.messageId.slice(0, 8)}`,
13855
+ args: {
13856
+ target: request.target,
13857
+ messageId: card.messageId
13858
+ },
13859
+ why: "A human must click the card to commit it; its outcome arrives in your inbox."
13860
+ },
13861
+ text: formatAgentActionCardPosted(request.target, card.messageId)
13862
+ };
13863
+ }
13864
+ //#endregion
13616
13865
  //#region src/raft.ts
13617
13866
  function createRaft(options) {
13618
13867
  const serverUrl = requireServerUrl(options.serverUrl);
13619
13868
  const authorization = `Bearer ${requireAgentCredential(options.credential)}`;
13620
13869
  const frontier = SeenFrontier.fromSnapshot(options.frontier);
13870
+ const session = new RaftStateSession(options.state, frontier, options.onStateSaveError);
13871
+ /** Load before, run, then save if the operation succeeded and changed the state. */
13872
+ const withState = async (run) => {
13873
+ await session.ensureLoaded();
13874
+ const outcome = await run();
13875
+ if (outcome.ok) await session.save();
13876
+ return outcome;
13877
+ };
13621
13878
  const routes = createRaftRoutes({
13622
13879
  serverUrl,
13623
13880
  fetch: options.fetch,
@@ -13638,6 +13895,27 @@ function createRaft(options) {
13638
13895
  throttle: options.throttle,
13639
13896
  auth: { authorization }
13640
13897
  } });
13898
+ /**
13899
+ * Send through the state session: a resend of the same logical message
13900
+ * (same target, content, attachments) reuses the held send's idempotency key;
13901
+ * a hold is remembered; a successful send clears it.
13902
+ */
13903
+ const sendWithState = (request) => withState(async () => {
13904
+ const contentHash = await hashRaftSendContent(request.target, request.content ?? "", request.attachmentIds ?? []);
13905
+ const pending = request.idempotencyKey ? void 0 : session.findContinuation(request.target, contentHash);
13906
+ const outcome = await sendMessage(routes, pending ? {
13907
+ ...request,
13908
+ idempotencyKey: pending.idempotencyKey
13909
+ } : request, frontier);
13910
+ if (outcome.ok && outcome.state === "held") session.rememberContinuation({
13911
+ target: request.target,
13912
+ idempotencyKey: outcome.data.idempotencyKey,
13913
+ contentHash,
13914
+ heldAt: (/* @__PURE__ */ new Date()).toISOString()
13915
+ });
13916
+ else if (outcome.ok && outcome.state === "sent") session.forgetContinuation(request.target, contentHash);
13917
+ return outcome;
13918
+ });
13641
13919
  return {
13642
13920
  identity: { whoami: () => getRaftContext(routes) },
13643
13921
  wake: {
@@ -13649,14 +13927,53 @@ function createRaft(options) {
13649
13927
  }
13650
13928
  },
13651
13929
  inbox: {
13652
- check: (request) => checkInbox(inboxApi, request, frontier),
13653
- drain: (request) => drainInbox(inboxApi, request, frontier),
13930
+ check: (request = {}) => withState(async () => {
13931
+ const since = request.since ?? session.cursor ?? void 0;
13932
+ const outcome = await checkInbox(inboxApi, {
13933
+ ...request,
13934
+ ...since === void 0 ? {} : { since }
13935
+ }, frontier);
13936
+ if (outcome.ok) {
13937
+ session.markDirty();
13938
+ if (since !== void 0 && (request.ack ?? "cursor") === "cursor") session.commit(since);
13939
+ if (outcome.data.ackMode === "cursor" && outcome.data.messages.length > 0) session.setPending(outcome.data.cursor);
13940
+ }
13941
+ return outcome;
13942
+ }),
13943
+ commit: async (target) => {
13944
+ await session.ensureLoaded();
13945
+ const cursor = session.commit(target === void 0 ? void 0 : target.cursor);
13946
+ return {
13947
+ cursor,
13948
+ saved: cursor === null ? true : await session.save()
13949
+ };
13950
+ },
13951
+ drain: (request = {}) => (async function* () {
13952
+ await session.ensureLoaded();
13953
+ const since = request.since ?? session.cursor ?? void 0;
13954
+ return yield* drainInbox(inboxApi, {
13955
+ ...request,
13956
+ ...since === void 0 ? {} : { since }
13957
+ }, frontier, async (sent, batch) => {
13958
+ if (sent !== null) session.commit(sent);
13959
+ if (batch.ackMode === "cursor" && batch.messages.length > 0) session.setPending(batch.cursor);
13960
+ session.markDirty();
13961
+ await session.save();
13962
+ });
13963
+ })(),
13654
13964
  list: (request) => listInbox(routes, request)
13655
13965
  },
13656
13966
  messages: {
13657
- read: (request) => readHistory(routes, request, frontier),
13658
- send: (request) => sendMessage(routes, request, frontier),
13659
- reply: (message, request) => replyTo(routes, message, request, frontier),
13967
+ read: (request) => withState(async () => {
13968
+ const outcome = await readHistory(routes, request, frontier);
13969
+ if (outcome.ok) session.markDirty();
13970
+ return outcome;
13971
+ }),
13972
+ send: (request) => sendWithState(request),
13973
+ reply: (message, request) => sendWithState({
13974
+ ...request,
13975
+ target: message.target
13976
+ }),
13660
13977
  search: (request) => searchMessages(routes, request),
13661
13978
  resolve: (request) => resolveMessage(routes, request),
13662
13979
  react: (request) => reactToMessage(routes, request, "add"),
@@ -13677,6 +13994,7 @@ function createRaft(options) {
13677
13994
  execute: (request) => executeMentionAction(routes, request),
13678
13995
  deliveries: (request) => senderMentionDeliveries(routes, request)
13679
13996
  },
13997
+ actions: { prepare: (request) => prepareActionCard(routes, request) },
13680
13998
  manual: {
13681
13999
  get: (request) => getManualTopic(routes, request),
13682
14000
  search: (request) => searchManual(routes, request)
@@ -13710,8 +14028,17 @@ function createRaft(options) {
13710
14028
  update: (request) => updateProfile(routes, request)
13711
14029
  },
13712
14030
  frontier,
14031
+ state: {
14032
+ save: async () => {
14033
+ await session.ensureLoaded();
14034
+ session.markDirty();
14035
+ return session.save();
14036
+ },
14037
+ load: () => session.ensureLoaded(),
14038
+ snapshot: () => session.snapshot(session.version ?? 0)
14039
+ },
13713
14040
  routes
13714
14041
  };
13715
14042
  }
13716
14043
  //#endregion
13717
- export { RAFT_INBOX_NOTICE_SCHEMA, RAFT_NOTICE_DELIVERY_ID_HEADER, RAFT_NOTICE_SIGNATURE_HEADER, RaftCredentialError, RaftSdkConfigurationError, SeenFrontier, bootstrapRaftCredential, createFileCredentialStore, createRaft, createRaftClient, createRaftClientFromStore, createRaftRoutes, describeRaftRoute, hasAgentMessageIdentity, inferAttachmentMimeType, joinRaftChannelByTarget, listRaftRoutes, parseRaftRegularChannelTarget, projectRaftMessage, projectRaftMessages, verifyInboxNotice };
14044
+ export { RAFT_INBOX_NOTICE_SCHEMA, RAFT_NOTICE_DELIVERY_ID_HEADER, RAFT_NOTICE_SIGNATURE_HEADER, RAFT_STATE_CONTINUATIONS_PER_TARGET, RAFT_STATE_CONTINUATIONS_TOTAL, RAFT_STATE_SCHEMA, RaftCredentialError, RaftSdkConfigurationError, SeenFrontier, bootstrapRaftCredential, createFileCredentialStore, createRaft, createRaftClient, createRaftClientFromStore, createRaftRoutes, describeRaftRoute, hasAgentMessageIdentity, hashRaftSendContent, inferAttachmentMimeType, joinRaftChannelByTarget, listRaftRoutes, parseRaftRegularChannelTarget, parseRaftState, projectRaftMessage, projectRaftMessages, verifyInboxNotice };
package/dist/index.d.ts CHANGED
@@ -444,6 +444,8 @@ export declare class SeenFrontier {
444
444
  private readonly targets;
445
445
  private readonly aliases;
446
446
  static fromSnapshot(snapshot: SeenFrontierSnapshot | null | undefined): SeenFrontier;
447
+ /** Merge a snapshot into this frontier by the same monotonic rules (never lowers a mark). */
448
+ absorb(snapshot: SeenFrontierSnapshot | null | undefined): void;
447
449
  /** Remember that the Server resolved `requested` to `canonical` (a thread spelling, a `~agent` suffix, …). */
448
450
  recordAlias(requested: string, canonical: string): void;
449
451
  canonical(target: string): string;
@@ -935,6 +937,57 @@ interface ManualContext {
935
937
  reason: string;
936
938
  }
937
939
  //#endregion
940
+ //#region ../shared/src/agentOps/state.d.ts
941
+ export declare const RAFT_STATE_SCHEMA: "raft-sdk-state.v1";
942
+ interface RaftStateContinuation {
943
+ target: string;
944
+ idempotencyKey: string;
945
+ /** Hex SHA-256 of target, content and attachment ids; a resend with the same hash is the same logical message. */
946
+ contentHash: string;
947
+ /** ISO time the send was held. */
948
+ heldAt: string;
949
+ }
950
+ interface RaftState {
951
+ schema: typeof RAFT_STATE_SCHEMA;
952
+ /** Revision, incremented on every save by this SDK. Compare-and-set stores compare against it. */
953
+ version: number;
954
+ /** Last committed inbox cursor; the next pull passes it as `since`. */
955
+ cursor: number | null;
956
+ /** Cursor of the batch returned but not yet committed. */
957
+ pendingCursor: number | null;
958
+ frontier: SeenFrontierSnapshot;
959
+ continuations?: RaftStateContinuation[];
960
+ }
961
+ interface RaftStateStore {
962
+ load(): Promise<RaftState | null>;
963
+ /**
964
+ * Persist `state`. `expectedVersion` is the version this client loaded
965
+ * (`undefined` when the store was empty). Throw to reject a stale write;
966
+ * stores without compare-and-set may ignore it.
967
+ */
968
+ save(state: RaftState, options: {
969
+ expectedVersion: number | undefined;
970
+ }): Promise<void>;
971
+ }
972
+ type RaftStateSaveErrorHandler = (error: unknown, context: {
973
+ phase: "load" | "save";
974
+ version: number | undefined;
975
+ }) => void;
976
+ export declare const RAFT_STATE_CONTINUATIONS_PER_TARGET = 3;
977
+ export declare const RAFT_STATE_CONTINUATIONS_TOTAL = 20;
978
+ /** Validate a loaded value; anything unrecognised is treated as an empty store (safe: see file header). */
979
+ export declare function parseRaftState(value: unknown): RaftState | null;
980
+ /** Hex SHA-256 identifying one logical message (WebCrypto; Workers-safe). */
981
+ export declare function hashRaftSendContent(target: string, content: string, attachmentIds?: readonly string[]): Promise<string>;
982
+ //#endregion
983
+ //#region ../shared/src/agentOps/actions.d.ts
984
+ type PrepareActionCardRequest = AgentApiActionPrepareBody;
985
+ interface RaftPreparedCard {
986
+ target: string;
987
+ /** The card message; a human commits it by clicking its action verb. */
988
+ messageId: string;
989
+ }
990
+ //#endregion
938
991
  //#region ../shared/src/agentApiRawClient.d.ts
939
992
  type AgentApiRawClientErrorReason = "missing_route" | "missing_path_param" | "request_contract_mismatch" | "transport_error" | "http_error" | "empty_response" | "response_contract_mismatch";
940
993
  interface AgentApiRawTransportRequest<K extends AgentApiRouteKey = AgentApiRouteKey> {
@@ -9299,6 +9352,22 @@ interface CreateRaftOptions {
9299
9352
  * is held once and returns the unread context, which is the safe default.
9300
9353
  */
9301
9354
  frontier?: SeenFrontierSnapshot | null;
9355
+ /**
9356
+ * Persist the client's state (committed and pending inbox cursors, the seen
9357
+ * frontier, held-send keys) across tool calls and processes. The store
9358
+ * implements `load()` and `save(state, { expectedVersion })`; the SDK loads
9359
+ * once before the first operation and saves after each successful operation
9360
+ * that changed the state (one attempt, never fails the operation).
9361
+ */
9362
+ state?: RaftStateStore;
9363
+ /** Called when loading or saving the state fails or is rejected as stale. */
9364
+ onStateSaveError?: RaftStateSaveErrorHandler;
9365
+ }
9366
+ interface RaftInboxCommitResult {
9367
+ /** The committed cursor, or null when there was nothing to commit. */
9368
+ cursor: number | null;
9369
+ /** Whether the state was persisted (true when no store is configured). */
9370
+ saved: boolean;
9302
9371
  }
9303
9372
  interface Raft {
9304
9373
  identity: {
@@ -9320,8 +9389,22 @@ interface Raft {
9320
9389
  };
9321
9390
  };
9322
9391
  inbox: {
9323
- /** One bounded pull. Nothing is acknowledged until the next call passes `data.cursor` as `since` (cursor mode). */
9392
+ /**
9393
+ * One bounded pull. Nothing is acknowledged by it. Without `since`, sends
9394
+ * the last committed cursor (from `state`), which is what acknowledges the
9395
+ * previously committed batch on the Server; the returned batch's cursor is
9396
+ * recorded as pending until you `commit()` it.
9397
+ */
9324
9398
  check(request?: CheckInboxRequest): Promise<CheckInboxOutcome>;
9399
+ /**
9400
+ * Mark a batch as processed. With no argument, commits the pending cursor
9401
+ * from the last `check()` (possibly in an earlier process, via `state`);
9402
+ * also accepts `{ cursor }` or a batch. Records only; the next `check()`
9403
+ * acknowledges it on the Server. The SDK never commits on its own.
9404
+ */
9405
+ commit(target?: {
9406
+ cursor: number | null;
9407
+ }): Promise<RaftInboxCommitResult>;
9325
9408
  /**
9326
9409
  * Pull until the Server reports nothing more, like `raft message check`, as
9327
9410
  * an async iterator: the pull that acknowledges a batch is only sent when
@@ -9376,6 +9459,14 @@ interface Raft {
9376
9459
  messageId: string;
9377
9460
  }): Promise<RaftOutcome<AgentApiSenderMentionDeliveriesResponse, "deliveries" | "empty">>;
9378
9461
  };
9462
+ actions: {
9463
+ /**
9464
+ * Post an action card for a human to confirm (channel:create,
9465
+ * channel:add_member, agent:create, and the integration card types). The
9466
+ * human who clicks it executes it as themselves.
9467
+ */
9468
+ prepare(request: PrepareActionCardRequest): Promise<RaftOutcome<RaftPreparedCard, "prepared">>;
9469
+ };
9379
9470
  manual: {
9380
9471
  /** Fetch a Manual topic; `intent` and `reason` are required and must never carry prompts, credentials, or message payloads. */
9381
9472
  get(request: {
@@ -9455,9 +9546,17 @@ interface Raft {
9455
9546
  };
9456
9547
  /** What this process has shown its model, per target; export it to survive restarts. */
9457
9548
  frontier: SeenFrontier;
9549
+ state: {
9550
+ /** Save now (for example after `frontier.recordHeld`). Best effort; returns whether it succeeded. */
9551
+ save(): Promise<boolean>;
9552
+ /** Load the persisted state now (operations do this automatically). */
9553
+ load(): Promise<void>;
9554
+ /** The current state value, as it would be saved. */
9555
+ snapshot(): RaftState;
9556
+ };
9458
9557
  /** Every Agent API route, typed from the shared contract. */
9459
9558
  routes: RaftRoutes;
9460
9559
  }
9461
9560
  export declare function createRaft(options: CreateRaftOptions): Raft;
9462
9561
  //#endregion
9463
- export type { AmendTaskOutcome, AmendTaskRequest, AssignTaskRequest, BootstrapRaftCredentialOptions, CheckInboxOutcome, CheckInboxRequest, ClaimTasksOutcome, ClaimTasksRequest, CreateRaftClientFromStoreOptions, CreateRaftClientOptions, CreateRaftOptions, CreateRaftRoutesOptions, CreateTasksRequest, DrainInboxRequest, ExecuteMentionActionRequest, ListInboxRequest, ListTasksRequest, ManualContext, Raft, RaftAckMode, RaftActionPrepareRequest, RaftActionPrepared, RaftApiError, RaftApiResult, RaftAppConfig, RaftAppConfigPatch, RaftAttachmentBytes, RaftAttachmentUploaded, RaftAvatarUpload, RaftChannelJoinClient, RaftChannelJoinClientResult, RaftChannelJoinError, RaftChannelJoinFailure, RaftChannelJoinOperation, RaftChannelJoinRequest, RaftChannelJoinResult, RaftChannelJoinSuccess, RaftChannelJoinTransportError, RaftChannelMuteState, RaftChannelRef, RaftClaimHeld, RaftClaimHeldLike, RaftClaimResult, RaftClaimRow, RaftClaimRowState, RaftClient, RaftClientError, RaftClientFailure, RaftClientResult, RaftClientSuccess, RaftClientThrottleOptions, RaftClientTransportRequest, RaftContextAgent, RaftContextData, RaftContextError, RaftContextResult, RaftContextServer, RaftCredentialErrorCode, RaftCredentialIdentity, RaftCredentialStore, RaftEvent, RaftEventAttachment, RaftEventExternalMessage, RaftEventsReceiveData, RaftEventsReceiveError, RaftEventsReceiveRequest, RaftEventsReceiveResult, RaftFailure, RaftHeld, RaftHeldBase, RaftHistoryPage, RaftInboxBatch, RaftInboxConversation, RaftInboxDrainSummary, RaftInboxListing, RaftInboxNotice, RaftManageClient, RaftMessage, RaftMessageAttachment, RaftMessageTask, RaftNextStep, RaftNoticeFlag, RaftNoticeTarget, RaftOpError, RaftOpErrorCode, RaftOutcome, RaftPendingMentions, RaftProfile, RaftProfileUpdate, RaftRouteAnnotations, RaftRouteInfo, RaftRouteKey, RaftRouteMeta, RaftRouteResult, RaftRouteRetryPolicy, RaftRoutes, RaftSdkConfigurationErrorCode, RaftSearchPage, RaftSendContinuation, RaftSenderType, RaftSent, RaftServerInfo, RaftServerProfile, RaftServerUpdate, RaftTaskBoard, RaftTaskStatus, RaftTasksCreated, RaftWebhookStatus, ReactRequest, ReadHistoryRequest, SearchMessagesRequest, SeenAttestation, SeenFrontierSnapshot, SendMessageOutcome, SendMessageRequest, ServerInfoRequest, ServerInfoSection, StoredRaftCredential, TaskRef, UpdateTaskStatusOutcome, UpdateTaskStatusRequest, UploadAttachmentRequest, VerifyNoticeInput, VerifyNoticeRejection, VerifyNoticeResult };
9562
+ export type { AmendTaskOutcome, AmendTaskRequest, AssignTaskRequest, BootstrapRaftCredentialOptions, CheckInboxOutcome, CheckInboxRequest, ClaimTasksOutcome, ClaimTasksRequest, CreateRaftClientFromStoreOptions, CreateRaftClientOptions, CreateRaftOptions, CreateRaftRoutesOptions, CreateTasksRequest, DrainInboxRequest, ExecuteMentionActionRequest, ListInboxRequest, ListTasksRequest, ManualContext, PrepareActionCardRequest, Raft, RaftAckMode, RaftActionPrepareRequest, RaftActionPrepared, RaftApiError, RaftApiResult, RaftAppConfig, RaftAppConfigPatch, RaftAttachmentBytes, RaftAttachmentUploaded, RaftAvatarUpload, RaftChannelJoinClient, RaftChannelJoinClientResult, RaftChannelJoinError, RaftChannelJoinFailure, RaftChannelJoinOperation, RaftChannelJoinRequest, RaftChannelJoinResult, RaftChannelJoinSuccess, RaftChannelJoinTransportError, RaftChannelMuteState, RaftChannelRef, RaftClaimHeld, RaftClaimHeldLike, RaftClaimResult, RaftClaimRow, RaftClaimRowState, RaftClient, RaftClientError, RaftClientFailure, RaftClientResult, RaftClientSuccess, RaftClientThrottleOptions, RaftClientTransportRequest, RaftContextAgent, RaftContextData, RaftContextError, RaftContextResult, RaftContextServer, RaftCredentialErrorCode, RaftCredentialIdentity, RaftCredentialStore, RaftEvent, RaftEventAttachment, RaftEventExternalMessage, RaftEventsReceiveData, RaftEventsReceiveError, RaftEventsReceiveRequest, RaftEventsReceiveResult, RaftFailure, RaftHeld, RaftHeldBase, RaftHistoryPage, RaftInboxBatch, RaftInboxCommitResult, RaftInboxConversation, RaftInboxDrainSummary, RaftInboxListing, RaftInboxNotice, RaftManageClient, RaftMessage, RaftMessageAttachment, RaftMessageTask, RaftNextStep, RaftNoticeFlag, RaftNoticeTarget, RaftOpError, RaftOpErrorCode, RaftOutcome, RaftPendingMentions, RaftPreparedCard, RaftProfile, RaftProfileUpdate, RaftRouteAnnotations, RaftRouteInfo, RaftRouteKey, RaftRouteMeta, RaftRouteResult, RaftRouteRetryPolicy, RaftRoutes, RaftSdkConfigurationErrorCode, RaftSearchPage, RaftSendContinuation, RaftSenderType, RaftSent, RaftServerInfo, RaftServerProfile, RaftServerUpdate, RaftState, RaftStateContinuation, RaftStateSaveErrorHandler, RaftStateStore, RaftTaskBoard, RaftTaskStatus, RaftTasksCreated, RaftWebhookStatus, ReactRequest, ReadHistoryRequest, SearchMessagesRequest, SeenAttestation, SeenFrontierSnapshot, SendMessageOutcome, SendMessageRequest, ServerInfoRequest, ServerInfoSection, StoredRaftCredential, TaskRef, UpdateTaskStatusOutcome, UpdateTaskStatusRequest, UploadAttachmentRequest, VerifyNoticeInput, VerifyNoticeRejection, VerifyNoticeResult };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-sdk",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.5",
4
4
  "license": "FSL-1.1-ALv2",
5
5
  "description": "Typed Raft Agent API client for external agents and bots.",
6
6
  "type": "module",