209 lines
8.5 KiB
Markdown
209 lines
8.5 KiB
Markdown
---
|
|
id: DIS-151
|
|
status: in-progress
|
|
phase: 1.5
|
|
priority: p1
|
|
labels: [phase:1.5, type:feat, priority:p1]
|
|
branch: refinement/channel-categories
|
|
assignee: developer-agent
|
|
started: 2026-04-10
|
|
pr: null
|
|
merged: null
|
|
---
|
|
|
|
# DIS-151: Channel-Kategorisierung (aktiv / inaktiv / archiv) + Lifecycle-Management
|
|
|
|
## Ziel
|
|
Discord-Channels werden beim Start von DisClaw automatisch in Kategorien eingeordnet.
|
|
Verwaiste Workspaces und Channels werden aufgeräumt. Live-Events halten den Zustand aktuell.
|
|
|
|
## Kontext
|
|
Beim Betrieb entstehen Inkonsistenzen: Channels ohne Workspace, Workspaces ohne Channel,
|
|
oder Agents die aus der DB entfernt wurden aber noch einen Workspace haben.
|
|
→ `docs/ideas/known-issues.md` KI-002, KI-002.1
|
|
|
|
## Scope
|
|
|
|
### Kategorien (werden beim ersten Start automatisch angelegt falls nicht vorhanden)
|
|
- **`🟢 Aktive Agents`** — Channel ist in DB, Workspace existiert unter `~/.disclaw/workspaces/`
|
|
- **`🔴 Inaktive Agents`** — Workspace existiert, aber Channel nicht in DB
|
|
- **`🗄️ Archiv`** — Kein DB-Eintrag, kein Workspace — Channel existiert aber ist verwaist
|
|
|
|
### Startup-Reconciliation (`src/lifecycle/reconcile.ts`)
|
|
Beim Start einmalig ausführen, nach DB-Init und Bot-Login:
|
|
|
|
1. **Alle Channels des Servers scannen** — `guild.channels.fetch()` für vollständigen Cache
|
|
2. **DB-Einträge abgleichen** — für jeden Workspace-Eintrag in DB prüfen ob Channel noch existiert
|
|
3. **Workspace-Verzeichnisse scannen** — alle Verzeichnisse unter `workspaces_root` mit `agent.yaml`
|
|
4. **Einordnung**:
|
|
- DB-Eintrag + Workspace vorhanden + Channel existiert → **aktiv**
|
|
- Workspace vorhanden, aber kein DB-Eintrag / kein Channel → **inaktiv**
|
|
- Weder DB noch Workspace, aber Channel existiert → **archiv**
|
|
- Workspace existiert, kein Channel in aktiv/inaktiv/archiv → Workspace-Verzeichnis löschen
|
|
|
|
### Live-Event: `channelDelete`
|
|
- Wenn ein Agent-Channel gelöscht wird: Workspace-Verzeichnis löschen, DB-Eintrag entfernen
|
|
- Wenn Management-Channel gelöscht wird: automatisch neu anlegen (KI-002.1)
|
|
- Kategorie-Channels (aktiv/inaktiv/archiv) werden bei Löschung neu angelegt
|
|
|
|
## Definition of Done
|
|
- [ ] Kategorien werden beim Start automatisch angelegt (falls nicht vorhanden)
|
|
- [ ] Channels werden korrekt eingeordnet (aktiv/inaktiv/archiv)
|
|
- [ ] Workspace-Verzeichnisse ohne Channel werden beim Start gelöscht
|
|
- [ ] `channelDelete`-Event: Workspace + DB-Eintrag werden entfernt
|
|
- [ ] Management-Channel-Löschung: wird neu angelegt
|
|
- [ ] Unit-Test `tests/unit/reconcile.test.ts`: alle Reconcile-Cases grün
|
|
- [ ] `npm run build && npm test` grün
|
|
|
|
---
|
|
|
|
## Implementierungsplan (Senior Developer Review)
|
|
|
|
### Analyse: Was fehlt
|
|
|
|
| Was | Status |
|
|
|-----|--------|
|
|
| `db.deleteWorkspaceByChannelId()` | fehlt — muss in `database.ts` ergänzt werden |
|
|
| `src/lifecycle/` | komplett neu |
|
|
| `channelDelete`-Event-Handler | fehlt in `bot.ts` |
|
|
| `new-agent.ts` übergibt kein `parent` | Channel landet in keiner Kategorie |
|
|
| `managementChannelId` ist lokale Variable | muss mutierbar sein für Delete-Handler |
|
|
|
|
### Neue Dateien
|
|
|
|
| Datei | Zweck |
|
|
|---|---|
|
|
| `src/lifecycle/categories.ts` | `ensureCategories()` + `moveToCategory()` |
|
|
| `src/lifecycle/reconcile.ts` | Startup-Reconciliation-Algorithmus |
|
|
| `src/lifecycle/channel-delete-handler.ts` | `channelDelete`-Event-Logik |
|
|
| `tests/unit/reconcile.test.ts` | Unit-Tests |
|
|
|
|
### Modifizierte Dateien
|
|
|
|
| Datei | Änderung |
|
|
|---|---|
|
|
| `src/db/database.ts` | `deleteWorkspaceByChannelId(channelId): boolean` |
|
|
| `src/bot.ts` | State-Objekt, `ensureCategories` + `reconcile` aufrufen, `channelDelete`-Listener |
|
|
| `src/commands/new-agent.ts` | `parent: activeCategoryId` beim Channel-Erstellen |
|
|
|
|
### Implementierungsreihenfolge
|
|
|
|
```
|
|
SCHRITT 1: src/db/database.ts
|
|
- deleteWorkspaceByChannelId(channelId: string): boolean
|
|
- Löscht zuerst conversations, dann workspace (manuelles CASCADE)
|
|
- Kein Schema-Change nötig
|
|
|
|
SCHRITT 2: src/lifecycle/categories.ts (NEU)
|
|
- CATEGORY_NAMES-Konstante exportieren
|
|
- ResolvedCategories-Interface exportieren
|
|
- ensureCategories(guild: Guild): Promise<ResolvedCategories>
|
|
- moveToCategory(channelId: string, category: CategoryChannel): Promise<void>
|
|
|
|
SCHRITT 3: src/lifecycle/reconcile.ts (NEU)
|
|
- ReconcileResult-Interface exportieren
|
|
- reconcile(guild, db, categories, workspacesRoot, managementChannelId): Promise<ReconcileResult>
|
|
- guild.channels.fetch() am Anfang für vollständigen Cache
|
|
- Management-Channel + Kategorie-Channels aus der Einordnung ausschließen
|
|
- workspace_path === "__management__" in DB-Abfragen filtern
|
|
- try/catch um jeden channel.setParent()-Aufruf
|
|
- try/catch um jedes fs.rmSync()
|
|
|
|
SCHRITT 4: src/lifecycle/channel-delete-handler.ts (NEU)
|
|
- ChannelDeleteContext-Interface:
|
|
db, config, guild, categories, managementChannelId,
|
|
setManagementChannelId, setCategories
|
|
- handleChannelDelete(channel, ctx): Promise<void>
|
|
- Fall 1: Agent-Channel → DB-Eintrag + Workspace-Dir löschen
|
|
- Fall 2: Management-Channel → neu erstellen, setManagementChannelId aufrufen
|
|
- Fall 3: Kategorie-Channel → ensureCategories() erneut aufrufen, setCategories aufrufen
|
|
|
|
SCHRITT 5: src/bot.ts ANPASSEN
|
|
- State-Objekt: { managementChannelId: string|null, categories: ResolvedCategories|null }
|
|
- Nach Management-Channel-Setup im ready-Handler:
|
|
a) categories = await ensureCategories(guild)
|
|
b) await reconcile(guild, db, categories, config.workspaces_root, managementChannelId)
|
|
- Neuer Listener: client.on("channelDelete", ch => handleChannelDelete(ch, ctx))
|
|
- categories an handleNewAgent() durchreichen
|
|
|
|
SCHRITT 6: src/commands/new-agent.ts ANPASSEN
|
|
- Signatur: handleNewAgent(interaction, db, config, activeCategoryId?: string)
|
|
- parent: activeCategoryId bei guild.channels.create() setzen
|
|
|
|
SCHRITT 7: tests/unit/reconcile.test.ts (NEU)
|
|
- Mock-Factories für Guild, DB, Categories
|
|
- vi.mock("node:fs") für Filesystem-Operationen
|
|
- Alle Test-Cases (siehe unten)
|
|
- npm run build && npm test grün
|
|
```
|
|
|
|
### Interfaces (Zusammenfassung)
|
|
|
|
```typescript
|
|
// src/lifecycle/categories.ts
|
|
export const CATEGORY_NAMES = {
|
|
active: "🟢 Aktive Agents",
|
|
inactive: "🔴 Inaktive Agents",
|
|
archive: "🗄️ Archiv",
|
|
} as const;
|
|
|
|
export interface ResolvedCategories {
|
|
active: CategoryChannel;
|
|
inactive: CategoryChannel;
|
|
archive: CategoryChannel;
|
|
}
|
|
|
|
// src/lifecycle/reconcile.ts
|
|
export interface ReconcileResult {
|
|
active: string[]; // channel IDs moved to active
|
|
inactive: string[]; // channel IDs moved to inactive
|
|
archived: string[]; // channel IDs moved to archive
|
|
deletedWorkspaces: string[]; // workspace dirs deleted
|
|
}
|
|
|
|
// src/lifecycle/channel-delete-handler.ts
|
|
export interface ChannelDeleteContext {
|
|
db: DisclawDatabase;
|
|
config: DisclawConfig;
|
|
guild: Guild;
|
|
categories: ResolvedCategories;
|
|
managementChannelId: string;
|
|
setManagementChannelId: (id: string) => void;
|
|
setCategories: (cats: ResolvedCategories) => void;
|
|
}
|
|
|
|
// src/db/database.ts (Erweiterung)
|
|
deleteWorkspaceByChannelId(channelId: string): boolean;
|
|
```
|
|
|
|
### Testplan: `tests/unit/reconcile.test.ts`
|
|
|
|
| # | Szenario | Erwartung |
|
|
|---|----------|-----------|
|
|
| 1 | DB + Workspace + Channel vorhanden | Channel → aktiv-Kategorie |
|
|
| 2 | Workspace + DB, kein Channel in Guild | Channel → inaktiv-Kategorie |
|
|
| 3 | Workspace, kein DB-Eintrag | → inaktiv |
|
|
| 4 | Channel in Guild, kein DB, kein Workspace | Channel → archiv-Kategorie |
|
|
| 5 | Workspace, kein Channel nirgends, kein DB | Workspace-Dir wird gelöscht |
|
|
| 6 | Management-Channel | wird nicht kategorisiert |
|
|
| 7 | Kategorie-Channels selbst | werden nicht als archiv eingestuft |
|
|
| 8 | Leerer workspaces_root, keine DB-Einträge | kein Fehler, leeres Result |
|
|
| 9 | channelDelete: Agent-Channel | DB-Eintrag + Workspace gelöscht |
|
|
| 10 | channelDelete: Management-Channel | Neuer Channel wird erstellt |
|
|
| 11 | channelDelete: Kategorie-Channel | Kategorie wird neu erstellt |
|
|
| 12 | ensureCategories: alle fehlen | 3 Kategorien werden erstellt |
|
|
| 13 | ensureCategories: teilweise vorhanden | Nur fehlende werden erstellt |
|
|
|
|
### Randfälle + Risiken
|
|
|
|
- `guild.channels.fetch()` vor Reconciliation — Cache kann nach `ready` unvollständig sein
|
|
- `workspace_path === "__management__"` in DB-Abfragen herausfiltern
|
|
- `fs.rmSync` auf Windows kann bei gesperrten Dateien fehlschlagen → immer `try/catch`
|
|
- Discord API Rate-Limits bei vielen Channels: `try/catch` pro `setParent()`, nicht alles parallel
|
|
- `channelDelete` kann während Reconciliation feuern → erst nach `await reconcile()` Listener registrieren
|
|
|
|
## Abhängigkeiten
|
|
Phase 0 + Phase 1 abgeschlossen.
|
|
|
|
## Spec
|
|
→ `docs/ideas/known-issues.md` KI-002, KI-002.1
|