diff --git a/packages/tasks/tasks/src/index.ts b/packages/tasks/tasks/src/index.ts index 17e617e8a7..e237aa0681 100644 --- a/packages/tasks/tasks/src/index.ts +++ b/packages/tasks/tasks/src/index.ts @@ -49,6 +49,13 @@ declare module 'cordis' { */ export abstract class TaskService extends Service { constructor(ctx: Context) { + // `abstract` erases at runtime, and this package name used to be the + // mountable concrete registry — a stale composition row would otherwise + // register a ctx.tasks with no method implementations and fail far from + // the misconfiguration. Fail loud at load instead. + if (new.target === TaskService) { + throw new Error('@deepseek-ai/dsh-tasks is the abstract task registry seam; load an implementation such as @deepseek-ai/dsh-tasks-local instead') + } super(ctx, 'tasks') } diff --git a/packages/tasks/tasks/tests/service.spec.ts b/packages/tasks/tasks/tests/service.spec.ts index d8d582e410..82fc415f51 100644 --- a/packages/tasks/tasks/tests/service.spec.ts +++ b/packages/tasks/tasks/tests/service.spec.ts @@ -79,4 +79,10 @@ describe('TaskService seam', () => { class SecondTaskService extends StubTaskService {} await expect(ctx.plugin(SecondTaskService)).rejects.toThrow(/service "tasks" has been registered/) }) + + it('mounting the abstract seam directly fails loudly at load (stale-composition fence)', async () => { + const ctx = new Context() + await expect(ctx.plugin(TaskService as unknown as typeof StubTaskService)) + .rejects.toThrow(/abstract task registry seam; load an implementation such as @deepseek-ai\/dsh-tasks-local/) + }) })