docs(watcher): clarify team task watch scope
This commit is contained in:
parent
bf7c850012
commit
c45e860d69
1 changed files with 32 additions and 0 deletions
|
|
@ -18,6 +18,8 @@ export interface TeamTaskWatchRegistryOptions {
|
||||||
|
|
||||||
const RECONCILE_INTERVAL_MS = 30_000;
|
const RECONCILE_INTERVAL_MS = 30_000;
|
||||||
|
|
||||||
|
// Keep this list aligned with FileWatcher.processTeamsChange().
|
||||||
|
// If a new team artifact should produce TeamChangeEvent, add it here too.
|
||||||
const TEAM_ROOT_FILES = new Set([
|
const TEAM_ROOT_FILES = new Set([
|
||||||
'config.json',
|
'config.json',
|
||||||
'processes.json',
|
'processes.json',
|
||||||
|
|
@ -27,6 +29,22 @@ const TEAM_ROOT_FILES = new Set([
|
||||||
OPENCODE_TASK_LOG_ATTRIBUTION_FILE,
|
OPENCODE_TASK_LOG_ATTRIBUTION_FILE,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shallow watcher registry for team and task artifacts.
|
||||||
|
*
|
||||||
|
* Why this exists:
|
||||||
|
* - Node recursive fs.watch on Linux expands into many inotify subscriptions.
|
||||||
|
* Large ~/.claude/teams trees can hit EMFILE/ENOSPC and freeze startup work.
|
||||||
|
* - FileWatcher only consumes a small set of team/task JSON artifacts, so a
|
||||||
|
* broad recursive watcher mostly watches runtime/log/member noise.
|
||||||
|
*
|
||||||
|
* Contract:
|
||||||
|
* - Watch only teams/, teams/<team>/, teams/<team>/inboxes/, tasks/, tasks/<team>/.
|
||||||
|
* - Do not enable Chokidar polling here. Polling is owned by FileWatcher fallback.
|
||||||
|
* - Initial app startup baseline must stay silent to avoid replaying old files.
|
||||||
|
* - Newly discovered targets are scanned once so files created before rebuild
|
||||||
|
* are not lost.
|
||||||
|
*/
|
||||||
export class TeamTaskWatchRegistry {
|
export class TeamTaskWatchRegistry {
|
||||||
private watcher: FSWatcher | null = null;
|
private watcher: FSWatcher | null = null;
|
||||||
private reconcileTimer: NodeJS.Timeout | null = null;
|
private reconcileTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
@ -49,6 +67,8 @@ export class TeamTaskWatchRegistry {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is target reconciliation, not content polling. It only rebuilds the
|
||||||
|
// shallow watch set when team/task/inbox directories appear or disappear.
|
||||||
this.reconcileTimer = setInterval(() => {
|
this.reconcileTimer = setInterval(() => {
|
||||||
void this.reconcileTargets();
|
void this.reconcileTargets();
|
||||||
}, RECONCILE_INTERVAL_MS);
|
}, RECONCILE_INTERVAL_MS);
|
||||||
|
|
@ -132,6 +152,8 @@ export class TeamTaskWatchRegistry {
|
||||||
this.watcher = nextWatcher;
|
this.watcher = nextWatcher;
|
||||||
this.targets = new Set(targets);
|
this.targets = new Set(targets);
|
||||||
this.targetKey = nextKey;
|
this.targetKey = nextKey;
|
||||||
|
// First registry build is app startup baseline and must not emit old files.
|
||||||
|
// Later rebuilds can emit existing files only for newly added targets.
|
||||||
const shouldEmitExistingFiles = this.initialTargetsCaptured;
|
const shouldEmitExistingFiles = this.initialTargetsCaptured;
|
||||||
this.initialTargetsCaptured = true;
|
this.initialTargetsCaptured = true;
|
||||||
|
|
||||||
|
|
@ -145,6 +167,8 @@ export class TeamTaskWatchRegistry {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addDir/unlinkDir can make the watch target set stale immediately.
|
||||||
|
// Periodic reconciliation is the backup path if the directory event is missed.
|
||||||
if (this.shouldReconcile(eventType, relativePath)) {
|
if (this.shouldReconcile(eventType, relativePath)) {
|
||||||
void this.reconcileTargets();
|
void this.reconcileTargets();
|
||||||
}
|
}
|
||||||
|
|
@ -184,6 +208,9 @@ export class TeamTaskWatchRegistry {
|
||||||
if (path.normalize(targetPath) === normalizedRoot) {
|
if (path.normalize(targetPath) === normalizedRoot) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Covers the race where a new team/task/inbox dir is created with JSON
|
||||||
|
// files before Chokidar has rebuilt its target list. Only immediate files
|
||||||
|
// are scanned, matching depth: 0.
|
||||||
const entries = await this.readDirectory(targetPath);
|
const entries = await this.readDirectory(targetPath);
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (this.closed || generation !== this.generation) {
|
if (this.closed || generation !== this.generation) {
|
||||||
|
|
@ -201,6 +228,9 @@ export class TeamTaskWatchRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async collectTargets(): Promise<string[]> {
|
private async collectTargets(): Promise<string[]> {
|
||||||
|
// Keep this intentionally shallow. Do not add members/, runtime/,
|
||||||
|
// .opencode-runtime/, logs, or other deep trees unless FileWatcher starts
|
||||||
|
// emitting user-visible events for those artifacts.
|
||||||
const targets = new Set<string>([path.normalize(this.options.rootPath)]);
|
const targets = new Set<string>([path.normalize(this.options.rootPath)]);
|
||||||
const rootEntries = await this.readDirectory(this.options.rootPath);
|
const rootEntries = await this.readDirectory(this.options.rootPath);
|
||||||
|
|
||||||
|
|
@ -276,6 +306,8 @@ export class TeamTaskWatchRegistry {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is the event gate. Expanding it changes the FileWatcher public event
|
||||||
|
// surface, so update tests and TeamChangeEvent consumers together.
|
||||||
const parts = relativePath.split('/').filter(Boolean);
|
const parts = relativePath.split('/').filter(Boolean);
|
||||||
if (this.options.kind === 'tasks') {
|
if (this.options.kind === 'tasks') {
|
||||||
return parts.length === 2 && !parts[1].startsWith('.') && parts[1].endsWith('.json');
|
return parts.length === 2 && !parts[1].startsWith('.') && parts[1].endsWith('.json');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue