@atlaskit/collab-provider 21.0.29 → 21.1.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @atlaskit/collab-provider
2
2
 
3
+ ## 21.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 21.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`895ac23f1a3be`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/895ac23f1a3be) -
14
+ Agent edit presence: surface agent-authored collaborative edits to other users.
15
+ - `collab-provider` detects agent-authored remote steps (`agentType`/`agentId`) and registers a
16
+ synthetic agent participant in the AI-provider (`agent:`) partition so the agent appears in
17
+ presence, with a 30s sliding inactivity window (CCI-18030). `editor-common` gains optional
18
+ `agentId`/`agentType` on the collab step types.
19
+ - `editor-plugin-collab-edit` + `editor-core` add the in-editor shimmer (CCI-18033): when an
20
+ agent-authored step lands, the top-level block(s) it touched are covered by a skeleton-loader
21
+ shimmer (grey skeleton with a moving highlight) with a Rovo agent telepointer/cursor — labelled
22
+ with the agent's type — at the end of the range, then removed on a timer to reveal the content.
23
+ Gated behind the default-OFF `platform_editor_agent_be_streaming` experiment; the `durationMs`
24
+ dynamic-config param controls how long the shimmer stays (0 disables it) and
25
+ `telepointerDisabled` hides the telepointer. `tmp-editor-statsig` registers the experiment.
26
+ - `ratcheting` excludes the new `agentShimmerStyles.ts` from the "No unsafe typography" rule (the
27
+ agent telepointer label mirrors the existing AI in-editor telepointer's sub-token 10px/9px
28
+ sizing, matching its already-excluded Compiled counterpart).
29
+
30
+ ### Patch Changes
31
+
32
+ - Updated dependencies
33
+
3
34
  ## 21.0.29
4
35
 
5
36
  ### Patch Changes
@@ -27,6 +27,9 @@ var _customErrors = require("../errors/custom-errors");
27
27
  var _catchupv = require("./catchupv2");
28
28
  var _stepQueueState = require("./step-queue-state");
29
29
  var _getConflictChanges = require("./getConflictChanges");
30
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
31
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
32
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
30
33
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
31
34
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
32
35
  // oxlint-disable-next-line @atlassian/no-restricted-imports
@@ -921,6 +924,9 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
921
924
  this.stepRejectCounter = 0;
922
925
  this.participantsService.emitTelepointersFromSteps(steps);
923
926
 
927
+ // Surface agent edit presence from agent-authored steps.
928
+ this.maybeAddAgentPresenceFromSteps(steps);
929
+
924
930
  // Resend local steps if none of the received steps originated with us!
925
931
  // Ignored via go/ees005
926
932
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -940,6 +946,52 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
940
946
  }
941
947
  }
942
948
  }
949
+
950
+ /**
951
+ * Detect agent-authored steps in a received batch and register
952
+ * each distinct agent as a local participant so it appears in the presence facepile.
953
+ *
954
+ * Gated behind a default-OFF feature gate: `collab-provider` is shared platform infra (used by
955
+ * Jira JWM/JPD etc.), so this is a strict no-op everywhere unless the gate is explicitly enabled.
956
+ *
957
+ * Isolated in its own try/catch so a presence failure can never affect step persistence.
958
+ *
959
+ * Known limitation (PoC): steps replayed via catch-up/reconnect can briefly re-surface an agent
960
+ * whose activity is stale — a freshness guard is a follow-up.
961
+ * @example
962
+ */
963
+ }, {
964
+ key: "maybeAddAgentPresenceFromSteps",
965
+ value: function maybeAddAgentPresenceFromSteps(steps) {
966
+ var _this3 = this;
967
+ if (!(0, _expValEquals.expValEquals)('platform_editor_agent_be_streaming', 'isEnabled', true)) {
968
+ return;
969
+ }
970
+ try {
971
+ var providerIds = new Set();
972
+ var _iterator = _createForOfIteratorHelper(steps),
973
+ _step;
974
+ try {
975
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
976
+ var step = _step.value;
977
+ var providerId = (0, _utils.getAgentProviderId)(step);
978
+ if (providerId) {
979
+ providerIds.add(providerId);
980
+ }
981
+ }
982
+ } catch (err) {
983
+ _iterator.e(err);
984
+ } finally {
985
+ _iterator.f();
986
+ }
987
+ providerIds.forEach(function (providerId) {
988
+ return _this3.participantsService.upsertAIProviderParticipantLocally(providerId);
989
+ });
990
+ } catch (error) {
991
+ var _this$analyticsHelper34;
992
+ (_this$analyticsHelper34 = this.analyticsHelper) === null || _this$analyticsHelper34 === void 0 || _this$analyticsHelper34.sendErrorEvent(error, 'Error while adding agent presence from steps');
993
+ }
994
+ }
943
995
  }, {
944
996
  key: "getIsNamespaceLocked",
945
997
  value:
@@ -987,8 +1039,8 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
987
1039
  var _this$getState0;
988
1040
  var state = (_this$getState0 = this.getState) === null || _this$getState0 === void 0 ? void 0 : _this$getState0.call(this);
989
1041
  if (!state) {
990
- var _this$analyticsHelper34;
991
- (_this$analyticsHelper34 = this.analyticsHelper) === null || _this$analyticsHelper34 === void 0 || _this$analyticsHelper34.sendErrorEvent(new Error('Editor state is undefined'), 'sendStepsFromCurrentState called without state');
1042
+ var _this$analyticsHelper35;
1043
+ (_this$analyticsHelper35 = this.analyticsHelper) === null || _this$analyticsHelper35 === void 0 || _this$analyticsHelper35.sendErrorEvent(new Error('Editor state is undefined'), 'sendStepsFromCurrentState called without state');
992
1044
  return;
993
1045
  }
994
1046
  this.send(null, null, state, sendAnalyticsEvent, reason);
@@ -1010,7 +1062,7 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
1010
1062
  ) {
1011
1063
  var _this$getState1,
1012
1064
  _this$getState10,
1013
- _this3 = this;
1065
+ _this4 = this;
1014
1066
  var offlineEditingEnabled = (0, _experiments.editorExperiment)('platform_editor_offline_editing_web', true);
1015
1067
  var onlineStepMergingEnabled = (0, _expValEquals.expValEquals)('platform_editor_enable_single_player_step_merging', 'isEnabled', true);
1016
1068
  // We don't trust `_newState` not to be stale
@@ -1039,8 +1091,8 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
1039
1091
  // to ensure that analytics events with the number of unconfirmed steps is only
1040
1092
  // sent once on connection (as opposed to on every step)
1041
1093
  if (sendAnalyticsEvent) {
1042
- var _this$analyticsHelper35;
1043
- (_this$analyticsHelper35 = this.analyticsHelper) === null || _this$analyticsHelper35 === void 0 || _this$analyticsHelper35.sendActionEvent(_const.EVENT_ACTION.HAS_UNCONFIRMED_STEPS, _const.EVENT_STATUS.INFO, {
1094
+ var _this$analyticsHelper36;
1095
+ (_this$analyticsHelper36 = this.analyticsHelper) === null || _this$analyticsHelper36 === void 0 || _this$analyticsHelper36.sendActionEvent(_const.EVENT_ACTION.HAS_UNCONFIRMED_STEPS, _const.EVENT_STATUS.INFO, {
1044
1096
  numUnconfirmedSteps: (unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length) || 0
1045
1097
  });
1046
1098
  }
@@ -1049,12 +1101,12 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
1049
1101
  }
1050
1102
  var rebased = tr === null || tr === void 0 ? void 0 : tr.getMeta('rebasedData');
1051
1103
  if (rebased) {
1052
- var _this$analyticsHelper36;
1104
+ var _this$analyticsHelper37;
1053
1105
  var obfuscatedUnconfirmedSteps = this.obfuscateStepsAndState(rebased.unconfirmedSteps).obfuscatedSteps;
1054
1106
  var obfuscatedRemoteSteps = this.obfuscateStepsAndState(rebased.remoteSteps);
1055
1107
  var obfuscatedRebasedSteps = this.obfuscateStepsAndState(unconfirmedSteps).obfuscatedSteps;
1056
1108
  // send analtyics on unconfirmed steps
1057
- (_this$analyticsHelper36 = this.analyticsHelper) === null || _this$analyticsHelper36 === void 0 || _this$analyticsHelper36.sendActionEvent(_const.EVENT_ACTION.STEPS_REBASED, _const.EVENT_STATUS.INFO, {
1109
+ (_this$analyticsHelper37 = this.analyticsHelper) === null || _this$analyticsHelper37 === void 0 || _this$analyticsHelper37.sendActionEvent(_const.EVENT_ACTION.STEPS_REBASED, _const.EVENT_STATUS.INFO, {
1058
1110
  obfuscatedUnconfirmedSteps: obfuscatedUnconfirmedSteps,
1059
1111
  obfuscatedRemoteSteps: obfuscatedRemoteSteps,
1060
1112
  obfuscatedRebasedSteps: obfuscatedRebasedSteps,
@@ -1075,8 +1127,8 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
1075
1127
  this.timeout = setTimeout(function () {
1076
1128
  // If the timer expires and we're still online, handle the offline steps.
1077
1129
  // Otherwise, clear the timer so it can restart when we're online again.
1078
- if (_this3.getConnected()) {
1079
- _this3.timeoutExceeded = true;
1130
+ if (_this4.getConnected()) {
1131
+ _this4.timeoutExceeded = true;
1080
1132
  var updatedUnconfirmedStepsData = (0, _prosemirrorCollab.sendableSteps)(newState);
1081
1133
  updatedUnconfirmedStepsData === null || updatedUnconfirmedStepsData === void 0 || updatedUnconfirmedStepsData.origins.forEach(function (origin) {
1082
1134
  if (origin instanceof _state.Transaction && origin.getMeta('isOffline')) {
@@ -1084,7 +1136,7 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
1084
1136
  }
1085
1137
  });
1086
1138
  } else {
1087
- _this3.timeout = undefined;
1139
+ _this4.timeout = undefined;
1088
1140
  }
1089
1141
  }, 6000);
1090
1142
  }
@@ -4,9 +4,10 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.isAIProviderID = exports.getSubProduct = exports.getStepsAdfWithObfuscation = exports.getStepUGCFreeDetails = exports.getStepTypes = exports.getStepPositions = exports.getProduct = exports.getObfuscatedSteps = exports.getDocAdfWithObfuscationFromJSON = exports.getDocAdfWithObfuscation = exports.createLogger = void 0;
7
+ exports.isAIProviderID = exports.getSubProduct = exports.getStepsAdfWithObfuscation = exports.getStepUGCFreeDetails = exports.getStepTypes = exports.getStepPositions = exports.getProduct = exports.getObfuscatedSteps = exports.getDocAdfWithObfuscationFromJSON = exports.getDocAdfWithObfuscation = exports.getAgentProviderId = exports.createLogger = void 0;
8
8
  exports.isGCPtenant = isGCPtenant;
9
9
  exports.logObfuscatedSteps = logObfuscatedSteps;
10
+ exports.normalizeAgentId = void 0;
10
11
  exports.sleep = sleep;
11
12
  exports.toObfuscatedSteps = toObfuscatedSteps;
12
13
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
@@ -55,6 +56,32 @@ function sleep(ms) {
55
56
  var isAIProviderID = exports.isAIProviderID = function isAIProviderID(id) {
56
57
  return typeof id === 'string' && id.startsWith('agent:');
57
58
  };
59
+ var IDENTITY_USER_ARI_PREFIX = 'ari:cloud:identity::user/';
60
+
61
+ /**
62
+ * Normalises an agent identifier that may arrive as an identity ARI
63
+ * (`ari:cloud:identity::user/<aaid>`) down to the bare AAID. Any other value is
64
+ * returned unchanged.
65
+ */
66
+ var normalizeAgentId = exports.normalizeAgentId = function normalizeAgentId(agentId) {
67
+ return agentId.startsWith(IDENTITY_USER_ARI_PREFIX) ? agentId.slice(IDENTITY_USER_ARI_PREFIX.length) : agentId;
68
+ };
69
+
70
+ /**
71
+ * Derives the synthetic AI-provider participant id (`agent:<id>`) for an
72
+ * agent-authored step, or `undefined` if the step is not agent-authored.
73
+ *
74
+ * `agentType` is the reliable "made by an agent" signal (present even when
75
+ * `agentId` is not). We key on the agent's AAID when available, otherwise the
76
+ * agent type (e.g. `mcp`/`twg`), otherwise a generic `agent`.
77
+ */
78
+ var getAgentProviderId = exports.getAgentProviderId = function getAgentProviderId(step) {
79
+ if (!step.agentType) {
80
+ return undefined;
81
+ }
82
+ var id = step.agentId ? normalizeAgentId(step.agentId) : step.agentType;
83
+ return "agent:".concat(id || 'agent');
84
+ };
58
85
  var getProduct = exports.getProduct = function getProduct(productInfo) {
59
86
  var _productInfo$product;
60
87
  return (_productInfo$product = productInfo === null || productInfo === void 0 ? void 0 : productInfo.product) !== null && _productInfo$product !== void 0 ? _productInfo$product : 'unknown';
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.SINGLE_COLLAB_MODE = exports.ParticipantsService = exports.MULTI_COLLAB_MODE = void 0;
7
+ exports.SINGLE_COLLAB_MODE = exports.ParticipantsService = exports.MULTI_COLLAB_MODE = exports.AGENT_PRESENCE_TTL_MS = void 0;
8
8
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
9
9
  var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
10
10
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
@@ -25,6 +25,11 @@ var UNIDENTIFIED = 'unidentified';
25
25
  var SINGLE_COLLAB_MODE = exports.SINGLE_COLLAB_MODE = 'single';
26
26
  var MULTI_COLLAB_MODE = exports.MULTI_COLLAB_MODE = 'collab';
27
27
 
28
+ // Sliding inactivity window for agent (AI provider) participants that are added locally from
29
+ // remote agent-authored steps. Reset on each new step for that agent.
30
+ // NOTE: temporary/demonstration lifecycle — long-term expiry may move to NCS.
31
+ var AGENT_PRESENCE_TTL_MS = exports.AGENT_PRESENCE_TTL_MS = 30 * 1000; // 30 seconds
32
+
28
33
  /**
29
34
  * This service is responsible for handling presence and participant events, as well as sending them on to the editor or NCS.
30
35
  * @param analyticsHelper Analytics helper instance
@@ -58,6 +63,8 @@ var ParticipantsService = exports.ParticipantsService = /*#__PURE__*/function ()
58
63
  function ParticipantsService(analyticsHelper, participantsState, emit, getUser, batchProps, channelBroadcast, sendPresenceJoined, getPresenceData, setUserId, getAIProviderActiveIds, fetchAnonymousAsset) {
59
64
  var _this = this;
60
65
  (0, _classCallCheck2.default)(this, ParticipantsService);
66
+ // Per-agent sliding inactivity timers, keyed by the agent participant sessionId.
67
+ (0, _defineProperty2.default)(this, "agentPresenceTimers", new Map());
61
68
  (0, _defineProperty2.default)(this, "currentlyPollingFetchUsers", true);
62
69
  (0, _defineProperty2.default)(this, "hasBatchFetchError", false);
63
70
  (0, _defineProperty2.default)(this, "sendPresenceActivityChanged", function () {
@@ -108,6 +115,93 @@ var ParticipantsService = exports.ParticipantsService = /*#__PURE__*/function ()
108
115
  });
109
116
  }
110
117
  });
118
+ /**
119
+ * Locally register — or refresh — an agent participant in the
120
+ * AI-provider (`agent:`) partition from a received agent-authored step, so the agent appears in
121
+ * the presence facepile.
122
+ *
123
+ * Local-only: mutates this client's participants-state and emits `presence` WITHOUT any socket
124
+ * broadcast. BE-originated agent steps are already fanned out to every client, so each client
125
+ * detects the same steps and adds the agent independently — no cross-client coordination needed.
126
+ *
127
+ * Deliberately does NOT go through the `getUser` hydration path: agents have no human profile,
128
+ * so `getUser('agent:<id>')` would 404/throw (and could drop the participant) or add latency;
129
+ * the agent's display identity is resolved downstream in the facepile.
130
+ *
131
+ * `presence` is emitted only on the FIRST add for an agent. Subsequent steps for the same agent
132
+ * refresh `lastActive` and reset the sliding inactivity timer silently, to avoid churning every
133
+ * presence consumer on each streamed step.
134
+ *
135
+ * @param providerId `agent:<aaid|type>` id of the agent
136
+ * @example
137
+ */
138
+ (0, _defineProperty2.default)(this, "upsertAIProviderParticipantLocally", function (providerId) {
139
+ var payload = _this.buildAIProviderPresencePayload(providerId);
140
+ var sessionId = payload.sessionId;
141
+ var existing = _this.participantsState.getBySessionId(sessionId);
142
+ var participant = _objectSpread(_objectSpread({}, payload), {}, {
143
+ // `payload.userId` is typed `string | undefined`; here it is always the (string)
144
+ // providerId, so pin it to satisfy ProviderParticipant.userId (string).
145
+ userId: providerId,
146
+ lastActive: payload.timestamp,
147
+ // name/avatar/email are intentionally empty: an agent has no human profile here. Its display
148
+ // identity (name + avatar) is resolved downstream in the facepile from the agent id, so the
149
+ // provider stays identity-free (no getUser/assistance lookup on this hot path).
150
+ name: '',
151
+ avatar: '',
152
+ email: ''
153
+ });
154
+ _this.participantsState.setBySessionId(sessionId, participant);
155
+
156
+ // Emit only on the first add; a refresh of an already-present agent shouldn't re-emit.
157
+ if (!existing) {
158
+ _this.emitPresence({
159
+ joined: [participant]
160
+ }, 'adding agent participant from remote step');
161
+ }
162
+ _this.resetAgentPresenceTimer(sessionId);
163
+ });
164
+ /**
165
+ * (Re)starts the sliding 30s inactivity timer for an agent participant. Called on every agent
166
+ * step so the window slides forward from the agent's most recent activity.
167
+ * @example
168
+ */
169
+ (0, _defineProperty2.default)(this, "resetAgentPresenceTimer", function (sessionId) {
170
+ var existingTimer = _this.agentPresenceTimers.get(sessionId);
171
+ if (existingTimer !== undefined) {
172
+ clearTimeout(existingTimer);
173
+ }
174
+ _this.agentPresenceTimers.set(sessionId, window.setTimeout(function () {
175
+ return _this.removeAgentParticipant(sessionId);
176
+ }, AGENT_PRESENCE_TTL_MS));
177
+ });
178
+ /**
179
+ * Removes an inactive agent participant once its sliding window elapses and emits the `presence`
180
+ * leave so the facepile drops it.
181
+ * @example
182
+ */
183
+ (0, _defineProperty2.default)(this, "removeAgentParticipant", function (sessionId) {
184
+ _this.agentPresenceTimers.delete(sessionId);
185
+ if (_this.participantsState.getBySessionId(sessionId)) {
186
+ _this.participantsState.removeBySessionId(sessionId);
187
+ _this.emitPresence({
188
+ left: [{
189
+ sessionId: sessionId
190
+ }]
191
+ }, 'removing inactive agent participant');
192
+ }
193
+ });
194
+ /**
195
+ * Clears all pending agent presence sliding timers (on disconnect/destroy) so they don't fire
196
+ * against cleared state.
197
+ * @example
198
+ */
199
+ (0, _defineProperty2.default)(this, "clearAgentPresenceTimers", function () {
200
+ _this.agentPresenceTimers.forEach(function (timer) {
201
+ return clearTimeout(timer);
202
+ });
203
+ _this.agentPresenceTimers.clear();
204
+ });
111
205
  (0, _defineProperty2.default)(this, "hasPresenceActivityChanged", function (previous, current) {
112
206
  return previous.presenceActivity !== current.presenceActivity;
113
207
  });
@@ -386,6 +480,8 @@ var ParticipantsService = exports.ParticipantsService = /*#__PURE__*/function ()
386
480
  (0, _defineProperty2.default)(this, "disconnect", function (reason, sessionId) {
387
481
  var left = _this.participantsState.getParticipants();
388
482
  _this.participantsState.clear();
483
+ // Stop any pending agent presence timers so they don't fire against cleared state.
484
+ _this.clearAgentPresenceTimers();
389
485
  try {
390
486
  _this.emit('disconnected', {
391
487
  reason: (0, _disconnectedReasonMapper.disconnectedReasonMapper)(reason),
@@ -654,6 +750,7 @@ var ParticipantsService = exports.ParticipantsService = /*#__PURE__*/function ()
654
750
  (0, _defineProperty2.default)(this, "clearTimers", function () {
655
751
  clearTimeout(_this.participantUpdateTimeout);
656
752
  clearTimeout(_this.presenceFetchTimeout);
753
+ _this.clearAgentPresenceTimers();
657
754
  });
658
755
  (0, _defineProperty2.default)(this, "sendPresence", function () {
659
756
  try {
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.version = exports.nextMajorVersion = exports.name = void 0;
7
7
  var name = exports.name = "@atlaskit/collab-provider";
8
- var version = exports.version = "21.0.28";
8
+ var version = exports.version = "21.1.0";
9
9
  var nextMajorVersion = exports.nextMajorVersion = function nextMajorVersion() {
10
10
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
11
11
  };
@@ -9,7 +9,7 @@ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
9
9
  import { ACK_MAX_TRY, EVENT_ACTION, EVENT_STATUS, CatchupEventReason } from '../helpers/const';
10
10
  import { MEASURE_NAME, startMeasure, stopMeasure } from '../analytics/performance';
11
11
  import { INTERNAL_ERROR_CODE } from '../errors/internal-errors';
12
- import { createLogger, getDocAdfWithObfuscationFromJSON, getObfuscatedSteps, getStepUGCFreeDetails, sleep } from '../helpers/utils';
12
+ import { createLogger, getAgentProviderId, getDocAdfWithObfuscationFromJSON, getObfuscatedSteps, getStepUGCFreeDetails, sleep } from '../helpers/utils';
13
13
  import { MAX_STEP_REJECTED_ERROR, MAX_STEP_REJECTED_ERROR_AGGRESSIVE } from '../provider';
14
14
  import { CommitStepService } from '../provider/commit-step';
15
15
  import { CantSyncUpError, UpdateDocumentError } from '../errors/custom-errors';
@@ -805,6 +805,9 @@ export class DocumentService {
805
805
  this.stepRejectCounter = 0;
806
806
  this.participantsService.emitTelepointersFromSteps(steps);
807
807
 
808
+ // Surface agent edit presence from agent-authored steps.
809
+ this.maybeAddAgentPresenceFromSteps(steps);
810
+
808
811
  // Resend local steps if none of the received steps originated with us!
809
812
  // Ignored via go/ees005
810
813
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -822,6 +825,38 @@ export class DocumentService {
822
825
  }
823
826
  }
824
827
  }
828
+
829
+ /**
830
+ * Detect agent-authored steps in a received batch and register
831
+ * each distinct agent as a local participant so it appears in the presence facepile.
832
+ *
833
+ * Gated behind a default-OFF feature gate: `collab-provider` is shared platform infra (used by
834
+ * Jira JWM/JPD etc.), so this is a strict no-op everywhere unless the gate is explicitly enabled.
835
+ *
836
+ * Isolated in its own try/catch so a presence failure can never affect step persistence.
837
+ *
838
+ * Known limitation (PoC): steps replayed via catch-up/reconnect can briefly re-surface an agent
839
+ * whose activity is stale — a freshness guard is a follow-up.
840
+ * @example
841
+ */
842
+ maybeAddAgentPresenceFromSteps(steps) {
843
+ if (!expValEquals('platform_editor_agent_be_streaming', 'isEnabled', true)) {
844
+ return;
845
+ }
846
+ try {
847
+ const providerIds = new Set();
848
+ for (const step of steps) {
849
+ const providerId = getAgentProviderId(step);
850
+ if (providerId) {
851
+ providerIds.add(providerId);
852
+ }
853
+ }
854
+ providerIds.forEach(providerId => this.participantsService.upsertAIProviderParticipantLocally(providerId));
855
+ } catch (error) {
856
+ var _this$analyticsHelper34;
857
+ (_this$analyticsHelper34 = this.analyticsHelper) === null || _this$analyticsHelper34 === void 0 ? void 0 : _this$analyticsHelper34.sendErrorEvent(error, 'Error while adding agent presence from steps');
858
+ }
859
+ }
825
860
  /**
826
861
  *
827
862
  * @example
@@ -862,8 +897,8 @@ export class DocumentService {
862
897
  var _this$getState0;
863
898
  const state = (_this$getState0 = this.getState) === null || _this$getState0 === void 0 ? void 0 : _this$getState0.call(this);
864
899
  if (!state) {
865
- var _this$analyticsHelper34;
866
- (_this$analyticsHelper34 = this.analyticsHelper) === null || _this$analyticsHelper34 === void 0 ? void 0 : _this$analyticsHelper34.sendErrorEvent(new Error('Editor state is undefined'), 'sendStepsFromCurrentState called without state');
900
+ var _this$analyticsHelper35;
901
+ (_this$analyticsHelper35 = this.analyticsHelper) === null || _this$analyticsHelper35 === void 0 ? void 0 : _this$analyticsHelper35.sendErrorEvent(new Error('Editor state is undefined'), 'sendStepsFromCurrentState called without state');
867
902
  return;
868
903
  }
869
904
  this.send(null, null, state, sendAnalyticsEvent, reason);
@@ -909,8 +944,8 @@ export class DocumentService {
909
944
  // to ensure that analytics events with the number of unconfirmed steps is only
910
945
  // sent once on connection (as opposed to on every step)
911
946
  if (sendAnalyticsEvent) {
912
- var _this$analyticsHelper35;
913
- (_this$analyticsHelper35 = this.analyticsHelper) === null || _this$analyticsHelper35 === void 0 ? void 0 : _this$analyticsHelper35.sendActionEvent(EVENT_ACTION.HAS_UNCONFIRMED_STEPS, EVENT_STATUS.INFO, {
947
+ var _this$analyticsHelper36;
948
+ (_this$analyticsHelper36 = this.analyticsHelper) === null || _this$analyticsHelper36 === void 0 ? void 0 : _this$analyticsHelper36.sendActionEvent(EVENT_ACTION.HAS_UNCONFIRMED_STEPS, EVENT_STATUS.INFO, {
914
949
  numUnconfirmedSteps: (unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length) || 0
915
950
  });
916
951
  }
@@ -919,12 +954,12 @@ export class DocumentService {
919
954
  }
920
955
  const rebased = tr === null || tr === void 0 ? void 0 : tr.getMeta('rebasedData');
921
956
  if (rebased) {
922
- var _this$analyticsHelper36;
957
+ var _this$analyticsHelper37;
923
958
  const obfuscatedUnconfirmedSteps = this.obfuscateStepsAndState(rebased.unconfirmedSteps).obfuscatedSteps;
924
959
  const obfuscatedRemoteSteps = this.obfuscateStepsAndState(rebased.remoteSteps);
925
960
  const obfuscatedRebasedSteps = this.obfuscateStepsAndState(unconfirmedSteps).obfuscatedSteps;
926
961
  // send analtyics on unconfirmed steps
927
- (_this$analyticsHelper36 = this.analyticsHelper) === null || _this$analyticsHelper36 === void 0 ? void 0 : _this$analyticsHelper36.sendActionEvent(EVENT_ACTION.STEPS_REBASED, EVENT_STATUS.INFO, {
962
+ (_this$analyticsHelper37 = this.analyticsHelper) === null || _this$analyticsHelper37 === void 0 ? void 0 : _this$analyticsHelper37.sendActionEvent(EVENT_ACTION.STEPS_REBASED, EVENT_STATUS.INFO, {
928
963
  obfuscatedUnconfirmedSteps,
929
964
  obfuscatedRemoteSteps,
930
965
  obfuscatedRebasedSteps,
@@ -33,6 +33,30 @@ export function sleep(ms) {
33
33
  * provider (e.g. immediate `presence` emission and agent-leave cleanup).
34
34
  */
35
35
  export const isAIProviderID = id => typeof id === 'string' && id.startsWith('agent:');
36
+ const IDENTITY_USER_ARI_PREFIX = 'ari:cloud:identity::user/';
37
+
38
+ /**
39
+ * Normalises an agent identifier that may arrive as an identity ARI
40
+ * (`ari:cloud:identity::user/<aaid>`) down to the bare AAID. Any other value is
41
+ * returned unchanged.
42
+ */
43
+ export const normalizeAgentId = agentId => agentId.startsWith(IDENTITY_USER_ARI_PREFIX) ? agentId.slice(IDENTITY_USER_ARI_PREFIX.length) : agentId;
44
+
45
+ /**
46
+ * Derives the synthetic AI-provider participant id (`agent:<id>`) for an
47
+ * agent-authored step, or `undefined` if the step is not agent-authored.
48
+ *
49
+ * `agentType` is the reliable "made by an agent" signal (present even when
50
+ * `agentId` is not). We key on the agent's AAID when available, otherwise the
51
+ * agent type (e.g. `mcp`/`twg`), otherwise a generic `agent`.
52
+ */
53
+ export const getAgentProviderId = step => {
54
+ if (!step.agentType) {
55
+ return undefined;
56
+ }
57
+ const id = step.agentId ? normalizeAgentId(step.agentId) : step.agentType;
58
+ return `agent:${id || 'agent'}`;
59
+ };
36
60
  export const getProduct = productInfo => {
37
61
  var _productInfo$product;
38
62
  return (_productInfo$product = productInfo === null || productInfo === void 0 ? void 0 : productInfo.product) !== null && _productInfo$product !== void 0 ? _productInfo$product : 'unknown';
@@ -12,6 +12,11 @@ const UNIDENTIFIED = 'unidentified';
12
12
  export const SINGLE_COLLAB_MODE = 'single';
13
13
  export const MULTI_COLLAB_MODE = 'collab';
14
14
 
15
+ // Sliding inactivity window for agent (AI provider) participants that are added locally from
16
+ // remote agent-authored steps. Reset on each new step for that agent.
17
+ // NOTE: temporary/demonstration lifecycle — long-term expiry may move to NCS.
18
+ export const AGENT_PRESENCE_TTL_MS = 30 * 1000; // 30 seconds
19
+
15
20
  /**
16
21
  * This service is responsible for handling presence and participant events, as well as sending them on to the editor or NCS.
17
22
  * @param analyticsHelper Analytics helper instance
@@ -43,6 +48,8 @@ export class ParticipantsService {
43
48
  * @example
44
49
  */
45
50
  constructor(analyticsHelper, participantsState, emit, getUser, batchProps, channelBroadcast, sendPresenceJoined, getPresenceData, setUserId, getAIProviderActiveIds, fetchAnonymousAsset) {
51
+ // Per-agent sliding inactivity timers, keyed by the agent participant sessionId.
52
+ _defineProperty(this, "agentPresenceTimers", new Map());
46
53
  _defineProperty(this, "currentlyPollingFetchUsers", true);
47
54
  _defineProperty(this, "hasBatchFetchError", false);
48
55
  _defineProperty(this, "sendPresenceActivityChanged", () => {
@@ -93,6 +100,92 @@ export class ParticipantsService {
93
100
  });
94
101
  }
95
102
  });
103
+ /**
104
+ * Locally register — or refresh — an agent participant in the
105
+ * AI-provider (`agent:`) partition from a received agent-authored step, so the agent appears in
106
+ * the presence facepile.
107
+ *
108
+ * Local-only: mutates this client's participants-state and emits `presence` WITHOUT any socket
109
+ * broadcast. BE-originated agent steps are already fanned out to every client, so each client
110
+ * detects the same steps and adds the agent independently — no cross-client coordination needed.
111
+ *
112
+ * Deliberately does NOT go through the `getUser` hydration path: agents have no human profile,
113
+ * so `getUser('agent:<id>')` would 404/throw (and could drop the participant) or add latency;
114
+ * the agent's display identity is resolved downstream in the facepile.
115
+ *
116
+ * `presence` is emitted only on the FIRST add for an agent. Subsequent steps for the same agent
117
+ * refresh `lastActive` and reset the sliding inactivity timer silently, to avoid churning every
118
+ * presence consumer on each streamed step.
119
+ *
120
+ * @param providerId `agent:<aaid|type>` id of the agent
121
+ * @example
122
+ */
123
+ _defineProperty(this, "upsertAIProviderParticipantLocally", providerId => {
124
+ const payload = this.buildAIProviderPresencePayload(providerId);
125
+ const {
126
+ sessionId
127
+ } = payload;
128
+ const existing = this.participantsState.getBySessionId(sessionId);
129
+ const participant = {
130
+ ...payload,
131
+ // `payload.userId` is typed `string | undefined`; here it is always the (string)
132
+ // providerId, so pin it to satisfy ProviderParticipant.userId (string).
133
+ userId: providerId,
134
+ lastActive: payload.timestamp,
135
+ // name/avatar/email are intentionally empty: an agent has no human profile here. Its display
136
+ // identity (name + avatar) is resolved downstream in the facepile from the agent id, so the
137
+ // provider stays identity-free (no getUser/assistance lookup on this hot path).
138
+ name: '',
139
+ avatar: '',
140
+ email: ''
141
+ };
142
+ this.participantsState.setBySessionId(sessionId, participant);
143
+
144
+ // Emit only on the first add; a refresh of an already-present agent shouldn't re-emit.
145
+ if (!existing) {
146
+ this.emitPresence({
147
+ joined: [participant]
148
+ }, 'adding agent participant from remote step');
149
+ }
150
+ this.resetAgentPresenceTimer(sessionId);
151
+ });
152
+ /**
153
+ * (Re)starts the sliding 30s inactivity timer for an agent participant. Called on every agent
154
+ * step so the window slides forward from the agent's most recent activity.
155
+ * @example
156
+ */
157
+ _defineProperty(this, "resetAgentPresenceTimer", sessionId => {
158
+ const existingTimer = this.agentPresenceTimers.get(sessionId);
159
+ if (existingTimer !== undefined) {
160
+ clearTimeout(existingTimer);
161
+ }
162
+ this.agentPresenceTimers.set(sessionId, window.setTimeout(() => this.removeAgentParticipant(sessionId), AGENT_PRESENCE_TTL_MS));
163
+ });
164
+ /**
165
+ * Removes an inactive agent participant once its sliding window elapses and emits the `presence`
166
+ * leave so the facepile drops it.
167
+ * @example
168
+ */
169
+ _defineProperty(this, "removeAgentParticipant", sessionId => {
170
+ this.agentPresenceTimers.delete(sessionId);
171
+ if (this.participantsState.getBySessionId(sessionId)) {
172
+ this.participantsState.removeBySessionId(sessionId);
173
+ this.emitPresence({
174
+ left: [{
175
+ sessionId
176
+ }]
177
+ }, 'removing inactive agent participant');
178
+ }
179
+ });
180
+ /**
181
+ * Clears all pending agent presence sliding timers (on disconnect/destroy) so they don't fire
182
+ * against cleared state.
183
+ * @example
184
+ */
185
+ _defineProperty(this, "clearAgentPresenceTimers", () => {
186
+ this.agentPresenceTimers.forEach(timer => clearTimeout(timer));
187
+ this.agentPresenceTimers.clear();
188
+ });
96
189
  _defineProperty(this, "hasPresenceActivityChanged", (previous, current) => {
97
190
  return previous.presenceActivity !== current.presenceActivity;
98
191
  });
@@ -294,6 +387,8 @@ export class ParticipantsService {
294
387
  _defineProperty(this, "disconnect", (reason, sessionId) => {
295
388
  const left = this.participantsState.getParticipants();
296
389
  this.participantsState.clear();
390
+ // Stop any pending agent presence timers so they don't fire against cleared state.
391
+ this.clearAgentPresenceTimers();
297
392
  try {
298
393
  this.emit('disconnected', {
299
394
  reason: disconnectedReasonMapper(reason),
@@ -495,6 +590,7 @@ export class ParticipantsService {
495
590
  _defineProperty(this, "clearTimers", () => {
496
591
  clearTimeout(this.participantUpdateTimeout);
497
592
  clearTimeout(this.presenceFetchTimeout);
593
+ this.clearAgentPresenceTimers();
498
594
  });
499
595
  _defineProperty(this, "sendPresence", () => {
500
596
  try {
@@ -1,5 +1,5 @@
1
1
  export const name = "@atlaskit/collab-provider";
2
- export const version = "21.0.28";
2
+ export const version = "21.1.0";
3
3
  export const nextMajorVersion = () => {
4
4
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
5
5
  };
@@ -2,6 +2,9 @@ import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
2
2
  import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
3
3
  import _createClass from "@babel/runtime/helpers/createClass";
4
4
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
5
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
6
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
7
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
5
8
  import _regeneratorRuntime from "@babel/runtime/regenerator";
6
9
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
7
10
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -15,7 +18,7 @@ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
15
18
  import { ACK_MAX_TRY, EVENT_ACTION, EVENT_STATUS, CatchupEventReason } from '../helpers/const';
16
19
  import { MEASURE_NAME, startMeasure, stopMeasure } from '../analytics/performance';
17
20
  import { INTERNAL_ERROR_CODE } from '../errors/internal-errors';
18
- import { createLogger, getDocAdfWithObfuscationFromJSON, getObfuscatedSteps, getStepUGCFreeDetails, sleep } from '../helpers/utils';
21
+ import { createLogger, getAgentProviderId, getDocAdfWithObfuscationFromJSON, getObfuscatedSteps, getStepUGCFreeDetails, sleep } from '../helpers/utils';
19
22
  import { MAX_STEP_REJECTED_ERROR, MAX_STEP_REJECTED_ERROR_AGGRESSIVE } from '../provider';
20
23
  import { CommitStepService } from '../provider/commit-step';
21
24
  import { CantSyncUpError, UpdateDocumentError } from '../errors/custom-errors';
@@ -915,6 +918,9 @@ export var DocumentService = /*#__PURE__*/function () {
915
918
  this.stepRejectCounter = 0;
916
919
  this.participantsService.emitTelepointersFromSteps(steps);
917
920
 
921
+ // Surface agent edit presence from agent-authored steps.
922
+ this.maybeAddAgentPresenceFromSteps(steps);
923
+
918
924
  // Resend local steps if none of the received steps originated with us!
919
925
  // Ignored via go/ees005
920
926
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -934,6 +940,52 @@ export var DocumentService = /*#__PURE__*/function () {
934
940
  }
935
941
  }
936
942
  }
943
+
944
+ /**
945
+ * Detect agent-authored steps in a received batch and register
946
+ * each distinct agent as a local participant so it appears in the presence facepile.
947
+ *
948
+ * Gated behind a default-OFF feature gate: `collab-provider` is shared platform infra (used by
949
+ * Jira JWM/JPD etc.), so this is a strict no-op everywhere unless the gate is explicitly enabled.
950
+ *
951
+ * Isolated in its own try/catch so a presence failure can never affect step persistence.
952
+ *
953
+ * Known limitation (PoC): steps replayed via catch-up/reconnect can briefly re-surface an agent
954
+ * whose activity is stale — a freshness guard is a follow-up.
955
+ * @example
956
+ */
957
+ }, {
958
+ key: "maybeAddAgentPresenceFromSteps",
959
+ value: function maybeAddAgentPresenceFromSteps(steps) {
960
+ var _this3 = this;
961
+ if (!expValEquals('platform_editor_agent_be_streaming', 'isEnabled', true)) {
962
+ return;
963
+ }
964
+ try {
965
+ var providerIds = new Set();
966
+ var _iterator = _createForOfIteratorHelper(steps),
967
+ _step;
968
+ try {
969
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
970
+ var step = _step.value;
971
+ var providerId = getAgentProviderId(step);
972
+ if (providerId) {
973
+ providerIds.add(providerId);
974
+ }
975
+ }
976
+ } catch (err) {
977
+ _iterator.e(err);
978
+ } finally {
979
+ _iterator.f();
980
+ }
981
+ providerIds.forEach(function (providerId) {
982
+ return _this3.participantsService.upsertAIProviderParticipantLocally(providerId);
983
+ });
984
+ } catch (error) {
985
+ var _this$analyticsHelper34;
986
+ (_this$analyticsHelper34 = this.analyticsHelper) === null || _this$analyticsHelper34 === void 0 || _this$analyticsHelper34.sendErrorEvent(error, 'Error while adding agent presence from steps');
987
+ }
988
+ }
937
989
  }, {
938
990
  key: "getIsNamespaceLocked",
939
991
  value:
@@ -981,8 +1033,8 @@ export var DocumentService = /*#__PURE__*/function () {
981
1033
  var _this$getState0;
982
1034
  var state = (_this$getState0 = this.getState) === null || _this$getState0 === void 0 ? void 0 : _this$getState0.call(this);
983
1035
  if (!state) {
984
- var _this$analyticsHelper34;
985
- (_this$analyticsHelper34 = this.analyticsHelper) === null || _this$analyticsHelper34 === void 0 || _this$analyticsHelper34.sendErrorEvent(new Error('Editor state is undefined'), 'sendStepsFromCurrentState called without state');
1036
+ var _this$analyticsHelper35;
1037
+ (_this$analyticsHelper35 = this.analyticsHelper) === null || _this$analyticsHelper35 === void 0 || _this$analyticsHelper35.sendErrorEvent(new Error('Editor state is undefined'), 'sendStepsFromCurrentState called without state');
986
1038
  return;
987
1039
  }
988
1040
  this.send(null, null, state, sendAnalyticsEvent, reason);
@@ -1004,7 +1056,7 @@ export var DocumentService = /*#__PURE__*/function () {
1004
1056
  ) {
1005
1057
  var _this$getState1,
1006
1058
  _this$getState10,
1007
- _this3 = this;
1059
+ _this4 = this;
1008
1060
  var offlineEditingEnabled = editorExperiment('platform_editor_offline_editing_web', true);
1009
1061
  var onlineStepMergingEnabled = expValEquals('platform_editor_enable_single_player_step_merging', 'isEnabled', true);
1010
1062
  // We don't trust `_newState` not to be stale
@@ -1033,8 +1085,8 @@ export var DocumentService = /*#__PURE__*/function () {
1033
1085
  // to ensure that analytics events with the number of unconfirmed steps is only
1034
1086
  // sent once on connection (as opposed to on every step)
1035
1087
  if (sendAnalyticsEvent) {
1036
- var _this$analyticsHelper35;
1037
- (_this$analyticsHelper35 = this.analyticsHelper) === null || _this$analyticsHelper35 === void 0 || _this$analyticsHelper35.sendActionEvent(EVENT_ACTION.HAS_UNCONFIRMED_STEPS, EVENT_STATUS.INFO, {
1088
+ var _this$analyticsHelper36;
1089
+ (_this$analyticsHelper36 = this.analyticsHelper) === null || _this$analyticsHelper36 === void 0 || _this$analyticsHelper36.sendActionEvent(EVENT_ACTION.HAS_UNCONFIRMED_STEPS, EVENT_STATUS.INFO, {
1038
1090
  numUnconfirmedSteps: (unconfirmedSteps === null || unconfirmedSteps === void 0 ? void 0 : unconfirmedSteps.length) || 0
1039
1091
  });
1040
1092
  }
@@ -1043,12 +1095,12 @@ export var DocumentService = /*#__PURE__*/function () {
1043
1095
  }
1044
1096
  var rebased = tr === null || tr === void 0 ? void 0 : tr.getMeta('rebasedData');
1045
1097
  if (rebased) {
1046
- var _this$analyticsHelper36;
1098
+ var _this$analyticsHelper37;
1047
1099
  var obfuscatedUnconfirmedSteps = this.obfuscateStepsAndState(rebased.unconfirmedSteps).obfuscatedSteps;
1048
1100
  var obfuscatedRemoteSteps = this.obfuscateStepsAndState(rebased.remoteSteps);
1049
1101
  var obfuscatedRebasedSteps = this.obfuscateStepsAndState(unconfirmedSteps).obfuscatedSteps;
1050
1102
  // send analtyics on unconfirmed steps
1051
- (_this$analyticsHelper36 = this.analyticsHelper) === null || _this$analyticsHelper36 === void 0 || _this$analyticsHelper36.sendActionEvent(EVENT_ACTION.STEPS_REBASED, EVENT_STATUS.INFO, {
1103
+ (_this$analyticsHelper37 = this.analyticsHelper) === null || _this$analyticsHelper37 === void 0 || _this$analyticsHelper37.sendActionEvent(EVENT_ACTION.STEPS_REBASED, EVENT_STATUS.INFO, {
1052
1104
  obfuscatedUnconfirmedSteps: obfuscatedUnconfirmedSteps,
1053
1105
  obfuscatedRemoteSteps: obfuscatedRemoteSteps,
1054
1106
  obfuscatedRebasedSteps: obfuscatedRebasedSteps,
@@ -1069,8 +1121,8 @@ export var DocumentService = /*#__PURE__*/function () {
1069
1121
  this.timeout = setTimeout(function () {
1070
1122
  // If the timer expires and we're still online, handle the offline steps.
1071
1123
  // Otherwise, clear the timer so it can restart when we're online again.
1072
- if (_this3.getConnected()) {
1073
- _this3.timeoutExceeded = true;
1124
+ if (_this4.getConnected()) {
1125
+ _this4.timeoutExceeded = true;
1074
1126
  var updatedUnconfirmedStepsData = sendableSteps(newState);
1075
1127
  updatedUnconfirmedStepsData === null || updatedUnconfirmedStepsData === void 0 || updatedUnconfirmedStepsData.origins.forEach(function (origin) {
1076
1128
  if (origin instanceof Transaction && origin.getMeta('isOffline')) {
@@ -1078,7 +1130,7 @@ export var DocumentService = /*#__PURE__*/function () {
1078
1130
  }
1079
1131
  });
1080
1132
  } else {
1081
- _this3.timeout = undefined;
1133
+ _this4.timeout = undefined;
1082
1134
  }
1083
1135
  }, 6000);
1084
1136
  }
@@ -45,6 +45,32 @@ export function sleep(ms) {
45
45
  export var isAIProviderID = function isAIProviderID(id) {
46
46
  return typeof id === 'string' && id.startsWith('agent:');
47
47
  };
48
+ var IDENTITY_USER_ARI_PREFIX = 'ari:cloud:identity::user/';
49
+
50
+ /**
51
+ * Normalises an agent identifier that may arrive as an identity ARI
52
+ * (`ari:cloud:identity::user/<aaid>`) down to the bare AAID. Any other value is
53
+ * returned unchanged.
54
+ */
55
+ export var normalizeAgentId = function normalizeAgentId(agentId) {
56
+ return agentId.startsWith(IDENTITY_USER_ARI_PREFIX) ? agentId.slice(IDENTITY_USER_ARI_PREFIX.length) : agentId;
57
+ };
58
+
59
+ /**
60
+ * Derives the synthetic AI-provider participant id (`agent:<id>`) for an
61
+ * agent-authored step, or `undefined` if the step is not agent-authored.
62
+ *
63
+ * `agentType` is the reliable "made by an agent" signal (present even when
64
+ * `agentId` is not). We key on the agent's AAID when available, otherwise the
65
+ * agent type (e.g. `mcp`/`twg`), otherwise a generic `agent`.
66
+ */
67
+ export var getAgentProviderId = function getAgentProviderId(step) {
68
+ if (!step.agentType) {
69
+ return undefined;
70
+ }
71
+ var id = step.agentId ? normalizeAgentId(step.agentId) : step.agentType;
72
+ return "agent:".concat(id || 'agent');
73
+ };
48
74
  export var getProduct = function getProduct(productInfo) {
49
75
  var _productInfo$product;
50
76
  return (_productInfo$product = productInfo === null || productInfo === void 0 ? void 0 : productInfo.product) !== null && _productInfo$product !== void 0 ? _productInfo$product : 'unknown';
@@ -18,6 +18,11 @@ var UNIDENTIFIED = 'unidentified';
18
18
  export var SINGLE_COLLAB_MODE = 'single';
19
19
  export var MULTI_COLLAB_MODE = 'collab';
20
20
 
21
+ // Sliding inactivity window for agent (AI provider) participants that are added locally from
22
+ // remote agent-authored steps. Reset on each new step for that agent.
23
+ // NOTE: temporary/demonstration lifecycle — long-term expiry may move to NCS.
24
+ export var AGENT_PRESENCE_TTL_MS = 30 * 1000; // 30 seconds
25
+
21
26
  /**
22
27
  * This service is responsible for handling presence and participant events, as well as sending them on to the editor or NCS.
23
28
  * @param analyticsHelper Analytics helper instance
@@ -51,6 +56,8 @@ export var ParticipantsService = /*#__PURE__*/function () {
51
56
  function ParticipantsService(analyticsHelper, participantsState, emit, getUser, batchProps, channelBroadcast, sendPresenceJoined, getPresenceData, setUserId, getAIProviderActiveIds, fetchAnonymousAsset) {
52
57
  var _this = this;
53
58
  _classCallCheck(this, ParticipantsService);
59
+ // Per-agent sliding inactivity timers, keyed by the agent participant sessionId.
60
+ _defineProperty(this, "agentPresenceTimers", new Map());
54
61
  _defineProperty(this, "currentlyPollingFetchUsers", true);
55
62
  _defineProperty(this, "hasBatchFetchError", false);
56
63
  _defineProperty(this, "sendPresenceActivityChanged", function () {
@@ -101,6 +108,93 @@ export var ParticipantsService = /*#__PURE__*/function () {
101
108
  });
102
109
  }
103
110
  });
111
+ /**
112
+ * Locally register — or refresh — an agent participant in the
113
+ * AI-provider (`agent:`) partition from a received agent-authored step, so the agent appears in
114
+ * the presence facepile.
115
+ *
116
+ * Local-only: mutates this client's participants-state and emits `presence` WITHOUT any socket
117
+ * broadcast. BE-originated agent steps are already fanned out to every client, so each client
118
+ * detects the same steps and adds the agent independently — no cross-client coordination needed.
119
+ *
120
+ * Deliberately does NOT go through the `getUser` hydration path: agents have no human profile,
121
+ * so `getUser('agent:<id>')` would 404/throw (and could drop the participant) or add latency;
122
+ * the agent's display identity is resolved downstream in the facepile.
123
+ *
124
+ * `presence` is emitted only on the FIRST add for an agent. Subsequent steps for the same agent
125
+ * refresh `lastActive` and reset the sliding inactivity timer silently, to avoid churning every
126
+ * presence consumer on each streamed step.
127
+ *
128
+ * @param providerId `agent:<aaid|type>` id of the agent
129
+ * @example
130
+ */
131
+ _defineProperty(this, "upsertAIProviderParticipantLocally", function (providerId) {
132
+ var payload = _this.buildAIProviderPresencePayload(providerId);
133
+ var sessionId = payload.sessionId;
134
+ var existing = _this.participantsState.getBySessionId(sessionId);
135
+ var participant = _objectSpread(_objectSpread({}, payload), {}, {
136
+ // `payload.userId` is typed `string | undefined`; here it is always the (string)
137
+ // providerId, so pin it to satisfy ProviderParticipant.userId (string).
138
+ userId: providerId,
139
+ lastActive: payload.timestamp,
140
+ // name/avatar/email are intentionally empty: an agent has no human profile here. Its display
141
+ // identity (name + avatar) is resolved downstream in the facepile from the agent id, so the
142
+ // provider stays identity-free (no getUser/assistance lookup on this hot path).
143
+ name: '',
144
+ avatar: '',
145
+ email: ''
146
+ });
147
+ _this.participantsState.setBySessionId(sessionId, participant);
148
+
149
+ // Emit only on the first add; a refresh of an already-present agent shouldn't re-emit.
150
+ if (!existing) {
151
+ _this.emitPresence({
152
+ joined: [participant]
153
+ }, 'adding agent participant from remote step');
154
+ }
155
+ _this.resetAgentPresenceTimer(sessionId);
156
+ });
157
+ /**
158
+ * (Re)starts the sliding 30s inactivity timer for an agent participant. Called on every agent
159
+ * step so the window slides forward from the agent's most recent activity.
160
+ * @example
161
+ */
162
+ _defineProperty(this, "resetAgentPresenceTimer", function (sessionId) {
163
+ var existingTimer = _this.agentPresenceTimers.get(sessionId);
164
+ if (existingTimer !== undefined) {
165
+ clearTimeout(existingTimer);
166
+ }
167
+ _this.agentPresenceTimers.set(sessionId, window.setTimeout(function () {
168
+ return _this.removeAgentParticipant(sessionId);
169
+ }, AGENT_PRESENCE_TTL_MS));
170
+ });
171
+ /**
172
+ * Removes an inactive agent participant once its sliding window elapses and emits the `presence`
173
+ * leave so the facepile drops it.
174
+ * @example
175
+ */
176
+ _defineProperty(this, "removeAgentParticipant", function (sessionId) {
177
+ _this.agentPresenceTimers.delete(sessionId);
178
+ if (_this.participantsState.getBySessionId(sessionId)) {
179
+ _this.participantsState.removeBySessionId(sessionId);
180
+ _this.emitPresence({
181
+ left: [{
182
+ sessionId: sessionId
183
+ }]
184
+ }, 'removing inactive agent participant');
185
+ }
186
+ });
187
+ /**
188
+ * Clears all pending agent presence sliding timers (on disconnect/destroy) so they don't fire
189
+ * against cleared state.
190
+ * @example
191
+ */
192
+ _defineProperty(this, "clearAgentPresenceTimers", function () {
193
+ _this.agentPresenceTimers.forEach(function (timer) {
194
+ return clearTimeout(timer);
195
+ });
196
+ _this.agentPresenceTimers.clear();
197
+ });
104
198
  _defineProperty(this, "hasPresenceActivityChanged", function (previous, current) {
105
199
  return previous.presenceActivity !== current.presenceActivity;
106
200
  });
@@ -379,6 +473,8 @@ export var ParticipantsService = /*#__PURE__*/function () {
379
473
  _defineProperty(this, "disconnect", function (reason, sessionId) {
380
474
  var left = _this.participantsState.getParticipants();
381
475
  _this.participantsState.clear();
476
+ // Stop any pending agent presence timers so they don't fire against cleared state.
477
+ _this.clearAgentPresenceTimers();
382
478
  try {
383
479
  _this.emit('disconnected', {
384
480
  reason: disconnectedReasonMapper(reason),
@@ -647,6 +743,7 @@ export var ParticipantsService = /*#__PURE__*/function () {
647
743
  _defineProperty(this, "clearTimers", function () {
648
744
  clearTimeout(_this.participantUpdateTimeout);
649
745
  clearTimeout(_this.presenceFetchTimeout);
746
+ _this.clearAgentPresenceTimers();
650
747
  });
651
748
  _defineProperty(this, "sendPresence", function () {
652
749
  try {
@@ -1,5 +1,5 @@
1
1
  export var name = "@atlaskit/collab-provider";
2
- export var version = "21.0.28";
2
+ export var version = "21.1.0";
3
3
  export var nextMajorVersion = function nextMajorVersion() {
4
4
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
5
5
  };
@@ -94,6 +94,20 @@ export declare class DocumentService implements DocumentServiceInterface {
94
94
  getCurrentState: () => Promise<ResolvedEditorState>;
95
95
  private isStepsFromNewClientIdForSameUserId;
96
96
  private processSteps;
97
+ /**
98
+ * Detect agent-authored steps in a received batch and register
99
+ * each distinct agent as a local participant so it appears in the presence facepile.
100
+ *
101
+ * Gated behind a default-OFF feature gate: `collab-provider` is shared platform infra (used by
102
+ * Jira JWM/JPD etc.), so this is a strict no-op everywhere unless the gate is explicitly enabled.
103
+ *
104
+ * Isolated in its own try/catch so a presence failure can never affect step persistence.
105
+ *
106
+ * Known limitation (PoC): steps replayed via catch-up/reconnect can briefly re-surface an agent
107
+ * whose activity is stale — a freshness guard is a follow-up.
108
+ * @example
109
+ */
110
+ private maybeAddAgentPresenceFromSteps;
97
111
  getUnconfirmedStepsOrigins: () => readonly Transaction[] | undefined;
98
112
  getUnconfirmedSteps: () => readonly ProseMirrorStep[] | undefined;
99
113
  private applyLocalSteps;
@@ -104,20 +118,9 @@ export declare class DocumentService implements DocumentServiceInterface {
104
118
  */
105
119
  onStepsAdded: (data: StepsPayload) => void;
106
120
  obfuscateStepsAndState: (unconfirmedSteps: readonly ProseMirrorStep[] | undefined, currentState?: ResolvedEditorState<JSONDocNode>) => {
121
+ obfuscatedDoc: string | ADFEntity | null | undefined;
107
122
  obfuscatedSteps: string | {
108
- stepType: {
109
- type: string;
110
- contentTypes: string | null;
111
- };
112
123
  stepContent: ADFEntity[] | null;
113
- stepPositions: {
114
- pos?: number | undefined;
115
- insert?: number | undefined;
116
- gapFrom?: number | undefined;
117
- gapTo?: number | undefined;
118
- from?: number | undefined;
119
- to?: number | undefined;
120
- };
121
124
  stepMetadata: {
122
125
  createdOffline?: boolean;
123
126
  prevStepId?: string;
@@ -129,8 +132,19 @@ export declare class DocumentService implements DocumentServiceInterface {
129
132
  traceId?: string;
130
133
  unconfirmedStepAfterRecovery?: boolean;
131
134
  } | undefined;
135
+ stepPositions: {
136
+ from?: number | undefined;
137
+ gapFrom?: number | undefined;
138
+ gapTo?: number | undefined;
139
+ insert?: number | undefined;
140
+ pos?: number | undefined;
141
+ to?: number | undefined;
142
+ };
143
+ stepType: {
144
+ contentTypes: string | null;
145
+ type: string;
146
+ };
132
147
  }[];
133
- obfuscatedDoc: string | ADFEntity | null | undefined;
134
148
  };
135
149
  onRestore: ({ doc, version, metadata, targetClientId, }: CollabInitPayload) => Promise<void>;
136
150
  getFinalAcknowledgedState: (reason: GetResolvedEditorStateReason) => Promise<ResolvedEditorState>;
@@ -18,6 +18,24 @@ export declare function sleep(ms: number): Promise<any>;
18
18
  * provider (e.g. immediate `presence` emission and agent-leave cleanup).
19
19
  */
20
20
  export declare const isAIProviderID: (id: string) => boolean;
21
+ /**
22
+ * Normalises an agent identifier that may arrive as an identity ARI
23
+ * (`ari:cloud:identity::user/<aaid>`) down to the bare AAID. Any other value is
24
+ * returned unchanged.
25
+ */
26
+ export declare const normalizeAgentId: (agentId: string) => string;
27
+ /**
28
+ * Derives the synthetic AI-provider participant id (`agent:<id>`) for an
29
+ * agent-authored step, or `undefined` if the step is not agent-authored.
30
+ *
31
+ * `agentType` is the reliable "made by an agent" signal (present even when
32
+ * `agentId` is not). We key on the agent's AAID when available, otherwise the
33
+ * agent type (e.g. `mcp`/`twg`), otherwise a generic `agent`.
34
+ */
35
+ export declare const getAgentProviderId: (step: {
36
+ agentId?: string;
37
+ agentType?: string;
38
+ }) => string | undefined;
21
39
  export declare const getProduct: (productInfo?: ProductInformation) => string;
22
40
  export declare const getSubProduct: (productInfo?: ProductInformation) => string;
23
41
  export type UGCFreeStepDetails = {
@@ -6,6 +6,7 @@ import { ParticipantsState } from './participants-state';
6
6
  import type { ParticipantFilter } from './participants-state';
7
7
  export declare const SINGLE_COLLAB_MODE = "single";
8
8
  export declare const MULTI_COLLAB_MODE = "collab";
9
+ export declare const AGENT_PRESENCE_TTL_MS: number;
9
10
  /**
10
11
  * This service is responsible for handling presence and participant events, as well as sending them on to the editor or NCS.
11
12
  * @param analyticsHelper Analytics helper instance
@@ -29,6 +30,7 @@ export declare class ParticipantsService {
29
30
  private participantUpdateTimeout;
30
31
  private presenceUpdateTimeout;
31
32
  private presenceFetchTimeout;
33
+ private agentPresenceTimers;
32
34
  private currentlyPollingFetchUsers;
33
35
  private hasBatchFetchError;
34
36
  private participantsState;
@@ -62,6 +64,45 @@ export declare class ParticipantsService {
62
64
  private sendAIProviderParticipantUpdated;
63
65
  private sendAIProviderParticipantLeft;
64
66
  private sendAIProvidersPresence;
67
+ /**
68
+ * Locally register — or refresh — an agent participant in the
69
+ * AI-provider (`agent:`) partition from a received agent-authored step, so the agent appears in
70
+ * the presence facepile.
71
+ *
72
+ * Local-only: mutates this client's participants-state and emits `presence` WITHOUT any socket
73
+ * broadcast. BE-originated agent steps are already fanned out to every client, so each client
74
+ * detects the same steps and adds the agent independently — no cross-client coordination needed.
75
+ *
76
+ * Deliberately does NOT go through the `getUser` hydration path: agents have no human profile,
77
+ * so `getUser('agent:<id>')` would 404/throw (and could drop the participant) or add latency;
78
+ * the agent's display identity is resolved downstream in the facepile.
79
+ *
80
+ * `presence` is emitted only on the FIRST add for an agent. Subsequent steps for the same agent
81
+ * refresh `lastActive` and reset the sliding inactivity timer silently, to avoid churning every
82
+ * presence consumer on each streamed step.
83
+ *
84
+ * @param providerId `agent:<aaid|type>` id of the agent
85
+ * @example
86
+ */
87
+ upsertAIProviderParticipantLocally: (providerId: string) => void;
88
+ /**
89
+ * (Re)starts the sliding 30s inactivity timer for an agent participant. Called on every agent
90
+ * step so the window slides forward from the agent's most recent activity.
91
+ * @example
92
+ */
93
+ private resetAgentPresenceTimer;
94
+ /**
95
+ * Removes an inactive agent participant once its sliding window elapses and emits the `presence`
96
+ * leave so the facepile drops it.
97
+ * @example
98
+ */
99
+ private removeAgentParticipant;
100
+ /**
101
+ * Clears all pending agent presence sliding timers (on disconnect/destroy) so they don't fire
102
+ * against cleared state.
103
+ * @example
104
+ */
105
+ private clearAgentPresenceTimers;
65
106
  private hasPresenceActivityChanged;
66
107
  private handleAnonymousUser;
67
108
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/collab-provider",
3
- "version": "21.0.29",
3
+ "version": "21.1.1",
4
4
  "description": "A provider for collaborative editing.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -21,7 +21,7 @@
21
21
  "@atlaskit/adf-utils": "^20.3.0",
22
22
  "@atlaskit/analytics-gas-types": "^6.0.0",
23
23
  "@atlaskit/analytics-listeners": "^11.1.0",
24
- "@atlaskit/anonymous-assets": "^2.6.0",
24
+ "@atlaskit/anonymous-assets": "^2.7.0",
25
25
  "@atlaskit/atlassian-context": "^1.1.0",
26
26
  "@atlaskit/editor-json-transformer": "^9.0.0",
27
27
  "@atlaskit/editor-prosemirror": "^8.0.0",
@@ -29,7 +29,7 @@
29
29
  "@atlaskit/platform-feature-flags": "^2.1.0",
30
30
  "@atlaskit/prosemirror-collab": "^1.0.0",
31
31
  "@atlaskit/react-ufo": "^7.3.0",
32
- "@atlaskit/tmp-editor-statsig": "^132.0.0",
32
+ "@atlaskit/tmp-editor-statsig": "^133.0.0",
33
33
  "@atlaskit/ufo": "^1.0.0",
34
34
  "@atlaskit/util-service-support": "^7.2.0",
35
35
  "@babel/runtime": "^7.0.0",
@@ -65,9 +65,12 @@
65
65
  },
66
66
  "collab_edit_via_websocket_only_for_gcp": {
67
67
  "type": "boolean"
68
+ },
69
+ "platform_editor_agent_be_streaming": {
70
+ "type": "experiment"
68
71
  }
69
72
  },
70
73
  "peerDependencies": {
71
- "@atlaskit/editor-common": "^116.35.0"
74
+ "@atlaskit/editor-common": "^116.39.0"
72
75
  }
73
76
  }