fix(skill): retain candidates across watcher failures
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/skill/skill-local/README.md
|
||||
README.md: 1fe545f15c12a6b540b1feaa7b612ae27c1f4f56
|
||||
README.zh.md: 15911b45c229f41ed2ebca4db22a2050b2d71431
|
||||
README.md: d4b6253c6667f4786d53ad6c291f9e96f82546c3
|
||||
README.zh.md: 7cede0ea64064ca4f170d81303085b40b5751f15
|
||||
|
||||
@@ -46,7 +46,7 @@ Existing skill roots are watched with Chokidar. The provider observes direct bun
|
||||
|
||||
A root that does not exist is followed from the nearest existing ancestor one missing path segment at a time. The next segment is probed with `fs.watchFile`; once `.agents`, `skills`, or the configured root appears, observation advances until Chokidar can attach to the real root. Root deletion reverses this process, so deleting and recreating an entire skills directory remains observable. Project-scoped watchers are bounded by `watchMaxProjects`; revisiting an evicted project reattaches observation during discovery.
|
||||
|
||||
The first-party filesystem `write` and `edit` tools also synchronously invalidate the provider through `fs/observed` when their target could affect a watched skill entry. This fast path makes the next model step observe its own filesystem mutation without waiting for the host watcher. External IDE, Git, shell, and process changes rely on Chokidar or the missing-path probe. Startup/runtime watcher failures are logged, make the current provider observation incomplete, and are retried; effect teardown closes every watcher and contains late callbacks.
|
||||
The first-party filesystem `write` and `edit` tools also synchronously invalidate the provider through `fs/observed` when their target could affect a watched skill entry. This fast path makes the next model step observe its own filesystem mutation without waiting for the host watcher. External IDE, Git, shell, and process changes rely on Chokidar or the missing-path probe. Startup/runtime watcher failures are logged and retried. Discovery still scans readable roots and returns their candidates for direct loading, but marks the observation incomplete so it is not cached or published as an authoritative model catalog. Effect teardown closes every watcher and contains late callbacks.
|
||||
|
||||
## Skill Format
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
不存在的根会从最近的现有祖先开始,每次沿一个缺失路径段跟踪。系统使用 `fs.watchFile` 探测下一段;当 `.agents`、`skills` 或已配置的根出现后,观察会逐级推进,直至 Chokidar 可以附加到真实根。根删除时,该过程反向执行,因此删除再重建整个 skills 目录仍可被观察到。按项目划分的 watcher 数量受 `watchMaxProjects` 限制;再次访问已被驱逐的项目时,发现阶段会重新附加观察。
|
||||
|
||||
如果第一方文件系统 `write` 和 `edit` 工具的目标可能影响受监视的 skill 条目,它们还会通过 `fs/observed` 同步使提供方失效。这条快速路径让模型的下一个步骤无需等待宿主 watcher,即可观察到自身的文件系统变更。外部 IDE、Git、shell 和进程产生的变更依赖 Chokidar 或缺失路径探测。watcher 启动或运行时失败会被记录,使提供方的当前观察不完整,并触发重试;effect 释放会关闭所有 watcher,并收束延迟回调。
|
||||
如果第一方文件系统 `write` 和 `edit` 工具的目标可能影响受监视的 skill 条目,它们还会通过 `fs/observed` 同步使提供方失效。这条快速路径让模型的下一个步骤无需等待宿主 watcher,即可观察到自身的文件系统变更。外部 IDE、Git、shell 和进程产生的变更依赖 Chokidar 或缺失路径探测。watcher 启动或运行时失败会被记录并触发重试。发现过程仍会扫描可读根目录,并返回其候选项供直接加载,但会将观测标记为不完整,因此不会缓存,也不会作为权威模型目录发布。effect 释放会关闭所有 watcher,并收束延迟回调。
|
||||
|
||||
## Skill 格式
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type SkillLookupOptions,
|
||||
type SkillProvider,
|
||||
type SkillProviderControl,
|
||||
type SkillProviderObservation,
|
||||
type SkillSource,
|
||||
} from '@deepseek-ai/dsh-skill'
|
||||
|
||||
@@ -161,18 +162,25 @@ export class LocalSkillProvider implements SkillProvider {
|
||||
/**
|
||||
* Discover local skill summaries for a cwd-sensitive workspace.
|
||||
* @param options - lookup options; `cwd` selects the project roots to scan.
|
||||
* @returns local provider candidates with stable root ranks.
|
||||
* @returns local provider candidates with stable root ranks; watcher startup
|
||||
* failure returns readable candidates as an incomplete observation.
|
||||
*/
|
||||
async list(options: SkillLookupOptions): Promise<SkillCandidate[]> {
|
||||
async list(options: SkillLookupOptions): Promise<SkillCandidate[] | SkillProviderObservation> {
|
||||
const roots = await this.roots(options.cwd)
|
||||
await this.watchManager.observeRoots(roots)
|
||||
let complete = true
|
||||
try {
|
||||
await this.watchManager.observeRoots(roots)
|
||||
} catch (error) {
|
||||
if (this.disposal !== undefined) throw error
|
||||
complete = false
|
||||
}
|
||||
const candidates: SkillCandidate[] = []
|
||||
for (const root of roots) {
|
||||
for (const skill of await discoverRoot(root, this.ctx)) {
|
||||
candidates.push(skill)
|
||||
}
|
||||
}
|
||||
return candidates
|
||||
return complete ? candidates : { candidates, complete }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -117,11 +117,15 @@ describe('skill-local watcher failures', () => {
|
||||
await fiber.dispose()
|
||||
})
|
||||
|
||||
it('marks a startup failure incomplete and retries discovery without caching it', async () => {
|
||||
it('keeps skills loadable across persistent watcher startup failures without caching them', async () => {
|
||||
const home = await tempDir('skill-watch-start-error')
|
||||
const root = join(home, '.dsh/skills')
|
||||
await writeSkill(root, 'retry-skill')
|
||||
watcherHarness.startupErrors.push(new Error('watch failed'))
|
||||
watcherHarness.startupErrors.push(
|
||||
new Error('watch failed once'),
|
||||
new Error('watch failed twice'),
|
||||
new Error('watch failed three times'),
|
||||
)
|
||||
watcherHarness.closeErrors = 1
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SkillService)
|
||||
@@ -135,13 +139,17 @@ describe('skill-local watcher failures', () => {
|
||||
watchStabilityThresholdMs: 20,
|
||||
})
|
||||
|
||||
expect(await ctx.skills.snapshot()).toEqual({ skills: [], complete: false })
|
||||
expect(await ctx.skills.snapshot()).toMatchObject({
|
||||
skills: [{ name: 'retry-skill' }],
|
||||
complete: true,
|
||||
complete: false,
|
||||
})
|
||||
expect(watcherHarness.watchers).toHaveLength(2)
|
||||
expect(watcherHarness.watchers[1]?.options).toMatchObject({
|
||||
expect((await ctx.skills.get('retry-skill'))?.content).toBe('Body.')
|
||||
expect(await ctx.skills.snapshot()).toMatchObject({
|
||||
skills: [{ name: 'retry-skill' }],
|
||||
complete: false,
|
||||
})
|
||||
expect(watcherHarness.watchers).toHaveLength(3)
|
||||
expect(watcherHarness.watchers[0]?.options).toMatchObject({
|
||||
atomic: true,
|
||||
depth: 1,
|
||||
followSymlinks: false,
|
||||
|
||||
Reference in New Issue
Block a user