@tangle-network/agent-app 0.60.210 → 0.60.212

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.
@@ -29,15 +29,22 @@ export declare function mintTurnStreamToken(channelName: string, secret: string,
29
29
  export declare function verifyTurnStreamToken(channelName: string, token: string, secret: string, now?: () => number): Promise<boolean>;
30
30
  /** Define the scope level for acquiring a turn lock within thread or workspace contexts */
31
31
  export type TurnLockScope = 'thread' | 'workspace';
32
- /** Channel for a thread-scoped lock. */
32
+ /** Channel for a thread's viewer signals. */
33
33
  export declare function threadChannelKey(workspaceId: string, threadId: string): string;
34
34
  /** Channel for workspace signals and the workspace-scoped lock. */
35
35
  export declare function workspaceChannelKey(workspaceId: string): string;
36
- /** The channel a lock lives on: workspace-scope locks serialize every thread
37
- * in the workspace (one shared sandbox), thread-scope locks serialize one
38
- * thread (router lane). Same keying as the reference consumer, so a product
39
- * swapping its fork for this package contends on identical instances. */
40
- export declare function turnLockChannelKey(workspaceId: string, threadId: string, scope: TurnLockScope): string;
36
+ /** The channel a lock lives on: the workspace channel, for both scopes.
37
+ * Workspace-scope locks serialize every thread in the workspace (one shared
38
+ * sandbox) and thread-scope locks serialize one thread (router lane); they
39
+ * stay independent because each has its own storage key
40
+ * ({@link turnLockStorageKey}). A thread lock on its own channel created a
41
+ * new Durable Object on every thread's first turn, 0.8-1.9 s inside Creative's
42
+ * admission on staging (2026-10-20) against an object that already exists. */
43
+ export declare function turnLockChannelKey(workspaceId: string, _threadId: string, _scope: TurnLockScope): string;
44
+ /** The storage key a lock lives under inside its channel's instance: the
45
+ * workspace-scope lock under `TURN_STREAM_STORAGE_KEYS.lock`, each thread's
46
+ * lock under `TURN_STREAM_STORAGE_KEYS.threadLockPrefix` + its thread id. */
47
+ export declare function turnLockStorageKey(scope: TurnLockScope, threadId: string): string;
41
48
  /** Channel for the optional Durable Object turn-event backend. */
42
49
  export declare function turnStorageChannelKey(turnId: string): string;
43
50
  /** Running-turn index for the optional Durable Object turn-event backend. */
@@ -147,6 +154,7 @@ export declare const TURN_STREAM_PATHS: {
147
154
  * a product extending the DO must not collide with these. */
148
155
  export declare const TURN_STREAM_STORAGE_KEYS: {
149
156
  readonly lock: 'chatTurnLock';
157
+ readonly threadLockPrefix: 'chatTurnLock:thread:';
150
158
  readonly activeThreads: 'activeThreads';
151
159
  readonly turnStatus: 'turnStatus';
152
160
  readonly turnScope: 'turnScopeIndex';
@@ -120,11 +120,16 @@ export declare class TurnStreamDO {
120
120
  private handleBroadcast;
121
121
  private loadActiveThreads;
122
122
  private persistActiveThreads;
123
- protected loadActiveLock(now?: number): Promise<DurableTurnLock | null>;
123
+ /** The live lock under `key` (default: the workspace-scope lock); an
124
+ * expired one is deleted and reads as none. */
125
+ protected loadActiveLock(now?: number, key?: string): Promise<DurableTurnLock | null>;
124
126
  private releaseActiveLock;
125
127
  /** Park a release on the lock itself; {@link completeDeferredLockRelease}
126
128
  * finishes it once the product's deferred condition settles. */
127
129
  private deferLockRelease;
130
+ /** Delete expired thread locks, so a thread whose release never arrived
131
+ * leaves no row behind once its lock's TTL passes. */
132
+ private pruneExpiredThreadLocks;
128
133
  private handleLockAcquire;
129
134
  private handleLockRelease;
130
135
  private handleLockReleaseInterrupted;
@@ -37,8 +37,11 @@ function threadChannelKey(workspaceId, threadId) {
37
37
  function workspaceChannelKey(workspaceId) {
38
38
  return workspaceId;
39
39
  }
40
- function turnLockChannelKey(workspaceId, threadId, scope) {
41
- return scope === "workspace" ? workspaceChannelKey(workspaceId) : threadChannelKey(workspaceId, threadId);
40
+ function turnLockChannelKey(workspaceId, _threadId, _scope) {
41
+ return workspaceChannelKey(workspaceId);
42
+ }
43
+ function turnLockStorageKey(scope, threadId) {
44
+ return scope === "workspace" ? TURN_STREAM_STORAGE_KEYS.lock : `${TURN_STREAM_STORAGE_KEYS.threadLockPrefix}${threadId}`;
42
45
  }
43
46
  function turnStorageChannelKey(turnId) {
44
47
  return `turn:${turnId}`;
@@ -107,6 +110,7 @@ var TURN_STREAM_PATHS = {
107
110
  };
108
111
  var TURN_STREAM_STORAGE_KEYS = {
109
112
  lock: "chatTurnLock",
113
+ threadLockPrefix: "chatTurnLock:thread:",
110
114
  activeThreads: "activeThreads",
111
115
  turnStatus: "turnStatus",
112
116
  turnScope: "turnScopeIndex",
@@ -130,6 +134,10 @@ async function jsonBody(request) {
130
134
  const body = await request.json().catch(() => null);
131
135
  return body && typeof body === "object" && !Array.isArray(body) ? body : null;
132
136
  }
137
+ function lockStorageKeyOf(body) {
138
+ const threadId = stringValue(body?.threadId);
139
+ return body?.scope === "thread" && threadId ? turnLockStorageKey("thread", threadId) : TURN_STREAM_STORAGE_KEYS.lock;
140
+ }
133
141
  var MAX_SCOPE_TURNS = 100;
134
142
  var TurnStreamDO = class {
135
143
  state;
@@ -195,7 +203,7 @@ var TurnStreamDO = class {
195
203
  case TURN_STREAM_PATHS.lockAcquire:
196
204
  return this.handleLockAcquire(request);
197
205
  case TURN_STREAM_PATHS.lockPeek:
198
- return jsonResponse({ active: await this.loadActiveLock() });
206
+ return jsonResponse({ active: await this.loadActiveLock(Date.now(), lockStorageKeyOf(await jsonBody(request))) });
199
207
  case TURN_STREAM_PATHS.lockRelease:
200
208
  return this.handleLockRelease(request);
201
209
  case TURN_STREAM_PATHS.lockReleaseInterrupted:
@@ -236,10 +244,14 @@ var TurnStreamDO = class {
236
244
  /** Complete a release parked by {@link shouldDeferLockRelease}. A subclass
237
245
  * calls this when its deferred condition settles. */
238
246
  async completeDeferredLockRelease(executionId) {
239
- const active = await this.loadActiveLock();
240
- if (!active?.releasePending || active.executionId !== executionId) return false;
241
- await this.state.storage.delete(TURN_STREAM_STORAGE_KEYS.lock);
242
- return true;
247
+ const threadLocks = await this.state.storage.list({ prefix: TURN_STREAM_STORAGE_KEYS.threadLockPrefix });
248
+ for (const key of [TURN_STREAM_STORAGE_KEYS.lock, ...threadLocks.keys()]) {
249
+ const active = await this.loadActiveLock(Date.now(), key);
250
+ if (!active?.releasePending || active.executionId !== executionId) continue;
251
+ await this.state.storage.delete(key);
252
+ return true;
253
+ }
254
+ return false;
243
255
  }
244
256
  /** Extra product state replayed to a socket during its `sync`, after the
245
257
  * base replay for its scope (e.g. an in-flight persistence status card).
@@ -355,27 +367,37 @@ var TurnStreamDO = class {
355
367
  await this.state.storage.put(TURN_STREAM_STORAGE_KEYS.activeThreads, Object.fromEntries(map));
356
368
  }
357
369
  // ── chat-turn lock ────────────────────────────────────────────────────────
358
- async loadActiveLock(now = Date.now()) {
359
- const stored = await this.state.storage.get(TURN_STREAM_STORAGE_KEYS.lock);
370
+ /** The live lock under `key` (default: the workspace-scope lock); an
371
+ * expired one is deleted and reads as none. */
372
+ async loadActiveLock(now = Date.now(), key = TURN_STREAM_STORAGE_KEYS.lock) {
373
+ const stored = await this.state.storage.get(key);
360
374
  const lock = activeTurnLock(stored, now);
361
- if (stored && !lock) await this.state.storage.delete(TURN_STREAM_STORAGE_KEYS.lock);
375
+ if (stored && !lock) await this.state.storage.delete(key);
362
376
  return lock;
363
377
  }
364
- async releaseActiveLock(input) {
365
- const active = await this.loadActiveLock();
378
+ async releaseActiveLock(key, input) {
379
+ const active = await this.loadActiveLock(Date.now(), key);
366
380
  if (!active || !turnLockMatchesRelease(active, input)) return false;
367
- await this.state.storage.delete(TURN_STREAM_STORAGE_KEYS.lock);
381
+ await this.state.storage.delete(key);
368
382
  return true;
369
383
  }
370
384
  /** Park a release on the lock itself; {@link completeDeferredLockRelease}
371
385
  * finishes it once the product's deferred condition settles. */
372
- async deferLockRelease(input) {
373
- const active = await this.loadActiveLock();
386
+ async deferLockRelease(key, input) {
387
+ const active = await this.loadActiveLock(Date.now(), key);
374
388
  if (!active || !turnLockMatchesRelease(active, input)) return false;
375
389
  if (active.releasePending) return true;
376
- await this.state.storage.put(TURN_STREAM_STORAGE_KEYS.lock, { ...active, releasePending: true });
390
+ await this.state.storage.put(key, { ...active, releasePending: true });
377
391
  return true;
378
392
  }
393
+ /** Delete expired thread locks, so a thread whose release never arrived
394
+ * leaves no row behind once its lock's TTL passes. */
395
+ async pruneExpiredThreadLocks(now) {
396
+ const threadLocks = await this.state.storage.list({ prefix: TURN_STREAM_STORAGE_KEYS.threadLockPrefix });
397
+ for (const [key, stored] of threadLocks) {
398
+ if (!activeTurnLock(stored, now)) await this.state.storage.delete(key);
399
+ }
400
+ }
379
401
  async handleLockAcquire(request) {
380
402
  const body = await jsonBody(request);
381
403
  const workspaceId = stringValue(body?.workspaceId);
@@ -387,16 +409,19 @@ var TurnStreamDO = class {
387
409
  if (!workspaceId || !threadId || !executionId || !lockId || !scope) {
388
410
  return jsonResponse({ error: "Missing workspaceId, threadId, executionId, lockId, or scope" }, 400);
389
411
  }
390
- const active = await this.loadActiveLock();
412
+ const key = turnLockStorageKey(scope, threadId);
413
+ const now = Date.now();
414
+ const active = await this.loadActiveLock(now, key);
391
415
  if (active) {
392
416
  return jsonResponse({ acquired: false, active }, 409);
393
417
  }
418
+ if (scope === "thread") await this.pruneExpiredThreadLocks(now);
394
419
  const lock = createTurnLock(
395
420
  { workspaceId, threadId, scope, executionId, lockId, ...turnId ? { turnId } : {} },
396
- Date.now(),
421
+ now,
397
422
  this.options.lockTtlMs ?? TURN_LOCK_TTL_MS
398
423
  );
399
- await this.state.storage.put(TURN_STREAM_STORAGE_KEYS.lock, lock);
424
+ await this.state.storage.put(key, lock);
400
425
  return jsonResponse({ acquired: true, lock });
401
426
  }
402
427
  async handleLockRelease(request) {
@@ -406,11 +431,12 @@ var TurnStreamDO = class {
406
431
  if (!executionId || !lockId) {
407
432
  return jsonResponse({ error: "Missing executionId or lockId" }, 400);
408
433
  }
434
+ const key = lockStorageKeyOf(body);
409
435
  if (await this.shouldDeferLockRelease(executionId)) {
410
- const deferred = await this.deferLockRelease({ executionId, lockId });
436
+ const deferred = await this.deferLockRelease(key, { executionId, lockId });
411
437
  return jsonResponse({ released: false, deferred });
412
438
  }
413
- const released = await this.releaseActiveLock({ executionId, lockId });
439
+ const released = await this.releaseActiveLock(key, { executionId, lockId });
414
440
  return jsonResponse({ released });
415
441
  }
416
442
  async handleLockReleaseInterrupted(request) {
@@ -421,7 +447,8 @@ var TurnStreamDO = class {
421
447
  return jsonResponse({ error: "Missing threadId or interruptedAt" }, 400);
422
448
  }
423
449
  const turnId = stringValue(body?.turnId) ?? void 0;
424
- const active = await this.loadActiveLock();
450
+ const key = lockStorageKeyOf(body);
451
+ const active = await this.loadActiveLock(Date.now(), key);
425
452
  if (!active) return jsonResponse({ released: false });
426
453
  if (await this.shouldDeferLockRelease(active.executionId)) {
427
454
  return jsonResponse({ released: false });
@@ -429,7 +456,7 @@ var TurnStreamDO = class {
429
456
  if (!interruptedReleaseApplies(active, { threadId, interruptedAt, ...turnId ? { turnId } : {} })) {
430
457
  return jsonResponse({ released: false });
431
458
  }
432
- await this.state.storage.delete(TURN_STREAM_STORAGE_KEYS.lock);
459
+ await this.state.storage.delete(key);
433
460
  return jsonResponse({ released: true });
434
461
  }
435
462
  // ── durable turn-event storage (TurnEventStore backing) ───────────────────
@@ -594,7 +621,10 @@ async function acquireDurableTurnLock(namespace, input, auth) {
594
621
  }
595
622
  async function peekDurableTurnLock(namespace, input, auth) {
596
623
  const key = turnLockChannelKey(input.workspaceId, input.threadId, input.scope);
597
- const result = await postJson(namespace, key, TURN_STREAM_PATHS.lockPeek, {}, auth);
624
+ const result = await postJson(namespace, key, TURN_STREAM_PATHS.lockPeek, {
625
+ scope: input.scope,
626
+ threadId: input.threadId
627
+ }, auth);
598
628
  if (result.status !== 200) throw new Error(`turn-stream lock peek failed with status ${result.status}`);
599
629
  return result.body.active ?? null;
600
630
  }
@@ -607,6 +637,7 @@ async function releaseInterruptedDurableTurnLock(namespace, input, auth) {
607
637
  for (const scope of scopes) {
608
638
  const key = turnLockChannelKey(input.workspaceId, input.threadId, scope);
609
639
  const result = await postJsonOk(namespace, key, TURN_STREAM_PATHS.lockReleaseInterrupted, {
640
+ scope,
610
641
  threadId: input.threadId,
611
642
  interruptedAt: input.interruptedAt,
612
643
  ...input.turnId ? { turnId: input.turnId } : {}
@@ -649,8 +680,8 @@ function defaultRefusalResponse(active, diagnostics) {
649
680
  }
650
681
  function createDurableTurnLock(options) {
651
682
  return {
652
- // The thread channel is the one a thread's first turn creates; the
653
- // workspace channel is shared by every thread and its broadcasts keep it up.
683
+ // Every lock lives on the workspace channel, so this wakes that object
684
+ // (evicted after idle) before the turn's acquire reaches it.
654
685
  async warm(target) {
655
686
  await peekDurableTurnLock(options.namespace, { workspaceId: target.tenantId, threadId: target.threadId, scope: "thread" }, options.auth);
656
687
  },
@@ -863,6 +894,7 @@ export {
863
894
  turnEventStorageKey,
864
895
  turnLockChannelKey,
865
896
  turnLockMatchesRelease,
897
+ turnLockStorageKey,
866
898
  turnStorageChannelKey,
867
899
  verifyTurnStreamToken,
868
900
  workspaceChannelKey