@scitrera/memorylayer-sdk 0.1.22 → 0.2.0

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/dist/client.js CHANGED
@@ -1,6 +1,9 @@
1
- import { MemoryLayerError, AuthenticationError, AuthorizationError, NotFoundError, ValidationError, EnterpriseRequiredError } from "./errors.js";
1
+ import { MemoryLayerError, AuthenticationError, AuthorizationError, NotFoundError, ValidationError, EnterpriseRequiredError, RateLimitError } from "./errors.js";
2
+ import { sleep } from "./utils.js";
2
3
  import { SkillsNamespace } from "./skills.js";
3
4
  import { McpServersNamespace } from "./mcp_servers.js";
5
+ import { KnowledgebaseNamespace } from "./knowledgebase.js";
6
+ import { RpgNamespace } from "./rpg.js";
4
7
  export class MemoryLayerClient {
5
8
  baseUrl;
6
9
  apiKey;
@@ -8,10 +11,17 @@ export class MemoryLayerClient {
8
11
  sessionId;
9
12
  timeout;
10
13
  defaultAuthority;
14
+ fetchImpl;
15
+ maxRetries;
16
+ retryBaseDelay;
11
17
  /** Skills namespace — access via `client.skills.list(...)` etc. */
12
18
  skills;
13
19
  /** MCP Servers namespace — access via `client.mcpServers.list(...)` etc. */
14
20
  mcpServers;
21
+ /** Knowledgebase namespace — access via `client.kb.get(...)` etc. */
22
+ kb;
23
+ /** Repository Planning Graph namespace — access via `client.rpg.sync(...)` etc. */
24
+ rpg;
15
25
  constructor(config = {}) {
16
26
  this.baseUrl = config.baseUrl ?? "http://localhost:61001";
17
27
  this.apiKey = config.apiKey;
@@ -19,8 +29,15 @@ export class MemoryLayerClient {
19
29
  this.sessionId = config.sessionId;
20
30
  this.timeout = config.timeout ?? 30000;
21
31
  this.defaultAuthority = config.defaultAuthority;
32
+ this.maxRetries = config.maxRetries ?? 3;
33
+ this.retryBaseDelay = config.retryBaseDelay ?? 500;
34
+ // Bind to globalThis so the default impl preserves `this === undefined`
35
+ // (fetch is sensitive to its receiver in some runtimes).
36
+ this.fetchImpl = config.fetch ?? fetch.bind(globalThis);
22
37
  this.skills = new SkillsNamespace(this);
23
38
  this.mcpServers = new McpServersNamespace(this);
39
+ this.kb = new KnowledgebaseNamespace(this);
40
+ this.rpg = new RpgNamespace(this);
24
41
  }
25
42
  /**
26
43
  * Returns a lightweight proxy that sends OBO headers for the given grant/subject
@@ -62,10 +79,24 @@ export class MemoryLayerClient {
62
79
  };
63
80
  return h;
64
81
  }
65
- async request(method, path, body, enterpriseFeature, authority) {
66
- const headers = {
67
- "Content-Type": "application/json",
68
- };
82
+ /**
83
+ * Build the standard request headers (auth, session, workspace, OBO authority).
84
+ * Centralized so every request helper applies identical header logic.
85
+ *
86
+ * Note: upload/stream paths (exportWorkspace, importWorkspaceStream,
87
+ * uploadDocument, uploadDataset, getPageImage) call this method and therefore
88
+ * intentionally inherit authority (X-Aether-*) headers when a defaultAuthority
89
+ * is set — consistent with every other request type.
90
+ *
91
+ * @param includeContentType Whether to set `Content-Type: application/json`.
92
+ * Omit for multipart/form-data (browser sets the boundary) or NDJSON bodies
93
+ * that set their own content type.
94
+ */
95
+ buildHeaders(authority, includeContentType = true) {
96
+ const headers = {};
97
+ if (includeContentType) {
98
+ headers["Content-Type"] = "application/json";
99
+ }
69
100
  if (this.apiKey) {
70
101
  headers["Authorization"] = `Bearer ${this.apiKey}`;
71
102
  }
@@ -76,30 +107,121 @@ export class MemoryLayerClient {
76
107
  headers["X-Workspace-ID"] = this.workspaceId;
77
108
  }
78
109
  Object.assign(headers, this.buildAuthorityHeaders(authority));
79
- const url = `${this.baseUrl}${path}`;
80
- const controller = new AbortController();
81
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
82
- try {
83
- const response = await fetch(url, {
84
- method,
85
- headers,
86
- body: body ? JSON.stringify(body) : undefined,
87
- signal: controller.signal,
88
- });
89
- clearTimeout(timeoutId);
90
- if (!response.ok) {
110
+ return headers;
111
+ }
112
+ /**
113
+ * Determine whether a failed response is transient and should be retried.
114
+ * Retries 429 (rate limit) and 5xx server errors. 501 is treated as a
115
+ * permanent "not implemented" signal and is never retried.
116
+ */
117
+ isRetryableStatus(status) {
118
+ if (status === 429)
119
+ return true;
120
+ if (status === 501)
121
+ return false;
122
+ return status >= 500 && status < 600;
123
+ }
124
+ /**
125
+ * Parse a `Retry-After` header (delta-seconds or HTTP-date) into milliseconds.
126
+ * Returns undefined when absent or unparseable.
127
+ */
128
+ parseRetryAfter(response) {
129
+ const raw = response.headers?.get?.("Retry-After");
130
+ if (!raw)
131
+ return undefined;
132
+ const seconds = Number(raw);
133
+ if (!Number.isNaN(seconds)) {
134
+ return Math.max(0, seconds * 1000);
135
+ }
136
+ const dateMs = Date.parse(raw);
137
+ if (!Number.isNaN(dateMs)) {
138
+ return Math.max(0, dateMs - Date.now());
139
+ }
140
+ return undefined;
141
+ }
142
+ /**
143
+ * Returns true when the HTTP method is safe to retry after a transient failure.
144
+ * POST is excluded to prevent duplicate writes (e.g. duplicate memories/edges
145
+ * after a post-commit 504). recall/reflect/mergeEntities are POST-but-read or
146
+ * write-once, but are deliberately left non-retried for safety and consistency
147
+ * with the Python SDK.
148
+ */
149
+ isIdempotentMethod(method) {
150
+ return ["GET", "HEAD", "OPTIONS", "PUT", "DELETE", "PATCH"].includes(method.toUpperCase());
151
+ }
152
+ /**
153
+ * Execute a fetch with bounded retry-with-backoff for transient failures
154
+ * (5xx, 429). Honors the `Retry-After` header when present, otherwise uses
155
+ * exponential backoff. Non-transient failures and successful responses are
156
+ * returned/raised immediately.
157
+ *
158
+ * Retries are gated on method idempotency: only GET/HEAD/OPTIONS/PUT/DELETE/PATCH
159
+ * are retried. POST is never retried automatically to avoid duplicate writes.
160
+ *
161
+ * This is the single network seam used by every request helper, so retry and
162
+ * header handling stay consistent across the client.
163
+ */
164
+ async executeFetch(url, init, enterpriseFeature) {
165
+ const canRetry = this.isIdempotentMethod(init.method ?? "GET");
166
+ let lastError;
167
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
168
+ const controller = new AbortController();
169
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
170
+ try {
171
+ const response = await this.fetchImpl(url, { ...init, signal: controller.signal });
172
+ clearTimeout(timeoutId);
173
+ if (response.ok) {
174
+ return response;
175
+ }
176
+ // Transient server-side failure: retry if attempts remain AND method is idempotent.
177
+ if (canRetry && attempt < this.maxRetries && this.isRetryableStatus(response.status)) {
178
+ const retryAfter = this.parseRetryAfter(response);
179
+ const delay = retryAfter ?? this.retryBaseDelay * Math.pow(2, attempt);
180
+ await sleep(delay);
181
+ continue;
182
+ }
183
+ // Permanent failure, non-idempotent method, or retries exhausted: surface a typed error.
91
184
  await this.handleError(response, enterpriseFeature);
92
185
  }
93
- if (response.status === 204) {
94
- return undefined;
186
+ catch (error) {
187
+ clearTimeout(timeoutId);
188
+ // Typed errors from handleError are terminal — never retry them.
189
+ if (error instanceof MemoryLayerError)
190
+ throw error;
191
+ // Network/abort error: retry if attempts remain AND method is idempotent.
192
+ lastError = error;
193
+ if (canRetry && attempt < this.maxRetries) {
194
+ await sleep(this.retryBaseDelay * Math.pow(2, attempt));
195
+ continue;
196
+ }
197
+ throw new MemoryLayerError(`Request failed: ${error}`);
95
198
  }
96
- return await response.json();
97
199
  }
98
- catch (error) {
99
- if (error instanceof MemoryLayerError)
100
- throw error;
101
- throw new MemoryLayerError(`Request failed: ${error}`);
200
+ // Unreachable in practice, but satisfies the type checker.
201
+ throw new MemoryLayerError(`Request failed: ${lastError}`);
202
+ }
203
+ async request(method, path, body, enterpriseFeature, authority, responseMode = "json") {
204
+ const headers = this.buildHeaders(authority);
205
+ const url = `${this.baseUrl}${path}`;
206
+ const response = await this.executeFetch(url, {
207
+ method,
208
+ headers,
209
+ body: body ? JSON.stringify(body) : undefined,
210
+ }, enterpriseFeature);
211
+ if (response.status === 204) {
212
+ return undefined;
213
+ }
214
+ // Non-JSON endpoints (e.g. GET /v1/skills/{id}/manifest → text/markdown,
215
+ // GET /v1/skills/{id}/files/{path} → text/x-python etc.) must be read as
216
+ // raw text/bytes; calling response.json() on them throws. Default stays
217
+ // JSON so existing callers are unaffected.
218
+ if (responseMode === "text") {
219
+ return await response.text();
102
220
  }
221
+ if (responseMode === "arraybuffer") {
222
+ return await response.arrayBuffer();
223
+ }
224
+ return await response.json();
103
225
  }
104
226
  async handleError(response, enterpriseFeature) {
105
227
  const body = await response.json().catch(() => ({}));
@@ -117,6 +239,10 @@ export class MemoryLayerClient {
117
239
  case 400:
118
240
  case 422:
119
241
  throw new ValidationError(message, body.details);
242
+ case 429: {
243
+ const retryAfterMs = this.parseRetryAfter(response);
244
+ throw new RateLimitError(message, retryAfterMs !== undefined ? retryAfterMs / 1000 : undefined);
245
+ }
120
246
  case 501:
121
247
  if (enterpriseFeature) {
122
248
  throw new EnterpriseRequiredError(enterpriseFeature);
@@ -138,6 +264,7 @@ export class MemoryLayerClient {
138
264
  tags: options.tags ?? [],
139
265
  metadata: options.metadata ?? {},
140
266
  associations: options.associations ?? [],
267
+ relations: options.relations ?? [],
141
268
  context_id: options.contextId,
142
269
  user_id: options.userId,
143
270
  };
@@ -164,10 +291,19 @@ export class MemoryLayerClient {
164
291
  max_expansion: options.maxExpansion,
165
292
  created_after: options.createdAfter?.toISOString(),
166
293
  created_before: options.createdBefore?.toISOString(),
294
+ offset: options.offset,
295
+ event_after: options.eventAfter?.toISOString(),
296
+ event_before: options.eventBefore?.toISOString(),
297
+ time_order: options.timeOrder,
298
+ include_global: options.includeGlobal,
299
+ include_global_user: options.includeGlobalUser,
167
300
  context: options.conversationContext ?? [],
168
301
  rag_threshold: options.ragThreshold,
169
302
  detail_level: options.detailLevel,
170
303
  user_id: options.userId,
304
+ budget_tokens: options.budgetTokens,
305
+ include_confidence: options.includeConfidence ?? true,
306
+ include_relations: options.includeRelations ?? true,
171
307
  };
172
308
  return this.request("POST", "/v1/memories/recall", body, undefined, options.authority);
173
309
  }
@@ -211,6 +347,54 @@ export class MemoryLayerClient {
211
347
  const response = await this.request("GET", `/v1/memories/${memoryId}/associations?direction=${direction}`);
212
348
  return response.associations;
213
349
  }
350
+ /**
351
+ * Update an association's strength and/or metadata.
352
+ *
353
+ * `memoryId` must be one of the association's endpoints (source or target);
354
+ * otherwise the server returns 404. Re-typing an edge is not supported —
355
+ * delete and recreate instead.
356
+ */
357
+ async updateAssociation(memoryId, associationId, updates) {
358
+ const body = {};
359
+ if (updates.strength !== undefined)
360
+ body.strength = updates.strength;
361
+ if (updates.metadata !== undefined)
362
+ body.metadata = updates.metadata;
363
+ await this.request("PATCH", `/v1/memories/${memoryId}/associations/${associationId}`, body);
364
+ }
365
+ /**
366
+ * Delete an association (graph edge) by ID.
367
+ *
368
+ * `memoryId` must be one of the association's endpoints (source or target);
369
+ * otherwise the server returns 404.
370
+ */
371
+ async deleteAssociation(memoryId, associationId) {
372
+ await this.request("DELETE", `/v1/memories/${memoryId}/associations/${associationId}`);
373
+ }
374
+ /**
375
+ * List/browse memories in the workspace ordered by recency (created_at desc).
376
+ *
377
+ * Unlike {@link recall} this performs no vector search — it is a plain
378
+ * filtered enumeration for browsing. Supports limit/offset pagination and
379
+ * optional type/subtype/tag/context filters.
380
+ */
381
+ async listMemories(options = {}) {
382
+ const params = new URLSearchParams();
383
+ if (options.limit !== undefined)
384
+ params.set("limit", String(options.limit));
385
+ if (options.offset !== undefined)
386
+ params.set("offset", String(options.offset));
387
+ if (options.type)
388
+ params.set("type", String(options.type));
389
+ if (options.subtype)
390
+ params.set("subtype", String(options.subtype));
391
+ if (options.tag)
392
+ params.set("tag", options.tag);
393
+ if (options.contextId)
394
+ params.set("context_id", options.contextId);
395
+ const query = params.toString();
396
+ return this.request("GET", `/v1/memories${query ? `?${query}` : ""}`);
397
+ }
214
398
  // Session operations
215
399
  /**
216
400
  * Create a new session.
@@ -257,6 +441,37 @@ export class MemoryLayerClient {
257
441
  const response = await this.request("GET", `/v1/sessions/${sessionId}`);
258
442
  return response.session;
259
443
  }
444
+ async createCheckpoint(sessionId, input) {
445
+ return this.request("POST", `/v1/sessions/${sessionId}/checkpoints`, {
446
+ transcript_segment: input.transcriptSegment,
447
+ content_hash: input.contentHash,
448
+ idempotency_key: input.idempotencyKey,
449
+ source_kind: input.sourceKind ?? "transcript",
450
+ source_sequence: input.sourceSequence,
451
+ source_boundary: input.sourceBoundary,
452
+ });
453
+ }
454
+ async getCheckpoint(sessionId, checkpointId) {
455
+ return this.request("GET", `/v1/sessions/${sessionId}/checkpoints/${checkpointId}`);
456
+ }
457
+ async getContextPack(sessionId, options = {}) {
458
+ return this.request("POST", `/v1/sessions/${sessionId}/context-pack`, {
459
+ topic: options.topic,
460
+ entity_ids: options.entityIds ?? [],
461
+ entity_names: options.entityNames ?? [],
462
+ budget_tokens: options.budgetTokens ?? 2048,
463
+ section_limits: options.sectionLimits,
464
+ include_directives: options.includeDirectives,
465
+ include_working_memory: options.includeWorkingMemory,
466
+ include_recent_activity: options.includeRecentActivity,
467
+ include_contradictions: options.includeContradictions,
468
+ include_sandbox_summary: options.includeSandboxSummary,
469
+ include_checkpoint_recovery: options.includeCheckpointRecovery,
470
+ });
471
+ }
472
+ async getContextDelta(sessionId, cursor, budgetTokens = 2048) {
473
+ return this.request("POST", `/v1/sessions/${sessionId}/context-delta`, { cursor, budget_tokens: budgetTokens });
474
+ }
260
475
  async deleteSession(sessionId) {
261
476
  await this.request("DELETE", `/v1/sessions/${sessionId}`);
262
477
  }
@@ -314,8 +529,8 @@ export class MemoryLayerClient {
314
529
  return response.briefing;
315
530
  }
316
531
  // Workspace operations
317
- async createWorkspace(name, settings) {
318
- const response = await this.request("POST", "/v1/workspaces", { name, settings: settings ?? {} });
532
+ async createWorkspace(name, settings, tags) {
533
+ const response = await this.request("POST", "/v1/workspaces", { name, settings: settings ?? {}, tags: tags ?? [] });
319
534
  return response.workspace;
320
535
  }
321
536
  async getWorkspace(workspaceId) {
@@ -325,8 +540,16 @@ export class MemoryLayerClient {
325
540
  const response = await this.request("GET", `/v1/workspaces/${id}`);
326
541
  return response.workspace;
327
542
  }
328
- async listWorkspaces() {
329
- const response = await this.request("GET", "/v1/workspaces");
543
+ async listWorkspaces(filter) {
544
+ let path = "/v1/workspaces";
545
+ if (filter?.tags && filter.tags.length > 0) {
546
+ const params = new URLSearchParams();
547
+ for (const tag of filter.tags)
548
+ params.append("tags", tag);
549
+ params.append("match", filter.match ?? "all");
550
+ path += `?${params.toString()}`;
551
+ }
552
+ const response = await this.request("GET", path);
330
553
  return response.workspaces;
331
554
  }
332
555
  async updateWorkspace(workspaceId, updates) {
@@ -345,6 +568,116 @@ export class MemoryLayerClient {
345
568
  const response = await this.request("GET", `/v1/workspaces/${this.workspaceId}/contexts`);
346
569
  return response.contexts;
347
570
  }
571
+ async deleteContext(contextId, workspaceId) {
572
+ const wsId = workspaceId ?? this.workspaceId;
573
+ if (!wsId)
574
+ throw new ValidationError("Workspace ID required");
575
+ await this.request("DELETE", `/v1/workspaces/${wsId}/contexts/${contextId}`);
576
+ }
577
+ // ------------------------------------------------------------------ //
578
+ // Entity Registry operations
579
+ //
580
+ // Gated server-side by MEMORYLAYER_ENTITY_REGISTRY_ENABLED. When the
581
+ // registry is disabled the server returns 501, surfaced here as
582
+ // EnterpriseRequiredError via the `enterpriseFeature` arg.
583
+ // ------------------------------------------------------------------ //
584
+ /** List canonical entities in a workspace (deterministic order by id). */
585
+ async listEntities(options = {}) {
586
+ const params = new URLSearchParams();
587
+ const wsId = options.workspaceId ?? this.workspaceId;
588
+ if (wsId)
589
+ params.set("workspace_id", wsId);
590
+ if (options.status)
591
+ params.set("status", options.status);
592
+ if (options.limit !== undefined)
593
+ params.set("limit", String(options.limit));
594
+ const query = params.toString();
595
+ return this.request("GET", `/v1/entities${query ? `?${query}` : ""}`, undefined, "Entity registry");
596
+ }
597
+ /** Get a single canonical entity by id. */
598
+ async getEntity(entityId, workspaceId) {
599
+ const params = new URLSearchParams();
600
+ const wsId = workspaceId ?? this.workspaceId;
601
+ if (wsId)
602
+ params.set("workspace_id", wsId);
603
+ const query = params.toString();
604
+ const response = await this.request("GET", `/v1/entities/${entityId}${query ? `?${query}` : ""}`, undefined, "Entity registry");
605
+ return response.entity;
606
+ }
607
+ /**
608
+ * Resolve a surface name/alias to an existing canonical entity (never
609
+ * creates). Throws NotFoundError if no entity matches.
610
+ */
611
+ async resolveEntity(name, options = {}) {
612
+ const params = new URLSearchParams();
613
+ params.set("name", name);
614
+ if (options.entityType)
615
+ params.set("entity_type", String(options.entityType));
616
+ const wsId = options.workspaceId ?? this.workspaceId;
617
+ if (wsId)
618
+ params.set("workspace_id", wsId);
619
+ return this.request("GET", `/v1/entities/resolve?${params.toString()}`, undefined, "Entity registry");
620
+ }
621
+ /**
622
+ * Merge `sourceId` into `targetId`; returns the surviving target entity.
623
+ * Requires the entity registry to be enabled (501 -> EnterpriseRequiredError).
624
+ */
625
+ async mergeEntities(options) {
626
+ const params = new URLSearchParams();
627
+ const wsId = options.workspaceId ?? this.workspaceId;
628
+ if (wsId)
629
+ params.set("workspace_id", wsId);
630
+ const query = params.toString();
631
+ const response = await this.request("POST", `/v1/entities/merge${query ? `?${query}` : ""}`, {
632
+ source_id: options.sourceId,
633
+ target_id: options.targetId,
634
+ reason: options.reason,
635
+ }, "Entity registry");
636
+ return response.entity;
637
+ }
638
+ // ------------------------------------------------------------------ //
639
+ // API Token operations (admin scope; gRPC-backed via Aether)
640
+ // ------------------------------------------------------------------ //
641
+ /** List API tokens. Set `includeRevoked` to also return revoked tokens. */
642
+ async listTokens(includeRevoked = false) {
643
+ const params = new URLSearchParams();
644
+ if (includeRevoked)
645
+ params.set("include_revoked", "true");
646
+ const query = params.toString();
647
+ const response = await this.request("GET", `/v1/tokens${query ? `?${query}` : ""}`);
648
+ return response.tokens;
649
+ }
650
+ /**
651
+ * Create a new API token. The returned object includes the plaintext
652
+ * `token` value, which is only ever available at creation time.
653
+ */
654
+ async createToken(options) {
655
+ const body = { name: options.name };
656
+ if (options.principalType !== undefined)
657
+ body.principal_type = options.principalType;
658
+ if (options.workspacePatterns !== undefined)
659
+ body.workspace_patterns = options.workspacePatterns;
660
+ if (options.scopes !== undefined)
661
+ body.scopes = options.scopes;
662
+ if (options.expiresInDays !== undefined)
663
+ body.expires_in_days = options.expiresInDays;
664
+ return this.request("POST", "/v1/tokens", body);
665
+ }
666
+ /** Get details for a single API token. */
667
+ async getToken(tokenId) {
668
+ return this.request("GET", `/v1/tokens/${tokenId}`);
669
+ }
670
+ /** Delete an API token. */
671
+ async deleteToken(tokenId) {
672
+ await this.request("DELETE", `/v1/tokens/${tokenId}`);
673
+ }
674
+ /**
675
+ * Revoke an API token. Revoked tokens are invalidated immediately but remain
676
+ * visible in listings (with `revoked=true`).
677
+ */
678
+ async revokeToken(tokenId) {
679
+ await this.request("POST", `/v1/tokens/${tokenId}/revoke`);
680
+ }
348
681
  // Batch operations
349
682
  async batchMemories(operations) {
350
683
  return this.request("POST", "/v1/memories/batch", { operations });
@@ -401,20 +734,11 @@ export class MemoryLayerClient {
401
734
  const query = params.toString() ? `?${params.toString()}` : '';
402
735
  // Fetch NDJSON response
403
736
  const url = `${this.baseUrl}/v1/workspaces/${id}/export${query}`;
404
- const headers = {};
405
- if (this.apiKey) {
406
- headers["Authorization"] = `Bearer ${this.apiKey}`;
407
- }
408
- if (this.sessionId) {
409
- headers["X-Session-ID"] = this.sessionId;
410
- }
411
- if (this.workspaceId) {
412
- headers["X-Workspace-ID"] = this.workspaceId;
413
- }
737
+ const headers = this.buildHeaders(undefined, false);
414
738
  const controller = new AbortController();
415
739
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
416
740
  try {
417
- const response = await fetch(url, {
741
+ const response = await this.fetchImpl(url, {
418
742
  method: "GET",
419
743
  headers,
420
744
  signal: controller.signal,
@@ -474,20 +798,11 @@ export class MemoryLayerClient {
474
798
  }
475
799
  const query = params.toString() ? `?${params.toString()}` : '';
476
800
  const url = `${this.baseUrl}/v1/workspaces/${id}/export${query}`;
477
- const headers = {};
478
- if (this.apiKey) {
479
- headers["Authorization"] = `Bearer ${this.apiKey}`;
480
- }
481
- if (this.sessionId) {
482
- headers["X-Session-ID"] = this.sessionId;
483
- }
484
- if (this.workspaceId) {
485
- headers["X-Workspace-ID"] = this.workspaceId;
486
- }
801
+ const headers = this.buildHeaders(undefined, false);
487
802
  const controller = new AbortController();
488
803
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
489
804
  try {
490
- const response = await fetch(url, {
805
+ const response = await this.fetchImpl(url, {
491
806
  method: "GET",
492
807
  headers,
493
808
  signal: controller.signal,
@@ -514,22 +829,12 @@ export class MemoryLayerClient {
514
829
  }
515
830
  async importWorkspaceStream(workspaceId, ndjsonBody) {
516
831
  const url = `${this.baseUrl}/v1/workspaces/${workspaceId}/import`;
517
- const headers = {
518
- "Content-Type": "application/x-ndjson",
519
- };
520
- if (this.apiKey) {
521
- headers["Authorization"] = `Bearer ${this.apiKey}`;
522
- }
523
- if (this.sessionId) {
524
- headers["X-Session-ID"] = this.sessionId;
525
- }
526
- if (this.workspaceId) {
527
- headers["X-Workspace-ID"] = this.workspaceId;
528
- }
832
+ const headers = this.buildHeaders(undefined, false);
833
+ headers["Content-Type"] = "application/x-ndjson";
529
834
  const controller = new AbortController();
530
835
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
531
836
  try {
532
- const response = await fetch(url, {
837
+ const response = await this.fetchImpl(url, {
533
838
  method: "POST",
534
839
  headers,
535
840
  body: ndjsonBody,
@@ -640,18 +945,12 @@ export class MemoryLayerClient {
640
945
  formData.append("importance", String(options.importance));
641
946
  if (options.retainOriginal !== undefined)
642
947
  formData.append("retain_original", String(options.retainOriginal));
643
- const headers = {};
644
- if (this.apiKey)
645
- headers["Authorization"] = `Bearer ${this.apiKey}`;
646
- if (this.sessionId)
647
- headers["X-Session-ID"] = this.sessionId;
648
- if (this.workspaceId)
649
- headers["X-Workspace-ID"] = this.workspaceId;
948
+ const headers = this.buildHeaders(undefined, false);
650
949
  const url = `${this.baseUrl}/v1/documents`;
651
950
  const controller = new AbortController();
652
951
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
653
952
  try {
654
- const response = await fetch(url, {
953
+ const response = await this.fetchImpl(url, {
655
954
  method: "POST",
656
955
  headers,
657
956
  body: formData,
@@ -719,18 +1018,12 @@ export class MemoryLayerClient {
719
1018
  * Get a page image as a Blob.
720
1019
  */
721
1020
  async getPageImage(documentId, pageId) {
722
- const headers = {};
723
- if (this.apiKey)
724
- headers["Authorization"] = `Bearer ${this.apiKey}`;
725
- if (this.sessionId)
726
- headers["X-Session-ID"] = this.sessionId;
727
- if (this.workspaceId)
728
- headers["X-Workspace-ID"] = this.workspaceId;
1021
+ const headers = this.buildHeaders(undefined, false);
729
1022
  const url = `${this.baseUrl}/v1/documents/${documentId}/pages/${pageId}/image`;
730
1023
  const controller = new AbortController();
731
1024
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
732
1025
  try {
733
- const response = await fetch(url, { method: "GET", headers, signal: controller.signal });
1026
+ const response = await this.fetchImpl(url, { method: "GET", headers, signal: controller.signal });
734
1027
  clearTimeout(timeoutId);
735
1028
  if (!response.ok) {
736
1029
  await this.handleError(response, "Document page images");
@@ -848,6 +1141,50 @@ export class MemoryLayerClient {
848
1141
  const query = params.toString();
849
1142
  await this.request("DELETE", `/v1/threads/${threadId}${query ? `?${query}` : ""}`);
850
1143
  }
1144
+ /**
1145
+ * List chat threads owned by a user across all workspaces.
1146
+ *
1147
+ * Unlike {@link listThreads}, this is keyed on (tenant, user, ownership) and
1148
+ * does NOT require a workspace filter — used for user-session-scoped views.
1149
+ */
1150
+ async listUserThreads(userId, options = {}) {
1151
+ const params = new URLSearchParams();
1152
+ if (options.ownership)
1153
+ params.set("ownership", options.ownership);
1154
+ if (options.scopeFilter)
1155
+ params.set("scope_filter", options.scopeFilter);
1156
+ if (options.limit !== undefined)
1157
+ params.set("limit", String(options.limit));
1158
+ if (options.offset !== undefined)
1159
+ params.set("offset", String(options.offset));
1160
+ const query = params.toString();
1161
+ const response = await this.request("GET", `/v1/threads/user/${userId}${query ? `?${query}` : ""}`);
1162
+ return response.threads;
1163
+ }
1164
+ /** Update a thread (e.g. rename or change metadata). */
1165
+ async updateThread(threadId, updates) {
1166
+ const params = new URLSearchParams();
1167
+ const wsId = updates.workspaceId ?? this.workspaceId;
1168
+ if (wsId)
1169
+ params.set("workspace_id", wsId);
1170
+ const query = params.toString();
1171
+ const body = {};
1172
+ if (updates.title !== undefined)
1173
+ body.title = updates.title;
1174
+ if (updates.metadata !== undefined)
1175
+ body.metadata = updates.metadata;
1176
+ const response = await this.request("PUT", `/v1/threads/${threadId}${query ? `?${query}` : ""}`, body);
1177
+ return response.thread;
1178
+ }
1179
+ /** Delete a single message from a thread. */
1180
+ async deleteMessage(threadId, messageId, workspaceId) {
1181
+ const params = new URLSearchParams();
1182
+ const wsId = workspaceId ?? this.workspaceId;
1183
+ if (wsId)
1184
+ params.set("workspace_id", wsId);
1185
+ const query = params.toString();
1186
+ await this.request("DELETE", `/v1/threads/${threadId}/messages/${messageId}${query ? `?${query}` : ""}`);
1187
+ }
851
1188
  async appendMessages(threadId, messages, workspaceId) {
852
1189
  const params = new URLSearchParams();
853
1190
  const wsId = workspaceId ?? this.workspaceId;
@@ -904,18 +1241,12 @@ export class MemoryLayerClient {
904
1241
  formData.append("detect_time_series", String(options.detectTimeSeries));
905
1242
  if (options.generateSummaries !== undefined)
906
1243
  formData.append("generate_summaries", String(options.generateSummaries));
907
- const headers = {};
908
- if (this.apiKey)
909
- headers["Authorization"] = `Bearer ${this.apiKey}`;
910
- if (this.sessionId)
911
- headers["X-Session-ID"] = this.sessionId;
912
- if (this.workspaceId)
913
- headers["X-Workspace-ID"] = this.workspaceId;
1244
+ const headers = this.buildHeaders(undefined, false);
914
1245
  const url = `${this.baseUrl}/v1/datasets`;
915
1246
  const controller = new AbortController();
916
1247
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
917
1248
  try {
918
- const response = await fetch(url, {
1249
+ const response = await this.fetchImpl(url, {
919
1250
  method: "POST",
920
1251
  headers,
921
1252
  body: formData,
@@ -1012,13 +1343,21 @@ export class MemoryLayerClient {
1012
1343
  await this.request("POST", `/v1/datasets/jobs/${jobId}/cancel`, undefined, "Dataset processing jobs");
1013
1344
  }
1014
1345
  /** Used internally by SkillsNamespace to make requests with OBO authority. */
1015
- async _skillsRequest(method, path, body, authority) {
1016
- return this.request(method, path, body, undefined, authority);
1346
+ async _skillsRequest(method, path, body, authority, responseMode = "json") {
1347
+ return this.request(method, path, body, undefined, authority, responseMode);
1017
1348
  }
1018
1349
  /** Used internally by McpServersNamespace to make requests with OBO authority. */
1019
1350
  async _mcpServersRequest(method, path, body, authority) {
1020
1351
  return this.request(method, path, body, undefined, authority);
1021
1352
  }
1353
+ /** Used internally by KnowledgebaseNamespace to make requests with OBO authority. */
1354
+ async _kbRequest(method, path, body, authority, responseMode = "json") {
1355
+ return this.request(method, path, body, undefined, authority, responseMode);
1356
+ }
1357
+ /** Used internally by RpgNamespace to make requests with OBO authority. */
1358
+ async _rpgRequest(method, path, body, authority) {
1359
+ return this.request(method, path, body, undefined, authority);
1360
+ }
1022
1361
  }
1023
1362
  /**
1024
1363
  * Lightweight OBO proxy returned by `client.actingFor()`.