@taskforcehq/taskforce 0.3.330 → 0.3.332
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/README.md +7 -2
- package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +2 -2
- package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.js +2 -2
- package/dist/components/features/planning/PlanningModule.js +15 -3
- package/dist/components/features/planning/planningMetadata.d.ts +10 -0
- package/dist/components/features/planning/planningMetadata.js +8 -0
- package/dist/components/views/StandaloneLayout.js +35 -1
- package/dist/components/views/panels/PlanningDrawer.d.ts +2 -1
- package/dist/components/views/panels/PlanningDrawer.js +12 -8
- package/dist/config/envSchema.js +7 -0
- package/dist/core/McpTokenService.d.ts +2 -0
- package/dist/core/McpTokenService.js +13 -5
- package/dist/core/Taskforce.d.ts +11 -3
- package/dist/core/Taskforce.js +33 -3
- package/dist/core/types.d.ts +1 -0
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +2 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +28 -9
- package/dist/mcp/httpProtocolRouting.d.ts +1 -0
- package/dist/mcp/httpProtocolRouting.js +8 -5
- package/dist/mcp/runtime.js +24 -5
- package/dist/mcp/taskPlanningRegistrar.js +2 -2
- package/dist/mcp/toolProfiles.js +1 -0
- package/dist/mcp/toolRegistry.js +8 -8
- package/dist/server/buildInfo.d.ts +1 -0
- package/dist/server/buildInfo.js +5 -1
- package/dist/server/cloudMcpCapacityTelemetry.d.ts +68 -0
- package/dist/server/cloudMcpCapacityTelemetry.js +221 -0
- package/dist/server/localWorkspaceSyncServerRuntime.js +13 -5
- package/dist/server/routes/admin.js +14 -2
- package/dist/server/routes/shared.d.ts +3 -0
- package/dist/server/routes/shared.js +280 -240
- package/dist/service/localServiceCli.d.ts +2 -2
- package/dist/service/localServiceCli.js +4 -3
- package/dist/services/executorPhaseAIssuerCore.d.ts +13 -0
- package/dist/services/executorPhaseAIssuerCore.js +107 -0
- package/dist/services/taskforceAgentMcpBridge.d.ts +1 -1
- package/dist/services/taskforceAgentMcpBridge.js +35 -5
- package/dist/shared/executorPhaseASchemaValidation.js +1 -1
- package/dist/storage/postgresAdapter.js +3 -1
- package/dist/storage/postgresRequestDiagnostics.d.ts +9 -1
- package/dist/storage/postgresRequestDiagnostics.js +15 -2
- package/dist/storage/postgresWorker.js +13 -3
- package/dist/storage/postgresWorkerProtocol.d.ts +6 -0
- package/dist/storage/workflowStore.d.ts +2 -0
- package/dist/storage/workflowStore.js +39 -2
- package/dist/sync/cloudSyncApi.js +4 -2
- package/dist/sync/collaborationSyncState.js +5 -0
- package/dist/sync/coordinator/localWorkspaceSyncRunner.d.ts +3 -1
- package/dist/sync/coordinator/localWorkspaceSyncRunner.js +18 -0
- package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
- package/dist/sync/syncApplyHandlers.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.js +72 -15
- package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.d.ts +7 -7
- package/dist/sync/v3/durableWorkflowRuntimeMutationRouter.js +50 -13
- package/dist/sync/v3/localOutboxRecoveryPreparationService.js +8 -2
- package/dist/sync/v3/localTaskCreateOutboxService.d.ts +3 -1
- package/dist/sync/v3/localTaskCreateOutboxService.js +6 -1
- package/dist/sync/v3/localTaskDeleteOutboxService.js +1 -0
- package/dist/sync/v3/localTaskLifecycleOutboxService.d.ts +2 -1
- package/dist/sync/v3/localTaskLifecycleOutboxService.js +5 -1
- package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -1
- package/dist/sync/v3/localTaskMetadataOutboxService.js +6 -2
- package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -1
- package/dist/sync/v3/localTaskStatusOutboxService.js +20 -5
- package/dist/sync/v3/syncDurabilityStore.d.ts +2 -1
- package/dist/sync/v3/syncDurabilityStore.js +34 -6
- package/dist/sync/v3/workflowRuntimeMutationService.d.ts +5 -0
- package/dist/sync/v3/workflowRuntimeMutationService.js +124 -71
- package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +25 -3
- package/dist/sync/v3/workspaceSyncV3Feed.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3Feed.js +6 -1
- package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +2 -0
- package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +49 -18
- package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3WorkflowRuntimeApply.js +50 -5
- package/dist/ui/assets/{AiIdentityRosterCard-OcSz1PkK.js → AiIdentityRosterCard-CDMPtnbC.js} +1 -1
- package/dist/ui/assets/{AiProfilesModule-ByhYW5Xi.js → AiProfilesModule-Cww85C4E.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTM6nIg_.js → AnnotatedAttachmentWorkspace-Dt9TKYSH.js} +1 -1
- package/dist/ui/assets/{AssetTraySortControl-DabEG8L4.js → AssetTraySortControl-CKgapnxu.js} +1 -1
- package/dist/ui/assets/{ContextAttachmentManager-Coa8yVGM.js → ContextAttachmentManager-T-cEVcOZ.js} +2 -2
- package/dist/ui/assets/{DocumentWorkspace-BB0qhdix.js → DocumentWorkspace-HD4MUjAZ.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-CZ8x4UJE.js → EntityActivityTimeline-_V0zfMuh.js} +1 -1
- package/dist/ui/assets/PlanningModule-mTkBygsB.js +1 -0
- package/dist/ui/assets/{PlansPage-D86pZSSl.js → PlansPage-Btiq06VO.js} +1 -1
- package/dist/ui/assets/{TaskContextUpload-BVIkU9yT.js → TaskContextUpload-C00NVUpn.js} +1 -1
- package/dist/ui/assets/{TaskSettings-Cv1mNhv_.js → TaskSettings-Cp-L-4xh.js} +1 -1
- package/dist/ui/assets/{TaskforceAgentsModule-gmH-PD2q.js → TaskforceAgentsModule-Coq9-PUp.js} +2 -2
- package/dist/ui/assets/{WorkflowManagerModule-DbfnHJ-f.js → WorkflowManagerModule-CuqRzMmM.js} +1 -1
- package/dist/ui/assets/index-DSQINwt9.js +7 -0
- package/dist/ui/index.html +1 -1
- package/package.json +3 -1
- package/dist/ui/assets/PlanningModule-pRPjR7v5.js +0 -1
- package/dist/ui/assets/index-CTsDBaef.js +0 -7
|
@@ -14,6 +14,8 @@ const TRANSITIVE_TAG_KEYS = [
|
|
|
14
14
|
'TaskforceWorkspaceHash',
|
|
15
15
|
'TaskforceSource',
|
|
16
16
|
];
|
|
17
|
+
const verifiedWorkloadJwtClaimsBrand = Symbol('verifiedExecutorPhaseAWorkloadJwtClaims');
|
|
18
|
+
const verifiedWorkloadJwtClaims = new WeakSet();
|
|
17
19
|
function timestamp(value, field) {
|
|
18
20
|
const parsed = Date.parse(value);
|
|
19
21
|
if (!Number.isFinite(parsed))
|
|
@@ -155,6 +157,111 @@ export async function signExecutorPhaseAWorkloadJwt(claims, signer) {
|
|
|
155
157
|
throw new TypeError('The OIDC signer returned a non-RSA-2048 signature.');
|
|
156
158
|
return `${signingInput}.${signature.toString('base64url')}`;
|
|
157
159
|
}
|
|
160
|
+
function decodeCanonicalJwtSegment(segment, field) {
|
|
161
|
+
if (!/^[A-Za-z0-9_-]+$/.test(segment))
|
|
162
|
+
throw new TypeError(`The workload JWT ${field} is not base64url.`);
|
|
163
|
+
const bytes = Buffer.from(segment, 'base64url');
|
|
164
|
+
if (bytes.toString('base64url') !== segment)
|
|
165
|
+
throw new TypeError(`The workload JWT ${field} is not canonical base64url.`);
|
|
166
|
+
let value;
|
|
167
|
+
try {
|
|
168
|
+
value = JSON.parse(bytes.toString('utf8'));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
throw new TypeError(`The workload JWT ${field} is not valid JSON.`);
|
|
172
|
+
}
|
|
173
|
+
if (canonicalizeExecutorPhaseAJson(value).toString('base64url') !== segment) {
|
|
174
|
+
throw new TypeError(`The workload JWT ${field} is not canonical JSON.`);
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
export function verifyExecutorPhaseAWorkloadJwt(jwt, bundle, keys, now = new Date()) {
|
|
179
|
+
if (jwt.length > 16_384)
|
|
180
|
+
throw new TypeError('The workload JWT exceeds the maximum size.');
|
|
181
|
+
const segments = jwt.split('.');
|
|
182
|
+
if (segments.length !== 3)
|
|
183
|
+
throw new TypeError('The workload JWT must contain exactly three segments.');
|
|
184
|
+
const [headerSegment, payloadSegment, signatureSegment] = segments;
|
|
185
|
+
const header = decodeCanonicalJwtSegment(headerSegment, 'header');
|
|
186
|
+
if (!header || typeof header !== 'object' || Array.isArray(header)
|
|
187
|
+
|| Object.keys(header).sort().join(',') !== 'alg,kid,typ'
|
|
188
|
+
|| header.alg !== 'RS256'
|
|
189
|
+
|| header.typ !== 'JWT'
|
|
190
|
+
|| typeof header.kid !== 'string') {
|
|
191
|
+
throw new TypeError('The workload JWT header is invalid.');
|
|
192
|
+
}
|
|
193
|
+
const key = keys.find((candidate) => candidate.keyId === header.kid);
|
|
194
|
+
const signature = Buffer.from(signatureSegment, 'base64url');
|
|
195
|
+
const nowMs = now.getTime();
|
|
196
|
+
if (new Set(keys.map((candidate) => candidate.keyId)).size !== keys.length
|
|
197
|
+
|| !key || (key.status !== 'active' && key.status !== 'retiring')
|
|
198
|
+
|| nowMs < timestamp(key.validFrom, 'key.validFrom')
|
|
199
|
+
|| nowMs >= timestamp(key.validUntil, 'key.validUntil') || key.publicKey.asymmetricKeyType !== 'rsa'
|
|
200
|
+
|| key.publicKey.asymmetricKeyDetails?.modulusLength !== 2048
|
|
201
|
+
|| !/^[A-Za-z0-9_-]+$/.test(signatureSegment)
|
|
202
|
+
|| signature.length !== 256 || signature.toString('base64url') !== signatureSegment
|
|
203
|
+
|| !verify('RSA-SHA256', Buffer.from(`${headerSegment}.${payloadSegment}`, 'ascii'), key.publicKey, signature)) {
|
|
204
|
+
throw new TypeError('The workload JWT signature or signing key is invalid.');
|
|
205
|
+
}
|
|
206
|
+
const claims = decodeCanonicalJwtSegment(payloadSegment, 'payload');
|
|
207
|
+
assertExecutorPhaseASchema('workloadJwtClaims', claims);
|
|
208
|
+
const capability = bundle.capability.payload;
|
|
209
|
+
const grant = bundle.grant.payload;
|
|
210
|
+
const nowSeconds = Math.floor(now.getTime() / 1000);
|
|
211
|
+
const skewSeconds = EXECUTOR_PHASE_A_CROSS_SERVICE_CLOCK_SKEW_MS / 1000;
|
|
212
|
+
const grantIssuedAt = Math.floor(timestamp(grant.issuedAt, 'grant.issuedAt') / 1000);
|
|
213
|
+
const grantExpiresAt = Math.floor(timestamp(grant.expiresAt, 'grant.expiresAt') / 1000);
|
|
214
|
+
const phaseDeadline = Math.floor(timestamp(grant.phase === 'cleanup' ? capability.cleanupDeadline : capability.mutationDeadline, 'phaseDeadline') / 1000);
|
|
215
|
+
const runDigest = createHash('sha256').update(capability.runId, 'utf8').digest('base64url');
|
|
216
|
+
const tags = claims['https://aws.amazon.com/tags'];
|
|
217
|
+
const expectedTags = grant.sessionTags;
|
|
218
|
+
if (claims.iss !== capability.oidcIssuer || claims.aud !== capability.oidcAudience
|
|
219
|
+
|| claims.sub !== `tfr:${runDigest}:${grant.phase}`
|
|
220
|
+
|| claims['https://aws.amazon.com/source_identity'] !== executorPhaseASourceIdentity(capability.runId, grant.phase)
|
|
221
|
+
|| claims.iat < grantIssuedAt - skewSeconds || claims.iat > nowSeconds + skewSeconds
|
|
222
|
+
|| claims.nbf !== claims.iat
|
|
223
|
+
|| nowSeconds >= claims.exp + skewSeconds || claims.exp <= claims.iat || claims.exp - claims.iat > JWT_TTL_SECONDS
|
|
224
|
+
|| claims.exp > grantExpiresAt || claims.exp > phaseDeadline
|
|
225
|
+
|| tags.principal_tags.TaskforceRun[0] !== expectedTags.TaskforceRun
|
|
226
|
+
|| tags.principal_tags.TaskforceReceipt[0] !== expectedTags.TaskforceReceipt
|
|
227
|
+
|| tags.principal_tags.TaskforcePhase[0] !== expectedTags.TaskforcePhase
|
|
228
|
+
|| tags.principal_tags.TaskforceWorkspaceHash[0] !== expectedTags.TaskforceWorkspaceHash
|
|
229
|
+
|| tags.principal_tags.TaskforceSource[0] !== expectedTags.TaskforceSource
|
|
230
|
+
|| claims.taskforce.receiptId !== capability.receiptId
|
|
231
|
+
|| claims.taskforce.receiptSha256 !== capability.receiptSha256
|
|
232
|
+
|| claims.taskforce.workspaceId !== capability.workspaceId
|
|
233
|
+
|| claims.taskforce.taskId !== capability.taskId
|
|
234
|
+
|| claims.taskforce.runId !== capability.runId
|
|
235
|
+
|| claims.taskforce.sourceCommit !== capability.sourceCommit
|
|
236
|
+
|| claims.taskforce.accountId !== capability.accountId
|
|
237
|
+
|| claims.taskforce.region !== capability.region
|
|
238
|
+
|| claims.taskforce.availabilityZone !== capability.availabilityZone
|
|
239
|
+
|| claims.taskforce.logicalEnvelopeSha256 !== capability.logicalEnvelopeSha256
|
|
240
|
+
|| claims.taskforce.budgetsSha256 !== capability.budgetsSha256
|
|
241
|
+
|| claims.taskforce.roleArn !== grant.roleArn
|
|
242
|
+
|| claims.taskforce.brokerKeyFingerprint !== capability.brokerKeyFingerprint
|
|
243
|
+
|| claims.taskforce.grantId !== grant.grantId
|
|
244
|
+
|| claims.taskforce.grantSha256 !== bundle.grantSha256
|
|
245
|
+
|| claims.taskforce.senderProofSha256 !== bundle.senderProofSha256) {
|
|
246
|
+
throw new TypeError('The workload JWT claims differ from the verified grant.');
|
|
247
|
+
}
|
|
248
|
+
const verified = structuredClone(claims);
|
|
249
|
+
const freeze = (value) => {
|
|
250
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value))
|
|
251
|
+
return;
|
|
252
|
+
for (const child of Object.values(value))
|
|
253
|
+
freeze(child);
|
|
254
|
+
Object.freeze(value);
|
|
255
|
+
};
|
|
256
|
+
freeze(verified);
|
|
257
|
+
verifiedWorkloadJwtClaims.add(verified);
|
|
258
|
+
return verified;
|
|
259
|
+
}
|
|
260
|
+
export function assertVerifiedExecutorPhaseAWorkloadJwtClaims(claims) {
|
|
261
|
+
if (!claims || typeof claims !== 'object' || !verifiedWorkloadJwtClaims.has(claims)) {
|
|
262
|
+
throw new TypeError('The broker requires runtime-verified workload JWT claims.');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
158
265
|
function verifyRedemptionResponse(response, request, cloudKeys, now) {
|
|
159
266
|
assertExecutorPhaseASchema('issuerRedemptionResponse', response);
|
|
160
267
|
const trustedKey = cloudKeys.find((key) => key.keyId === response.envelope.keyId && key.status !== 'disabled');
|
|
@@ -59,7 +59,7 @@ export declare function taskforceAgentMcpRequestId(call: TaskforceAgentMcpToolCa
|
|
|
59
59
|
declare const MANAGED_AGENT_CAPABILITY_TOOLS: Readonly<{
|
|
60
60
|
readonly tasks: readonly ["add_comment", "add_task_checklist_item", "complete_workflow_step", "get_schedule_board", "get_stats", "get_task", "get_task_attachments", "get_task_checklist", "get_task_workflow", "list_task_relationships", "list_tasks", "replace_task_checklist", "search_tasks", "update_task_checklist_item"];
|
|
61
61
|
readonly planning: readonly ["add_initiative_comment", "add_workstream_comment", "get_initiative", "get_workstream", "list_initiatives", "list_workstreams"];
|
|
62
|
-
readonly documents: readonly ["get_document", "get_document_reviews", "search_documents"];
|
|
62
|
+
readonly documents: readonly ["get_document", "get_document_reviews", "review_document", "search_documents"];
|
|
63
63
|
readonly images: readonly ["get_image", "get_image_annotations", "search_images"];
|
|
64
64
|
readonly workspace: readonly ["get_config", "get_current_profile", "list_assignees"];
|
|
65
65
|
}>;
|
|
@@ -75,6 +75,7 @@ const MANAGED_AGENT_CAPABILITY_TOOLS = Object.freeze({
|
|
|
75
75
|
documents: [
|
|
76
76
|
'get_document',
|
|
77
77
|
'get_document_reviews',
|
|
78
|
+
'review_document',
|
|
78
79
|
'search_documents',
|
|
79
80
|
],
|
|
80
81
|
images: [
|
|
@@ -187,21 +188,50 @@ function buildToolInstructions(baseSystem, tools, allowedMutationTaskId, readOnl
|
|
|
187
188
|
const toolLines = tools.map((tool) => (`- ${tool.name}: ${compactText(tool.description, 180)} Args: ${describeToolSchema(tool.inputSchema)}`));
|
|
188
189
|
const taskScope = String(allowedMutationTaskId || '').trim();
|
|
189
190
|
const canCompleteWorkflow = tools.some((tool) => tool.name === 'complete_workflow_step');
|
|
191
|
+
const canReviewDocuments = tools.some((tool) => tool.name === 'review_document');
|
|
192
|
+
const canAddComments = tools.some((tool) => [
|
|
193
|
+
'add_comment',
|
|
194
|
+
'add_initiative_comment',
|
|
195
|
+
'add_workstream_comment',
|
|
196
|
+
].includes(tool.name));
|
|
197
|
+
const canChangeChecklists = tools.some((tool) => [
|
|
198
|
+
'add_task_checklist_item',
|
|
199
|
+
'replace_task_checklist',
|
|
200
|
+
'update_task_checklist_item',
|
|
201
|
+
].includes(tool.name));
|
|
202
|
+
const allowedWrites = [
|
|
203
|
+
...(canAddComments ? ['comments'] : []),
|
|
204
|
+
...(canChangeChecklists ? ['checklist changes'] : []),
|
|
205
|
+
...(canReviewDocuments ? ['document review feedback'] : []),
|
|
206
|
+
...(canCompleteWorkflow ? ['completion of the current owned workflow step'] : []),
|
|
207
|
+
];
|
|
208
|
+
const allowedWritesSentence = allowedWrites.length > 0
|
|
209
|
+
? `Allowed writes: ${allowedWrites.join(', ')}.${canCompleteWorkflow ? ' Use complete_workflow_step only after finishing that step.' : ''}`
|
|
210
|
+
: 'No Taskforce writes are enabled for this run.';
|
|
211
|
+
const availableToolNames = new Set(tools.map((tool) => tool.name));
|
|
212
|
+
const selectedToolCallExample = [
|
|
213
|
+
{ name: 'get_task', arguments: { id: 'T-123' } },
|
|
214
|
+
{ name: 'get_document', arguments: { documentId: 'D-123' } },
|
|
215
|
+
{ name: 'get_initiative', arguments: { id: 'IN-123' } },
|
|
216
|
+
{ name: 'get_workstream', arguments: { id: 'WS-123' } },
|
|
217
|
+
{ name: 'get_config', arguments: {} },
|
|
218
|
+
].find((example) => availableToolNames.has(example.name));
|
|
219
|
+
const toolCallExample = selectedToolCallExample
|
|
220
|
+
? { tool: selectedToolCallExample.name, arguments: selectedToolCallExample.arguments }
|
|
221
|
+
: { tool: tools[0]?.name, arguments: {} };
|
|
190
222
|
return [
|
|
191
223
|
baseSystem,
|
|
192
224
|
'',
|
|
193
225
|
'Taskforce project tools are attached invisibly by the server. Use them when the user asks about current Taskforce tasks, initiatives, workstreams, documents, images, annotations, config, stats, or specific reference labels such as T-123, IN-123, WS-123, D-123, or I-123.',
|
|
194
226
|
readOnly
|
|
195
227
|
? 'This run is read-only. Use Taskforce tools only to inspect data; no Taskforce writes are available.'
|
|
196
|
-
:
|
|
197
|
-
? 'Allowed writes: task comments, checklist changes, and completion of the current owned workflow step. Use complete_workflow_step only after finishing that step.'
|
|
198
|
-
: 'Allowed writes: task comments and checklist changes.',
|
|
228
|
+
: allowedWritesSentence,
|
|
199
229
|
...(canCompleteWorkflow && !readOnly ? [
|
|
200
230
|
'For workflow-step work, call get_task_workflow before acting. Reuse its completionCall arguments exactly; never invent or reuse workflow IDs. If requiresCommentEvidence is true, call add_comment and add its returned comment.id as evidenceCommentId. After all required work succeeds, call complete_workflow_step before answering. A comment or other task mutation does not complete the workflow step.',
|
|
201
231
|
] : []),
|
|
202
|
-
...(taskScope ? [`This run is task-scoped to ${taskScope}.
|
|
232
|
+
...(taskScope ? [`This run is task-scoped to ${taskScope}. Task-targeted mutations must target only this task.`] : []),
|
|
203
233
|
'To use a Taskforce tool, respond with only one JSON object and no prose:',
|
|
204
|
-
|
|
234
|
+
JSON.stringify(toolCallExample),
|
|
205
235
|
'After the server returns a tool result, answer the user normally unless another Taskforce tool is required; if another tool is required, respond with only one more tool-call JSON object.',
|
|
206
236
|
'Do not mention MCP setup or credentials.',
|
|
207
237
|
'Available Taskforce tools:',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Ajv2020 from 'ajv/dist/2020.js';
|
|
1
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
2
2
|
import approvalReceiptSchema from './executorPhaseASchemas/approval-receipt.schema.json' with { type: 'json' };
|
|
3
3
|
import driftAttestationSchema from './executorPhaseASchemas/drift-attestation.schema.json' with { type: 'json' };
|
|
4
4
|
import driftRequestSchema from './executorPhaseASchemas/drift-request.schema.json' with { type: 'json' };
|
|
@@ -179,8 +179,9 @@ export class PostgresAdapter {
|
|
|
179
179
|
}
|
|
180
180
|
const waitStartedAt = performance.now();
|
|
181
181
|
const waitResult = Atomics.wait(status, RESPONSE_STATUS_INDEX, RESPONSE_INITIAL, this.deadlines.responseTimeoutMs);
|
|
182
|
-
|
|
182
|
+
const workerWaitMs = performance.now() - waitStartedAt;
|
|
183
183
|
if (waitResult === 'timed-out') {
|
|
184
|
+
recordPostgresWorkerWait(payload.op, workerWaitMs);
|
|
184
185
|
this.quarantineWorker(worker);
|
|
185
186
|
throw new PostgresWorkerOutcomeUnknownError(payload.op);
|
|
186
187
|
}
|
|
@@ -190,6 +191,7 @@ export class PostgresAdapter {
|
|
|
190
191
|
}
|
|
191
192
|
const body = this.decoder.decode(responseBytes.subarray(0, responseLength));
|
|
192
193
|
const parsed = JSON.parse(body);
|
|
194
|
+
recordPostgresWorkerWait(payload.op, workerWaitMs, parsed.pool);
|
|
193
195
|
this.consecutiveUnexpectedExits = 0;
|
|
194
196
|
if (!parsed.ok && parsed.errorCode === 'RESPONSE_BUFFER_TOO_SMALL') {
|
|
195
197
|
const requiredBytes = Number(parsed.requiredBytes);
|
|
@@ -2,7 +2,15 @@ export type RequestPerformanceDiagnostics = {
|
|
|
2
2
|
postgresQueryCount: number;
|
|
3
3
|
postgresWorkerCallCount: number;
|
|
4
4
|
postgresWorkerWaitMs: number;
|
|
5
|
+
postgresWorkerWaitMaxMs: number;
|
|
6
|
+
postgresPoolWaitingMax: number;
|
|
7
|
+
postgresPoolTotalMax: number;
|
|
8
|
+
postgresPoolIdleMin: number | null;
|
|
5
9
|
};
|
|
6
10
|
export declare function runWithRequestPerformanceDiagnostics<T>(enabled: boolean, callback: () => T): T;
|
|
7
|
-
export declare function recordPostgresWorkerWait(operation: string, durationMs: number
|
|
11
|
+
export declare function recordPostgresWorkerWait(operation: string, durationMs: number, pool?: {
|
|
12
|
+
totalCount: number;
|
|
13
|
+
idleCount: number;
|
|
14
|
+
waitingCount: number;
|
|
15
|
+
}): void;
|
|
8
16
|
export declare function getRequestPerformanceDiagnostics(): RequestPerformanceDiagnostics | null;
|
|
@@ -7,16 +7,29 @@ export function runWithRequestPerformanceDiagnostics(enabled, callback) {
|
|
|
7
7
|
postgresQueryCount: 0,
|
|
8
8
|
postgresWorkerCallCount: 0,
|
|
9
9
|
postgresWorkerWaitMs: 0,
|
|
10
|
+
postgresWorkerWaitMaxMs: 0,
|
|
11
|
+
postgresPoolWaitingMax: 0,
|
|
12
|
+
postgresPoolTotalMax: 0,
|
|
13
|
+
postgresPoolIdleMin: null,
|
|
10
14
|
}, callback);
|
|
11
15
|
}
|
|
12
|
-
export function recordPostgresWorkerWait(operation, durationMs) {
|
|
16
|
+
export function recordPostgresWorkerWait(operation, durationMs, pool) {
|
|
13
17
|
const diagnostics = requestPerformanceStorage.getStore();
|
|
14
18
|
if (!diagnostics)
|
|
15
19
|
return;
|
|
16
20
|
diagnostics.postgresWorkerCallCount += 1;
|
|
17
21
|
if (operation === 'query')
|
|
18
22
|
diagnostics.postgresQueryCount += 1;
|
|
19
|
-
|
|
23
|
+
const boundedDurationMs = Math.max(0, durationMs);
|
|
24
|
+
diagnostics.postgresWorkerWaitMs += boundedDurationMs;
|
|
25
|
+
diagnostics.postgresWorkerWaitMaxMs = Math.max(diagnostics.postgresWorkerWaitMaxMs, boundedDurationMs);
|
|
26
|
+
if (pool) {
|
|
27
|
+
diagnostics.postgresPoolWaitingMax = Math.max(diagnostics.postgresPoolWaitingMax, pool.waitingCount);
|
|
28
|
+
diagnostics.postgresPoolTotalMax = Math.max(diagnostics.postgresPoolTotalMax, pool.totalCount);
|
|
29
|
+
diagnostics.postgresPoolIdleMin = diagnostics.postgresPoolIdleMin === null
|
|
30
|
+
? pool.idleCount
|
|
31
|
+
: Math.min(diagnostics.postgresPoolIdleMin, pool.idleCount);
|
|
32
|
+
}
|
|
20
33
|
}
|
|
21
34
|
export function getRequestPerformanceDiagnostics() {
|
|
22
35
|
const diagnostics = requestPerformanceStorage.getStore();
|
|
@@ -111,6 +111,16 @@ if (!runtimeOptions.deadlines) {
|
|
|
111
111
|
}
|
|
112
112
|
const pool = createPool(connectionString, runtimeOptions.deadlines);
|
|
113
113
|
const txClients = new Map();
|
|
114
|
+
function withPoolSnapshot(payload) {
|
|
115
|
+
return {
|
|
116
|
+
...payload,
|
|
117
|
+
pool: {
|
|
118
|
+
totalCount: Math.max(0, Number(pool.totalCount) || 0),
|
|
119
|
+
idleCount: Math.max(0, Number(pool.idleCount) || 0),
|
|
120
|
+
waitingCount: Math.max(0, Number(pool.waitingCount) || 0),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
114
124
|
async function handleMessage(message) {
|
|
115
125
|
const { payload } = message;
|
|
116
126
|
const txId = payload.txId ? String(payload.txId) : null;
|
|
@@ -174,7 +184,7 @@ parentPort.on('message', async (message) => {
|
|
|
174
184
|
return;
|
|
175
185
|
}
|
|
176
186
|
const response = await handleMessage(message);
|
|
177
|
-
writeResponse(message.statusBuffer, message.responseBuffer, response);
|
|
187
|
+
writeResponse(message.statusBuffer, message.responseBuffer, withPoolSnapshot(response));
|
|
178
188
|
}
|
|
179
189
|
catch (error) {
|
|
180
190
|
const messageText = error instanceof Error ? error.message : String(error);
|
|
@@ -182,10 +192,10 @@ parentPort.on('message', async (message) => {
|
|
|
182
192
|
&& typeof error.code === 'string'
|
|
183
193
|
? String(error.code)
|
|
184
194
|
: undefined;
|
|
185
|
-
writeResponse(message.statusBuffer, message.responseBuffer, {
|
|
195
|
+
writeResponse(message.statusBuffer, message.responseBuffer, withPoolSnapshot({
|
|
186
196
|
ok: false,
|
|
187
197
|
error: messageText,
|
|
188
198
|
...(databaseErrorCode ? { databaseErrorCode } : {}),
|
|
189
|
-
});
|
|
199
|
+
}));
|
|
190
200
|
}
|
|
191
201
|
});
|
|
@@ -10,6 +10,11 @@ export type PostgresWorkerQueryResult = {
|
|
|
10
10
|
rows: Record<string, unknown>[];
|
|
11
11
|
rowCount?: number;
|
|
12
12
|
};
|
|
13
|
+
export type PostgresWorkerPoolSnapshot = {
|
|
14
|
+
totalCount: number;
|
|
15
|
+
idleCount: number;
|
|
16
|
+
waitingCount: number;
|
|
17
|
+
};
|
|
13
18
|
export type PostgresWorkerResponsePayload = {
|
|
14
19
|
ok: boolean;
|
|
15
20
|
result?: PostgresWorkerQueryResult;
|
|
@@ -18,6 +23,7 @@ export type PostgresWorkerResponsePayload = {
|
|
|
18
23
|
databaseErrorCode?: string;
|
|
19
24
|
requiredBytes?: number;
|
|
20
25
|
responseToken?: string;
|
|
26
|
+
pool?: PostgresWorkerPoolSnapshot;
|
|
21
27
|
};
|
|
22
28
|
export type PostgresWorkerDeadlines = {
|
|
23
29
|
responseTimeoutMs: number;
|
|
@@ -200,7 +200,9 @@ export declare class WorkflowStore {
|
|
|
200
200
|
installAssignmentForSync(workspaceIdRaw: string, snapshot: WorkflowAssignmentSyncRecord, options: {
|
|
201
201
|
authority: 'pending-intent' | 'cloud-execution';
|
|
202
202
|
expectedTaskId: string;
|
|
203
|
+
replaceAuthoritativeHistory?: boolean;
|
|
203
204
|
}): WorkflowExecutionStateRecord;
|
|
205
|
+
private runAssignmentSyncTransaction;
|
|
204
206
|
appendExecutionTransition(input: {
|
|
205
207
|
workspaceId: string;
|
|
206
208
|
executionId: string;
|
|
@@ -759,7 +759,7 @@ export class WorkflowStore {
|
|
|
759
759
|
|| snapshot.execution.updatedAt !== snapshotProjection.updatedAt) {
|
|
760
760
|
throw new WorkflowAssignmentSyncConflictError('Workflow assignment execution does not match its event history.');
|
|
761
761
|
}
|
|
762
|
-
return this.
|
|
762
|
+
return this.runAssignmentSyncTransaction(Boolean(options.authority === 'cloud-execution' && options.replaceAuthoritativeHistory), () => {
|
|
763
763
|
const existingForTask = this.getActiveExecutionForTask(workspaceId, snapshot.execution.taskId);
|
|
764
764
|
if (existingForTask && existingForTask.id !== snapshot.execution.id) {
|
|
765
765
|
throw new WorkflowAssignmentSyncConflictError('Task already has a different active workflow execution.');
|
|
@@ -852,7 +852,14 @@ export class WorkflowStore {
|
|
|
852
852
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
853
853
|
`).run(snapshot.execution.id, this.tenantId, workspaceId, snapshot.execution.taskId, snapshot.template.id, snapshot.version.id, snapshot.execution.status, snapshot.execution.currentStepId || null, snapshot.execution.createdByActorId || null, snapshot.execution.updatedByActorId || null, snapshot.execution.createdAt, snapshot.execution.updatedAt, snapshot.execution.startedAt || null, snapshot.execution.completedAt || null, snapshot.execution.endedAt || null, snapshot.execution.deletedAt || null);
|
|
854
854
|
}
|
|
855
|
-
|
|
855
|
+
let canonicalEvents = this.listExecutionEvents(workspaceId, snapshot.execution.id);
|
|
856
|
+
if (options.authority === 'cloud-execution' && options.replaceAuthoritativeHistory) {
|
|
857
|
+
this.db.prepare(`
|
|
858
|
+
DELETE FROM workflow_execution_events
|
|
859
|
+
WHERE tenant_id = ? AND workspace_id = ? AND workflow_execution_id = ?
|
|
860
|
+
`).run(this.tenantId, workspaceId, snapshot.execution.id);
|
|
861
|
+
canonicalEvents = [];
|
|
862
|
+
}
|
|
856
863
|
const authoritativeHistoryIsExactPrefix = options.authority === 'cloud-execution'
|
|
857
864
|
&& snapshot.events.length < canonicalEvents.length
|
|
858
865
|
&& snapshot.events.every((event, index) => canonicalJson(event) === canonicalJson(canonicalEvents[index]));
|
|
@@ -908,6 +915,36 @@ export class WorkflowStore {
|
|
|
908
915
|
`).run(snapshotProjection.status, snapshotProjection.currentStepId || null, snapshot.execution.updatedByActorId || null, snapshotProjection.updatedAt, snapshotProjection.startedAt || null, snapshotProjection.completedAt || null, snapshotProjection.endedAt || null, snapshot.execution.deletedAt || null, this.tenantId, workspaceId, snapshot.execution.id);
|
|
909
916
|
}
|
|
910
917
|
return this.getExecutionStateSnapshot(workspaceId, snapshot.execution.id);
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
runAssignmentSyncTransaction(replaceAuthoritativeHistory, apply) {
|
|
921
|
+
return this.db.transaction(() => {
|
|
922
|
+
if (!replaceAuthoritativeHistory)
|
|
923
|
+
return apply();
|
|
924
|
+
if (this.db.provider === 'postgres') {
|
|
925
|
+
throw new WorkflowAssignmentSyncConflictError('Authoritative workflow history replacement is local-runtime only.');
|
|
926
|
+
}
|
|
927
|
+
this.db.exec(`
|
|
928
|
+
DROP TRIGGER IF EXISTS trg_workflow_execution_events_no_update;
|
|
929
|
+
DROP TRIGGER IF EXISTS trg_workflow_execution_events_no_delete;
|
|
930
|
+
`);
|
|
931
|
+
try {
|
|
932
|
+
return apply();
|
|
933
|
+
}
|
|
934
|
+
finally {
|
|
935
|
+
this.db.exec(`
|
|
936
|
+
CREATE TRIGGER IF NOT EXISTS trg_workflow_execution_events_no_update
|
|
937
|
+
BEFORE UPDATE ON workflow_execution_events
|
|
938
|
+
BEGIN
|
|
939
|
+
SELECT RAISE(ABORT, 'Workflow execution events are append-only');
|
|
940
|
+
END;
|
|
941
|
+
CREATE TRIGGER IF NOT EXISTS trg_workflow_execution_events_no_delete
|
|
942
|
+
BEFORE DELETE ON workflow_execution_events
|
|
943
|
+
BEGIN
|
|
944
|
+
SELECT RAISE(ABORT, 'Workflow execution events are append-only');
|
|
945
|
+
END;
|
|
946
|
+
`);
|
|
947
|
+
}
|
|
911
948
|
})();
|
|
912
949
|
}
|
|
913
950
|
appendExecutionTransition(input) {
|
|
@@ -558,8 +558,10 @@ export async function prepareWorkspacePushChanges(input, dependencies = {}) {
|
|
|
558
558
|
.filter((taskId) => taskUpsertIdSet.has(taskId) && !activeV3TaskCreateIds.has(taskId));
|
|
559
559
|
}
|
|
560
560
|
const ownershipFilteredChanges = input.changes.filter((change) => {
|
|
561
|
-
if (change.op === 'upsert')
|
|
562
|
-
|
|
561
|
+
if (change.op === 'upsert') {
|
|
562
|
+
const taskId = String(change.task?.id || '').trim();
|
|
563
|
+
return !activeV3TaskCreateIds.has(taskId) && !activeV3TaskDeleteIds.has(taskId);
|
|
564
|
+
}
|
|
563
565
|
if (change.op === 'delete') {
|
|
564
566
|
return !activeV3TaskDeleteIds.has(String(change.taskId || '').trim());
|
|
565
567
|
}
|
|
@@ -32,6 +32,11 @@ export function listWorkspaceSyncDocumentReviewCommentsSnapshot(deps, workspaceI
|
|
|
32
32
|
authorActorId: comment.authorActorId,
|
|
33
33
|
updatedByActorId: comment.updatedByActorId,
|
|
34
34
|
deletedByActorId: comment.deletedByActorId,
|
|
35
|
+
parentCommentId: comment.parentCommentId,
|
|
36
|
+
threadStatus: comment.threadStatus,
|
|
37
|
+
threadStatusUpdatedAt: comment.threadStatusUpdatedAt,
|
|
38
|
+
resolvedAt: comment.resolvedAt,
|
|
39
|
+
resolvedByActorId: comment.resolvedByActorId,
|
|
35
40
|
createdAt: comment.createdAt,
|
|
36
41
|
updatedAt: comment.updatedAt,
|
|
37
42
|
deletedAt: comment.deletedAt,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LocalSyncCoordinatorCommandRecord } from './localSyncCoordinatorTypes.js';
|
|
2
2
|
import type { LocalSyncCoordinatorStore } from './localSyncCoordinatorStore.js';
|
|
3
|
-
import { type LocalWorkspaceSyncRunnerCheckpoint, type LocalWorkspaceSyncRunnerState, type LocalWorkspaceSyncRunnerStateStore } from './localWorkspaceSyncRunnerState.js';
|
|
3
|
+
import { type LocalWorkspaceSyncRunnerCheckpoint, type LocalWorkspaceSyncRunnerRetryKind, type LocalWorkspaceSyncRunnerState, type LocalWorkspaceSyncRunnerStateStore } from './localWorkspaceSyncRunnerState.js';
|
|
4
4
|
import type { WorkspaceSyncCredentialBroker } from './workspaceSyncCredentialBroker.js';
|
|
5
5
|
export interface LocalWorkspaceSyncRunnerOperationInput {
|
|
6
6
|
command: LocalSyncCoordinatorCommandRecord | null;
|
|
@@ -55,6 +55,7 @@ export declare class LocalWorkspaceSyncRunnerError extends Error {
|
|
|
55
55
|
readonly code: 'SYNC_RUNNER_NOT_STARTED' | 'SYNC_RUNNER_ALREADY_STARTED' | 'SYNC_RUNNER_COORDINATOR_FENCE_INVALID' | 'SYNC_RUNNER_COMMAND_INVALID' | 'SYNC_RUNNER_COMMAND_DISABLED' | 'SYNC_RUNNER_DRAIN_TIMEOUT';
|
|
56
56
|
constructor(message: string, code: 'SYNC_RUNNER_NOT_STARTED' | 'SYNC_RUNNER_ALREADY_STARTED' | 'SYNC_RUNNER_COORDINATOR_FENCE_INVALID' | 'SYNC_RUNNER_COMMAND_INVALID' | 'SYNC_RUNNER_COMMAND_DISABLED' | 'SYNC_RUNNER_DRAIN_TIMEOUT');
|
|
57
57
|
}
|
|
58
|
+
export declare function canCoordinatorRecoveryPreemptRetry(kind: LocalWorkspaceSyncRunnerRetryKind): boolean;
|
|
58
59
|
export declare class LocalWorkspaceSyncRunner {
|
|
59
60
|
private readonly options;
|
|
60
61
|
private context;
|
|
@@ -87,6 +88,7 @@ export declare class LocalWorkspaceSyncRunner {
|
|
|
87
88
|
activate(): void;
|
|
88
89
|
executeCommand(commandId: string): Promise<LocalSyncCoordinatorCommandRecord | null>;
|
|
89
90
|
inspect(): Promise<void>;
|
|
91
|
+
recoverCoordinatorObligationsDue(isRecoveryStillRequired: () => boolean): Promise<void>;
|
|
90
92
|
pullDue(): Promise<void>;
|
|
91
93
|
retryDue(): Promise<void>;
|
|
92
94
|
pushDue(): Promise<void>;
|
|
@@ -21,6 +21,14 @@ const CONTENT_PREREQUISITE_BYPASSED_RETRY_KINDS = new Set([
|
|
|
21
21
|
'pull',
|
|
22
22
|
'repair',
|
|
23
23
|
]);
|
|
24
|
+
const COORDINATOR_RECOVERY_BYPASSED_RETRY_KINDS = new Set([
|
|
25
|
+
'pull',
|
|
26
|
+
'push',
|
|
27
|
+
'v3-dispatch',
|
|
28
|
+
]);
|
|
29
|
+
export function canCoordinatorRecoveryPreemptRetry(kind) {
|
|
30
|
+
return COORDINATOR_RECOVERY_BYPASSED_RETRY_KINDS.has(kind);
|
|
31
|
+
}
|
|
24
32
|
function cloneCheckpoint(checkpoint) {
|
|
25
33
|
return JSON.parse(canonicalWorkspaceSyncJson(checkpoint));
|
|
26
34
|
}
|
|
@@ -140,6 +148,16 @@ export class LocalWorkspaceSyncRunner {
|
|
|
140
148
|
}
|
|
141
149
|
return this.enqueue(async () => this.runInspection());
|
|
142
150
|
}
|
|
151
|
+
recoverCoordinatorObligationsDue(isRecoveryStillRequired) {
|
|
152
|
+
if (!this.context || this.terminated || !this.acceptingWork) {
|
|
153
|
+
return Promise.resolve();
|
|
154
|
+
}
|
|
155
|
+
return this.enqueue(async () => {
|
|
156
|
+
if (!isRecoveryStillRequired())
|
|
157
|
+
return;
|
|
158
|
+
await this.runInspection(this.options.operations.sync, { bypassRetryKinds: COORDINATOR_RECOVERY_BYPASSED_RETRY_KINDS });
|
|
159
|
+
});
|
|
160
|
+
}
|
|
143
161
|
pullDue() {
|
|
144
162
|
if (!this.context || this.terminated || !this.acceptingWork) {
|
|
145
163
|
return Promise.resolve();
|
|
@@ -317,6 +317,7 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
|
|
|
317
317
|
taskforceAgentPushActorUserId: String(options.actorUserId || '').trim() || null,
|
|
318
318
|
taskforceAgentPushAccessPreflighted: true,
|
|
319
319
|
} : {}),
|
|
320
|
+
coexistenceDirection: options.coexistenceDirection,
|
|
320
321
|
recordSyncDiagnostic,
|
|
321
322
|
createSyncRouteError,
|
|
322
323
|
});
|
|
@@ -777,6 +778,7 @@ export function createWorkspaceSyncV2ChangeDispatcher(dependencies) {
|
|
|
777
778
|
taskforceAgentPushActorUserId: String(options.actorUserId || '').trim() || null,
|
|
778
779
|
taskforceAgentPushAccessPreflighted: true,
|
|
779
780
|
} : {}),
|
|
781
|
+
coexistenceDirection: options.coexistenceDirection,
|
|
780
782
|
recordSyncDiagnostic,
|
|
781
783
|
createSyncRouteError,
|
|
782
784
|
});
|
|
@@ -257,6 +257,7 @@ export declare function applyCollaborationSyncChanges(input: {
|
|
|
257
257
|
incomingAgentConversations?: Map<string, WorkspaceSyncAgentConversationSnapshot>;
|
|
258
258
|
taskforceAgentPushActorUserId?: string | null;
|
|
259
259
|
taskforceAgentPushAccessPreflighted?: boolean;
|
|
260
|
+
coexistenceDirection?: 'local-pull' | 'cloud-push';
|
|
260
261
|
recordSyncDiagnostic: RecordSyncDiagnostic;
|
|
261
262
|
createSyncRouteError: CreateSyncRouteError;
|
|
262
263
|
}): void;
|
|
@@ -1053,7 +1053,7 @@ export async function applyContentSyncChanges(input) {
|
|
|
1053
1053
|
}
|
|
1054
1054
|
}
|
|
1055
1055
|
export function applyCollaborationSyncChanges(input) {
|
|
1056
|
-
const { core, workspaceId, incomingDocumentReviewSessions, incomingDocumentReviewSessionDeletes, incomingDocumentReviewComments, incomingDocumentReviewCommentDeletes, incomingAnnotatedAttachmentSessions, incomingAnnotatedAttachmentSessionDeletes, incomingTaskforceAgents = new Map(), incomingAgentRoles = new Map(), incomingTaskforceAgentSkills = new Map(), incomingAgentConversations = new Map(), taskforceAgentPushActorUserId, taskforceAgentPushAccessPreflighted = false, recordSyncDiagnostic, createSyncRouteError, } = input;
|
|
1056
|
+
const { core, workspaceId, incomingDocumentReviewSessions, incomingDocumentReviewSessionDeletes, incomingDocumentReviewComments, incomingDocumentReviewCommentDeletes, incomingAnnotatedAttachmentSessions, incomingAnnotatedAttachmentSessionDeletes, incomingTaskforceAgents = new Map(), incomingAgentRoles = new Map(), incomingTaskforceAgentSkills = new Map(), incomingAgentConversations = new Map(), taskforceAgentPushActorUserId, taskforceAgentPushAccessPreflighted = false, coexistenceDirection, recordSyncDiagnostic, createSyncRouteError, } = input;
|
|
1057
1057
|
if (taskforceAgentPushActorUserId !== undefined && !taskforceAgentPushAccessPreflighted) {
|
|
1058
1058
|
assertTaskforceAgentSyncPushAccess({
|
|
1059
1059
|
agents: incomingTaskforceAgents.values(),
|
|
@@ -1141,6 +1141,48 @@ export function applyCollaborationSyncChanges(input) {
|
|
|
1141
1141
|
.digest('hex')
|
|
1142
1142
|
: '';
|
|
1143
1143
|
const metadataCompare = resolveSyncConflictOrdering(comment.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
|
|
1144
|
+
const parentCommentConflict = Boolean(existing
|
|
1145
|
+
&& comment.parentCommentId !== undefined
|
|
1146
|
+
&& comment.parentCommentId !== existing.parentCommentId);
|
|
1147
|
+
const nonThreadFieldsMatch = Boolean(existing
|
|
1148
|
+
&& comment.sessionId === existing.sessionId
|
|
1149
|
+
&& comment.body === existing.body
|
|
1150
|
+
&& JSON.stringify(comment.anchor ?? null) === JSON.stringify(existing.anchor ?? null)
|
|
1151
|
+
&& comment.order === existing.order
|
|
1152
|
+
&& (comment.authorActorId ?? null) === (existing.authorActorId ?? null)
|
|
1153
|
+
&& (comment.updatedByActorId ?? null) === (existing.updatedByActorId ?? null)
|
|
1154
|
+
&& (comment.deletedByActorId ?? null) === (existing.deletedByActorId ?? null)
|
|
1155
|
+
&& comment.createdAt === existing.createdAt
|
|
1156
|
+
&& comment.updatedAt === existing.updatedAt
|
|
1157
|
+
&& (comment.deletedAt ?? null) === (existing.deletedAt ?? null));
|
|
1158
|
+
const incomingIsLegacyProjectedRoot = Boolean(nonThreadFieldsMatch
|
|
1159
|
+
&& comment.parentCommentId === null
|
|
1160
|
+
&& comment.threadStatus === 'open'
|
|
1161
|
+
&& comment.threadStatusUpdatedAt === comment.createdAt
|
|
1162
|
+
&& !comment.resolvedAt
|
|
1163
|
+
&& !comment.resolvedByActorId);
|
|
1164
|
+
const existingIsLegacyProjectedRoot = Boolean(nonThreadFieldsMatch
|
|
1165
|
+
&& existing
|
|
1166
|
+
&& existing.parentCommentId === null
|
|
1167
|
+
&& existing.threadStatus === 'open'
|
|
1168
|
+
&& existing.threadStatusUpdatedAt === existing.createdAt
|
|
1169
|
+
&& !existing.resolvedAt
|
|
1170
|
+
&& !existing.resolvedByActorId);
|
|
1171
|
+
const preserveLocalReplyParent = parentCommentConflict
|
|
1172
|
+
&& coexistenceDirection === 'local-pull'
|
|
1173
|
+
&& Boolean(existing?.parentCommentId)
|
|
1174
|
+
&& incomingIsLegacyProjectedRoot;
|
|
1175
|
+
const reconcileAuthoritativeParent = parentCommentConflict
|
|
1176
|
+
&& coexistenceDirection === 'local-pull'
|
|
1177
|
+
&& !preserveLocalReplyParent;
|
|
1178
|
+
const reconcileLegacyCloudRoot = parentCommentConflict
|
|
1179
|
+
&& coexistenceDirection === 'cloud-push'
|
|
1180
|
+
&& existingIsLegacyProjectedRoot
|
|
1181
|
+
&& Boolean(comment.parentCommentId);
|
|
1182
|
+
const applyIncomingMetadata = metadataCompare >= 0
|
|
1183
|
+
&& !(parentCommentConflict
|
|
1184
|
+
&& coexistenceDirection === 'cloud-push'
|
|
1185
|
+
&& !reconcileLegacyCloudRoot);
|
|
1144
1186
|
const incomingLifecycleTieBreak = createHash('sha256')
|
|
1145
1187
|
.update(JSON.stringify({
|
|
1146
1188
|
threadStatus: comment.threadStatus,
|
|
@@ -1160,28 +1202,43 @@ export function applyCollaborationSyncChanges(input) {
|
|
|
1160
1202
|
const lifecycleCompare = comment.threadStatusUpdatedAt === undefined
|
|
1161
1203
|
? -1
|
|
1162
1204
|
: resolveSyncConflictOrdering(String(comment.threadStatusUpdatedAt || '').trim(), String(existing?.threadStatusUpdatedAt || '').trim(), incomingLifecycleTieBreak, existingLifecycleTieBreak);
|
|
1163
|
-
if (metadataCompare < 0
|
|
1205
|
+
if (metadataCompare < 0
|
|
1206
|
+
&& lifecycleCompare < 0
|
|
1207
|
+
&& !reconcileAuthoritativeParent
|
|
1208
|
+
&& !reconcileLegacyCloudRoot)
|
|
1164
1209
|
continue;
|
|
1165
|
-
|
|
1210
|
+
const commentForSync = {
|
|
1166
1211
|
id: comment.id,
|
|
1167
1212
|
tenantId: core.getTenantId(),
|
|
1168
1213
|
workspaceId,
|
|
1169
|
-
sessionId:
|
|
1170
|
-
body:
|
|
1171
|
-
anchor: (
|
|
1172
|
-
order:
|
|
1173
|
-
authorActorId:
|
|
1174
|
-
updatedByActorId:
|
|
1175
|
-
deletedByActorId:
|
|
1176
|
-
parentCommentId:
|
|
1214
|
+
sessionId: applyIncomingMetadata ? comment.sessionId : existing.sessionId,
|
|
1215
|
+
body: applyIncomingMetadata ? comment.body : existing.body,
|
|
1216
|
+
anchor: (applyIncomingMetadata ? comment.anchor : existing.anchor),
|
|
1217
|
+
order: applyIncomingMetadata ? comment.order : existing.order,
|
|
1218
|
+
authorActorId: applyIncomingMetadata ? comment.authorActorId : existing.authorActorId,
|
|
1219
|
+
updatedByActorId: applyIncomingMetadata ? comment.updatedByActorId : existing.updatedByActorId,
|
|
1220
|
+
deletedByActorId: applyIncomingMetadata ? comment.deletedByActorId : existing.deletedByActorId,
|
|
1221
|
+
parentCommentId: preserveLocalReplyParent
|
|
1222
|
+
? existing.parentCommentId
|
|
1223
|
+
: reconcileAuthoritativeParent || reconcileLegacyCloudRoot || applyIncomingMetadata
|
|
1224
|
+
? comment.parentCommentId
|
|
1225
|
+
: existing.parentCommentId,
|
|
1177
1226
|
threadStatus: lifecycleCompare >= 0 ? comment.threadStatus : undefined,
|
|
1178
1227
|
threadStatusUpdatedAt: lifecycleCompare >= 0 ? comment.threadStatusUpdatedAt : undefined,
|
|
1179
1228
|
resolvedAt: lifecycleCompare >= 0 ? comment.resolvedAt : undefined,
|
|
1180
1229
|
resolvedByActorId: lifecycleCompare >= 0 ? comment.resolvedByActorId : undefined,
|
|
1181
|
-
createdAt:
|
|
1182
|
-
updatedAt:
|
|
1183
|
-
deletedAt:
|
|
1184
|
-
}
|
|
1230
|
+
createdAt: applyIncomingMetadata ? comment.createdAt : existing.createdAt,
|
|
1231
|
+
updatedAt: applyIncomingMetadata ? comment.updatedAt : existing.updatedAt,
|
|
1232
|
+
deletedAt: applyIncomingMetadata ? comment.deletedAt : existing.deletedAt,
|
|
1233
|
+
};
|
|
1234
|
+
if (reconcileAuthoritativeParent || reconcileLegacyCloudRoot) {
|
|
1235
|
+
core.upsertDocumentReviewCommentForSync(commentForSync, {
|
|
1236
|
+
allowAuthoritativeThreadReconciliation: true,
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
core.upsertDocumentReviewCommentForSync(commentForSync);
|
|
1241
|
+
}
|
|
1185
1242
|
if (comment.parentCommentId) {
|
|
1186
1243
|
const rootExists = core.listDocumentReviewCommentsForSync({
|
|
1187
1244
|
workspaceId,
|