@taskforcehq/taskforce 0.3.330 → 0.3.333
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/localService.js +1 -0
- package/dist/server/routes/shared.d.ts +3 -0
- package/dist/server/routes/shared.js +280 -240
- package/dist/server/routes/syncPushRoutes.d.ts +3 -0
- package/dist/server/routes/syncPushRoutes.js +8 -2
- 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/coordinator/localWorkspaceSyncRunnerState.d.ts +1 -0
- package/dist/sync/coordinator/localWorkspaceSyncRunnerState.js +7 -6
- package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +2 -2
- package/dist/sync/engine/workspaceSyncEngineLocalApply.d.ts +1 -0
- package/dist/sync/engine/workspaceSyncEnginePushCompletion.d.ts +6 -0
- package/dist/sync/engine/workspaceSyncEnginePushCompletion.js +34 -1
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +2 -2
- package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +4 -1
- package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.d.ts +1 -0
- package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +55 -10
- package/dist/sync/syncApplyHandlers.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.js +72 -15
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +5 -2
- 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-Cfvb9Nuv.js} +1 -1
- package/dist/ui/assets/{AiProfilesModule-ByhYW5Xi.js → AiProfilesModule-0oR70eFG.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTM6nIg_.js → AnnotatedAttachmentWorkspace-C33iH_r_.js} +1 -1
- package/dist/ui/assets/{AssetTraySortControl-DabEG8L4.js → AssetTraySortControl-BAqxUtke.js} +1 -1
- package/dist/ui/assets/{ContextAttachmentManager-Coa8yVGM.js → ContextAttachmentManager-BudIbOR9.js} +2 -2
- package/dist/ui/assets/{DocumentWorkspace-BB0qhdix.js → DocumentWorkspace-A8dFQyId.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-CZ8x4UJE.js → EntityActivityTimeline-g7vtdsx1.js} +1 -1
- package/dist/ui/assets/PlanningModule-D-DeQs4F.js +1 -0
- package/dist/ui/assets/{PlansPage-D86pZSSl.js → PlansPage-BWmfGi0Z.js} +1 -1
- package/dist/ui/assets/{TaskContextUpload-BVIkU9yT.js → TaskContextUpload-BA_lQmS9.js} +1 -1
- package/dist/ui/assets/{TaskSettings-Cv1mNhv_.js → TaskSettings-DAlH4YIP.js} +1 -1
- package/dist/ui/assets/{TaskforceAgentsModule-gmH-PD2q.js → TaskforceAgentsModule-DmeCf6pJ.js} +2 -2
- package/dist/ui/assets/{WorkflowManagerModule-DbfnHJ-f.js → WorkflowManagerModule-p08eDOaa.js} +1 -1
- package/dist/ui/assets/index-GHJgcW3j.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
|
@@ -2,7 +2,6 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { applyLocalServiceCloudTarget, localServiceCloudTargetsEqual, parseLocalServiceCloudTarget, resolveLocalServiceCloudTarget, } from '../config/localServiceCloudTarget.js';
|
|
4
4
|
import { localServiceRuntimeContractsEqual, parseLocalServiceRuntimeContract, resolveLocalServiceRuntimeContract, } from '../config/localServiceRuntimeContract.js';
|
|
5
|
-
import { createStandaloneServer } from '../server/index.js';
|
|
6
5
|
import { inspectLocalServiceDescriptor, redactLocalServiceDescriptor, } from '../server/localServiceLease.js';
|
|
7
6
|
import { acquireLocalServiceOwnershipForRuntime, } from '../server/localServiceOwnershipBootstrap.js';
|
|
8
7
|
import { discoverOrStartLocalService, LOCAL_SERVICE_API_BASE_PATH, LOCAL_SERVICE_PROTOCOL_VERSION, resolveLocalServiceLeaseInput, spawnDetachedLocalService, waitForLocalServiceDescriptor, } from './localServiceClientLifecycle.js';
|
|
@@ -126,7 +125,9 @@ async function runForeground(command, options) {
|
|
|
126
125
|
});
|
|
127
126
|
};
|
|
128
127
|
try {
|
|
129
|
-
|
|
128
|
+
const createServer = options.createServer
|
|
129
|
+
|| (await import('../server/index.js')).createStandaloneServer;
|
|
130
|
+
server = createServer({
|
|
130
131
|
projectRoot: command.projectRoot,
|
|
131
132
|
port: 0,
|
|
132
133
|
localSyncProcessLock: ownership.syncProcessLock || undefined,
|
|
@@ -370,7 +371,7 @@ export async function runLocalServiceCli(argv, input) {
|
|
|
370
371
|
executablePath: input.executablePath,
|
|
371
372
|
cliEntryPath: input.cliEntryPath,
|
|
372
373
|
execArgv: input.execArgv || [],
|
|
373
|
-
createServer: input.createServer
|
|
374
|
+
createServer: input.createServer,
|
|
374
375
|
acquireOwnership: input.acquireOwnership || acquireLocalServiceOwnershipForRuntime,
|
|
375
376
|
inspectDescriptor: input.inspectDescriptor || inspectLocalServiceDescriptor,
|
|
376
377
|
spawnDetached: input.spawnDetached || spawnDetachedLocalService,
|
|
@@ -17,6 +17,17 @@ export interface ExecutorPhaseAOidcKey {
|
|
|
17
17
|
lastIssuedAt: string | null;
|
|
18
18
|
publishUntil: string | null;
|
|
19
19
|
}
|
|
20
|
+
export interface ExecutorPhaseAOidcVerificationKey {
|
|
21
|
+
keyId: string;
|
|
22
|
+
publicKey: KeyObject;
|
|
23
|
+
status: 'active' | 'retiring' | 'disabled';
|
|
24
|
+
validFrom: string;
|
|
25
|
+
validUntil: string;
|
|
26
|
+
}
|
|
27
|
+
declare const verifiedWorkloadJwtClaimsBrand: unique symbol;
|
|
28
|
+
export type VerifiedExecutorPhaseAWorkloadJwtClaims = ExecutorPhaseAWorkloadJwtClaims & {
|
|
29
|
+
readonly [verifiedWorkloadJwtClaimsBrand]: true;
|
|
30
|
+
};
|
|
20
31
|
export interface ExecutorPhaseAWorkloadJwtClaims {
|
|
21
32
|
iss: string;
|
|
22
33
|
aud: string;
|
|
@@ -103,6 +114,8 @@ export declare function buildExecutorPhaseAOidcDiscovery(issuer: string): {
|
|
|
103
114
|
};
|
|
104
115
|
export declare function buildExecutorPhaseAWorkloadJwtClaims(bundle: VerifiedExecutorPhaseAWorkloadGrantBundle, issuer: string, audience: string, tokenId: string, now: Date): ExecutorPhaseAWorkloadJwtClaims;
|
|
105
116
|
export declare function signExecutorPhaseAWorkloadJwt(claims: ExecutorPhaseAWorkloadJwtClaims, signer: ExecutorPhaseARs256Signer): Promise<string>;
|
|
117
|
+
export declare function verifyExecutorPhaseAWorkloadJwt(jwt: string, bundle: VerifiedExecutorPhaseAWorkloadGrantBundle, keys: readonly ExecutorPhaseAOidcVerificationKey[], now?: Date): VerifiedExecutorPhaseAWorkloadJwtClaims;
|
|
118
|
+
export declare function assertVerifiedExecutorPhaseAWorkloadJwtClaims(claims: VerifiedExecutorPhaseAWorkloadJwtClaims): void;
|
|
106
119
|
export declare class ExecutorPhaseAIssuerCore {
|
|
107
120
|
private readonly options;
|
|
108
121
|
constructor(options: {
|
|
@@ -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();
|
|
@@ -82,6 +82,7 @@ export interface LocalWorkspaceSyncSettledV2PushBatch {
|
|
|
82
82
|
task: WorkspaceSyncTaskSnapshot;
|
|
83
83
|
durableRevision?: WorkspaceSyncDurableTaskRevision;
|
|
84
84
|
}>;
|
|
85
|
+
settledTaskDeleteIds?: string[];
|
|
85
86
|
}
|
|
86
87
|
export interface LocalWorkspaceSyncPendingV2PushBatchProgress {
|
|
87
88
|
totalBatches: number;
|
|
@@ -528,11 +528,7 @@ function isPendingV2Push(value) {
|
|
|
528
528
|
&& settledBatchOperationIdentities.every((identity) => /^[a-f0-9]{64}$/.test(identity))
|
|
529
529
|
&& settledBatchContributions !== null
|
|
530
530
|
&& settledBatchContributions.every((entry) => (isRecord(entry)
|
|
531
|
-
&&
|
|
532
|
-
'operationIdentity',
|
|
533
|
-
'settlementIdentityFingerprint',
|
|
534
|
-
'appliedTaskUpserts',
|
|
535
|
-
])
|
|
531
|
+
&& hasRequiredAndAllowedKeys(entry, ['operationIdentity', 'settlementIdentityFingerprint', 'appliedTaskUpserts'], ['settledTaskDeleteIds'])
|
|
536
532
|
&& typeof entry.operationIdentity === 'string'
|
|
537
533
|
&& /^[a-f0-9]{64}$/.test(entry.operationIdentity)
|
|
538
534
|
&& typeof entry.settlementIdentityFingerprint === 'string'
|
|
@@ -545,7 +541,11 @@ function isPendingV2Push(value) {
|
|
|
545
541
|
&& isNonEmptyText(upsert.task.id)
|
|
546
542
|
&& isJson(upsert.task)
|
|
547
543
|
&& (upsert.durableRevision === undefined
|
|
548
|
-
|| (isRecord(upsert.durableRevision) && isJson(upsert.durableRevision)))))
|
|
544
|
+
|| (isRecord(upsert.durableRevision) && isJson(upsert.durableRevision)))))
|
|
545
|
+
&& (entry.settledTaskDeleteIds === undefined
|
|
546
|
+
|| (isStringArray(entry.settledTaskDeleteIds)
|
|
547
|
+
&& new Set(entry.settledTaskDeleteIds).size
|
|
548
|
+
=== entry.settledTaskDeleteIds.length))))
|
|
549
549
|
&& new Set(settledBatchContributions.map((entry) => entry.operationIdentity)).size === settledBatchContributions.length
|
|
550
550
|
&& settledBatchContributions.every((entry) => (settledBatchOperationIdentities.includes(entry.operationIdentity))));
|
|
551
551
|
const settlementContributionsMatchPlan = hasLegacyShape || (preparedPlan !== null
|
|
@@ -563,6 +563,7 @@ function isPendingV2Push(value) {
|
|
|
563
563
|
operationIdentity: entry.operationIdentity,
|
|
564
564
|
changes,
|
|
565
565
|
appliedTaskUpserts: entry.appliedTaskUpserts,
|
|
566
|
+
settledTaskDeleteIds: entry.settledTaskDeleteIds,
|
|
566
567
|
excludedProjections: preparedPlan.ownership.combinedExcludedProjections,
|
|
567
568
|
})) === entry.settlementIdentityFingerprint;
|
|
568
569
|
}
|
|
@@ -2,7 +2,7 @@ import { workspaceSyncGatewayOperationClassFor, workspaceSyncGatewayTimeoutMs, }
|
|
|
2
2
|
import { canonicalWorkspaceSyncJson } from '../v3/canonicalWorkspaceSyncJson.js';
|
|
3
3
|
import { WORKSPACE_SYNC_V3_COMBINED_COVERAGE_SELECTOR, resolveWorkspaceSyncV3CombinedCoverageForExclusions, } from '../v3/workspaceSyncV3CombinedWireContract.js';
|
|
4
4
|
import { WORKSPACE_SYNC_ENGINE_TRANSFER_COMPLETION_VERSION, } from './workspaceSyncEngineTransferCompletion.js';
|
|
5
|
-
import {
|
|
5
|
+
import { parseWorkspaceSyncEnginePushTaskSettlement, parseWorkspaceSyncEngineV2PushBoundary, workspaceSyncEngineExpectedTaskEchoIds, } from './workspaceSyncEnginePushCompletion.js';
|
|
6
6
|
const PERMIT_DEADLINE_SLACK_MS = 5_000;
|
|
7
7
|
function isPlainObject(value) {
|
|
8
8
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -388,7 +388,7 @@ export function createBrowserGatewayWorkspaceSyncEngineRemoteServices(options) {
|
|
|
388
388
|
requireAcceptedJsonObject: true,
|
|
389
389
|
validateAcceptedData: (value) => {
|
|
390
390
|
try {
|
|
391
|
-
|
|
391
|
+
parseWorkspaceSyncEnginePushTaskSettlement(value, expectedTaskEchoIds);
|
|
392
392
|
return true;
|
|
393
393
|
}
|
|
394
394
|
catch {
|
|
@@ -11,6 +11,7 @@ export interface WorkspaceSyncEngineLocalApplyResult {
|
|
|
11
11
|
result: unknown;
|
|
12
12
|
deleteCount: number;
|
|
13
13
|
emittedEventIds: string[];
|
|
14
|
+
settledTaskDeleteIds?: string[];
|
|
14
15
|
}
|
|
15
16
|
export interface WorkspaceSyncEngineLocalContentReader {
|
|
16
17
|
loadDocument(workspaceId: string, path: string): Promise<{
|