@skaile/workspaces 6.11.0 → 6.12.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.
@@ -1,5 +1,5 @@
1
- import { mentionsAgent } from '../chunk-F2IBDVBP.js';
2
- export { CHANNEL_PLATFORMS, makeConversationKey, makeQuestionCard, markdownToTeamsText, mentionsAgent, normalizeMentions, parseConversationKey, teamsFormatConverter } from '../chunk-F2IBDVBP.js';
1
+ import { mentionsAgent } from '../chunk-VHRGCLCR.js';
2
+ export { CHANNEL_PLATFORMS, makeConversationKey, makeQuestionCard, markdownToTeamsText, mentionsAgent, normalizeMentions, parseConversationKey, teamsFormatConverter } from '../chunk-VHRGCLCR.js';
3
3
  import '../chunk-NSBPE2FW.js';
4
4
  import { ulid } from 'ulid';
5
5
 
@@ -10,6 +10,100 @@ var consoleChannelLogger = {
10
10
  error: (m, e, d) => console.error(`[channels] ${m}`, e ?? "", d ?? "")
11
11
  };
12
12
 
13
+ // channels/src/context-batch.ts
14
+ var DEFAULT_FLUSH_AFTER_MS = 3e4;
15
+ var DEFAULT_MAX_BATCH = 20;
16
+ var DEFAULT_HOURLY_CAP = 200;
17
+ var HOUR_MS = 36e5;
18
+ var ContextBatcher = class {
19
+ flushAfterMs;
20
+ maxBatch;
21
+ hourlyCap;
22
+ now;
23
+ onDue;
24
+ logger;
25
+ conversationKey;
26
+ pending = [];
27
+ timer;
28
+ windowStart = Number.NEGATIVE_INFINITY;
29
+ acceptedInWindow = 0;
30
+ droppedInWindow = 0;
31
+ constructor(opts) {
32
+ this.flushAfterMs = opts.options?.flushAfterMs ?? DEFAULT_FLUSH_AFTER_MS;
33
+ this.maxBatch = opts.options?.maxBatch ?? DEFAULT_MAX_BATCH;
34
+ this.hourlyCap = opts.options?.hourlyCap ?? DEFAULT_HOURLY_CAP;
35
+ this.now = opts.now;
36
+ this.onDue = opts.onDue;
37
+ this.logger = opts.logger;
38
+ this.conversationKey = opts.conversationKey;
39
+ }
40
+ /** Messages waiting for the next flush. */
41
+ get size() {
42
+ return this.pending.length;
43
+ }
44
+ /** Buffers `msg` unless the hourly cap is spent. `"full"` means flush now. */
45
+ add(msg) {
46
+ this.rollWindow();
47
+ if (this.acceptedInWindow >= this.hourlyCap) {
48
+ this.droppedInWindow += 1;
49
+ if (this.droppedInWindow === 1) {
50
+ this.logger.warn(
51
+ "read-along context cap reached; dropping context for the rest of the hour",
52
+ {
53
+ conversationKey: this.conversationKey,
54
+ hourlyCap: this.hourlyCap
55
+ }
56
+ );
57
+ }
58
+ return "dropped";
59
+ }
60
+ this.acceptedInWindow += 1;
61
+ this.pending.push(msg);
62
+ if (this.pending.length >= this.maxBatch) return "full";
63
+ if (this.timer === void 0) {
64
+ this.timer = setTimeout(() => {
65
+ this.timer = void 0;
66
+ this.onDue();
67
+ }, this.flushAfterMs);
68
+ }
69
+ return "buffered";
70
+ }
71
+ /** Drains the pending batch (possibly empty) and cancels its timer. */
72
+ take() {
73
+ this.clearTimer();
74
+ const batch = this.pending;
75
+ this.pending = [];
76
+ return batch;
77
+ }
78
+ /** Cancels the timer and logs any drops still unreported. Pending messages are left to `take()`. */
79
+ dispose() {
80
+ this.clearTimer();
81
+ this.reportDropped();
82
+ }
83
+ rollWindow() {
84
+ const now = this.now();
85
+ if (now - this.windowStart < HOUR_MS) return;
86
+ this.reportDropped();
87
+ this.windowStart = now;
88
+ this.acceptedInWindow = 0;
89
+ this.droppedInWindow = 0;
90
+ }
91
+ reportDropped() {
92
+ if (this.droppedInWindow === 0) return;
93
+ this.logger.warn("read-along context dropped over the hourly cap", {
94
+ conversationKey: this.conversationKey,
95
+ dropped: this.droppedInWindow,
96
+ hourlyCap: this.hourlyCap
97
+ });
98
+ this.droppedInWindow = 0;
99
+ }
100
+ clearTimer() {
101
+ if (this.timer === void 0) return;
102
+ clearTimeout(this.timer);
103
+ this.timer = void 0;
104
+ }
105
+ };
106
+
13
107
  // channels/src/dedupe.ts
14
108
  var TtlDedupeStore = class {
15
109
  ttlMs;
@@ -434,6 +528,9 @@ var ChunkedMessageStream = class {
434
528
 
435
529
  // channels/src/egress/output-mirror.ts
436
530
  var TYPING_THROTTLE_MS = 3e3;
531
+ function endsTurn(e) {
532
+ return e.type === "finished" || e.type === "status" && e.phase === "idle";
533
+ }
437
534
  var OutputMirror = class {
438
535
  opts;
439
536
  turn;
@@ -463,6 +560,9 @@ var OutputMirror = class {
463
560
  case "error":
464
561
  this.postAside({ markdown: e.message, kind: "error" });
465
562
  break;
563
+ case "status":
564
+ if (endsTurn(e)) this.finishTurn();
565
+ break;
466
566
  case "typing":
467
567
  this.onTyping(e.isTyping);
468
568
  break;
@@ -635,6 +735,7 @@ function inboundMessageRef(ch) {
635
735
  };
636
736
  }
637
737
  var textOf = (msg) => msg.payload.type === "prompt" ? msg.payload.prompt : "";
738
+ var isMentioned = (msg, ch) => ch.mentioned ?? mentionsAgent(msg.meta?.mentions ?? []);
638
739
  function withSenderId(msg, senderId) {
639
740
  return { ...msg, meta: { ...msg.meta, senderId } };
640
741
  }
@@ -831,10 +932,10 @@ var ChannelEngine = class {
831
932
  tenantId: ch.tenantId
832
933
  };
833
934
  const host = await ports.identity.resolveActor(actor);
935
+ const mentioned = isMentioned(evt, ch);
834
936
  let binding = await ports.bindings.get(ch.conversationKey);
835
937
  if (!binding?.enabled) {
836
- const mentions = evt.meta?.mentions ?? [];
837
- if (!mentionsAgent(mentions) || !ports.session.createSession) return;
938
+ if (!mentioned || !ports.session.createSession) return;
838
939
  if (host === null && this.unmappedActorPolicy === "reject") {
839
940
  await this.postRejectNotice(adapter, ch, actor);
840
941
  return;
@@ -851,13 +952,17 @@ var ChannelEngine = class {
851
952
  sessionId: ref.sessionId,
852
953
  platform: ch.platform,
853
954
  tenantId: ch.tenantId,
854
- mirrorMode: "agent-only",
955
+ mirrorMode: this.opts.lazyBindMirrorMode ?? "agent-only",
855
956
  enabled: true,
856
957
  createdBy: host?.id ?? `external:${actor.externalId}`,
857
958
  createdAt: new Date(this.now()).toISOString()
858
959
  };
859
960
  await ports.bindings.put(binding);
860
961
  }
962
+ if (!mentioned && ports.session.appendContext) {
963
+ await this.appendAsContext(binding, evt, actor, host);
964
+ return;
965
+ }
861
966
  let delivery = evt;
862
967
  if (host === null) {
863
968
  if (this.unmappedActorPolicy === "reject") {
@@ -870,11 +975,41 @@ var ChannelEngine = class {
870
975
  }
871
976
  this.ensureMirror(binding);
872
977
  const state = this.stateFor(binding);
978
+ await this.flushContext(state);
873
979
  state.delivered.set(textOf(delivery), true);
874
980
  state.chatInitiated = true;
875
981
  await ports.session.deliver(binding, delivery);
876
982
  this.armPlaceholder(binding, state);
877
983
  }
984
+ /**
985
+ * Read-along: queues a message that did not mention the bot as session
986
+ * context. It never starts a turn, so an unmapped actor needs no reject
987
+ * notice: the message is framed as untrusted and carries no senderId.
988
+ */
989
+ async appendAsContext(binding, evt, actor, host) {
990
+ if (textOf(evt).trim() === "") return;
991
+ const msg = host === null ? frameAsExternal(evt, actor) : withSenderId(evt, host.id);
992
+ this.ensureMirror(binding);
993
+ const state = this.stateFor(binding);
994
+ const added = state.context.add(msg);
995
+ if (added === "dropped") return;
996
+ state.delivered.set(textOf(msg), true);
997
+ if (added === "full") await this.flushContext(state);
998
+ }
999
+ /** Hands the pending read-along batch to `SessionPort.appendContext`; logs, never throws. */
1000
+ async flushContext(state) {
1001
+ const appendContext = this.opts.ports.session.appendContext;
1002
+ const batch = state.context.take();
1003
+ if (batch.length === 0 || !appendContext) return;
1004
+ try {
1005
+ await appendContext.call(this.opts.ports.session, state.binding, batch);
1006
+ } catch (err) {
1007
+ this.logger.error("appendContext failed; read-along context lost", err, {
1008
+ conversationKey: state.binding.conversationKey,
1009
+ messages: batch.length
1010
+ });
1011
+ }
1012
+ }
878
1013
  /** "Teams: <snippet>" — title for a lazily-created thread session. */
879
1014
  sessionTitle(ch, text) {
880
1015
  const snippet = text.trim().slice(0, 60) || ch.conversationKey;
@@ -898,24 +1033,34 @@ var ChannelEngine = class {
898
1033
  this.egressRefs.set(egressRefKey(ref), true);
899
1034
  }
900
1035
  stateFor(binding) {
901
- let state = this.states.get(binding.conversationKey);
902
- if (!state) {
903
- state = {
904
- binding,
905
- chatInitiated: false,
906
- delivered: new TtlCache({
907
- ttlMs: DELIVERED_CONTENT_TTL_MS,
908
- maxEntries: DELIVERED_CONTENT_MAX_ENTRIES,
909
- now: this.now
910
- }),
911
- replyRefs: new TtlCache({
912
- ttlMs: REPLY_REF_TTL_MS,
913
- maxEntries: REPLY_REF_MAX_ENTRIES,
914
- now: this.now
915
- })
916
- };
917
- this.states.set(binding.conversationKey, state);
918
- }
1036
+ const existing = this.states.get(binding.conversationKey);
1037
+ if (existing) return existing;
1038
+ const key = binding.conversationKey;
1039
+ const state = {
1040
+ binding,
1041
+ chatInitiated: false,
1042
+ delivered: new TtlCache({
1043
+ ttlMs: DELIVERED_CONTENT_TTL_MS,
1044
+ maxEntries: DELIVERED_CONTENT_MAX_ENTRIES,
1045
+ now: this.now
1046
+ }),
1047
+ replyRefs: new TtlCache({
1048
+ ttlMs: REPLY_REF_TTL_MS,
1049
+ maxEntries: REPLY_REF_MAX_ENTRIES,
1050
+ now: this.now
1051
+ }),
1052
+ context: new ContextBatcher({
1053
+ options: this.opts.contextBatch,
1054
+ conversationKey: key,
1055
+ now: this.now,
1056
+ logger: this.logger,
1057
+ // Queued on the conversation's chain, so a timed flush never overtakes a prompt.
1058
+ onDue: () => {
1059
+ void this.enqueueOnKey(key, () => this.flushContext(state));
1060
+ }
1061
+ })
1062
+ };
1063
+ this.states.set(key, state);
919
1064
  return state;
920
1065
  }
921
1066
  /** Idempotent: one OutputMirror + session subscription per bound conversation. */
@@ -943,7 +1088,9 @@ var ChannelEngine = class {
943
1088
  }
944
1089
  /** Engine-side filtering in front of the mirror: placeholder, loop guard, turn origin. */
945
1090
  onAgentEvent(state, mirror, e) {
946
- if (e.type === "text" || e.type === "finished") this.disarmPlaceholder(state);
1091
+ if (e.type === "text" || e.type === "finished" || e.type === "error") {
1092
+ this.disarmPlaceholder(state);
1093
+ }
947
1094
  if (e.type === "user_message" && state.delivered.has(e.content)) return;
948
1095
  if (state.binding.mirrorMode === "chat-initiated" && !state.chatInitiated && (e.type === "text" || e.type === "text_retract" || e.type === "question" || e.type === "error" || e.type === "typing")) {
949
1096
  return;
@@ -993,8 +1140,10 @@ var ChannelEngine = class {
993
1140
  });
994
1141
  }
995
1142
  }
996
- /** Disarm, unsubscribe, and flush/dispose the mirror of one binding. */
1143
+ /** Flush pending context, disarm, unsubscribe, and flush/dispose the mirror of one binding. */
997
1144
  async teardownState(state) {
1145
+ await this.flushContext(state);
1146
+ state.context.dispose();
998
1147
  this.disarmPlaceholder(state);
999
1148
  state.unsubscribe?.();
1000
1149
  state.unsubscribe = void 0;
@@ -1088,6 +1237,6 @@ function buildLookupUserTool(adapter, renderer) {
1088
1237
  }
1089
1238
  var LOOKUP_USER_PROMPT_FRAGMENT = "To @-mention a person in the chat channel, first call the lookup_user tool with their name, email, or id, then paste the returned mentionToken into your message verbatim \u2014 do not alter or re-format it. If lookup_user finds nobody, refer to the person by plain name instead.";
1090
1239
 
1091
- export { ChannelEngine, LOOKUP_USER_PROMPT_FRAGMENT, OutputMirror, REJECT_NOTICE, TtlCache, TtlDedupeStore, buildLookupUserTool, consoleChannelLogger, mrkdwnFormatConverter };
1240
+ export { ChannelEngine, ContextBatcher, LOOKUP_USER_PROMPT_FRAGMENT, OutputMirror, REJECT_NOTICE, TtlCache, TtlDedupeStore, buildLookupUserTool, consoleChannelLogger, endsTurn, mrkdwnFormatConverter };
1092
1241
  //# sourceMappingURL=index.js.map
1093
1242
  //# sourceMappingURL=index.js.map