Skip to content

Commit f014cd1

Browse files
os-zhuangclaude
andcommitted
fix(service-ai): built-in agent/skill registration is an UPSERT, not an exists-gate
The exists-gate froze the FIRST shipped definition of data_chat / data_explorer / actions_executor forever once sys_metadata became durable (ADR-0007 report-back): every persona/skill improvement — including the ADR-0040 unified assistant — silently never reached existing environments. Built-in records are platform-owned (ADR-0040): tenants customize by defining a CUSTOM agent and binding it via app.defaultAgent, not by editing built-ins in place — so a content-compare refresh clobbers nothing legit. Stored == shipped → no-op; differs (or absent) → register + info log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3e2a3d2 commit f014cd1

1 file changed

Lines changed: 28 additions & 57 deletions

File tree

packages/services/service-ai/src/plugin.ts

Lines changed: 28 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -750,65 +750,36 @@ export class AIServicePlugin implements Plugin {
750750
ctx.logger.info(`[AI] ${DATA_TOOL_DEFINITIONS.length} data tools registered as metadata`);
751751
}
752752

753-
// Register the built-in data_chat agent (requires metadata service)
753+
// Register the built-in agent + skills (requires metadata service).
754+
//
755+
// UPSERT, not exists-gate (ADR-0040): built-in records are
756+
// platform-owned — when the shipped definition changes (new skills,
757+
// new instructions), existing environments must pick it up on next
758+
// boot. The old exists-gate froze the FIRST shipped version forever
759+
// once sys_metadata became durable, which silently stranded every
760+
// persona/skill improvement on existing envs. Tenants who want a
761+
// different assistant define a CUSTOM agent and bind it via
762+
// app.defaultAgent — editing built-ins in place is not a supported
763+
// path, so an unconditional content-refresh clobbers nothing legit.
754764
if (metadataService) {
755-
try {
756-
const agentExists =
757-
typeof metadataService.exists === 'function'
758-
? await withTimeout(metadataService.exists('agent', DATA_CHAT_AGENT.name))
759-
: false;
760-
761-
if (agentExists === null) {
762-
ctx.logger.warn('[AI] Metadata service timed out checking data_chat agent, skipping');
763-
} else if (!agentExists) {
764-
await withTimeout(metadataService.register('agent', DATA_CHAT_AGENT.name, DATA_CHAT_AGENT));
765-
console.log('[AI] Registered data_chat agent to metadataService');
766-
ctx.logger.info('[AI] data_chat agent registered');
767-
} else {
768-
console.log('[AI] data_chat agent already exists, skipping');
769-
ctx.logger.debug('[AI] data_chat agent already exists, skipping auto-registration');
770-
}
771-
} catch (err) {
772-
ctx.logger.warn('[AI] Failed to register data_chat agent', err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
773-
}
774-
775-
// Register the built-in data_explorer skill (capability bundle for data_chat)
776-
try {
777-
const skillExists =
778-
typeof metadataService.exists === 'function'
779-
? await withTimeout(metadataService.exists('skill', DATA_EXPLORER_SKILL.name))
780-
: false;
781-
782-
if (skillExists === null) {
783-
ctx.logger.warn('[AI] Metadata service timed out checking data_explorer skill, skipping');
784-
} else if (!skillExists) {
785-
await withTimeout(metadataService.register('skill', DATA_EXPLORER_SKILL.name, DATA_EXPLORER_SKILL));
786-
ctx.logger.info('[AI] data_explorer skill registered');
787-
} else {
788-
ctx.logger.debug('[AI] data_explorer skill already exists, skipping auto-registration');
789-
}
790-
} catch (err) {
791-
ctx.logger.warn('[AI] Failed to register data_explorer skill', err instanceof Error ? { error: err.message } : { error: String(err) });
792-
}
793-
794-
// Register the built-in actions_executor skill (write-side bundle for data_chat)
795-
try {
796-
const skillExists =
797-
typeof metadataService.exists === 'function'
798-
? await withTimeout(metadataService.exists('skill', ACTIONS_EXECUTOR_SKILL.name))
799-
: false;
800-
801-
if (skillExists === null) {
802-
ctx.logger.warn('[AI] Metadata service timed out checking actions_executor skill, skipping');
803-
} else if (!skillExists) {
804-
await withTimeout(metadataService.register('skill', ACTIONS_EXECUTOR_SKILL.name, ACTIONS_EXECUTOR_SKILL));
805-
ctx.logger.info('[AI] actions_executor skill registered');
806-
} else {
807-
ctx.logger.debug('[AI] actions_executor skill already exists, skipping auto-registration');
765+
const upsertBuiltin = async (type: string, name: string, def: unknown): Promise<void> => {
766+
try {
767+
const stored = await withTimeout(metadataService.get(type, name));
768+
if (stored !== null && stored !== undefined && JSON.stringify(stored) === JSON.stringify(def)) {
769+
ctx.logger.debug(`[AI] built-in ${type} ${name} up to date`);
770+
return;
771+
}
772+
await withTimeout(metadataService.register(type, name, def));
773+
ctx.logger.info(
774+
stored ? `[AI] built-in ${type} ${name} refreshed (shipped definition changed)` : `[AI] built-in ${type} ${name} registered`,
775+
);
776+
} catch (err) {
777+
ctx.logger.warn(`[AI] Failed to register built-in ${type} ${name}`, err instanceof Error ? { error: err.message } : { error: String(err) });
808778
}
809-
} catch (err) {
810-
ctx.logger.warn('[AI] Failed to register actions_executor skill', err instanceof Error ? { error: err.message } : { error: String(err) });
811-
}
779+
};
780+
await upsertBuiltin('agent', DATA_CHAT_AGENT.name, DATA_CHAT_AGENT);
781+
await upsertBuiltin('skill', DATA_EXPLORER_SKILL.name, DATA_EXPLORER_SKILL);
782+
await upsertBuiltin('skill', ACTIONS_EXECUTOR_SKILL.name, ACTIONS_EXECUTOR_SKILL);
812783
}
813784
}
814785
} catch {

0 commit comments

Comments
 (0)