fix(server): index & gc queue & llm compatibility (#15528)

fix #15523
fix #15526

#### PR Dependency Tree


* **PR #15528** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Improved search generation cleanup, provider error reporting, and
reconciliation reliability.
- Retired search resources are cleaned up safely, including after
credential changes.
- Prompt size checks now ignore tool parameters and provide clearer
errors.
- Reserved documents are protected from accidental cleanup, and
malformed identifiers are rejected.

- **Configuration**
- Managed Copilot profiles require explicit, non-duplicated model
assignments.
  - Improved managed provider profile migration.

- **Performance & Reliability**
- Reduced unnecessary search-history cleanup and adjusted
consistency-check intervals.
- Failed reconciliation jobs stop after one attempt and are removed
automatically.

- **Data Updates**
  - Updated legacy AI session prompt names to current labels.
  - Improved cloud load-balancer health-check configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-08-26 15:57:06 +08:00
committed by GitHub
parent 8e8a781ed1
commit bd095495da
30 changed files with 691 additions and 203 deletions
@@ -0,0 +1,20 @@
UPDATE ai_sessions_metadata
SET prompt_name = CASE prompt_name
WHEN 'Search With AFFiNE AI' THEN 'Chat With AFFiNE AI'
WHEN 'debug:action:dalle3' THEN 'Generate image'
WHEN 'debug:action:fal-sd15' THEN 'Generate image'
WHEN 'debug:action:gpt-image-1' THEN 'Generate image'
WHEN 'debug:action:fal-remove-bg' THEN 'Remove background'
WHEN 'debug:action:fal-upscaler' THEN 'Upscale image'
WHEN 'debug:action:fal-face-to-sticker' THEN 'Convert to sticker'
ELSE prompt_name
END
WHERE prompt_name IN (
'Search With AFFiNE AI',
'debug:action:dalle3',
'debug:action:fal-sd15',
'debug:action:gpt-image-1',
'debug:action:fal-remove-bg',
'debug:action:fal-upscaler',
'debug:action:fal-face-to-sticker'
);
@@ -1444,6 +1444,17 @@ test('workspace sync push-doc-update should enforce doc update permissions', asy
);
t.is(userdataError.name, 'SPACE_ACCESS_DENIED');
const malformedDatabaseError = getErrorResponse(
t,
await emitWithAck(socket, 'space:push-doc-update', {
spaceType: 'workspace',
spaceId: workspace.id,
docId: 'db$docProperties',
update: createYjsUpdateBase64(),
})
);
t.is(malformedDatabaseError.name, 'SPACE_ACCESS_DENIED');
const updates = await db.update.count({
where: {
workspaceId: workspace.id,
@@ -277,7 +277,11 @@ export class BackendRuntimeSearchJob {
await this.queue.add(
'backendRuntime.reconcileSearchProjection',
{ limit: 100 },
{ jobId: 'backend-runtime-search-reconciliation', removeOnFail: true }
{
jobId: 'backend-runtime-search-reconciliation',
attempts: 1,
removeOnFail: true,
}
);
}
@@ -33,7 +33,7 @@ import {
SpaceAccessDenied,
} from '../../base';
import { Models } from '../../models';
import { authorizeUserdataDocSubject } from '../../native';
import { authorizeReservedDocSubject } from '../../native';
import { CurrentUser } from '../auth';
import {
DocReader,
@@ -469,7 +469,7 @@ export class SpaceSyncGateway
await this.ac.user(userId).doc(spaceId, docId).assert(action);
}
private assertUserdataSubject(
private assertReservedDocSubject(
spaceType: SpaceType,
userId: string,
workspaceId: string,
@@ -477,7 +477,7 @@ export class SpaceSyncGateway
) {
if (
spaceType === SpaceType.Workspace &&
!authorizeUserdataDocSubject(userId, workspaceId, docId)
!authorizeReservedDocSubject(userId, workspaceId, docId)
) {
throw new SpaceAccessDenied({ spaceId: workspaceId });
}
@@ -928,7 +928,12 @@ export class SpaceSyncGateway
}
try {
this.assertUserdataSubject(event.spaceType, userId, spaceId, docId);
this.assertReservedDocSubject(
event.spaceType,
userId,
spaceId,
docId
);
await this.assertDocActionAllowed(
event.spaceType,
userId,
@@ -1095,7 +1100,7 @@ export class SpaceSyncGateway
if (space.docId === undefined) {
continue;
}
this.assertUserdataSubject(
this.assertReservedDocSubject(
space.spaceType,
user.id,
space.spaceId,
@@ -1212,7 +1217,7 @@ export class SpaceSyncGateway
const id = new DocID(docId, spaceId);
const adapter = this.selectAdapter(client, spaceType);
adapter.assertIn(spaceId);
this.assertUserdataSubject(spaceType, user.id, spaceId, id.guid);
this.assertReservedDocSubject(spaceType, user.id, spaceId, id.guid);
await this.assertDocActionAllowed(
spaceType,
user.id,
@@ -1247,7 +1252,7 @@ export class SpaceSyncGateway
@MessageBody() { spaceType, spaceId, docId }: DeleteDocMessage
): Promise<EventResponse<{ success: true }>> {
const adapter = this.selectAdapter(client, spaceType);
this.assertUserdataSubject(spaceType, user.id, spaceId, docId);
this.assertReservedDocSubject(spaceType, user.id, spaceId, docId);
await this.assertDocActionAllowed(
spaceType,
user.id,
@@ -1273,7 +1278,7 @@ export class SpaceSyncGateway
const adapter = this.selectAdapter(client, spaceType);
// Quota recovery mode is intentionally not applied to sync.
this.assertUserdataSubject(spaceType, user.id, spaceId, docId);
this.assertReservedDocSubject(spaceType, user.id, spaceId, docId);
await this.assertDocActionAllowed(
spaceType,
user.id,
@@ -137,9 +137,10 @@ test('managed provider migration preserves explicit profiles and converts legacy
});
const profiles = [
{
id: 'openai-default',
type: 'openai',
id: 'cloudflare-existing',
type: 'cloudflareWorkersAi',
priority: 7,
models: ['@cf/baai/bge-reranker-base'],
config: { apiKey: 'profile-key' },
},
];
@@ -148,12 +149,16 @@ test('managed provider migration preserves explicit profiles and converts legacy
{ id: 'copilot.providers.profiles', value: profiles },
{
id: 'copilot.providers.openai',
value: { apiKey: 'shadowed-legacy-key' },
value: { apiKey: 'openai-key' },
},
{
id: 'copilot.providers.gemini',
value: { apiKey: 'gemini-key' },
},
{
id: 'copilot.providers.geminiVertex',
value: { projectId: 'gemini-vertex-project' },
},
{
id: 'copilot.providers.defaults',
value: { fallback: 'openai-default' },
@@ -169,18 +174,38 @@ test('managed provider migration preserves explicit profiles and converts legacy
});
t.deepEqual(migrated.value, [
...profiles,
{
id: 'openai-default',
type: 'openai',
priority: 7,
models: ['gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-image-1', 'gpt-4o-mini'],
config: { apiKey: 'openai-key' },
},
{
id: 'gemini-default',
type: 'gemini',
priority: 4,
models: ['gemini-3.7-flash', 'gemini-embedding-001'],
config: { apiKey: 'gemini-key' },
},
{
id: 'geminiVertex-default',
type: 'geminiVertex',
priority: 3,
models: ['gemini-3.7-flash'],
config: { projectId: 'gemini-vertex-project' },
enabled: false,
},
]);
t.is(
await t.context.db.appConfig.count({
where: {
id: {
in: ['copilot.providers.openai', 'copilot.providers.gemini'],
in: [
'copilot.providers.openai',
'copilot.providers.gemini',
'copilot.providers.geminiVertex',
],
},
},
}),
@@ -13,6 +13,15 @@ const PROVIDERS = [
] as const;
const PROVIDER_IDS = PROVIDERS.map(provider => `copilot.providers.${provider}`);
const PROVIDER_MODELS: Record<(typeof PROVIDERS)[number], string[]> = {
openai: ['gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-image-1', 'gpt-4o-mini'],
cloudflareWorkersAi: ['@cf/baai/bge-reranker-base'],
fal: ['lora/image-to-image', 'workflowutils/teed'],
gemini: ['gemini-3.7-flash', 'gemini-embedding-001'],
geminiVertex: ['gemini-3.7-flash'],
anthropic: ['claude-sonnet-4-6'],
anthropicVertex: ['claude-sonnet-4-6'],
};
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
@@ -58,23 +67,39 @@ export class ConvergeManagedProviderProfiles1786810000000 {
: []
)
);
const assignedModels = new Set(
profiles.flatMap(profile =>
isRecord(profile) &&
profile.enabled !== false &&
Array.isArray(profile.models)
? profile.models.filter(
(model): model is string => typeof model === 'string'
)
: []
)
);
for (const [index, provider] of PROVIDERS.entries()) {
const legacy = byId.get(`copilot.providers.${provider}`);
if (!legacy) {
continue;
}
if (!legacy) continue;
if (!isRecord(legacy.value)) {
throw new Error(`copilot.providers.${provider} must be an object`);
}
const id = `${provider}-default`;
if (!profileIds.has(id)) {
const models = PROVIDER_MODELS[provider].filter(
model => !assignedModels.has(model)
);
const enabled = models.length > 0;
profiles.push({
id,
type: provider,
priority: PROVIDERS.length - index,
models: enabled ? models : PROVIDER_MODELS[provider],
config: legacy.value,
...(enabled ? {} : { enabled: false }),
});
models.forEach(model => assignedModels.add(model));
profileIds.add(id);
}
}
+2 -2
View File
@@ -231,8 +231,8 @@ import type {
} from './plugins/copilot/runtime/contracts/tool-contract';
export const mergeUpdatesInApplyWay = serverNativeModule.mergeUpdatesInApplyWay;
export const authorizeUserdataDocSubject =
serverNativeModule.authorizeUserdataDocSubject;
export const authorizeReservedDocSubject =
serverNativeModule.authorizeReservedDocSubject;
export const authSessionAccessTokenKeyId =
serverNativeModule.authSessionAccessTokenKeyId;
export const createAuthSessionRefreshToken =
@@ -104,6 +104,7 @@ test('does not schedule or run native search reconciliation when disabled', asyn
await job.scheduleReconciliation();
t.deepEqual(queue.add.firstCall.args[2], {
jobId: 'backend-runtime-search-reconciliation',
attempts: 1,
removeOnFail: true,
});
});