@atlaskit/collab-provider 21.0.28 → 21.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/dist/cjs/document/document-service.js +62 -10
- package/dist/cjs/helpers/utils.js +28 -1
- package/dist/cjs/participants/participants-service.js +98 -1
- package/dist/cjs/version-wrapper.js +1 -1
- package/dist/es2019/document/document-service.js +42 -7
- package/dist/es2019/helpers/utils.js +24 -0
- package/dist/es2019/participants/participants-service.js +96 -0
- package/dist/es2019/version-wrapper.js +1 -1
- package/dist/esm/document/document-service.js +63 -11
- package/dist/esm/helpers/utils.js +26 -0
- package/dist/esm/participants/participants-service.js +97 -0
- package/dist/esm/version-wrapper.js +1 -1
- package/dist/types/document/document-service.d.ts +27 -13
- package/dist/types/helpers/utils.d.ts +18 -0
- package/dist/types/participants/participants-service.d.ts +41 -0
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @atlaskit/collab-provider
|
|
2
2
|
|
|
3
|
+
## 21.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`895ac23f1a3be`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/895ac23f1a3be) -
|
|
8
|
+
Agent edit presence: surface agent-authored collaborative edits to other users.
|
|
9
|
+
- `collab-provider` detects agent-authored remote steps (`agentType`/`agentId`) and registers a
|
|
10
|
+
synthetic agent participant in the AI-provider (`agent:`) partition so the agent appears in
|
|
11
|
+
presence, with a 30s sliding inactivity window (CCI-18030). `editor-common` gains optional
|
|
12
|
+
`agentId`/`agentType` on the collab step types.
|
|
13
|
+
- `editor-plugin-collab-edit` + `editor-core` add the in-editor shimmer (CCI-18033): when an
|
|
14
|
+
agent-authored step lands, the top-level block(s) it touched are covered by a skeleton-loader
|
|
15
|
+
shimmer (grey skeleton with a moving highlight) with a Rovo agent telepointer/cursor — labelled
|
|
16
|
+
with the agent's type — at the end of the range, then removed on a timer to reveal the content.
|
|
17
|
+
Gated behind the default-OFF `platform_editor_agent_be_streaming` experiment; the `durationMs`
|
|
18
|
+
dynamic-config param controls how long the shimmer stays (0 disables it) and
|
|
19
|
+
`telepointerDisabled` hides the telepointer. `tmp-editor-statsig` registers the experiment.
|
|
20
|
+
- `ratcheting` excludes the new `agentShimmerStyles.ts` from the "No unsafe typography" rule (the
|
|
21
|
+
agent telepointer label mirrors the existing AI in-editor telepointer's sub-token 10px/9px
|
|
22
|
+
sizing, matching its already-excluded Compiled counterpart).
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies
|
|
27
|
+
|
|
28
|
+
## 21.0.29
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- Updated dependencies
|
|
33
|
+
|
|
3
34
|
## 21.0.28
|
|
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$
|
|
991
|
-
(_this$
|
|
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
|
-
|
|
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$
|
|
1043
|
-
(_this$
|
|
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$
|
|
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$
|
|
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 (
|
|
1079
|
-
|
|
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
|
-
|
|
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.
|
|
8
|
+
var version = exports.version = "21.0.29";
|
|
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$
|
|
866
|
-
(_this$
|
|
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$
|
|
913
|
-
(_this$
|
|
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$
|
|
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$
|
|
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 {
|
|
@@ -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$
|
|
985
|
-
(_this$
|
|
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
|
-
|
|
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$
|
|
1037
|
-
(_this$
|
|
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$
|
|
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$
|
|
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 (
|
|
1073
|
-
|
|
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
|
-
|
|
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 {
|
|
@@ -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
|
|
3
|
+
"version": "21.1.0",
|
|
4
4
|
"description": "A provider for collaborative editing.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -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": "^
|
|
32
|
+
"@atlaskit/tmp-editor-statsig": "^132.3.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.
|
|
74
|
+
"@atlaskit/editor-common": "^116.36.0"
|
|
72
75
|
}
|
|
73
76
|
}
|