Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,11 @@ The bot updates automatically when running `make dev` - no manual steps needed!
- Rate limiting is disabled in local development (dispatcher.disableRateLimit: true in values-local.yaml)
- To manually rebuild worker image if needed: `docker build -f Dockerfile.worker -t claude-worker:latest .`

## Conversation Persistence
## Persistent Storage

The Slack bot automatically persists conversations using Claude CLI's session management:
Worker pods now use persistent volumes for data storage:

1. **Automatic Session Management**: Each Slack thread gets its own Claude session ID for conversation continuity
2. **Syncing Projects**: Use `./scripts/sync-claude-projects.sh` to copy Claude projects from host `~/.claude/projects/[dir]` to repository `.claude/projects/[relativedir]`
3. **Container Setup**: The worker container automatically extracts `.claude/projects` data to `~/.claude/projects` with absolute paths
4. **Auto-Resume**: The worker automatically resumes conversations using Claude CLI's built-in `--resume` functionality when continuing a thread
5. **Git Commits**: When creating PRs, conversations are preserved in the `.claude/projects` directory for future reference
1. **Persistent Volumes**: Each worker pod mounts a persistent volume at `/workspace` to preserve data across pod restarts
2. **Auto-Resume**: The worker automatically resumes conversations using Claude CLI's built-in `--resume` functionality when continuing a thread in the same persistent volume
3. **Data Persistence**: All workspace data is preserved in the persistent volume, eliminating the need for conversation file syncing

18 changes: 18 additions & 0 deletions charts/peerbot/templates/worker-pvc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{{- if .Values.worker.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "peerbot.fullname" . }}-worker-pvc
labels:
{{- include "peerbot.labels" . | nindent 4 }}
app.kubernetes.io/component: worker
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.worker.persistence.size }}
{{- if .Values.worker.persistence.storageClass }}
storageClassName: {{ .Values.worker.persistence.storageClass }}
{{- end }}
{{- end }}
6 changes: 6 additions & 0 deletions charts/peerbot/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ worker:
# Workspace configuration
workspace:
sizeLimit: 10Gi

# Persistent storage configuration
persistence:
enabled: true
size: 10Gi
storageClass: "" # Use default storage class

# Slack configuration
slack:
Expand Down
4 changes: 2 additions & 2 deletions packages/dispatcher/src/kubernetes/job-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,8 @@ export class KubernetesJobManager {
volumes: [
{
name: "workspace",
emptyDir: {
sizeLimit: "10Gi",
persistentVolumeClaim: {
claimName: "peerbot-worker-pvc",
},
},
],
Expand Down
122 changes: 0 additions & 122 deletions packages/worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,116 +152,6 @@ export class ClaudeWorker {
}


/**
* Save Claude session ID mapping for thread
*/
private async saveSessionMapping(claudeSessionId: string): Promise<void> {
try {
const fs = await import('fs').then(m => m.promises);
const path = await import('path');

const sessionDir = path.join('/workspace', this.config.username, '.claude', 'projects', this.config.username);
await fs.mkdir(sessionDir, { recursive: true });

const mappingFile = path.join(sessionDir, `${this.config.sessionKey}.mapping`);
await fs.writeFile(mappingFile, claudeSessionId, 'utf8');

logger.info(`Saved session mapping: ${this.config.sessionKey} -> ${claudeSessionId}`);
} catch (error) {
logger.error(`Failed to save session mapping for ${this.config.sessionKey}:`, error);
}
}

/**
* Sync conversation files - copy the current session's JSONL file from ~/.claude/projects
*/
private async syncConversationFiles(): Promise<void> {
try {
const fs = await import('fs').then(m => m.promises);
const path = await import('path');

logger.info("Syncing conversation file for current session...");

// Get the session ID from the Claude execution logs
// We need to find the session ID that was created for this execution
const logPath = `${process.env.RUNNER_TEMP || "/tmp"}/claude-execution-output.json`;
let sessionId: string | null = null;

try {
const logContent = await fs.readFile(logPath, 'utf-8');
const lines = logContent.split('\n').filter(line => line.trim());

// Look for the session_id in the log entries
for (const line of lines) {
try {
const entry = JSON.parse(line);
if (entry.session_id) {
sessionId = entry.session_id;
break;
}
} catch {
// Skip invalid JSON lines
}
}
} catch (logError) {
logger.warn("Could not read Claude execution log:", logError);
}

if (!sessionId) {
logger.warn("Could not find session ID, skipping conversation sync");
return;
}

logger.info(`Found session ID: ${sessionId}`);

// Save session mapping for future resumption
await this.saveSessionMapping(sessionId);

// Paths for the conversation file
const homeClaudeDir = '/home/claude/.claude/projects';
const workspaceDir = `/workspace/${this.config.username}`;
const workspaceName = this.config.username;
const sessionFile = `${sessionId}.jsonl`;

const srcPath = path.join(homeClaudeDir, workspaceName, sessionFile);
const destDir = path.join(workspaceDir, '.claude', 'projects');
const destPath = path.join(destDir, sessionFile);

logger.info(`Copying conversation file from ${srcPath} to ${destPath}`);

// Check if source file exists
try {
await fs.access(srcPath);
} catch {
logger.info(`No conversation file found at ${srcPath}`);
return;
}

// Ensure destination directory exists
await fs.mkdir(destDir, { recursive: true });

// Copy the conversation file
await fs.copyFile(srcPath, destPath);
logger.info(`Successfully synced conversation file: ${sessionFile}`);

// Commit the conversation file
try {
const status = await this.workspaceManager.getRepositoryStatus();
if (status.hasChanges) {
await this.workspaceManager.commitAndPush(
`Save conversation: ${sessionFile}`
);
logger.info(`Committed conversation file to repository`);
}
} catch (error) {
logger.warn("Failed to commit conversation file:", error);
}

} catch (error) {
logger.error("Error syncing conversation files:", error);
throw error;
}
}

/**
* Execute the worker job
Expand Down Expand Up @@ -430,12 +320,6 @@ export class ClaudeWorker {
logger.warn("Final push failed:", pushError);
}

// Sync conversation files back to repository
try {
await this.syncConversationFiles();
} catch (syncError) {
logger.warn("Conversation file sync failed:", syncError);
}

if (result.success) {
logger.info("Calling slackIntegration.updateProgress...");
Expand Down Expand Up @@ -489,12 +373,6 @@ export class ClaudeWorker {
logger.warn("Error push failed:", pushError);
}

// Sync conversation files even on error
try {
await this.syncConversationFiles();
} catch (syncError) {
logger.warn("Error conversation file sync failed:", syncError);
}

// Update Slack with error
await this.slackIntegration.updateProgress(
Expand Down
Loading