disclaw/tasks/DIS-151.md
Nick Tabeling f18c63cd12
Some checks failed
CI / build-and-test (ubuntu-latest) (pull_request) Has been cancelled
CI / build-and-test (windows-latest) (pull_request) Has been cancelled
CI / lint (pull_request) Has been cancelled
chore: mark DIS-151 done, move DIS-152 to ready
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 15:23:18 +02:00

8.5 KiB

id status phase priority labels branch assignee started pr merged
DIS-151 done 1.5 p1
phase:1.5
type:feat
priority:p1
refinement/channel-categories developer-agent 2026-04-10 #60 2026-04-10

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 scannenguild.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)

// 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