@statewavedev/sdk 0.9.0 → 0.10.1

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
@@ -12,6 +12,12 @@ Official TypeScript SDK for [Statewave](https://github.com/smaramwbc/statewave)
12
12
 
13
13
  > ⚠️ **v0.9.0 is a breaking change.** The entire SDK surface — request params *and* response fields — is now idiomatic **camelCase** (`subjectId`, `maxTokens`, `createdAt`, `receiptId`, …). The wire protocol is unchanged; the client maps to/from the server's snake_case transparently. `payload`, `metadata`, and `provenance` are passed through verbatim — their inner keys are never rewritten. See [CHANGELOG](CHANGELOG.md#090) for the full rename table and migration steps.
14
14
 
15
+ > **New to Statewave?** This SDK is a thin client for a running **Statewave
16
+ > server**. If you don't have one yet, the
17
+ > [Getting Started guide](https://github.com/smaramwbc/statewave-docs/blob/main/getting-started.md)
18
+ > brings one up with Docker Compose in about 5 minutes. Every example below
19
+ > assumes a server reachable at `http://localhost:8100`.
20
+
15
21
  ## Install
16
22
 
17
23
  ```bash
@@ -91,9 +97,9 @@ console.log(`${timeline.episodes.length} episodes, ${timeline.memories.length} m
91
97
  await sw.deleteSubject("user-42");
92
98
  ```
93
99
 
94
- ## Governance & audit (v0.8)
100
+ ## Governance & audit (v0.8+)
95
101
 
96
- The SDK surfaces the [state-assembly receipts](https://github.com/smaramwbc/statewave-docs/blob/main/receipts.md) and [sensitivity-labels / policy](https://github.com/smaramwbc/statewave-docs/blob/main/sensitivity-labels.md) layer added in server v0.8.
102
+ The SDK surfaces the [state-assembly receipts](https://github.com/smaramwbc/statewave-docs/blob/main/receipts.md) and [sensitivity-labels / policy](https://github.com/smaramwbc/statewave-docs/blob/main/sensitivity-labels.md) layer added in server v0.8, plus the v0.9 [HMAC signing](https://github.com/smaramwbc/statewave/blob/main/docs/state-assembly-receipts.md) and [as-of replay](https://github.com/smaramwbc/statewave/blob/main/docs/replay.md) surfaces.
97
103
 
98
104
  ```typescript
99
105
  import { StatewaveClient } from "@statewavedev/sdk";
@@ -134,6 +140,39 @@ for (const r of receipts) {
134
140
  console.log(r.receiptId, r.task);
135
141
  }
136
142
 
143
+ // Verify the HMAC signature on a stored receipt (v0.9+).
144
+ // `valid` is true | false | null — see ReceiptVerifyResult for the
145
+ // full reason vocabulary (no_signature / key_unavailable / etc.).
146
+ if (bundle.receiptId) {
147
+ const verdict = await sw.verifyReceipt(bundle.receiptId);
148
+ if (verdict.valid === true) {
149
+ console.log(`signature OK — signed by ${verdict.keyId}`);
150
+ } else if (verdict.valid === false) {
151
+ console.log("signature mismatch — body may have been tampered with");
152
+ } else {
153
+ console.log(`verdict undetermined: ${verdict.reason}`);
154
+ }
155
+ }
156
+
157
+ // Replay the receipt against current memories using the original
158
+ // policy bundle captured on the receipt (v0.9+). Returns a diff
159
+ // envelope showing what changed since emission. Pre-v0.9 receipts
160
+ // throw StatewaveUnreplayableError with reason="missing_policy_snapshot".
161
+ import { StatewaveUnreplayableError } from "@statewavedev/sdk";
162
+ try {
163
+ const replay = await sw.replayReceipt(bundle.receiptId!);
164
+ if (replay.diff.contextHash.changed) {
165
+ console.log(`replay differs from original: new id ${replay.replayReceiptId}`);
166
+ }
167
+ } catch (err) {
168
+ if (err instanceof StatewaveUnreplayableError) {
169
+ // err.reason ∈ {"missing_policy_snapshot", "nested_replay", "invalid_snapshot"}
170
+ console.log(`replay refused: ${err.reason}`);
171
+ } else {
172
+ throw err;
173
+ }
174
+ }
175
+
137
176
  // Set per-memory sensitivity labels (server normalizes — dedup, lowercase, trim).
138
177
  // Memories with labels become subject to any active policy bundle for the tenant.
139
178
  const updated = await sw.setMemoryLabels({
@@ -145,6 +184,59 @@ console.log(updated.sensitivityLabels); // → ["financial", "pii"]
145
184
 
146
185
  Receipts and the policy engine cooperate: every assembly call records its policy decisions into `receipt.policy.filtersApplied` (one entry per memory the policy fired on) and `receipt.policy.filtersSkipped` (per-rule summary of what didn't fire). In `log_only` mode (the tenant default) the receipt is the full audit trail without filtering; under `enforce` denied memories are dropped before they reach the assembly and the deny is still recorded. See [`receipts.md`](https://github.com/smaramwbc/statewave-docs/blob/main/receipts.md) and [`sensitivity-labels.md`](https://github.com/smaramwbc/statewave-docs/blob/main/sensitivity-labels.md) for the full schemas and policy YAML format.
147
186
 
187
+ ## Support-agent endpoints
188
+
189
+ Statewave's support wedge — customer health scoring, SLA tracking, resolution state, and structured escalation briefs — is exposed through ergonomic SDK methods (server v0.6+).
190
+
191
+ ```typescript
192
+ import { StatewaveClient } from "@statewavedev/sdk";
193
+
194
+ const sw = new StatewaveClient("http://localhost:8100");
195
+
196
+ // Customer health score (0–100) with the explainable factors behind it.
197
+ const health = await sw.getHealth("customer:globex");
198
+ console.log(`${health.score}/100 — ${health.state}`);
199
+ for (const f of health.factors) {
200
+ console.log(` ${f.signal}: ${f.impact >= 0 ? "+" : ""}${f.impact} (${f.detail})`);
201
+ }
202
+
203
+ // SLA metrics — first-response / resolution times and breach counts.
204
+ // Thresholds are optional; they default server-side to 5 min / 24 h.
205
+ const sla = await sw.getSLA({
206
+ subjectId: "customer:globex",
207
+ firstResponseThresholdMinutes: 10,
208
+ resolutionThresholdHours: 48,
209
+ });
210
+ console.log(`${sla.resolvedSessions}/${sla.totalSessions} resolved, ${sla.resolutionBreachCount} SLA breaches`);
211
+
212
+ // Track resolution state for a session (upserts by subject + session).
213
+ await sw.createResolution({
214
+ subjectId: "customer:globex",
215
+ sessionId: "ticket-8842",
216
+ status: "resolved",
217
+ resolutionSummary: "Issued refund for the duplicate charge",
218
+ });
219
+
220
+ // List resolutions, optionally filtered by status.
221
+ const openItems = await sw.listResolutions({
222
+ subjectId: "customer:globex",
223
+ status: "open",
224
+ });
225
+
226
+ // Generate a handoff context pack for escalation or shift change.
227
+ // `handoffNotes` is a pre-rendered markdown brief for human or LLM use.
228
+ const handoff = await sw.createHandoff({
229
+ subjectId: "customer:globex",
230
+ sessionId: "ticket-8842",
231
+ reason: "escalation",
232
+ callerId: "agent-7",
233
+ callerType: "support_agent",
234
+ });
235
+ console.log(handoff.handoffNotes);
236
+ ```
237
+
238
+ `getHealth`, `getSLA`, `createResolution`, `listResolutions`, and `createHandoff` respect the same auth, tenant-scoping, and retry behaviour as the rest of the client. `createHandoff` shares `getContext`'s caller-identity gate — when the tenant config sets `require_caller_identity: true`, both `callerId` and `callerType` are mandatory.
239
+
148
240
  ## Error handling
149
241
 
150
242
  ```typescript
@@ -187,10 +279,18 @@ All response types are fully typed:
187
279
  - `BatchCreateResult` — batch ingestion response
188
280
  - `SubjectSummary` — subject with episode/memory counts
189
281
  - `ListSubjectsResult` — paginated subject listing
190
- - `Receipt` + `ReceiptSelectedEntry` + `ReceiptPolicy` + `ReceiptOutput` — state-assembly audit artifact (v0.8) and its nested shapes
282
+ - `Receipt` + `ReceiptSelectedEntry` + `ReceiptPolicy` + `ReceiptOutput` — state-assembly audit artifact (v0.8+) and its nested shapes; v0.9 added HMAC signature fields (`receiptSignatureKeyId`, `receiptSignatureAlgorithm`), `policySnapshot` for replay, and `region` for residency
283
+ - `ReceiptVerifyResult` — `valid` (true | false | null) + `keyId` + `algorithm` + `reason` for the v0.9 HMAC verify endpoint
284
+ - `ReceiptReplayResult` / `ReceiptReplayDiff` — original + replay receipt ids plus the structural diff envelope from `POST /v1/receipts/{id}/replay` (v0.9)
285
+ - `StatewaveUnreplayableError` — thrown by `replayReceipt(...)` on HTTP 422; `.reason` is a discriminated union of `"missing_policy_snapshot" | "nested_replay" | "invalid_snapshot"`
191
286
  - `ReceiptList` — cursor-paginated receipt listing
287
+ - `Health` + `HealthFactor` — customer health score and its explainable factors
288
+ - `SLASummary` + `SessionSLA` — SLA metrics, aggregate and per-session
289
+ - `Handoff` + `ResolutionSummaryItem` — handoff context pack and its prior-resolution items
290
+ - `Resolution` — resolution tracking record
291
+ - `HealthState` / `ResolutionStatus` — string-literal status unions
192
292
 
193
- Param types: `CreateEpisodeParams`, `SearchMemoriesParams`, `GetContextParams`, `ListReceiptsParams`, `SetMemoryLabelsParams`
293
+ Param types: `CreateEpisodeParams`, `SearchMemoriesParams`, `GetContextParams`, `ListReceiptsParams`, `SetMemoryLabelsParams`, `GetSLAParams`, `CreateHandoffParams`, `CreateResolutionParams`, `ListResolutionsParams`
194
294
 
195
295
  ## Running tests
196
296
 
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BatchCreateResult, ClientOptions, CompileJob, CompileResult, ContextBundle, CreateEpisodeParams, DeleteResult, Episode, GetContextParams, ListReceiptsParams, ListSubjectsResult, Memory, Receipt, ReceiptList, SearchMemoriesParams, SearchResult, SetMemoryLabelsParams, Timeline } from "./types.js";
1
+ import type { BatchCreateResult, ClientOptions, CompileJob, CompileResult, ContextBundle, CreateEpisodeParams, CreateHandoffParams, CreateResolutionParams, DeleteResult, Episode, GetContextParams, GetSLAParams, Handoff, Health, ListReceiptsParams, ListResolutionsParams, ListSubjectsResult, Memory, Receipt, ReceiptList, ReceiptReplayResult, ReceiptVerifyResult, Resolution, SLASummary, SearchMemoriesParams, SearchResult, SetMemoryLabelsParams, Timeline, UnreplayableReason } from "./types.js";
2
2
  /** Structured error from the Statewave API. */
3
3
  export declare class StatewaveAPIError extends Error {
4
4
  readonly statusCode: number;
@@ -11,6 +11,26 @@ export declare class StatewaveAPIError extends Error {
11
11
  export declare class StatewaveConnectionError extends Error {
12
12
  constructor(message?: string);
13
13
  }
14
+ /**
15
+ * Raised by `replayReceipt(...)` when the server refuses with HTTP 422.
16
+ * Subclass of `StatewaveAPIError` so generic handlers still catch it;
17
+ * adds a typed `reason` field so callers can branch on the structured
18
+ * refusal vocabulary without parsing the error code.
19
+ *
20
+ * `reason` is a discriminated union of:
21
+ * - `"missing_policy_snapshot"` — pre-v0.9 receipt. No
22
+ * `policySnapshot` was captured at emission and the replay engine
23
+ * cannot synthesise one retroactively.
24
+ * - `"nested_replay"` — the receipt is itself a replay
25
+ * (`mode === "as_of_replay"`). v0.9 ships one level only; replay
26
+ * the source receipt referenced by `parentReceiptId` instead.
27
+ * - `"invalid_snapshot"` — the snapshot's YAML failed to parse.
28
+ * Tampering or corruption at the column level.
29
+ */
30
+ export declare class StatewaveUnreplayableError extends StatewaveAPIError {
31
+ readonly reason: UnreplayableReason;
32
+ constructor(reason: UnreplayableReason, statusCode: number, code: string, message: string, details?: unknown, requestId?: string);
33
+ }
14
34
  export declare class StatewaveClient {
15
35
  private baseUrl;
16
36
  private defaultHeaders;
@@ -47,6 +67,81 @@ export declare class StatewaveClient {
47
67
  * to fetch the next page.
48
68
  */
49
69
  listReceipts(params: ListReceiptsParams): Promise<ReceiptList>;
70
+ /**
71
+ * Verify the HMAC signature on a stored receipt (v0.9+ #157).
72
+ *
73
+ * Returns a `ReceiptVerifyResult` with `valid` ∈ `{true, false, null}`:
74
+ * - `true` — signature matches the canonical body (`reason === "ok"`).
75
+ * - `false` — signature does not cover the body
76
+ * (`reason === "signature_mismatch"`).
77
+ * - `null` — verdict could not be determined; `reason` is one of
78
+ * `"no_signature"` (unsigned receipt — pre-v0.9 or tenant didn't
79
+ * opt in), `"key_unavailable"` (the keyId rotated out of operator
80
+ * config), or `"unsupported_algorithm"` (forward-compat).
81
+ *
82
+ * Comparison is constant-time on the server side. The signing key
83
+ * bytes never appear on the response — only the public `keyId` is
84
+ * echoed.
85
+ *
86
+ * Throws `StatewaveAPIError` on 404 (receipt not found or belongs to
87
+ * a different tenant — indistinguishable on the wire) and other
88
+ * non-2xx responses.
89
+ */
90
+ verifyReceipt(receiptId: string): Promise<ReceiptVerifyResult>;
91
+ /**
92
+ * Re-run the original retrieval against current memories using the
93
+ * original policy bundle captured in the receipt's `policySnapshot`
94
+ * (v0.9+ #159).
95
+ *
96
+ * Emits a new `mode="as_of_replay"` receipt with `parentReceiptId`
97
+ * pointing at the source; the original receipt is **never**
98
+ * modified. Returns the new `replayReceiptId` plus a structural
99
+ * diff envelope (added/removed selected entries, filter changes,
100
+ * context-hash diff).
101
+ *
102
+ * Semantic: current code + original policy. Replay is *not*
103
+ * byte-for-byte reproduction; memories that were added, tombstoned,
104
+ * or supersession-resolved between the original emission and now
105
+ * will appear in the diff. See `docs/replay.md` in the server repo
106
+ * for the design rationale.
107
+ *
108
+ * Throws `StatewaveUnreplayableError` (HTTP 422) when:
109
+ * - `reason === "missing_policy_snapshot"` — pre-v0.9 receipt.
110
+ * - `reason === "nested_replay"` — the receipt is itself a replay.
111
+ * - `reason === "invalid_snapshot"` — snapshot YAML failed to parse.
112
+ *
113
+ * Throws `StatewaveAPIError` on 404 and other non-2xx responses.
114
+ */
115
+ replayReceipt(receiptId: string): Promise<ReceiptReplayResult>;
116
+ /**
117
+ * Compute the customer health score (0–100) for a subject, with the
118
+ * explainable factors that drove it. Backs proactive risk triage.
119
+ */
120
+ getHealth(subjectId: string): Promise<Health>;
121
+ /**
122
+ * Compute SLA metrics for a subject — first-response and resolution
123
+ * times plus breach flags, aggregated across the subject's sessions.
124
+ * Both thresholds fall back to the server defaults (5 minutes /
125
+ * 24 hours) when omitted.
126
+ */
127
+ getSLA(params: GetSLAParams): Promise<SLASummary>;
128
+ /**
129
+ * Generate a handoff context pack — a structured escalation brief for
130
+ * shift change or agent transfer. Same caller-identity gate as
131
+ * `getContext`: when the tenant sets `require_caller_identity: true`,
132
+ * both `callerId` and `callerType` are mandatory.
133
+ */
134
+ createHandoff(params: CreateHandoffParams): Promise<Handoff>;
135
+ /**
136
+ * Create or update a resolution record for a support session.
137
+ * Upserts by `subjectId` + `sessionId`.
138
+ */
139
+ createResolution(params: CreateResolutionParams): Promise<Resolution>;
140
+ /**
141
+ * List resolution records for a subject, optionally filtered to a
142
+ * single status.
143
+ */
144
+ listResolutions(params: ListResolutionsParams): Promise<Resolution[]>;
50
145
  getTimeline(subjectId: string): Promise<Timeline>;
51
146
  deleteSubject(subjectId: string): Promise<DeleteResult>;
52
147
  listSubjects(params?: {
package/dist/client.js CHANGED
@@ -59,6 +59,40 @@ export class StatewaveConnectionError extends Error {
59
59
  this.name = "StatewaveConnectionError";
60
60
  }
61
61
  }
62
+ /** The documented refusal vocabulary for the v0.9 replay endpoint.
63
+ * Mirrors the `UnreplayableReason` type alias in `./types.ts`.
64
+ * Auto-promotion to `StatewaveUnreplayableError` only happens when
65
+ * the server returns a code with one of these reasons — a future
66
+ * unknown reason stays on the generic `StatewaveAPIError` path. */
67
+ const UNREPLAYABLE_REASONS = new Set([
68
+ "missing_policy_snapshot",
69
+ "nested_replay",
70
+ "invalid_snapshot",
71
+ ]);
72
+ /**
73
+ * Raised by `replayReceipt(...)` when the server refuses with HTTP 422.
74
+ * Subclass of `StatewaveAPIError` so generic handlers still catch it;
75
+ * adds a typed `reason` field so callers can branch on the structured
76
+ * refusal vocabulary without parsing the error code.
77
+ *
78
+ * `reason` is a discriminated union of:
79
+ * - `"missing_policy_snapshot"` — pre-v0.9 receipt. No
80
+ * `policySnapshot` was captured at emission and the replay engine
81
+ * cannot synthesise one retroactively.
82
+ * - `"nested_replay"` — the receipt is itself a replay
83
+ * (`mode === "as_of_replay"`). v0.9 ships one level only; replay
84
+ * the source receipt referenced by `parentReceiptId` instead.
85
+ * - `"invalid_snapshot"` — the snapshot's YAML failed to parse.
86
+ * Tampering or corruption at the column level.
87
+ */
88
+ export class StatewaveUnreplayableError extends StatewaveAPIError {
89
+ reason;
90
+ constructor(reason, statusCode, code, message, details, requestId) {
91
+ super(statusCode, code, message, details, requestId);
92
+ this.name = "StatewaveUnreplayableError";
93
+ this.reason = reason;
94
+ }
95
+ }
62
96
  export class StatewaveClient {
63
97
  baseUrl;
64
98
  defaultHeaders;
@@ -195,6 +229,128 @@ export class StatewaveClient {
195
229
  qs.set("limit", String(params.limit));
196
230
  return this.get(`/v1/receipts?${qs}`);
197
231
  }
232
+ /**
233
+ * Verify the HMAC signature on a stored receipt (v0.9+ #157).
234
+ *
235
+ * Returns a `ReceiptVerifyResult` with `valid` ∈ `{true, false, null}`:
236
+ * - `true` — signature matches the canonical body (`reason === "ok"`).
237
+ * - `false` — signature does not cover the body
238
+ * (`reason === "signature_mismatch"`).
239
+ * - `null` — verdict could not be determined; `reason` is one of
240
+ * `"no_signature"` (unsigned receipt — pre-v0.9 or tenant didn't
241
+ * opt in), `"key_unavailable"` (the keyId rotated out of operator
242
+ * config), or `"unsupported_algorithm"` (forward-compat).
243
+ *
244
+ * Comparison is constant-time on the server side. The signing key
245
+ * bytes never appear on the response — only the public `keyId` is
246
+ * echoed.
247
+ *
248
+ * Throws `StatewaveAPIError` on 404 (receipt not found or belongs to
249
+ * a different tenant — indistinguishable on the wire) and other
250
+ * non-2xx responses.
251
+ */
252
+ async verifyReceipt(receiptId) {
253
+ return this.get(`/v1/receipts/${encodeURIComponent(receiptId)}/verify`);
254
+ }
255
+ /**
256
+ * Re-run the original retrieval against current memories using the
257
+ * original policy bundle captured in the receipt's `policySnapshot`
258
+ * (v0.9+ #159).
259
+ *
260
+ * Emits a new `mode="as_of_replay"` receipt with `parentReceiptId`
261
+ * pointing at the source; the original receipt is **never**
262
+ * modified. Returns the new `replayReceiptId` plus a structural
263
+ * diff envelope (added/removed selected entries, filter changes,
264
+ * context-hash diff).
265
+ *
266
+ * Semantic: current code + original policy. Replay is *not*
267
+ * byte-for-byte reproduction; memories that were added, tombstoned,
268
+ * or supersession-resolved between the original emission and now
269
+ * will appear in the diff. See `docs/replay.md` in the server repo
270
+ * for the design rationale.
271
+ *
272
+ * Throws `StatewaveUnreplayableError` (HTTP 422) when:
273
+ * - `reason === "missing_policy_snapshot"` — pre-v0.9 receipt.
274
+ * - `reason === "nested_replay"` — the receipt is itself a replay.
275
+ * - `reason === "invalid_snapshot"` — snapshot YAML failed to parse.
276
+ *
277
+ * Throws `StatewaveAPIError` on 404 and other non-2xx responses.
278
+ */
279
+ async replayReceipt(receiptId) {
280
+ return this.post(`/v1/receipts/${encodeURIComponent(receiptId)}/replay`, undefined);
281
+ }
282
+ // -- Support: health, SLA, handoff, resolutions ----------------------
283
+ /**
284
+ * Compute the customer health score (0–100) for a subject, with the
285
+ * explainable factors that drove it. Backs proactive risk triage.
286
+ */
287
+ async getHealth(subjectId) {
288
+ return this.get(`/v1/subjects/${encodeURIComponent(subjectId)}/health`);
289
+ }
290
+ /**
291
+ * Compute SLA metrics for a subject — first-response and resolution
292
+ * times plus breach flags, aggregated across the subject's sessions.
293
+ * Both thresholds fall back to the server defaults (5 minutes /
294
+ * 24 hours) when omitted.
295
+ */
296
+ async getSLA(params) {
297
+ const qs = new URLSearchParams();
298
+ if (params.firstResponseThresholdMinutes !== undefined) {
299
+ qs.set("first_response_threshold_minutes", String(params.firstResponseThresholdMinutes));
300
+ }
301
+ if (params.resolutionThresholdHours !== undefined) {
302
+ qs.set("resolution_threshold_hours", String(params.resolutionThresholdHours));
303
+ }
304
+ const query = qs.toString();
305
+ return this.get(`/v1/subjects/${encodeURIComponent(params.subjectId)}/sla${query ? `?${query}` : ""}`);
306
+ }
307
+ /**
308
+ * Generate a handoff context pack — a structured escalation brief for
309
+ * shift change or agent transfer. Same caller-identity gate as
310
+ * `getContext`: when the tenant sets `require_caller_identity: true`,
311
+ * both `callerId` and `callerType` are mandatory.
312
+ */
313
+ async createHandoff(params) {
314
+ return this.post("/v1/handoff", {
315
+ subjectId: params.subjectId,
316
+ sessionId: params.sessionId,
317
+ ...(params.reason !== undefined && { reason: params.reason }),
318
+ ...(params.maxTokens !== undefined && { maxTokens: params.maxTokens }),
319
+ ...(params.emitReceipt !== undefined && { emitReceipt: params.emitReceipt }),
320
+ ...(params.queryId !== undefined && { queryId: params.queryId }),
321
+ ...(params.taskId !== undefined && { taskId: params.taskId }),
322
+ ...(params.parentReceiptId !== undefined && {
323
+ parentReceiptId: params.parentReceiptId,
324
+ }),
325
+ ...(params.callerId !== undefined && { callerId: params.callerId }),
326
+ ...(params.callerType !== undefined && { callerType: params.callerType }),
327
+ });
328
+ }
329
+ /**
330
+ * Create or update a resolution record for a support session.
331
+ * Upserts by `subjectId` + `sessionId`.
332
+ */
333
+ async createResolution(params) {
334
+ return this.post("/v1/resolutions", {
335
+ subjectId: params.subjectId,
336
+ sessionId: params.sessionId,
337
+ ...(params.status !== undefined && { status: params.status }),
338
+ ...(params.resolutionSummary !== undefined && {
339
+ resolutionSummary: params.resolutionSummary,
340
+ }),
341
+ ...(params.metadata !== undefined && { metadata: params.metadata }),
342
+ });
343
+ }
344
+ /**
345
+ * List resolution records for a subject, optionally filtered to a
346
+ * single status.
347
+ */
348
+ async listResolutions(params) {
349
+ const qs = new URLSearchParams({ subject_id: params.subjectId });
350
+ if (params.status !== undefined)
351
+ qs.set("status", params.status);
352
+ return this.get(`/v1/resolutions?${qs}`);
353
+ }
198
354
  async getTimeline(subjectId) {
199
355
  return this.get(`/v1/timeline?subject_id=${encodeURIComponent(subjectId)}`);
200
356
  }
@@ -281,6 +437,17 @@ export class StatewaveClient {
281
437
  const body = await resp.json();
282
438
  const err = body?.error;
283
439
  if (err && typeof err.code === "string") {
440
+ // Promote unreplayable.<reason> refusals into a typed
441
+ // exception so callers can `catch (e) { if (e instanceof
442
+ // StatewaveUnreplayableError) ... e.reason }` without
443
+ // string-matching the error code. Forward-compat: an
444
+ // unrecognised future reason stays on the generic path.
445
+ if (resp.status === 422 && err.code.startsWith("unreplayable.")) {
446
+ const reason = err.code.slice("unreplayable.".length);
447
+ if (UNREPLAYABLE_REASONS.has(reason)) {
448
+ throw new StatewaveUnreplayableError(reason, resp.status, err.code, err.message ?? resp.statusText, err.details, err.request_id);
449
+ }
450
+ }
284
451
  throw new StatewaveAPIError(resp.status, err.code, err.message ?? resp.statusText, err.details, err.request_id);
285
452
  }
286
453
  }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { StatewaveClient, StatewaveAPIError, StatewaveConnectionError } from "./client.js";
1
+ export { StatewaveClient, StatewaveAPIError, StatewaveConnectionError, StatewaveUnreplayableError, } from "./client.js";
2
2
  export type * from "./types.js";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { StatewaveClient, StatewaveAPIError, StatewaveConnectionError } from "./client.js";
1
+ export { StatewaveClient, StatewaveAPIError, StatewaveConnectionError, StatewaveUnreplayableError, } from "./client.js";
package/dist/types.d.ts CHANGED
@@ -101,14 +101,38 @@ export interface ReceiptOutput {
101
101
  canonicalizationVersion: number;
102
102
  tokenEstimate: number;
103
103
  }
104
+ /**
105
+ * v0.9 (#159) — self-contained policy bundle envelope embedded on
106
+ * every v0.9+ receipt. Self-sufficient: the replay engine evaluates
107
+ * against this bundle even if the live `policy_bundles` row has
108
+ * since been deleted or overwritten.
109
+ *
110
+ * - A null inner pair (`bundleHash` AND `bundleYaml` both null)
111
+ * records "no policy bundle was active at emission" — a valid,
112
+ * replayable state.
113
+ * - The whole envelope being absent (`Receipt.policySnapshot ===
114
+ * undefined`) marks "pre-v0.9 receipt, no snapshot was ever
115
+ * captured" — the replay endpoint refuses those.
116
+ */
117
+ export interface PolicySnapshot {
118
+ bundleHash: string | null;
119
+ bundleYaml: string | null;
120
+ /** ISO-8601 UTC timestamp captured at receipt emission. */
121
+ capturedAt: string;
122
+ }
104
123
  /**
105
124
  * Immutable per-retrieval audit artifact for a single context assembly.
106
125
  * See `docs/state-assembly-receipts.md` in the server repository.
126
+ *
127
+ * The `mode` discriminator distinguishes:
128
+ * - `"retrieval"` — receipts emitted by `/v1/context` + `/v1/handoff`.
129
+ * - `"as_of_replay"` — receipts emitted by `POST /v1/receipts/{id}/replay`
130
+ * (v0.9+); the `parentReceiptId` points at the source receipt.
107
131
  */
108
132
  export interface Receipt {
109
133
  receiptId: string;
110
134
  parentReceiptId: string | null;
111
- mode: "retrieval" | string;
135
+ mode: "retrieval" | "as_of_replay" | string;
112
136
  queryId: string | null;
113
137
  taskId: string | null;
114
138
  tenantId: string | null;
@@ -119,8 +143,24 @@ export interface Receipt {
119
143
  selectedEntries: ReceiptSelectedEntry[];
120
144
  policy: ReceiptPolicy;
121
145
  output: ReceiptOutput;
146
+ /** Server region the receipt was emitted from (v0.9+ residency).
147
+ * `null` in single-region deployments. */
122
148
  region: string | null;
149
+ /** HMAC-SHA256 hex digest over the canonical body (v0.9+ #157).
150
+ * `null` for pre-v0.9 receipts or tenants without signing
151
+ * configured — those verify cleanly as
152
+ * `{valid: null, reason: "no_signature"}`. */
123
153
  receiptSignature: string | null;
154
+ /** Operator key id used to sign (v0.9+). `null`/`undefined` when unsigned. */
155
+ receiptSignatureKeyId?: string | null;
156
+ /** Algorithm + canonical-form version (e.g. "hmac-sha256-canonical-v1")
157
+ * (v0.9+). `null`/`undefined` when unsigned. */
158
+ receiptSignatureAlgorithm?: string | null;
159
+ /** Embedded policy bundle YAML + hash + capture timestamp (v0.9+ #159).
160
+ * See `PolicySnapshot`. `undefined` for pre-v0.9 receipts (the
161
+ * replay endpoint refuses those with
162
+ * `unreplayable.missing_policy_snapshot`). */
163
+ policySnapshot?: PolicySnapshot | null;
124
164
  }
125
165
  export interface ReceiptList {
126
166
  receipts: Receipt[];
@@ -134,6 +174,75 @@ export interface ListReceiptsParams {
134
174
  cursor?: string;
135
175
  limit?: number;
136
176
  }
177
+ /**
178
+ * Result of `GET /v1/receipts/{id}/verify` (v0.9+ #157).
179
+ *
180
+ * `valid` is the verdict:
181
+ * - `true` — HMAC matches the canonical body. `reason === "ok"`.
182
+ * - `false` — math checked, signature does not cover the body.
183
+ * `reason === "signature_mismatch"`.
184
+ * - `null` — verdict could not be determined. `reason` is one of:
185
+ * - `"no_signature"` — receipt is unsigned (pre-v0.9 or tenant
186
+ * didn't opt in).
187
+ * - `"key_unavailable"` — the `keyId` rotated out of operator
188
+ * config; receipt is no longer verifiable on this binary.
189
+ * - `"unsupported_algorithm"` — receipt signed under a canonical
190
+ * form / algorithm variant this binary doesn't implement.
191
+ *
192
+ * Comparison is constant-time on the server side; the signing key
193
+ * bytes never appear on the response.
194
+ */
195
+ export interface ReceiptVerifyResult {
196
+ valid: boolean | null;
197
+ keyId: string | null;
198
+ algorithm: string | null;
199
+ reason: "ok" | "signature_mismatch" | "no_signature" | "key_unavailable" | "unsupported_algorithm" | string;
200
+ }
201
+ /**
202
+ * Structural diff envelope returned by `POST /v1/receipts/{id}/replay`.
203
+ * Entries are matched by their `memoryId` / `episodeId` so re-ranking
204
+ * the same entry is reported under `common`, not as add+remove.
205
+ */
206
+ export interface ReceiptReplayDiff {
207
+ contextHash: {
208
+ original: string | null;
209
+ replay: string | null;
210
+ changed: boolean;
211
+ };
212
+ selectedEntries: {
213
+ added: ReceiptSelectedEntry[];
214
+ removed: ReceiptSelectedEntry[];
215
+ common: number;
216
+ };
217
+ filtersApplied: {
218
+ added: unknown[];
219
+ removed: unknown[];
220
+ };
221
+ }
222
+ /**
223
+ * Response from `POST /v1/receipts/{id}/replay` (v0.9+ #159).
224
+ *
225
+ * Semantic: current code + original policy. Replay re-runs the
226
+ * original retrieval against the *current* memory state but with
227
+ * the *original* policy bundle frozen on the receipt's
228
+ * `policySnapshot`. The original receipt is never modified;
229
+ * `replayReceiptId` points at a new `mode="as_of_replay"` receipt
230
+ * linked back to the source via `parentReceiptId`.
231
+ *
232
+ * `replayReceiptId` is `null` when the replay-receipt write itself
233
+ * failed (rare, fail-open path). The `diff` envelope is still
234
+ * authoritative in that case.
235
+ */
236
+ export interface ReceiptReplayResult {
237
+ originalReceiptId: string;
238
+ replayReceiptId: string | null;
239
+ diff: ReceiptReplayDiff;
240
+ }
241
+ /** The set of refusal reasons the server returns from
242
+ * `POST /v1/receipts/{id}/replay` when a receipt cannot be replayed.
243
+ * Used by `StatewaveUnreplayableError.reason` so callers can switch
244
+ * on the structured value without parsing error code strings. */
245
+ export type UnreplayableReason = "missing_policy_snapshot" | "nested_replay" | "invalid_snapshot";
137
246
  export interface Timeline {
138
247
  subjectId: string;
139
248
  episodes: Episode[];
@@ -166,6 +275,95 @@ export interface CompileJob {
166
275
  memories?: Memory[];
167
276
  error?: string;
168
277
  }
278
+ /** Support health-state bucket. */
279
+ export type HealthState = "healthy" | "watch" | "at_risk";
280
+ /** Resolution lifecycle status. */
281
+ export type ResolutionStatus = "open" | "resolved" | "unresolved";
282
+ /** One explainable factor behind a customer health score. */
283
+ export interface HealthFactor {
284
+ /** Stable signal identifier, e.g. `sla_resolution_breaches`. */
285
+ signal: string;
286
+ /** Signed score contribution — a negative impact drags the score down. */
287
+ impact: number;
288
+ /** Human-readable explanation of the factor. */
289
+ detail: string;
290
+ }
291
+ /** Customer health score (0–100) with the factors that drove it. */
292
+ export interface Health {
293
+ subjectId: string;
294
+ score: number;
295
+ state: HealthState;
296
+ factors: HealthFactor[];
297
+ }
298
+ /** SLA metrics for a single support session. */
299
+ export interface SessionSLA {
300
+ sessionId: string;
301
+ /** `resolved` | `open`. */
302
+ status: string;
303
+ firstMessageAt: string | null;
304
+ firstResponseAt: string | null;
305
+ resolvedAt: string | null;
306
+ firstResponseSeconds: number | null;
307
+ resolutionSeconds: number | null;
308
+ openDurationSeconds: number | null;
309
+ firstResponseBreached: boolean;
310
+ resolutionBreached: boolean;
311
+ }
312
+ /** Aggregate SLA metrics for a subject across all of its sessions. */
313
+ export interface SLASummary {
314
+ subjectId: string;
315
+ totalSessions: number;
316
+ resolvedSessions: number;
317
+ openSessions: number;
318
+ avgFirstResponseSeconds: number | null;
319
+ avgResolutionSeconds: number | null;
320
+ firstResponseBreachCount: number;
321
+ resolutionBreachCount: number;
322
+ sessions: SessionSLA[];
323
+ }
324
+ /** A prior resolution surfaced inside a handoff brief. */
325
+ export interface ResolutionSummaryItem {
326
+ sessionId: string;
327
+ status: string;
328
+ summary: string | null;
329
+ resolvedAt: string | null;
330
+ }
331
+ /** Structured escalation brief — the handoff context pack. */
332
+ export interface Handoff {
333
+ subjectId: string;
334
+ sessionId: string;
335
+ reason: string;
336
+ generatedAt: string;
337
+ customerSummary: string;
338
+ activeIssue: string;
339
+ attemptedSteps: string[];
340
+ keyFacts: string[];
341
+ resolutionHistory: ResolutionSummaryItem[];
342
+ recentContext: string[];
343
+ healthScore: number | null;
344
+ healthState: HealthState | null;
345
+ healthFactors: HealthFactor[];
346
+ /** Pre-rendered markdown brief, ready for human or LLM consumption. */
347
+ handoffNotes: string;
348
+ tokenEstimate: number;
349
+ provenance: Record<string, unknown>;
350
+ /** ULID of the state-assembly receipt, when one was emitted. */
351
+ receiptId?: string | null;
352
+ /** True iff a receipt was successfully written for this call. */
353
+ receiptEmitted?: boolean;
354
+ }
355
+ /** Resolution tracking record for a support session. */
356
+ export interface Resolution {
357
+ id: string;
358
+ subjectId: string;
359
+ sessionId: string;
360
+ status: ResolutionStatus;
361
+ resolutionSummary: string | null;
362
+ resolvedAt: string | null;
363
+ metadata: Record<string, unknown>;
364
+ createdAt: string;
365
+ updatedAt: string;
366
+ }
169
367
  export interface CreateEpisodeParams {
170
368
  subjectId: string;
171
369
  source: string;
@@ -213,6 +411,53 @@ export interface SetMemoryLabelsParams {
213
411
  */
214
412
  sensitivityLabels: string[];
215
413
  }
414
+ export interface GetSLAParams {
415
+ subjectId: string;
416
+ /** First-response SLA threshold in minutes (server default: 5). */
417
+ firstResponseThresholdMinutes?: number;
418
+ /** Resolution SLA threshold in hours (server default: 24). */
419
+ resolutionThresholdHours?: number;
420
+ }
421
+ export interface CreateHandoffParams {
422
+ subjectId: string;
423
+ /** Session being handed off. */
424
+ sessionId: string;
425
+ /** Why the handoff is happening (server default: "escalation"). */
426
+ reason?: string;
427
+ /** Token budget for the assembled brief. */
428
+ maxTokens?: number;
429
+ /**
430
+ * Opt in to emitting a state-assembly receipt for this call. The
431
+ * tenant config can also force emission on or off independently of
432
+ * this flag.
433
+ */
434
+ emitReceipt?: boolean;
435
+ queryId?: string;
436
+ taskId?: string;
437
+ parentReceiptId?: string;
438
+ /**
439
+ * Caller identity consumed by the sensitivity-label policy layer
440
+ * (#50). When the tenant config sets `require_caller_identity: true`,
441
+ * both `callerId` and `callerType` are mandatory.
442
+ */
443
+ callerId?: string;
444
+ callerType?: string;
445
+ }
446
+ export interface CreateResolutionParams {
447
+ subjectId: string;
448
+ sessionId: string;
449
+ /** Lifecycle status (server default: "open"). */
450
+ status?: ResolutionStatus;
451
+ /** Short human summary of how the session was resolved. */
452
+ resolutionSummary?: string;
453
+ /** Free-form caller-owned bag; inner keys round-trip verbatim. */
454
+ metadata?: Record<string, unknown>;
455
+ }
456
+ export interface ListResolutionsParams {
457
+ subjectId: string;
458
+ /** Filter to a single status. Omit to list every resolution. */
459
+ status?: ResolutionStatus;
460
+ }
216
461
  export interface ClientOptions {
217
462
  baseUrl?: string;
218
463
  apiKey?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewavedev/sdk",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Official TypeScript SDK for Statewave — the open-source memory runtime for AI agents.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",