@dudousxd/nestjs-agent 1.36.1 → 1.36.2

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.
@@ -1,25 +1,12 @@
1
1
  import { AgentStore, AgentRunInput, AgentLoopResult, LlmStepEnvelope, BufferedModelTurnResult, ToolStepEnvelope, AgentRunner, TokenStreamSink, AgentRunStartOptions, HumanReply } from '@dudousxd/nestjs-agent-core';
2
2
  import { WorkflowService } from '@dudousxd/nestjs-durable';
3
3
  import { RunGateway, WorkflowCtx } from '@dudousxd/nestjs-durable-core';
4
- import { A as AgentDepsFactory, C as ChatQueueService, a as AgentSurface, b as AgentModuleOptions } from '../chat-queue.service-CF2fcJDx.cjs';
4
+ import { A as AgentDepsFactory, C as ChatQueueService, a as AgentSurface, b as AgentModuleOptions } from '../chat-queue.service-fopUN313.cjs';
5
5
  import { DynamicModule } from '@nestjs/common';
6
6
  import '@dudousxd/nestjs-agent-core/genui';
7
7
  import '@standard-schema/spec';
8
8
  import '../genui/index.cjs';
9
9
 
10
- /**
11
- * The agent turn AS a durable workflow. Persist/stream checkpoints are `ctx.localStep`s — the
12
- * in-process primitive, NOT the always-dispatched `ctx.step`: their names are dynamic checkpoint
13
- * identities (`persist:toolcall:<id>`), not routable worker groups, and their deps (store, sink)
14
- * live in THIS workflow worker's DI. The two LONG steps (model call, tool execution) are `ctx.step`s
15
- * routed to `AgentRunSteps`, so the run is never pinned to this pod while they execute — the same
16
- * thing `ctx.step` means anywhere in this ecosystem, and the reason a `@AiTool` handler must
17
- * establish whatever execution context it needs itself. HITL is `ctx.waitForSignal`, and sub-agent
18
- * delegation is `ctx.child(AgentRunWorkflow)` — a replay-safe, observable child run (it shows up as
19
- * a node in the durable dashboard). A child streams into its top-level ancestor's sink
20
- * (`sinkRunId`) so the human watching the parent sees it and can approve its action tools; the
21
- * approval routes to the child's own run via `runForToolCall`.
22
- */
23
10
  declare class AgentRunWorkflow {
24
11
  private readonly factory;
25
12
  private readonly store;
@@ -65,10 +52,11 @@ declare class AgentRunWorkflow {
65
52
  * `DurableAgentRunner.cancel` puts it — `cancelling` while a compensating cancel is in flight,
66
53
  * `cancelled` once it has settled.
67
54
  *
68
- * Costs a run read at each of the loop's observation points (two per step). That is a row the
69
- * control plane already has, against a turn that is otherwise making model calls, and it buys the
70
- * property that matters: the answer comes from a place BOTH the canceller and every pod that might
71
- * replay this run can see.
55
+ * Costs a run read at each of the loop's observation points (two per step): the run's own row
56
+ * where the gateway can read it alone, else its detail, which carries every checkpoint of the run.
57
+ * That row is one the control plane already has, against a turn that is otherwise making model
58
+ * calls, and it buys the property that matters: the answer comes from a place BOTH the canceller
59
+ * and every pod that might replay this run can see.
72
60
  *
73
61
  * Failing to ask is not a cancel. A tenant with no gateway wired, or an operator briefly
74
62
  * unreachable, answers "no" and the run carries on — the alternative is failing healthy runs
@@ -1,25 +1,12 @@
1
1
  import { AgentStore, AgentRunInput, AgentLoopResult, LlmStepEnvelope, BufferedModelTurnResult, ToolStepEnvelope, AgentRunner, TokenStreamSink, AgentRunStartOptions, HumanReply } from '@dudousxd/nestjs-agent-core';
2
2
  import { WorkflowService } from '@dudousxd/nestjs-durable';
3
3
  import { RunGateway, WorkflowCtx } from '@dudousxd/nestjs-durable-core';
4
- import { A as AgentDepsFactory, C as ChatQueueService, a as AgentSurface, b as AgentModuleOptions } from '../chat-queue.service-CzXsKVb1.js';
4
+ import { A as AgentDepsFactory, C as ChatQueueService, a as AgentSurface, b as AgentModuleOptions } from '../chat-queue.service-YjiKVTKE.js';
5
5
  import { DynamicModule } from '@nestjs/common';
6
6
  import '@dudousxd/nestjs-agent-core/genui';
7
7
  import '@standard-schema/spec';
8
8
  import '../genui/index.js';
9
9
 
10
- /**
11
- * The agent turn AS a durable workflow. Persist/stream checkpoints are `ctx.localStep`s — the
12
- * in-process primitive, NOT the always-dispatched `ctx.step`: their names are dynamic checkpoint
13
- * identities (`persist:toolcall:<id>`), not routable worker groups, and their deps (store, sink)
14
- * live in THIS workflow worker's DI. The two LONG steps (model call, tool execution) are `ctx.step`s
15
- * routed to `AgentRunSteps`, so the run is never pinned to this pod while they execute — the same
16
- * thing `ctx.step` means anywhere in this ecosystem, and the reason a `@AiTool` handler must
17
- * establish whatever execution context it needs itself. HITL is `ctx.waitForSignal`, and sub-agent
18
- * delegation is `ctx.child(AgentRunWorkflow)` — a replay-safe, observable child run (it shows up as
19
- * a node in the durable dashboard). A child streams into its top-level ancestor's sink
20
- * (`sinkRunId`) so the human watching the parent sees it and can approve its action tools; the
21
- * approval routes to the child's own run via `runForToolCall`.
22
- */
23
10
  declare class AgentRunWorkflow {
24
11
  private readonly factory;
25
12
  private readonly store;
@@ -65,10 +52,11 @@ declare class AgentRunWorkflow {
65
52
  * `DurableAgentRunner.cancel` puts it — `cancelling` while a compensating cancel is in flight,
66
53
  * `cancelled` once it has settled.
67
54
  *
68
- * Costs a run read at each of the loop's observation points (two per step). That is a row the
69
- * control plane already has, against a turn that is otherwise making model calls, and it buys the
70
- * property that matters: the answer comes from a place BOTH the canceller and every pod that might
71
- * replay this run can see.
55
+ * Costs a run read at each of the loop's observation points (two per step): the run's own row
56
+ * where the gateway can read it alone, else its detail, which carries every checkpoint of the run.
57
+ * That row is one the control plane already has, against a turn that is otherwise making model
58
+ * calls, and it buys the property that matters: the answer comes from a place BOTH the canceller
59
+ * and every pod that might replay this run can see.
72
60
  *
73
61
  * Failing to ask is not a cancel. A tenant with no gateway wired, or an operator briefly
74
62
  * unreachable, answers "no" and the run carries on — the alternative is failing healthy runs
@@ -552,10 +552,11 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
552
552
  * `DurableAgentRunner.cancel` puts it — `cancelling` while a compensating cancel is in flight,
553
553
  * `cancelled` once it has settled.
554
554
  *
555
- * Costs a run read at each of the loop's observation points (two per step). That is a row the
556
- * control plane already has, against a turn that is otherwise making model calls, and it buys the
557
- * property that matters: the answer comes from a place BOTH the canceller and every pod that might
558
- * replay this run can see.
555
+ * Costs a run read at each of the loop's observation points (two per step): the run's own row
556
+ * where the gateway can read it alone, else its detail, which carries every checkpoint of the run.
557
+ * That row is one the control plane already has, against a turn that is otherwise making model
558
+ * calls, and it buys the property that matters: the answer comes from a place BOTH the canceller
559
+ * and every pod that might replay this run can see.
559
560
  *
560
561
  * Failing to ask is not a cancel. A tenant with no gateway wired, or an operator briefly
561
562
  * unreachable, answers "no" and the run carries on — the alternative is failing healthy runs
@@ -563,8 +564,10 @@ var AgentRunWorkflow = class _AgentRunWorkflow {
563
564
  * can never disagree about what was seen here.
564
565
  */
565
566
  async isCancelled(runId) {
567
+ const runs = this.runs;
566
568
  try {
567
- const status = (await this.runs?.getRunDetail(runId))?.run.status;
569
+ const run = typeof runs?.getRun === "function" ? await runs.getRun(runId) : (await runs?.getRunDetail(runId))?.run;
570
+ const status = run?.status;
568
571
  return status === "cancelled" || status === "cancelling";
569
572
  } catch {
570
573
  return false;
@@ -876,49 +879,121 @@ import { Inject as Inject3, Injectable as Injectable3, Logger as Logger2, Option
876
879
 
877
880
  // src/in-process-sink.ts
878
881
  import { AgentStreamError } from "@dudousxd/nestjs-agent-core";
882
+ var LIVE_TTL_MS = 36e5;
883
+ var ENDED_GRACE_MS = 6e4;
879
884
  var InProcessTokenStreamSink = class {
880
885
  static {
881
886
  __name(this, "InProcessTokenStreamSink");
882
887
  }
883
888
  runs = /* @__PURE__ */ new Map();
889
+ /** Subscribers of a run nothing has opened yet, woken by its `open()`. */
890
+ waiting = /* @__PURE__ */ new Map();
884
891
  buffer(runId) {
885
892
  let buf = this.runs.get(runId);
886
893
  if (buf === void 0) {
887
894
  buf = {
888
895
  chunks: [],
889
896
  ended: false,
890
- notify: /* @__PURE__ */ new Set()
897
+ notify: /* @__PURE__ */ new Set(),
898
+ lastWriteAt: Date.now(),
899
+ expiry: void 0
891
900
  };
892
901
  this.runs.set(runId, buf);
902
+ this.expireAfter(runId, buf, LIVE_TTL_MS);
903
+ const waiters = this.waiting.get(runId);
904
+ this.waiting.delete(runId);
905
+ for (const resolve2 of waiters ?? []) {
906
+ resolve2();
907
+ }
893
908
  }
894
909
  return buf;
895
910
  }
911
+ expireAfter(runId, buf, ms) {
912
+ if (buf.expiry !== void 0) {
913
+ clearTimeout(buf.expiry);
914
+ }
915
+ buf.expiry = setTimeout(() => {
916
+ buf.expiry = void 0;
917
+ const idle = Date.now() - buf.lastWriteAt;
918
+ if (!buf.ended && idle < LIVE_TTL_MS) {
919
+ this.expireAfter(runId, buf, LIVE_TTL_MS - idle);
920
+ return;
921
+ }
922
+ this.drop(runId, buf);
923
+ }, ms);
924
+ buf.expiry.unref?.();
925
+ }
926
+ /** Forget the run, and let anyone still following it finish as if it had ended. */
927
+ drop(runId, buf) {
928
+ if (buf.expiry !== void 0) {
929
+ clearTimeout(buf.expiry);
930
+ buf.expiry = void 0;
931
+ }
932
+ if (this.runs.get(runId) === buf) {
933
+ this.runs.delete(runId);
934
+ }
935
+ buf.ended = true;
936
+ this.wake(buf);
937
+ }
896
938
  wake(buf) {
897
939
  for (const resolve2 of buf.notify) {
898
940
  resolve2();
899
941
  }
900
942
  buf.notify.clear();
901
943
  }
944
+ finish(runId, buf) {
945
+ buf.ended = true;
946
+ this.wake(buf);
947
+ this.expireAfter(runId, buf, ENDED_GRACE_MS);
948
+ }
949
+ /**
950
+ * The run's writer. Each call finds the run's buffer afresh, so a run that went quiet past the
951
+ * live TTL (parked on a person) and then writes again starts a new buffer its next subscriber
952
+ * follows, rather than writing into one nothing reads.
953
+ */
902
954
  open(runId) {
903
- const buf = this.buffer(runId);
955
+ this.buffer(runId);
904
956
  return {
905
957
  write: /* @__PURE__ */ __name((chunk) => {
958
+ const buf = this.buffer(runId);
906
959
  buf.chunks.push(chunk);
960
+ buf.lastWriteAt = Date.now();
907
961
  this.wake(buf);
908
962
  }, "write"),
909
963
  end: /* @__PURE__ */ __name(() => {
910
- buf.ended = true;
911
- this.wake(buf);
964
+ this.finish(runId, this.buffer(runId));
912
965
  }, "end"),
913
966
  fail: /* @__PURE__ */ __name((error) => {
967
+ const buf = this.buffer(runId);
914
968
  buf.failure = error;
915
- buf.ended = true;
916
- this.wake(buf);
969
+ this.finish(runId, buf);
917
970
  }, "fail")
918
971
  };
919
972
  }
920
- async *subscribe(runId) {
921
- const buf = this.buffer(runId);
973
+ async *subscribe(runId, options = {}) {
974
+ const { signal } = options;
975
+ let buf = this.runs.get(runId);
976
+ while (buf === void 0) {
977
+ const waiters = this.waiting.get(runId) ?? /* @__PURE__ */ new Set();
978
+ this.waiting.set(runId, waiters);
979
+ let resolve2 = /* @__PURE__ */ __name(() => {
980
+ }, "resolve");
981
+ const opened = new Promise((r) => {
982
+ resolve2 = r;
983
+ });
984
+ waiters.add(resolve2);
985
+ try {
986
+ if (!await untilAborted(opened, signal)) {
987
+ return;
988
+ }
989
+ } finally {
990
+ waiters.delete(resolve2);
991
+ if (waiters.size === 0 && this.waiting.get(runId) === waiters) {
992
+ this.waiting.delete(runId);
993
+ }
994
+ }
995
+ buf = this.runs.get(runId);
996
+ }
922
997
  let index = 0;
923
998
  while (true) {
924
999
  while (index < buf.chunks.length) {
@@ -934,13 +1009,56 @@ var InProcessTokenStreamSink = class {
934
1009
  }
935
1010
  return;
936
1011
  }
937
- await new Promise((resolve2) => buf.notify.add(resolve2));
1012
+ const current = buf;
1013
+ let resolve2 = /* @__PURE__ */ __name(() => {
1014
+ }, "resolve");
1015
+ const woken = new Promise((r) => {
1016
+ resolve2 = r;
1017
+ });
1018
+ current.notify.add(resolve2);
1019
+ try {
1020
+ if (!await untilAborted(woken, signal)) {
1021
+ return;
1022
+ }
1023
+ } finally {
1024
+ current.notify.delete(resolve2);
1025
+ }
938
1026
  }
939
1027
  }
940
1028
  close(runId) {
1029
+ const buf = this.runs.get(runId);
1030
+ if (buf?.expiry !== void 0) {
1031
+ clearTimeout(buf.expiry);
1032
+ }
941
1033
  this.runs.delete(runId);
942
1034
  }
943
1035
  };
1036
+ async function untilAborted(promise, signal) {
1037
+ if (signal === void 0) {
1038
+ await promise;
1039
+ return true;
1040
+ }
1041
+ if (signal.aborted) {
1042
+ return false;
1043
+ }
1044
+ let onAbort = /* @__PURE__ */ __name(() => {
1045
+ }, "onAbort");
1046
+ const aborted = new Promise((resolve2) => {
1047
+ onAbort = /* @__PURE__ */ __name(() => resolve2(false), "onAbort");
1048
+ signal.addEventListener("abort", onAbort, {
1049
+ once: true
1050
+ });
1051
+ });
1052
+ try {
1053
+ return await Promise.race([
1054
+ promise.then(() => true),
1055
+ aborted
1056
+ ]);
1057
+ } finally {
1058
+ signal.removeEventListener("abort", onAbort);
1059
+ }
1060
+ }
1061
+ __name(untilAborted, "untilAborted");
944
1062
 
945
1063
  // src/durable/durable-agent-runner.ts
946
1064
  function _ts_decorate3(decorators, target, key, desc) {
@@ -2816,6 +2934,26 @@ var RunNotActiveException = class extends ConflictException {
2816
2934
  }
2817
2935
  };
2818
2936
 
2937
+ // src/thread-reads.ts
2938
+ function threadReads(store, threadId, separately) {
2939
+ const projecting = store;
2940
+ if (typeof projecting.threadSettings !== "function") {
2941
+ return separately;
2942
+ }
2943
+ let settings;
2944
+ const read = /* @__PURE__ */ __name(() => {
2945
+ settings ??= projecting.threadSettings?.(threadId) ?? Promise.resolve(null);
2946
+ return settings;
2947
+ }, "read");
2948
+ return {
2949
+ owner: /* @__PURE__ */ __name(async () => (await read())?.actorRef ?? null, "owner"),
2950
+ defaultAgent: /* @__PURE__ */ __name(async () => (await read())?.defaultAgent ?? null, "defaultAgent"),
2951
+ persona: /* @__PURE__ */ __name(async () => (await read())?.persona ?? null, "persona"),
2952
+ model: /* @__PURE__ */ __name(async () => (await read())?.model ?? null, "model")
2953
+ };
2954
+ }
2955
+ __name(threadReads, "threadReads");
2956
+
2819
2957
  // src/agent.service.ts
2820
2958
  function _ts_decorate9(decorators, target, key, desc) {
2821
2959
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -3079,7 +3217,7 @@ var AgentService = class {
3079
3217
  * else the thread's pinned one, else none. An agent the catalog locks to one model runs on it
3080
3218
  * whatever was sent or pinned, and a send naming another model is refused.
3081
3219
  */
3082
- async resolveModel(actor, agent, requested, threadId) {
3220
+ async resolveModel(actor, agent, requested, threadId, reads) {
3083
3221
  const locked = this.models !== void 0 ? (await this.models.list({
3084
3222
  actor,
3085
3223
  agent
@@ -3090,7 +3228,7 @@ var AgentService = class {
3090
3228
  }
3091
3229
  return locked.model;
3092
3230
  }
3093
- const model = requested ?? (threadId !== void 0 ? await this.threadModel(threadId) : null);
3231
+ const model = requested ?? (threadId !== void 0 ? await (reads?.model() ?? this.threadModel(threadId)) : null);
3094
3232
  if (model === null || model === void 0) {
3095
3233
  return void 0;
3096
3234
  }
@@ -3150,10 +3288,11 @@ var AgentService = class {
3150
3288
  if ("proposalDecision" in decision) return decision;
3151
3289
  }
3152
3290
  await this.assertWithinQuota(params.actor);
3153
- const target = this.deps.resolveAgent(await this.resolveAgentName(params.agentName, params.threadId));
3291
+ const reads = params.threadId !== void 0 ? this.threadReads(params.threadId) : void 0;
3292
+ const target = this.deps.resolveAgent(await this.resolveAgentName(params.agentName, params.threadId, reads));
3154
3293
  const agentName = target.agentName;
3155
- const persona = await this.resolvePersona(agentName, params.personaId ?? target.persona, params.threadId);
3156
- const model = await this.resolveModel(params.actor, agentName, params.model, params.threadId);
3294
+ const persona = await this.resolvePersona(agentName, params.personaId ?? target.persona, params.threadId, reads);
3295
+ const model = await this.resolveModel(params.actor, agentName, params.model, params.threadId, reads);
3157
3296
  const attachments = await this.resolveAttachments(params.actor, params.attachments ?? []);
3158
3297
  let threadId = params.threadId;
3159
3298
  if (threadId === void 0) {
@@ -3177,9 +3316,9 @@ var AgentService = class {
3177
3316
  });
3178
3317
  threadId = created.id;
3179
3318
  } else {
3180
- if (params.authorized !== true) await this.assertOwnsThread(params.actor, threadId);
3319
+ if (params.authorized !== true) await this.assertOwnsThread(params.actor, threadId, reads);
3181
3320
  if (params.personaId !== void 0) {
3182
- await this.pinThreadPersona(threadId, params.personaId);
3321
+ await this.pinThreadPersona(threadId, params.personaId, reads);
3183
3322
  }
3184
3323
  }
3185
3324
  const input = {
@@ -3505,8 +3644,8 @@ var AgentService = class {
3505
3644
  * {@link subscribeAs} instead: a runId is the only thing standing between a caller and another
3506
3645
  * actor's answer, tool arguments and tool results.
3507
3646
  */
3508
- subscribe(runId) {
3509
- return this.deps.forAgent().sink.subscribe(runId);
3647
+ subscribe(runId, options) {
3648
+ return this.deps.forAgent().sink.subscribe(runId, options);
3510
3649
  }
3511
3650
  /**
3512
3651
  * {@link subscribe}, gated the way {@link cancel} is: only the actor whose thread is currently
@@ -3514,9 +3653,9 @@ var AgentService = class {
3514
3653
  * replayed — the runner clears the thread's active stream as the run ends, and that field is what
3515
3654
  * ownership is derived from.
3516
3655
  */
3517
- async subscribeAs(actor, runId) {
3656
+ async subscribeAs(actor, runId, options) {
3518
3657
  await this.assertOwnsActiveStream(actor, runId);
3519
- return this.subscribe(runId);
3658
+ return this.subscribe(runId, options);
3520
3659
  }
3521
3660
  /**
3522
3661
  * Approve a parked action call as `actor`. Who may is the call's recorded approver's business
@@ -3723,7 +3862,7 @@ var AgentService = class {
3723
3862
  * The persona this send runs under — see {@link AgentDepsFactory.resolvePersona}. The thread's
3724
3863
  * pinned persona is read only when the send names none AND the agent has personas to pick from.
3725
3864
  */
3726
- async resolvePersona(agentName, requested, threadId) {
3865
+ async resolvePersona(agentName, requested, threadId, reads) {
3727
3866
  const needsThread = requested === void 0 && threadId !== void 0 && this.deps.personaCatalog(agentName).length > 0;
3728
3867
  return this.deps.resolvePersona({
3729
3868
  agentName,
@@ -3731,16 +3870,16 @@ var AgentService = class {
3731
3870
  requested
3732
3871
  } : {},
3733
3872
  ...needsThread ? {
3734
- threadPersona: await threadPersona(this.store, threadId)
3873
+ threadPersona: await (reads?.persona() ?? threadPersona(this.store, threadId))
3735
3874
  } : {}
3736
3875
  });
3737
3876
  }
3738
3877
  /** Pin `persona` on the thread when it is not already — a no-op on a store that cannot. */
3739
- async pinThreadPersona(threadId, persona) {
3878
+ async pinThreadPersona(threadId, persona, reads) {
3740
3879
  if (this.store.updateThread === void 0) {
3741
3880
  return;
3742
3881
  }
3743
- if (await threadPersona(this.store, threadId) !== persona) {
3882
+ if (await (reads?.persona() ?? threadPersona(this.store, threadId)) !== persona) {
3744
3883
  await this.store.updateThread(threadId, {
3745
3884
  persona
3746
3885
  });
@@ -3767,18 +3906,27 @@ var AgentService = class {
3767
3906
  * workflow, for a string. A store predating {@link ThreadDefaultAgentReader} still answers, via
3768
3907
  * the full read.
3769
3908
  */
3770
- async resolveAgentName(agentName, threadId) {
3909
+ async resolveAgentName(agentName, threadId, reads) {
3771
3910
  if (agentName !== void 0) {
3772
3911
  return agentName;
3773
3912
  }
3774
3913
  if (threadId !== void 0) {
3775
- const threadDefault = await this.threadDefaultAgent(threadId);
3914
+ const threadDefault = await (reads?.defaultAgent() ?? this.threadDefaultAgent(threadId));
3776
3915
  if (threadDefault !== null) {
3777
3916
  return threadDefault;
3778
3917
  }
3779
3918
  }
3780
3919
  return this.deps.defaultAgentName();
3781
3920
  }
3921
+ /** Everything a send reads off `threadId`, in one read where the store allows it. */
3922
+ threadReads(threadId) {
3923
+ return threadReads(this.store, threadId, {
3924
+ owner: /* @__PURE__ */ __name(() => this.store.ownerOfThread(threadId), "owner"),
3925
+ defaultAgent: /* @__PURE__ */ __name(() => this.threadDefaultAgent(threadId), "defaultAgent"),
3926
+ persona: /* @__PURE__ */ __name(() => threadPersona(this.store, threadId), "persona"),
3927
+ model: /* @__PURE__ */ __name(() => this.threadModel(threadId), "model")
3928
+ });
3929
+ }
3782
3930
  async threadDefaultAgent(threadId) {
3783
3931
  const projecting = this.store;
3784
3932
  if (typeof projecting.defaultAgentForThread === "function") {
@@ -3948,8 +4096,8 @@ var AgentService = class {
3948
4096
  * Authorization seam for thread-scoped endpoints: the caller must own the thread. A missing
3949
4097
  * thread is a 404; someone else's thread is a 403 — one actor never touches another's thread.
3950
4098
  */
3951
- async assertOwnsThread(actor, threadId) {
3952
- const owner = await this.store.ownerOfThread(threadId);
4099
+ async assertOwnsThread(actor, threadId, reads) {
4100
+ const owner = await (reads?.owner() ?? this.store.ownerOfThread(threadId));
3953
4101
  if (owner === null) {
3954
4102
  throw new NotFoundException2(`thread ${threadId} not found`);
3955
4103
  }
@@ -4389,6 +4537,18 @@ AttachmentsController = _ts_decorate12([
4389
4537
  import { AGENT_ACTOR_RESOLVER as AGENT_ACTOR_RESOLVER2, AgentStreamError as AgentStreamError2, streamFailure as streamFailure2 } from "@dudousxd/nestjs-agent-core";
4390
4538
  import { validateUiCapabilities } from "@dudousxd/nestjs-agent-core/genui";
4391
4539
  import { BadRequestException as BadRequestException4, Body, Controller as Controller3, Get as Get3, Inject as Inject11, Param, Post as Post2, Query, Req as Req2, Res } from "@nestjs/common";
4540
+
4541
+ // src/controller/reader-gone.ts
4542
+ function readerGone(res) {
4543
+ const controller = new AbortController();
4544
+ res.once("close", () => controller.abort());
4545
+ return {
4546
+ signal: controller.signal
4547
+ };
4548
+ }
4549
+ __name(readerGone, "readerGone");
4550
+
4551
+ // src/controller/chat.controller.ts
4392
4552
  function _ts_decorate13(decorators, target, key, desc) {
4393
4553
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4394
4554
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -4522,7 +4682,8 @@ var ChatController = class {
4522
4682
  return;
4523
4683
  }
4524
4684
  const { runId, threadId } = result;
4525
- await this.pipe(res, runId, this.agent.subscribe(runId), threadId);
4685
+ const gone = readerGone(res);
4686
+ await this.pipe(res, runId, this.agent.subscribe(runId, gone), threadId, 0, gone.signal);
4526
4687
  }
4527
4688
  /**
4528
4689
  * Attach to a run's stream: every buffered frame, then live ones. `?after=<seq>` (or the SSE
@@ -4532,7 +4693,8 @@ var ChatController = class {
4532
4693
  async stream(req, runId, res, after) {
4533
4694
  const actor = await this.actorResolver.resolve(req);
4534
4695
  const cursor = parseCursor(after) ?? parseCursor(req.headers["last-event-id"]) ?? 0;
4535
- await this.pipe(res, runId, await this.agent.subscribeAs(actor, runId), void 0, cursor);
4696
+ const gone = readerGone(res);
4697
+ await this.pipe(res, runId, await this.agent.subscribeAs(actor, runId, gone), void 0, cursor, gone.signal);
4536
4698
  }
4537
4699
  async cancel(req, runId) {
4538
4700
  const actor = await this.actorResolver.resolve(req);
@@ -4548,8 +4710,11 @@ var ChatController = class {
4548
4710
  * and on any later `GET …/stream`, whichever replica serves it. A reconnecting client passes the
4549
4711
  * last one it saw as `?after=`; frames at or below it are read off the sink but not re-sent.
4550
4712
  * `meta`, `done` and `error` carry no id: `meta` is re-sent on every attach, the terminals once.
4713
+ *
4714
+ * A reader that disconnects aborts `gone`, which the sink honours by ending the subscription even
4715
+ * while the run is silent (parked on a person, or never writing again).
4551
4716
  */
4552
- async pipe(res, runId, events, threadId, after = 0) {
4717
+ async pipe(res, runId, events, threadId, after = 0, gone) {
4553
4718
  res.setHeader("Content-Type", "text/event-stream");
4554
4719
  res.setHeader("Cache-Control", "no-cache");
4555
4720
  res.setHeader("Connection", "keep-alive");
@@ -4578,6 +4743,9 @@ data: ${line}
4578
4743
  }, "forward");
4579
4744
  try {
4580
4745
  for await (const chunk of events) {
4746
+ if (gone?.aborted) {
4747
+ return;
4748
+ }
4581
4749
  buffer += decoder.decode(chunk, {
4582
4750
  stream: true
4583
4751
  });
@@ -4591,11 +4759,17 @@ data: ${line}
4591
4759
  newline = buffer.indexOf("\n");
4592
4760
  }
4593
4761
  }
4762
+ if (gone?.aborted) {
4763
+ return;
4764
+ }
4594
4765
  if (buffer.length > 0) {
4595
4766
  forward(buffer);
4596
4767
  }
4597
4768
  res.write("event: done\ndata: {}\n\n");
4598
4769
  } catch (error) {
4770
+ if (gone?.aborted) {
4771
+ return;
4772
+ }
4599
4773
  const payload = error instanceof AgentStreamError2 ? {
4600
4774
  code: error.code,
4601
4775
  message: error.message
@@ -6496,13 +6670,22 @@ var LedgerQuotaProvider = class {
6496
6670
  async report(query) {
6497
6671
  const now = query.now ?? /* @__PURE__ */ new Date();
6498
6672
  const actorRef = query.actor.id;
6499
- const windows = [
6500
- await this.dayWindow(actorRef, now)
6501
- ];
6502
- if (this.store.usageBetween !== void 0) {
6503
- const range = quotaPeriodRange("month", now);
6504
- const used = await this.store.usageBetween(actorRef, range.fromDay, range.toDay);
6505
- windows.push(this.window("month", used.usedTokens, this.usd(used), range.resetsAt));
6673
+ const day = quotaPeriodRange("day", now);
6674
+ const month = quotaPeriodRange("month", now);
6675
+ const windows = [];
6676
+ if (this.store.usageWindows !== void 0) {
6677
+ const [dayUsed, monthUsed] = await this.store.usageWindows(actorRef, [
6678
+ day,
6679
+ month
6680
+ ]);
6681
+ windows.push(this.usedWindow("day", dayUsed, day.resetsAt));
6682
+ windows.push(this.usedWindow("month", monthUsed, month.resetsAt));
6683
+ } else {
6684
+ windows.push(this.usedWindow("day", await this.store.quotaToday(actorRef, day.fromDay), day.resetsAt));
6685
+ if (this.store.usageBetween !== void 0) {
6686
+ const used = await this.store.usageBetween(actorRef, month.fromDay, month.toDay);
6687
+ windows.push(this.usedWindow("month", used, month.resetsAt));
6688
+ }
6506
6689
  }
6507
6690
  const blocked = exhaustedWindow(windows);
6508
6691
  const warning = quotaWarning(windows);
@@ -6516,10 +6699,8 @@ var LedgerQuotaProvider = class {
6516
6699
  } : {}
6517
6700
  };
6518
6701
  }
6519
- async dayWindow(actorRef, now) {
6520
- const range = quotaPeriodRange("day", now);
6521
- const used = await this.store.quotaToday(actorRef, range.fromDay);
6522
- return this.window("day", used.usedTokens, this.usd(used), range.resetsAt);
6702
+ usedWindow(period, used, resetsAt) {
6703
+ return this.window(period, used?.usedTokens ?? 0, used === void 0 ? 0 : this.usd(used), resetsAt);
6523
6704
  }
6524
6705
  /** The window's USD: every priced row, or (with `countEstimatedCost: false`) reported cost only. */
6525
6706
  usd(used) {