@dudousxd/nestjs-agent 1.36.1 → 1.37.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/index.d.cts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { DynamicModule, Type, OnApplicationBootstrap, OnModuleInit, ConflictException, OnModuleDestroy } from '@nestjs/common';
2
- import { b as AgentModuleOptions, c as AgentModuleAsyncOptions, d as AgentProtocolAdapter, A as AgentDepsFactory, e as AgentSkillsOptions, C as ChatQueueService } from './chat-queue.service-CF2fcJDx.cjs';
3
- export { f as AGENT_TOOL_BRAND, g as AgentAttachmentsOptions, h as AgentDeps, i as AgentEngine, j as AgentMemoryOptions, a as AgentSurface, F as FunctionalTool, L as LedgerQuotaProvider, Q as QueuePlan, k as QueueSettleOutcome, l as QueuedTurn, m as QueuedTurnStarter, n as QuotaLimits, o as QuotaWindowLimits, p as defineTool, q as delegateToolName, r as provideAgentTool, s as provideAgentTools, u as utcDay } from './chat-queue.service-CF2fcJDx.cjs';
2
+ import { b as AgentModuleOptions, c as AgentModuleAsyncOptions, d as AgentProtocolAdapter, A as AgentDepsFactory, e as AgentSkillsOptions, C as ChatQueueService } from './chat-queue.service-fopUN313.cjs';
3
+ export { f as AGENT_TOOL_BRAND, g as AgentAttachmentsOptions, h as AgentDeps, i as AgentEngine, j as AgentMemoryOptions, a as AgentSurface, F as FunctionalTool, L as LedgerQuotaProvider, Q as QueuePlan, k as QueueSettleOutcome, l as QueuedTurn, m as QueuedTurnStarter, n as QuotaLimits, o as QuotaWindowLimits, p as defineTool, q as delegateToolName, r as provideAgentTool, s as provideAgentTools, u as utcDay } from './chat-queue.service-fopUN313.cjs';
4
4
  import * as _dudousxd_nestjs_agent_core from '@dudousxd/nestjs-agent-core';
5
- import { ActorResolver, AttachmentStagingStore, AgentApprovalPort, Actor, AgentStore, StagedAttachment, MessageAttachment, ModelCatalog, AgentClientConfig, ToolSpec, ToolPresentation, AgentHistoryWindow, AgentIntake, Persona, SkillContext, ToolRegistry, AgentRegistry, PromptContributor, SkillsConfig, SkillCatalogEntry, MessageFeedbackValue, MessageFeedback, ModelCatalogView, ToolCatalogEntry, MemoryConfig, MemoryDigestEntry, SkillProvider, AgentRunner, AgentRunInput, AgentRunStartOptions, HumanReply, ChatQueueState, TokenStreamSink, SinkWriter, ActionProposalOutcome, ActionProposal } from '@dudousxd/nestjs-agent-core';
5
+ import { ActorResolver, AttachmentStagingStore, AgentApprovalPort, Actor, AgentStore, StagedAttachment, MessageAttachment, ModelCatalog, AgentClientConfig, ToolSpec, ToolPresentation, AgentHistoryWindow, AgentIntake, Persona, SkillContext, ToolRegistry, AgentRegistry, PromptContributor, SkillsConfig, SkillCatalogEntry, MessageFeedbackValue, MessageFeedback, ModelCatalogView, ToolCatalogEntry, MemoryConfig, MemoryDigestEntry, SkillProvider, AgentRunner, AgentRunInput, AgentRunStartOptions, HumanReply, ChatQueueState, TokenStreamSink, SinkWriter, SubscribeOptions, ActionProposalOutcome, ActionProposal } from '@dudousxd/nestjs-agent-core';
6
6
  export * from '@dudousxd/nestjs-agent-core';
7
7
  export { FunctionalToolDefinition } from '@dudousxd/nestjs-agent-core';
8
8
  import { A2uiOptions } from '@dudousxd/nestjs-agent-core/a2ui';
9
9
  import { Request, Response } from 'express';
10
- import { A as AgentService, a as ActionProposalService } from './index-NF93YDF4.cjs';
11
- export { b as A2uiAdapterOptions, c as A2uiRunHandler, d as AttachmentLimits, C as ChatParams, e as ChatSendMode, f as ChatSendResult, D as DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES, g as DEFAULT_MAX_ATTACHMENT_BYTES, M as MAX_ATTACHMENTS_PER_MESSAGE, N as NO_USER_MESSAGE_CODE, Q as QueuedSend, T as ThreadDefaultAgentReader, h as ThreadModelReader, i as a2uiAdapter, j as attachmentLimits } from './index-NF93YDF4.cjs';
10
+ import { A as AgentService, a as ActionProposalService } from './index-4caNqEQ-.cjs';
11
+ export { b as A2uiAdapterOptions, c as A2uiRunHandler, d as AttachmentLimits, C as ChatParams, e as ChatSendMode, f as ChatSendResult, D as DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES, g as DEFAULT_MAX_ATTACHMENT_BYTES, F as ForkThreadParams, M as MAX_ATTACHMENTS_PER_MESSAGE, N as NO_USER_MESSAGE_CODE, Q as QueuedSend, T as ThreadDefaultAgentReader, h as ThreadModelReader, i as a2uiAdapter, j as attachmentLimits } from './index-4caNqEQ-.cjs';
12
12
  import { AgentGenui } from './genui/index.cjs';
13
13
  import { StandardSchemaV1 } from '@standard-schema/spec';
14
14
  import { DiscoveryService, MetadataScanner } from '@nestjs/core';
@@ -870,7 +870,8 @@ declare const AGENT_SERVICE: unique symbol;
870
870
 
871
871
  /**
872
872
  * The default `TokenStreamSink`: buffers chunks per run in-process so a reconnecting subscriber
873
- * replays everything so far, then follows live.
873
+ * replays everything so far, then follows live. A run's buffer is dropped {@link ENDED_GRACE_MS}
874
+ * after it ends, or {@link LIVE_TTL_MS} after its last write if it never does.
874
875
  *
875
876
  * Single-replica only. Because the buffer lives in one process's memory, a resume/reconnect that
876
877
  * lands on a different replica than the one running the turn finds an empty buffer — the stream
@@ -879,10 +880,21 @@ declare const AGENT_SERVICE: unique symbol;
879
880
  */
880
881
  declare class InProcessTokenStreamSink implements TokenStreamSink {
881
882
  private readonly runs;
883
+ /** Subscribers of a run nothing has opened yet, woken by its `open()`. */
884
+ private readonly waiting;
882
885
  private buffer;
886
+ private expireAfter;
887
+ /** Forget the run, and let anyone still following it finish as if it had ended. */
888
+ private drop;
883
889
  private wake;
890
+ private finish;
891
+ /**
892
+ * The run's writer. Each call finds the run's buffer afresh, so a run that went quiet past the
893
+ * live TTL (parked on a person) and then writes again starts a new buffer its next subscriber
894
+ * follows, rather than writing into one nothing reads.
895
+ */
884
896
  open(runId: string): SinkWriter;
885
- subscribe(runId: string): AsyncIterable<Uint8Array>;
897
+ subscribe(runId: string, options?: SubscribeOptions): AsyncIterable<Uint8Array>;
886
898
  close(runId: string): void;
887
899
  }
888
900
 
package/dist/index.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { DynamicModule, Type, OnApplicationBootstrap, OnModuleInit, ConflictException, OnModuleDestroy } from '@nestjs/common';
2
- import { b as AgentModuleOptions, c as AgentModuleAsyncOptions, d as AgentProtocolAdapter, A as AgentDepsFactory, e as AgentSkillsOptions, C as ChatQueueService } from './chat-queue.service-CzXsKVb1.js';
3
- export { f as AGENT_TOOL_BRAND, g as AgentAttachmentsOptions, h as AgentDeps, i as AgentEngine, j as AgentMemoryOptions, a as AgentSurface, F as FunctionalTool, L as LedgerQuotaProvider, Q as QueuePlan, k as QueueSettleOutcome, l as QueuedTurn, m as QueuedTurnStarter, n as QuotaLimits, o as QuotaWindowLimits, p as defineTool, q as delegateToolName, r as provideAgentTool, s as provideAgentTools, u as utcDay } from './chat-queue.service-CzXsKVb1.js';
2
+ import { b as AgentModuleOptions, c as AgentModuleAsyncOptions, d as AgentProtocolAdapter, A as AgentDepsFactory, e as AgentSkillsOptions, C as ChatQueueService } from './chat-queue.service-YjiKVTKE.js';
3
+ export { f as AGENT_TOOL_BRAND, g as AgentAttachmentsOptions, h as AgentDeps, i as AgentEngine, j as AgentMemoryOptions, a as AgentSurface, F as FunctionalTool, L as LedgerQuotaProvider, Q as QueuePlan, k as QueueSettleOutcome, l as QueuedTurn, m as QueuedTurnStarter, n as QuotaLimits, o as QuotaWindowLimits, p as defineTool, q as delegateToolName, r as provideAgentTool, s as provideAgentTools, u as utcDay } from './chat-queue.service-YjiKVTKE.js';
4
4
  import * as _dudousxd_nestjs_agent_core from '@dudousxd/nestjs-agent-core';
5
- import { ActorResolver, AttachmentStagingStore, AgentApprovalPort, Actor, AgentStore, StagedAttachment, MessageAttachment, ModelCatalog, AgentClientConfig, ToolSpec, ToolPresentation, AgentHistoryWindow, AgentIntake, Persona, SkillContext, ToolRegistry, AgentRegistry, PromptContributor, SkillsConfig, SkillCatalogEntry, MessageFeedbackValue, MessageFeedback, ModelCatalogView, ToolCatalogEntry, MemoryConfig, MemoryDigestEntry, SkillProvider, AgentRunner, AgentRunInput, AgentRunStartOptions, HumanReply, ChatQueueState, TokenStreamSink, SinkWriter, ActionProposalOutcome, ActionProposal } from '@dudousxd/nestjs-agent-core';
5
+ import { ActorResolver, AttachmentStagingStore, AgentApprovalPort, Actor, AgentStore, StagedAttachment, MessageAttachment, ModelCatalog, AgentClientConfig, ToolSpec, ToolPresentation, AgentHistoryWindow, AgentIntake, Persona, SkillContext, ToolRegistry, AgentRegistry, PromptContributor, SkillsConfig, SkillCatalogEntry, MessageFeedbackValue, MessageFeedback, ModelCatalogView, ToolCatalogEntry, MemoryConfig, MemoryDigestEntry, SkillProvider, AgentRunner, AgentRunInput, AgentRunStartOptions, HumanReply, ChatQueueState, TokenStreamSink, SinkWriter, SubscribeOptions, ActionProposalOutcome, ActionProposal } from '@dudousxd/nestjs-agent-core';
6
6
  export * from '@dudousxd/nestjs-agent-core';
7
7
  export { FunctionalToolDefinition } from '@dudousxd/nestjs-agent-core';
8
8
  import { A2uiOptions } from '@dudousxd/nestjs-agent-core/a2ui';
9
9
  import { Request, Response } from 'express';
10
- import { A as AgentService, a as ActionProposalService } from './index-DpS9P7so.js';
11
- export { b as A2uiAdapterOptions, c as A2uiRunHandler, d as AttachmentLimits, C as ChatParams, e as ChatSendMode, f as ChatSendResult, D as DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES, g as DEFAULT_MAX_ATTACHMENT_BYTES, M as MAX_ATTACHMENTS_PER_MESSAGE, N as NO_USER_MESSAGE_CODE, Q as QueuedSend, T as ThreadDefaultAgentReader, h as ThreadModelReader, i as a2uiAdapter, j as attachmentLimits } from './index-DpS9P7so.js';
10
+ import { A as AgentService, a as ActionProposalService } from './index-yC_Ra3CE.js';
11
+ export { b as A2uiAdapterOptions, c as A2uiRunHandler, d as AttachmentLimits, C as ChatParams, e as ChatSendMode, f as ChatSendResult, D as DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES, g as DEFAULT_MAX_ATTACHMENT_BYTES, F as ForkThreadParams, M as MAX_ATTACHMENTS_PER_MESSAGE, N as NO_USER_MESSAGE_CODE, Q as QueuedSend, T as ThreadDefaultAgentReader, h as ThreadModelReader, i as a2uiAdapter, j as attachmentLimits } from './index-yC_Ra3CE.js';
12
12
  import { AgentGenui } from './genui/index.js';
13
13
  import { StandardSchemaV1 } from '@standard-schema/spec';
14
14
  import { DiscoveryService, MetadataScanner } from '@nestjs/core';
@@ -870,7 +870,8 @@ declare const AGENT_SERVICE: unique symbol;
870
870
 
871
871
  /**
872
872
  * The default `TokenStreamSink`: buffers chunks per run in-process so a reconnecting subscriber
873
- * replays everything so far, then follows live.
873
+ * replays everything so far, then follows live. A run's buffer is dropped {@link ENDED_GRACE_MS}
874
+ * after it ends, or {@link LIVE_TTL_MS} after its last write if it never does.
874
875
  *
875
876
  * Single-replica only. Because the buffer lives in one process's memory, a resume/reconnect that
876
877
  * lands on a different replica than the one running the turn finds an empty buffer — the stream
@@ -879,10 +880,21 @@ declare const AGENT_SERVICE: unique symbol;
879
880
  */
880
881
  declare class InProcessTokenStreamSink implements TokenStreamSink {
881
882
  private readonly runs;
883
+ /** Subscribers of a run nothing has opened yet, woken by its `open()`. */
884
+ private readonly waiting;
882
885
  private buffer;
886
+ private expireAfter;
887
+ /** Forget the run, and let anyone still following it finish as if it had ended. */
888
+ private drop;
883
889
  private wake;
890
+ private finish;
891
+ /**
892
+ * The run's writer. Each call finds the run's buffer afresh, so a run that went quiet past the
893
+ * live TTL (parked on a person) and then writes again starts a new buffer its next subscriber
894
+ * follows, rather than writing into one nothing reads.
895
+ */
884
896
  open(runId: string): SinkWriter;
885
- subscribe(runId: string): AsyncIterable<Uint8Array>;
897
+ subscribe(runId: string, options?: SubscribeOptions): AsyncIterable<Uint8Array>;
886
898
  close(runId: string): void;
887
899
  }
888
900
 
package/dist/index.js CHANGED
@@ -1584,6 +1584,26 @@ var RunNotActiveException = class extends ConflictException {
1584
1584
  }
1585
1585
  };
1586
1586
 
1587
+ // src/thread-reads.ts
1588
+ function threadReads(store, threadId, separately) {
1589
+ const projecting = store;
1590
+ if (typeof projecting.threadSettings !== "function") {
1591
+ return separately;
1592
+ }
1593
+ let settings;
1594
+ const read = /* @__PURE__ */ __name(() => {
1595
+ settings ??= projecting.threadSettings?.(threadId) ?? Promise.resolve(null);
1596
+ return settings;
1597
+ }, "read");
1598
+ return {
1599
+ owner: /* @__PURE__ */ __name(async () => (await read())?.actorRef ?? null, "owner"),
1600
+ defaultAgent: /* @__PURE__ */ __name(async () => (await read())?.defaultAgent ?? null, "defaultAgent"),
1601
+ persona: /* @__PURE__ */ __name(async () => (await read())?.persona ?? null, "persona"),
1602
+ model: /* @__PURE__ */ __name(async () => (await read())?.model ?? null, "model")
1603
+ };
1604
+ }
1605
+ __name(threadReads, "threadReads");
1606
+
1587
1607
  // src/agent.service.ts
1588
1608
  function _ts_decorate5(decorators, target, key, desc) {
1589
1609
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -1847,7 +1867,7 @@ var AgentService2 = class {
1847
1867
  * else the thread's pinned one, else none. An agent the catalog locks to one model runs on it
1848
1868
  * whatever was sent or pinned, and a send naming another model is refused.
1849
1869
  */
1850
- async resolveModel(actor, agent, requested, threadId) {
1870
+ async resolveModel(actor, agent, requested, threadId, reads) {
1851
1871
  const locked = this.models !== void 0 ? (await this.models.list({
1852
1872
  actor,
1853
1873
  agent
@@ -1858,7 +1878,7 @@ var AgentService2 = class {
1858
1878
  }
1859
1879
  return locked.model;
1860
1880
  }
1861
- const model = requested ?? (threadId !== void 0 ? await this.threadModel(threadId) : null);
1881
+ const model = requested ?? (threadId !== void 0 ? await (reads?.model() ?? this.threadModel(threadId)) : null);
1862
1882
  if (model === null || model === void 0) {
1863
1883
  return void 0;
1864
1884
  }
@@ -1918,10 +1938,11 @@ var AgentService2 = class {
1918
1938
  if ("proposalDecision" in decision) return decision;
1919
1939
  }
1920
1940
  await this.assertWithinQuota(params.actor);
1921
- const target = this.deps.resolveAgent(await this.resolveAgentName(params.agentName, params.threadId));
1941
+ const reads = params.threadId !== void 0 ? this.threadReads(params.threadId) : void 0;
1942
+ const target = this.deps.resolveAgent(await this.resolveAgentName(params.agentName, params.threadId, reads));
1922
1943
  const agentName = target.agentName;
1923
- const persona = await this.resolvePersona(agentName, params.personaId ?? target.persona, params.threadId);
1924
- const model = await this.resolveModel(params.actor, agentName, params.model, params.threadId);
1944
+ const persona = await this.resolvePersona(agentName, params.personaId ?? target.persona, params.threadId, reads);
1945
+ const model = await this.resolveModel(params.actor, agentName, params.model, params.threadId, reads);
1925
1946
  const attachments = await this.resolveAttachments(params.actor, params.attachments ?? []);
1926
1947
  let threadId = params.threadId;
1927
1948
  if (threadId === void 0) {
@@ -1945,9 +1966,9 @@ var AgentService2 = class {
1945
1966
  });
1946
1967
  threadId = created.id;
1947
1968
  } else {
1948
- if (params.authorized !== true) await this.assertOwnsThread(params.actor, threadId);
1969
+ if (params.authorized !== true) await this.assertOwnsThread(params.actor, threadId, reads);
1949
1970
  if (params.personaId !== void 0) {
1950
- await this.pinThreadPersona(threadId, params.personaId);
1971
+ await this.pinThreadPersona(threadId, params.personaId, reads);
1951
1972
  }
1952
1973
  }
1953
1974
  const input = {
@@ -2273,8 +2294,8 @@ var AgentService2 = class {
2273
2294
  * {@link subscribeAs} instead: a runId is the only thing standing between a caller and another
2274
2295
  * actor's answer, tool arguments and tool results.
2275
2296
  */
2276
- subscribe(runId) {
2277
- return this.deps.forAgent().sink.subscribe(runId);
2297
+ subscribe(runId, options) {
2298
+ return this.deps.forAgent().sink.subscribe(runId, options);
2278
2299
  }
2279
2300
  /**
2280
2301
  * {@link subscribe}, gated the way {@link cancel} is: only the actor whose thread is currently
@@ -2282,9 +2303,9 @@ var AgentService2 = class {
2282
2303
  * replayed — the runner clears the thread's active stream as the run ends, and that field is what
2283
2304
  * ownership is derived from.
2284
2305
  */
2285
- async subscribeAs(actor, runId) {
2306
+ async subscribeAs(actor, runId, options) {
2286
2307
  await this.assertOwnsActiveStream(actor, runId);
2287
- return this.subscribe(runId);
2308
+ return this.subscribe(runId, options);
2288
2309
  }
2289
2310
  /**
2290
2311
  * Approve a parked action call as `actor`. Who may is the call's recorded approver's business
@@ -2437,9 +2458,28 @@ var AgentService2 = class {
2437
2458
  await this.assertOwnsThread(actor, threadId);
2438
2459
  return this.store.softDeleteThread(threadId);
2439
2460
  }
2440
- async forkThread(actor, threadId, fromMessageId) {
2441
- await this.assertOwnsThread(actor, threadId);
2442
- return this.store.forkThread(threadId, fromMessageId);
2461
+ /**
2462
+ * Copy a thread up to `fromMessageId` (inclusive) into a new one. By default only the thread's
2463
+ * owner may, and the copy is theirs. With `params.authorized`, the host has vouched for `actor`
2464
+ * (a share link it checked) and the copy is `actor`'s — see {@link ForkThreadParams}.
2465
+ */
2466
+ async forkThread(actor, threadId, fromMessageId, params = {}) {
2467
+ if (params.authorized === true) {
2468
+ if (await this.store.ownerOfThread(threadId) === null) {
2469
+ throw new NotFoundException2(`thread ${threadId} not found`);
2470
+ }
2471
+ } else {
2472
+ await this.assertOwnsThread(actor, threadId);
2473
+ }
2474
+ const options = {
2475
+ ...params.authorized === true ? {
2476
+ actorRef: actor.id
2477
+ } : {},
2478
+ ...params.tenantRef !== void 0 ? {
2479
+ tenantRef: params.tenantRef
2480
+ } : {}
2481
+ };
2482
+ return this.store.forkThread(threadId, fromMessageId, options);
2443
2483
  }
2444
2484
  /** Kept for source compatibility — a thin wrapper over the more general `updateThread`. */
2445
2485
  async renameThread(actor, threadId, title) {
@@ -2491,7 +2531,7 @@ var AgentService2 = class {
2491
2531
  * The persona this send runs under — see {@link AgentDepsFactory.resolvePersona}. The thread's
2492
2532
  * pinned persona is read only when the send names none AND the agent has personas to pick from.
2493
2533
  */
2494
- async resolvePersona(agentName, requested, threadId) {
2534
+ async resolvePersona(agentName, requested, threadId, reads) {
2495
2535
  const needsThread = requested === void 0 && threadId !== void 0 && this.deps.personaCatalog(agentName).length > 0;
2496
2536
  return this.deps.resolvePersona({
2497
2537
  agentName,
@@ -2499,16 +2539,16 @@ var AgentService2 = class {
2499
2539
  requested
2500
2540
  } : {},
2501
2541
  ...needsThread ? {
2502
- threadPersona: await threadPersona(this.store, threadId)
2542
+ threadPersona: await (reads?.persona() ?? threadPersona(this.store, threadId))
2503
2543
  } : {}
2504
2544
  });
2505
2545
  }
2506
2546
  /** Pin `persona` on the thread when it is not already — a no-op on a store that cannot. */
2507
- async pinThreadPersona(threadId, persona) {
2547
+ async pinThreadPersona(threadId, persona, reads) {
2508
2548
  if (this.store.updateThread === void 0) {
2509
2549
  return;
2510
2550
  }
2511
- if (await threadPersona(this.store, threadId) !== persona) {
2551
+ if (await (reads?.persona() ?? threadPersona(this.store, threadId)) !== persona) {
2512
2552
  await this.store.updateThread(threadId, {
2513
2553
  persona
2514
2554
  });
@@ -2535,18 +2575,27 @@ var AgentService2 = class {
2535
2575
  * workflow, for a string. A store predating {@link ThreadDefaultAgentReader} still answers, via
2536
2576
  * the full read.
2537
2577
  */
2538
- async resolveAgentName(agentName, threadId) {
2578
+ async resolveAgentName(agentName, threadId, reads) {
2539
2579
  if (agentName !== void 0) {
2540
2580
  return agentName;
2541
2581
  }
2542
2582
  if (threadId !== void 0) {
2543
- const threadDefault = await this.threadDefaultAgent(threadId);
2583
+ const threadDefault = await (reads?.defaultAgent() ?? this.threadDefaultAgent(threadId));
2544
2584
  if (threadDefault !== null) {
2545
2585
  return threadDefault;
2546
2586
  }
2547
2587
  }
2548
2588
  return this.deps.defaultAgentName();
2549
2589
  }
2590
+ /** Everything a send reads off `threadId`, in one read where the store allows it. */
2591
+ threadReads(threadId) {
2592
+ return threadReads(this.store, threadId, {
2593
+ owner: /* @__PURE__ */ __name(() => this.store.ownerOfThread(threadId), "owner"),
2594
+ defaultAgent: /* @__PURE__ */ __name(() => this.threadDefaultAgent(threadId), "defaultAgent"),
2595
+ persona: /* @__PURE__ */ __name(() => threadPersona(this.store, threadId), "persona"),
2596
+ model: /* @__PURE__ */ __name(() => this.threadModel(threadId), "model")
2597
+ });
2598
+ }
2550
2599
  async threadDefaultAgent(threadId) {
2551
2600
  const projecting = this.store;
2552
2601
  if (typeof projecting.defaultAgentForThread === "function") {
@@ -2716,8 +2765,8 @@ var AgentService2 = class {
2716
2765
  * Authorization seam for thread-scoped endpoints: the caller must own the thread. A missing
2717
2766
  * thread is a 404; someone else's thread is a 403 — one actor never touches another's thread.
2718
2767
  */
2719
- async assertOwnsThread(actor, threadId) {
2720
- const owner = await this.store.ownerOfThread(threadId);
2768
+ async assertOwnsThread(actor, threadId, reads) {
2769
+ const owner = await (reads?.owner() ?? this.store.ownerOfThread(threadId));
2721
2770
  if (owner === null) {
2722
2771
  throw new NotFoundException2(`thread ${threadId} not found`);
2723
2772
  }
@@ -3157,6 +3206,18 @@ AttachmentsController = _ts_decorate8([
3157
3206
  import { AGENT_ACTOR_RESOLVER as AGENT_ACTOR_RESOLVER2, AgentStreamError, streamFailure } from "@dudousxd/nestjs-agent-core";
3158
3207
  import { validateUiCapabilities } from "@dudousxd/nestjs-agent-core/genui";
3159
3208
  import { BadRequestException as BadRequestException4, Body, Controller as Controller3, Get as Get3, Inject as Inject8, Param, Post as Post2, Query, Req as Req2, Res } from "@nestjs/common";
3209
+
3210
+ // src/controller/reader-gone.ts
3211
+ function readerGone(res) {
3212
+ const controller = new AbortController();
3213
+ res.once("close", () => controller.abort());
3214
+ return {
3215
+ signal: controller.signal
3216
+ };
3217
+ }
3218
+ __name(readerGone, "readerGone");
3219
+
3220
+ // src/controller/chat.controller.ts
3160
3221
  function _ts_decorate9(decorators, target, key, desc) {
3161
3222
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3162
3223
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -3290,7 +3351,8 @@ var ChatController = class {
3290
3351
  return;
3291
3352
  }
3292
3353
  const { runId, threadId } = result;
3293
- await this.pipe(res, runId, this.agent.subscribe(runId), threadId);
3354
+ const gone = readerGone(res);
3355
+ await this.pipe(res, runId, this.agent.subscribe(runId, gone), threadId, 0, gone.signal);
3294
3356
  }
3295
3357
  /**
3296
3358
  * Attach to a run's stream: every buffered frame, then live ones. `?after=<seq>` (or the SSE
@@ -3300,7 +3362,8 @@ var ChatController = class {
3300
3362
  async stream(req, runId, res, after) {
3301
3363
  const actor = await this.actorResolver.resolve(req);
3302
3364
  const cursor = parseCursor(after) ?? parseCursor(req.headers["last-event-id"]) ?? 0;
3303
- await this.pipe(res, runId, await this.agent.subscribeAs(actor, runId), void 0, cursor);
3365
+ const gone = readerGone(res);
3366
+ await this.pipe(res, runId, await this.agent.subscribeAs(actor, runId, gone), void 0, cursor, gone.signal);
3304
3367
  }
3305
3368
  async cancel(req, runId) {
3306
3369
  const actor = await this.actorResolver.resolve(req);
@@ -3316,8 +3379,11 @@ var ChatController = class {
3316
3379
  * and on any later `GET …/stream`, whichever replica serves it. A reconnecting client passes the
3317
3380
  * last one it saw as `?after=`; frames at or below it are read off the sink but not re-sent.
3318
3381
  * `meta`, `done` and `error` carry no id: `meta` is re-sent on every attach, the terminals once.
3382
+ *
3383
+ * A reader that disconnects aborts `gone`, which the sink honours by ending the subscription even
3384
+ * while the run is silent (parked on a person, or never writing again).
3319
3385
  */
3320
- async pipe(res, runId, events, threadId, after = 0) {
3386
+ async pipe(res, runId, events, threadId, after = 0, gone) {
3321
3387
  res.setHeader("Content-Type", "text/event-stream");
3322
3388
  res.setHeader("Cache-Control", "no-cache");
3323
3389
  res.setHeader("Connection", "keep-alive");
@@ -3346,6 +3412,9 @@ data: ${line}
3346
3412
  }, "forward");
3347
3413
  try {
3348
3414
  for await (const chunk of events) {
3415
+ if (gone?.aborted) {
3416
+ return;
3417
+ }
3349
3418
  buffer += decoder.decode(chunk, {
3350
3419
  stream: true
3351
3420
  });
@@ -3359,11 +3428,17 @@ data: ${line}
3359
3428
  newline = buffer.indexOf("\n");
3360
3429
  }
3361
3430
  }
3431
+ if (gone?.aborted) {
3432
+ return;
3433
+ }
3362
3434
  if (buffer.length > 0) {
3363
3435
  forward(buffer);
3364
3436
  }
3365
3437
  res.write("event: done\ndata: {}\n\n");
3366
3438
  } catch (error) {
3439
+ if (gone?.aborted) {
3440
+ return;
3441
+ }
3367
3442
  const payload = error instanceof AgentStreamError ? {
3368
3443
  code: error.code,
3369
3444
  message: error.message
@@ -5293,49 +5368,121 @@ SkillDiscoveryService = _ts_decorate22([
5293
5368
 
5294
5369
  // src/in-process-sink.ts
5295
5370
  import { AgentStreamError as AgentStreamError2 } from "@dudousxd/nestjs-agent-core";
5371
+ var LIVE_TTL_MS = 36e5;
5372
+ var ENDED_GRACE_MS = 6e4;
5296
5373
  var InProcessTokenStreamSink = class {
5297
5374
  static {
5298
5375
  __name(this, "InProcessTokenStreamSink");
5299
5376
  }
5300
5377
  runs = /* @__PURE__ */ new Map();
5378
+ /** Subscribers of a run nothing has opened yet, woken by its `open()`. */
5379
+ waiting = /* @__PURE__ */ new Map();
5301
5380
  buffer(runId) {
5302
5381
  let buf = this.runs.get(runId);
5303
5382
  if (buf === void 0) {
5304
5383
  buf = {
5305
5384
  chunks: [],
5306
5385
  ended: false,
5307
- notify: /* @__PURE__ */ new Set()
5386
+ notify: /* @__PURE__ */ new Set(),
5387
+ lastWriteAt: Date.now(),
5388
+ expiry: void 0
5308
5389
  };
5309
5390
  this.runs.set(runId, buf);
5391
+ this.expireAfter(runId, buf, LIVE_TTL_MS);
5392
+ const waiters = this.waiting.get(runId);
5393
+ this.waiting.delete(runId);
5394
+ for (const resolve2 of waiters ?? []) {
5395
+ resolve2();
5396
+ }
5310
5397
  }
5311
5398
  return buf;
5312
5399
  }
5400
+ expireAfter(runId, buf, ms) {
5401
+ if (buf.expiry !== void 0) {
5402
+ clearTimeout(buf.expiry);
5403
+ }
5404
+ buf.expiry = setTimeout(() => {
5405
+ buf.expiry = void 0;
5406
+ const idle = Date.now() - buf.lastWriteAt;
5407
+ if (!buf.ended && idle < LIVE_TTL_MS) {
5408
+ this.expireAfter(runId, buf, LIVE_TTL_MS - idle);
5409
+ return;
5410
+ }
5411
+ this.drop(runId, buf);
5412
+ }, ms);
5413
+ buf.expiry.unref?.();
5414
+ }
5415
+ /** Forget the run, and let anyone still following it finish as if it had ended. */
5416
+ drop(runId, buf) {
5417
+ if (buf.expiry !== void 0) {
5418
+ clearTimeout(buf.expiry);
5419
+ buf.expiry = void 0;
5420
+ }
5421
+ if (this.runs.get(runId) === buf) {
5422
+ this.runs.delete(runId);
5423
+ }
5424
+ buf.ended = true;
5425
+ this.wake(buf);
5426
+ }
5313
5427
  wake(buf) {
5314
5428
  for (const resolve2 of buf.notify) {
5315
5429
  resolve2();
5316
5430
  }
5317
5431
  buf.notify.clear();
5318
5432
  }
5433
+ finish(runId, buf) {
5434
+ buf.ended = true;
5435
+ this.wake(buf);
5436
+ this.expireAfter(runId, buf, ENDED_GRACE_MS);
5437
+ }
5438
+ /**
5439
+ * The run's writer. Each call finds the run's buffer afresh, so a run that went quiet past the
5440
+ * live TTL (parked on a person) and then writes again starts a new buffer its next subscriber
5441
+ * follows, rather than writing into one nothing reads.
5442
+ */
5319
5443
  open(runId) {
5320
- const buf = this.buffer(runId);
5444
+ this.buffer(runId);
5321
5445
  return {
5322
5446
  write: /* @__PURE__ */ __name((chunk) => {
5447
+ const buf = this.buffer(runId);
5323
5448
  buf.chunks.push(chunk);
5449
+ buf.lastWriteAt = Date.now();
5324
5450
  this.wake(buf);
5325
5451
  }, "write"),
5326
5452
  end: /* @__PURE__ */ __name(() => {
5327
- buf.ended = true;
5328
- this.wake(buf);
5453
+ this.finish(runId, this.buffer(runId));
5329
5454
  }, "end"),
5330
5455
  fail: /* @__PURE__ */ __name((error) => {
5456
+ const buf = this.buffer(runId);
5331
5457
  buf.failure = error;
5332
- buf.ended = true;
5333
- this.wake(buf);
5458
+ this.finish(runId, buf);
5334
5459
  }, "fail")
5335
5460
  };
5336
5461
  }
5337
- async *subscribe(runId) {
5338
- const buf = this.buffer(runId);
5462
+ async *subscribe(runId, options = {}) {
5463
+ const { signal } = options;
5464
+ let buf = this.runs.get(runId);
5465
+ while (buf === void 0) {
5466
+ const waiters = this.waiting.get(runId) ?? /* @__PURE__ */ new Set();
5467
+ this.waiting.set(runId, waiters);
5468
+ let resolve2 = /* @__PURE__ */ __name(() => {
5469
+ }, "resolve");
5470
+ const opened = new Promise((r) => {
5471
+ resolve2 = r;
5472
+ });
5473
+ waiters.add(resolve2);
5474
+ try {
5475
+ if (!await untilAborted(opened, signal)) {
5476
+ return;
5477
+ }
5478
+ } finally {
5479
+ waiters.delete(resolve2);
5480
+ if (waiters.size === 0 && this.waiting.get(runId) === waiters) {
5481
+ this.waiting.delete(runId);
5482
+ }
5483
+ }
5484
+ buf = this.runs.get(runId);
5485
+ }
5339
5486
  let index = 0;
5340
5487
  while (true) {
5341
5488
  while (index < buf.chunks.length) {
@@ -5351,13 +5498,56 @@ var InProcessTokenStreamSink = class {
5351
5498
  }
5352
5499
  return;
5353
5500
  }
5354
- await new Promise((resolve2) => buf.notify.add(resolve2));
5501
+ const current = buf;
5502
+ let resolve2 = /* @__PURE__ */ __name(() => {
5503
+ }, "resolve");
5504
+ const woken = new Promise((r) => {
5505
+ resolve2 = r;
5506
+ });
5507
+ current.notify.add(resolve2);
5508
+ try {
5509
+ if (!await untilAborted(woken, signal)) {
5510
+ return;
5511
+ }
5512
+ } finally {
5513
+ current.notify.delete(resolve2);
5514
+ }
5355
5515
  }
5356
5516
  }
5357
5517
  close(runId) {
5518
+ const buf = this.runs.get(runId);
5519
+ if (buf?.expiry !== void 0) {
5520
+ clearTimeout(buf.expiry);
5521
+ }
5358
5522
  this.runs.delete(runId);
5359
5523
  }
5360
5524
  };
5525
+ async function untilAborted(promise, signal) {
5526
+ if (signal === void 0) {
5527
+ await promise;
5528
+ return true;
5529
+ }
5530
+ if (signal.aborted) {
5531
+ return false;
5532
+ }
5533
+ let onAbort = /* @__PURE__ */ __name(() => {
5534
+ }, "onAbort");
5535
+ const aborted = new Promise((resolve2) => {
5536
+ onAbort = /* @__PURE__ */ __name(() => resolve2(false), "onAbort");
5537
+ signal.addEventListener("abort", onAbort, {
5538
+ once: true
5539
+ });
5540
+ });
5541
+ try {
5542
+ return await Promise.race([
5543
+ promise.then(() => true),
5544
+ aborted
5545
+ ]);
5546
+ } finally {
5547
+ signal.removeEventListener("abort", onAbort);
5548
+ }
5549
+ }
5550
+ __name(untilAborted, "untilAborted");
5361
5551
 
5362
5552
  // src/ledger-quota-provider.ts
5363
5553
  import { exhaustedWindow, quotaPeriodRange, quotaWarning } from "@dudousxd/nestjs-agent-core";
@@ -5376,13 +5566,22 @@ var LedgerQuotaProvider = class {
5376
5566
  async report(query) {
5377
5567
  const now = query.now ?? /* @__PURE__ */ new Date();
5378
5568
  const actorRef = query.actor.id;
5379
- const windows = [
5380
- await this.dayWindow(actorRef, now)
5381
- ];
5382
- if (this.store.usageBetween !== void 0) {
5383
- const range = quotaPeriodRange("month", now);
5384
- const used = await this.store.usageBetween(actorRef, range.fromDay, range.toDay);
5385
- windows.push(this.window("month", used.usedTokens, this.usd(used), range.resetsAt));
5569
+ const day = quotaPeriodRange("day", now);
5570
+ const month = quotaPeriodRange("month", now);
5571
+ const windows = [];
5572
+ if (this.store.usageWindows !== void 0) {
5573
+ const [dayUsed, monthUsed] = await this.store.usageWindows(actorRef, [
5574
+ day,
5575
+ month
5576
+ ]);
5577
+ windows.push(this.usedWindow("day", dayUsed, day.resetsAt));
5578
+ windows.push(this.usedWindow("month", monthUsed, month.resetsAt));
5579
+ } else {
5580
+ windows.push(this.usedWindow("day", await this.store.quotaToday(actorRef, day.fromDay), day.resetsAt));
5581
+ if (this.store.usageBetween !== void 0) {
5582
+ const used = await this.store.usageBetween(actorRef, month.fromDay, month.toDay);
5583
+ windows.push(this.usedWindow("month", used, month.resetsAt));
5584
+ }
5386
5585
  }
5387
5586
  const blocked = exhaustedWindow(windows);
5388
5587
  const warning = quotaWarning(windows);
@@ -5396,10 +5595,8 @@ var LedgerQuotaProvider = class {
5396
5595
  } : {}
5397
5596
  };
5398
5597
  }
5399
- async dayWindow(actorRef, now) {
5400
- const range = quotaPeriodRange("day", now);
5401
- const used = await this.store.quotaToday(actorRef, range.fromDay);
5402
- return this.window("day", used.usedTokens, this.usd(used), range.resetsAt);
5598
+ usedWindow(period, used, resetsAt) {
5599
+ return this.window(period, used?.usedTokens ?? 0, used === void 0 ? 0 : this.usd(used), resetsAt);
5403
5600
  }
5404
5601
  /** The window's USD: every priced row, or (with `countEstimatedCost: false`) reported cost only. */
5405
5602
  usd(used) {
@@ -7244,7 +7441,7 @@ var AgUiRunHandler = class {
7244
7441
  seq: 0
7245
7442
  };
7246
7443
  try {
7247
- const frames = agUiFramesFromNdjson(this.agent.subscribe(runId));
7444
+ const frames = agUiFramesFromNdjson(this.agent.subscribe(runId, readerGone(res)));
7248
7445
  for await (const event of agUiEvents(frames, {
7249
7446
  ...options,
7250
7447
  cursor
@@ -7575,7 +7772,7 @@ var A2uiRunHandler = class {
7575
7772
  `);
7576
7773
  }, "write");
7577
7774
  try {
7578
- const frames = agUiFramesFromNdjson2(this.agent.subscribe(runId));
7775
+ const frames = agUiFramesFromNdjson2(this.agent.subscribe(runId, readerGone(res)));
7579
7776
  for await (const event of agUiEvents2(frames, {
7580
7777
  threadId: options.threadId ?? runId,
7581
7778
  runId,