8.6 KiB
8.6 KiB
| id | status | phase | priority | labels | branch | assignee | started | pr | merged | |||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| DIS-151 | in-progress | 1.5 | p1 |
|
refinement/channel-categories | developer-agent | 2026-04-10 | null | 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 kein Channel in Discord (oder Channel nicht in DB)🗄️ Archiv— Kein DB-Eintrag, kein Workspace — Channel existiert aber ist unbekannt
Startup-Reconciliation (src/lifecycle/reconcile.ts)
Beim Start einmalig ausführen, nach DB-Init und Bot-Login:
- Alle Channels des Servers scannen —
guild.channels.fetch()für vollständigen Cache - DB-Einträge abgleichen — für jeden Workspace-Eintrag in DB prüfen ob Channel noch existiert
- Workspace-Verzeichnisse scannen — alle Verzeichnisse unter
workspaces_rootmitagent.yaml - 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 testgrü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)
// 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 nachreadyunvollständig seinworkspace_path === "__management__"in DB-Abfragen herausfilternfs.rmSyncauf Windows kann bei gesperrten Dateien fehlschlagen → immertry/catch- Discord API Rate-Limits bei vielen Channels:
try/catchprosetParent(), nicht alles parallel channelDeletekann während Reconciliation feuern → erst nachawait reconcile()Listener registrieren
Abhängigkeiten
Phase 0 + Phase 1 abgeschlossen.
Spec
→ docs/ideas/known-issues.md KI-002, KI-002.1