@codemcp/workflows-core 3.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/LICENSE +674 -0
- package/dist/config-manager.d.ts +24 -0
- package/dist/config-manager.js +68 -0
- package/dist/config-manager.js.map +1 -0
- package/dist/conversation-manager.d.ts +97 -0
- package/dist/conversation-manager.js +367 -0
- package/dist/conversation-manager.js.map +1 -0
- package/dist/database.d.ts +73 -0
- package/dist/database.js +500 -0
- package/dist/database.js.map +1 -0
- package/dist/file-detection-manager.d.ts +53 -0
- package/dist/file-detection-manager.js +221 -0
- package/dist/file-detection-manager.js.map +1 -0
- package/dist/git-manager.d.ts +14 -0
- package/dist/git-manager.js +59 -0
- package/dist/git-manager.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/instruction-generator.d.ts +69 -0
- package/dist/instruction-generator.js +133 -0
- package/dist/instruction-generator.js.map +1 -0
- package/dist/interaction-logger.d.ts +37 -0
- package/dist/interaction-logger.js +87 -0
- package/dist/interaction-logger.js.map +1 -0
- package/dist/logger.d.ts +64 -0
- package/dist/logger.js +283 -0
- package/dist/logger.js.map +1 -0
- package/dist/path-validation-utils.d.ts +51 -0
- package/dist/path-validation-utils.js +202 -0
- package/dist/path-validation-utils.js.map +1 -0
- package/dist/plan-manager.d.ts +65 -0
- package/dist/plan-manager.js +256 -0
- package/dist/plan-manager.js.map +1 -0
- package/dist/project-docs-manager.d.ts +119 -0
- package/dist/project-docs-manager.js +357 -0
- package/dist/project-docs-manager.js.map +1 -0
- package/dist/state-machine-loader.d.ts +60 -0
- package/dist/state-machine-loader.js +235 -0
- package/dist/state-machine-loader.js.map +1 -0
- package/dist/state-machine-types.d.ts +58 -0
- package/dist/state-machine-types.js +7 -0
- package/dist/state-machine-types.js.map +1 -0
- package/dist/state-machine.d.ts +52 -0
- package/dist/state-machine.js +256 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/system-prompt-generator.d.ts +17 -0
- package/dist/system-prompt-generator.js +113 -0
- package/dist/system-prompt-generator.js.map +1 -0
- package/dist/template-manager.d.ts +61 -0
- package/dist/template-manager.js +229 -0
- package/dist/template-manager.js.map +1 -0
- package/dist/transition-engine.d.ts +70 -0
- package/dist/transition-engine.js +240 -0
- package/dist/transition-engine.js.map +1 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-manager.d.ts +89 -0
- package/dist/workflow-manager.js +466 -0
- package/dist/workflow-manager.js.map +1 -0
- package/package.json +27 -0
- package/src/config-manager.ts +96 -0
- package/src/conversation-manager.ts +492 -0
- package/src/database.ts +685 -0
- package/src/file-detection-manager.ts +302 -0
- package/src/git-manager.ts +64 -0
- package/src/index.ts +28 -0
- package/src/instruction-generator.ts +210 -0
- package/src/interaction-logger.ts +109 -0
- package/src/logger.ts +353 -0
- package/src/path-validation-utils.ts +261 -0
- package/src/plan-manager.ts +323 -0
- package/src/project-docs-manager.ts +522 -0
- package/src/state-machine-loader.ts +308 -0
- package/src/state-machine-types.ts +72 -0
- package/src/state-machine.ts +370 -0
- package/src/system-prompt-generator.ts +122 -0
- package/src/template-manager.ts +321 -0
- package/src/transition-engine.ts +386 -0
- package/src/types.ts +60 -0
- package/src/workflow-manager.ts +601 -0
- package/test/unit/conversation-manager.test.ts +179 -0
- package/test/unit/custom-workflow-loading.test.ts +174 -0
- package/test/unit/directory-linking-and-extensions.test.ts +338 -0
- package/test/unit/file-linking-integration.test.ts +256 -0
- package/test/unit/git-commit-integration.test.ts +91 -0
- package/test/unit/git-manager.test.ts +86 -0
- package/test/unit/install-workflow.test.ts +138 -0
- package/test/unit/instruction-generator.test.ts +247 -0
- package/test/unit/list-workflows-filtering.test.ts +68 -0
- package/test/unit/none-template-functionality.test.ts +224 -0
- package/test/unit/project-docs-manager.test.ts +337 -0
- package/test/unit/state-machine-loader.test.ts +234 -0
- package/test/unit/template-manager.test.ts +217 -0
- package/test/unit/validate-workflow-name.test.ts +150 -0
- package/test/unit/workflow-domain-filtering.test.ts +75 -0
- package/test/unit/workflow-enum-generation.test.ts +92 -0
- package/test/unit/workflow-manager-enhanced-path-resolution.test.ts +369 -0
- package/test/unit/workflow-manager-path-resolution.test.ts +150 -0
- package/test/unit/workflow-migration.test.ts +155 -0
- package/test/unit/workflow-override-by-name.test.ts +116 -0
- package/test/unit/workflow-prioritization.test.ts +38 -0
- package/test/unit/workflow-validation.test.ts +303 -0
- package/test/utils/e2e-test-setup.ts +453 -0
- package/test/utils/run-server-in-dir.sh +27 -0
- package/test/utils/temp-files.ts +308 -0
- package/test/utils/test-access.ts +79 -0
- package/test/utils/test-helpers.ts +286 -0
- package/test/utils/test-setup.ts +78 -0
- package/tsconfig.build.json +21 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +18 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
/**
|
2
|
+
* Configuration Manager
|
3
|
+
*
|
4
|
+
* Handles loading and validation of project configuration from .vibe/config.yaml
|
5
|
+
*/
|
6
|
+
|
7
|
+
import fs from 'node:fs';
|
8
|
+
import path from 'node:path';
|
9
|
+
import yaml from 'js-yaml';
|
10
|
+
import { createLogger } from './logger.js';
|
11
|
+
|
12
|
+
const logger = createLogger('ConfigManager');
|
13
|
+
|
14
|
+
export interface ProjectConfig {
|
15
|
+
enabled_workflows?: string[];
|
16
|
+
}
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Manages project configuration loading and validation
|
20
|
+
*/
|
21
|
+
export class ConfigManager {
|
22
|
+
private static readonly CONFIG_FILENAME = 'config.yaml';
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Load project configuration from .vibe/config.yaml
|
26
|
+
* Returns null if no config file exists (backward compatibility)
|
27
|
+
* Throws error for invalid configuration
|
28
|
+
*/
|
29
|
+
public static loadProjectConfig(projectPath: string): ProjectConfig | null {
|
30
|
+
const configPath = path.join(projectPath, '.vibe', this.CONFIG_FILENAME);
|
31
|
+
|
32
|
+
// No config file = backward compatibility (all workflows available)
|
33
|
+
if (!fs.existsSync(configPath)) {
|
34
|
+
logger.debug('No config file found, using defaults', { configPath });
|
35
|
+
return null;
|
36
|
+
}
|
37
|
+
|
38
|
+
try {
|
39
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
40
|
+
const config = yaml.load(configContent) as ProjectConfig;
|
41
|
+
|
42
|
+
this.validateConfig(config, configPath);
|
43
|
+
|
44
|
+
logger.info('Loaded project configuration', {
|
45
|
+
configPath,
|
46
|
+
enabledWorkflows: config.enabled_workflows?.length || 0,
|
47
|
+
});
|
48
|
+
|
49
|
+
return config;
|
50
|
+
} catch (error) {
|
51
|
+
if (error instanceof yaml.YAMLException) {
|
52
|
+
throw new Error(
|
53
|
+
`Invalid YAML in config file ${configPath}: ${error.message}`
|
54
|
+
);
|
55
|
+
}
|
56
|
+
throw new Error(`Failed to load config file ${configPath}: ${error}`);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
/**
|
61
|
+
* Validate configuration structure and content
|
62
|
+
*/
|
63
|
+
private static validateConfig(
|
64
|
+
config: ProjectConfig,
|
65
|
+
configPath: string
|
66
|
+
): void {
|
67
|
+
if (!config || typeof config !== 'object') {
|
68
|
+
throw new Error(
|
69
|
+
`Invalid config file ${configPath}: must be a YAML object`
|
70
|
+
);
|
71
|
+
}
|
72
|
+
|
73
|
+
if (config.enabled_workflows !== undefined) {
|
74
|
+
if (!Array.isArray(config.enabled_workflows)) {
|
75
|
+
throw new Error(
|
76
|
+
`Invalid config file ${configPath}: enabled_workflows must be an array`
|
77
|
+
);
|
78
|
+
}
|
79
|
+
|
80
|
+
if (config.enabled_workflows.length === 0) {
|
81
|
+
throw new Error(
|
82
|
+
`Invalid config file ${configPath}: enabled_workflows cannot be empty`
|
83
|
+
);
|
84
|
+
}
|
85
|
+
|
86
|
+
// Validate all entries are strings
|
87
|
+
for (const workflow of config.enabled_workflows) {
|
88
|
+
if (typeof workflow !== 'string' || workflow.trim() === '') {
|
89
|
+
throw new Error(
|
90
|
+
`Invalid config file ${configPath}: all workflow names must be non-empty strings`
|
91
|
+
);
|
92
|
+
}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
}
|
@@ -0,0 +1,492 @@
|
|
1
|
+
/**
|
2
|
+
* Conversation Manager
|
3
|
+
*
|
4
|
+
* Handles conversation identification, state persistence, and coordination
|
5
|
+
* between components. Generates unique conversation identifiers from
|
6
|
+
* project path + git branch combination.
|
7
|
+
*/
|
8
|
+
|
9
|
+
import { execSync } from 'node:child_process';
|
10
|
+
import { resolve } from 'node:path';
|
11
|
+
import { existsSync } from 'node:fs';
|
12
|
+
import { createLogger } from './logger.js';
|
13
|
+
import { Database } from './database.js';
|
14
|
+
import type { ConversationState, ConversationContext } from './types.js';
|
15
|
+
import { WorkflowManager } from './workflow-manager.js';
|
16
|
+
import { PlanManager } from './plan-manager.js';
|
17
|
+
|
18
|
+
const logger = createLogger('ConversationManager');
|
19
|
+
|
20
|
+
export class ConversationManager {
|
21
|
+
private database: Database;
|
22
|
+
private projectPath: string;
|
23
|
+
private workflowManager: WorkflowManager;
|
24
|
+
|
25
|
+
constructor(
|
26
|
+
database: Database,
|
27
|
+
workflowManager: WorkflowManager,
|
28
|
+
projectPath: string
|
29
|
+
) {
|
30
|
+
this.database = database;
|
31
|
+
this.workflowManager = workflowManager;
|
32
|
+
this.projectPath = projectPath;
|
33
|
+
}
|
34
|
+
|
35
|
+
/**
|
36
|
+
* Get conversation state by ID
|
37
|
+
*/
|
38
|
+
async getConversationState(
|
39
|
+
conversationId: string
|
40
|
+
): Promise<ConversationState | null> {
|
41
|
+
return await this.database.getConversationState(conversationId);
|
42
|
+
}
|
43
|
+
|
44
|
+
/**
|
45
|
+
* Get the current conversation context
|
46
|
+
*
|
47
|
+
* Detects the current project path and git branch, then retrieves an existing
|
48
|
+
* conversation state for this context. Does NOT create a new conversation.
|
49
|
+
*
|
50
|
+
* @throws Error if no conversation exists for this context
|
51
|
+
*/
|
52
|
+
async getConversationContext(): Promise<ConversationContext> {
|
53
|
+
const projectPath = this.getProjectPath();
|
54
|
+
const gitBranch = this.getGitBranch(projectPath);
|
55
|
+
|
56
|
+
logger.debug('Getting conversation context', { projectPath, gitBranch });
|
57
|
+
|
58
|
+
// Generate a unique conversation ID based on project path and git branch
|
59
|
+
const conversationId = this.generateConversationId(projectPath, gitBranch);
|
60
|
+
|
61
|
+
// Try to find existing conversation state
|
62
|
+
const state = await this.database.getConversationState(conversationId);
|
63
|
+
|
64
|
+
// If no existing state, throw an error - conversation must be created with start_development first
|
65
|
+
if (!state) {
|
66
|
+
logger.warn('No conversation found for context', {
|
67
|
+
projectPath,
|
68
|
+
gitBranch,
|
69
|
+
conversationId,
|
70
|
+
});
|
71
|
+
throw new Error(
|
72
|
+
'No development conversation exists for this project. Use the start_development tool first to initialize development with a workflow.'
|
73
|
+
);
|
74
|
+
}
|
75
|
+
|
76
|
+
// Return the conversation context
|
77
|
+
return {
|
78
|
+
conversationId: state.conversationId,
|
79
|
+
projectPath: state.projectPath,
|
80
|
+
gitBranch: state.gitBranch,
|
81
|
+
currentPhase: state.currentPhase,
|
82
|
+
planFilePath: state.planFilePath,
|
83
|
+
workflowName: state.workflowName,
|
84
|
+
};
|
85
|
+
}
|
86
|
+
|
87
|
+
/**
|
88
|
+
* Create a new conversation context
|
89
|
+
*
|
90
|
+
* This should only be called by the start_development tool to explicitly
|
91
|
+
* create a new conversation with a selected workflow.
|
92
|
+
*
|
93
|
+
* @param workflowName - The workflow to use for this conversation
|
94
|
+
* @returns The newly created conversation context
|
95
|
+
*/
|
96
|
+
async createConversationContext(
|
97
|
+
workflowName: string
|
98
|
+
): Promise<ConversationContext> {
|
99
|
+
const projectPath = this.getProjectPath();
|
100
|
+
const gitBranch = this.getGitBranch(projectPath);
|
101
|
+
|
102
|
+
logger.debug('Creating conversation context', {
|
103
|
+
projectPath,
|
104
|
+
gitBranch,
|
105
|
+
workflowName,
|
106
|
+
});
|
107
|
+
|
108
|
+
// Generate a unique conversation ID based on project path and git branch
|
109
|
+
const conversationId = this.generateConversationId(projectPath, gitBranch);
|
110
|
+
|
111
|
+
// Check if a conversation already exists
|
112
|
+
const existingState =
|
113
|
+
await this.database.getConversationState(conversationId);
|
114
|
+
|
115
|
+
if (existingState) {
|
116
|
+
logger.debug('Conversation already exists, returning existing context', {
|
117
|
+
conversationId,
|
118
|
+
});
|
119
|
+
return {
|
120
|
+
conversationId: existingState.conversationId,
|
121
|
+
projectPath: existingState.projectPath,
|
122
|
+
gitBranch: existingState.gitBranch,
|
123
|
+
currentPhase: existingState.currentPhase,
|
124
|
+
planFilePath: existingState.planFilePath,
|
125
|
+
workflowName: existingState.workflowName,
|
126
|
+
};
|
127
|
+
}
|
128
|
+
|
129
|
+
// Create a new conversation state
|
130
|
+
const state = await this.createNewConversationState(
|
131
|
+
conversationId,
|
132
|
+
projectPath,
|
133
|
+
gitBranch,
|
134
|
+
workflowName
|
135
|
+
);
|
136
|
+
|
137
|
+
// Return the conversation context
|
138
|
+
return {
|
139
|
+
conversationId: state.conversationId,
|
140
|
+
projectPath: state.projectPath,
|
141
|
+
gitBranch: state.gitBranch,
|
142
|
+
currentPhase: state.currentPhase,
|
143
|
+
planFilePath: state.planFilePath,
|
144
|
+
workflowName: state.workflowName,
|
145
|
+
};
|
146
|
+
}
|
147
|
+
|
148
|
+
/**
|
149
|
+
* Update the conversation state
|
150
|
+
*
|
151
|
+
* @param conversationId - ID of the conversation to update
|
152
|
+
* @param updates - Partial state updates to apply
|
153
|
+
*/
|
154
|
+
async updateConversationState(
|
155
|
+
conversationId: string,
|
156
|
+
updates: Partial<
|
157
|
+
Pick<
|
158
|
+
ConversationState,
|
159
|
+
| 'currentPhase'
|
160
|
+
| 'planFilePath'
|
161
|
+
| 'workflowName'
|
162
|
+
| 'gitCommitConfig'
|
163
|
+
| 'requireReviewsBeforePhaseTransition'
|
164
|
+
>
|
165
|
+
>
|
166
|
+
): Promise<void> {
|
167
|
+
logger.debug('Updating conversation state', { conversationId, updates });
|
168
|
+
|
169
|
+
// Get current state
|
170
|
+
const currentState =
|
171
|
+
await this.database.getConversationState(conversationId);
|
172
|
+
|
173
|
+
if (!currentState) {
|
174
|
+
throw new Error(`Conversation state not found for ID: ${conversationId}`);
|
175
|
+
}
|
176
|
+
|
177
|
+
// Apply updates
|
178
|
+
const updatedState: ConversationState = {
|
179
|
+
...currentState,
|
180
|
+
...updates,
|
181
|
+
updatedAt: new Date().toISOString(),
|
182
|
+
};
|
183
|
+
|
184
|
+
// Save updated state
|
185
|
+
await this.database.saveConversationState(updatedState);
|
186
|
+
|
187
|
+
logger.info('Conversation state updated', {
|
188
|
+
conversationId,
|
189
|
+
currentPhase: updatedState.currentPhase,
|
190
|
+
});
|
191
|
+
}
|
192
|
+
|
193
|
+
/**
|
194
|
+
* Create a new conversation state
|
195
|
+
*
|
196
|
+
* @param conversationId - ID for the new conversation
|
197
|
+
* @param projectPath - Path to the project
|
198
|
+
* @param gitBranch - Git branch name
|
199
|
+
*/
|
200
|
+
private async createNewConversationState(
|
201
|
+
conversationId: string,
|
202
|
+
projectPath: string,
|
203
|
+
gitBranch: string,
|
204
|
+
workflowName: string = 'waterfall'
|
205
|
+
): Promise<ConversationState> {
|
206
|
+
logger.info('Creating new conversation state', {
|
207
|
+
conversationId,
|
208
|
+
projectPath,
|
209
|
+
gitBranch,
|
210
|
+
});
|
211
|
+
|
212
|
+
const timestamp = new Date().toISOString();
|
213
|
+
|
214
|
+
// Generate a plan file path based on the branch name
|
215
|
+
const planFileName =
|
216
|
+
gitBranch === 'main' || gitBranch === 'master'
|
217
|
+
? 'development-plan.md'
|
218
|
+
: `development-plan-${gitBranch}.md`;
|
219
|
+
|
220
|
+
const planFilePath = resolve(projectPath, '.vibe', planFileName);
|
221
|
+
|
222
|
+
// Get initial state from the appropriate workflow
|
223
|
+
const stateMachine = this.workflowManager.loadWorkflowForProject(
|
224
|
+
projectPath,
|
225
|
+
workflowName
|
226
|
+
);
|
227
|
+
const initialPhase = stateMachine.initial_state;
|
228
|
+
|
229
|
+
// Create new state
|
230
|
+
const newState: ConversationState = {
|
231
|
+
conversationId,
|
232
|
+
projectPath,
|
233
|
+
gitBranch,
|
234
|
+
currentPhase: initialPhase,
|
235
|
+
planFilePath,
|
236
|
+
workflowName,
|
237
|
+
requireReviewsBeforePhaseTransition: false, // Default to false for new conversations
|
238
|
+
createdAt: timestamp,
|
239
|
+
updatedAt: timestamp,
|
240
|
+
};
|
241
|
+
|
242
|
+
// Save to database
|
243
|
+
await this.database.saveConversationState(newState);
|
244
|
+
|
245
|
+
logger.info('New conversation state created', {
|
246
|
+
conversationId,
|
247
|
+
planFilePath,
|
248
|
+
initialPhase,
|
249
|
+
});
|
250
|
+
|
251
|
+
return newState;
|
252
|
+
}
|
253
|
+
|
254
|
+
/**
|
255
|
+
* Generate a unique conversation ID based on project path and git branch
|
256
|
+
*
|
257
|
+
* @param projectPath - Path to the project
|
258
|
+
* @param gitBranch - Git branch name
|
259
|
+
*/
|
260
|
+
private generateConversationId(
|
261
|
+
projectPath: string,
|
262
|
+
gitBranch: string
|
263
|
+
): string {
|
264
|
+
// Extract project name from path
|
265
|
+
const projectName = projectPath.split('/').pop() || 'unknown-project';
|
266
|
+
|
267
|
+
// Clean branch name for use in ID
|
268
|
+
const cleanBranch = gitBranch
|
269
|
+
.replace(/[^a-zA-Z0-9-]/g, '-')
|
270
|
+
.replace(/-+/g, '-')
|
271
|
+
.replace(/^-|-$/g, '');
|
272
|
+
|
273
|
+
// For tests, use a deterministic ID
|
274
|
+
if (process.env.NODE_ENV === 'test') {
|
275
|
+
return `${projectName}-${cleanBranch}-p423k1`;
|
276
|
+
}
|
277
|
+
|
278
|
+
// Generate a deterministic ID based on project path and branch
|
279
|
+
// This ensures the same project/branch combination always gets the same conversation ID
|
280
|
+
let hash = 0;
|
281
|
+
const str = `${projectPath}:${gitBranch}`;
|
282
|
+
for (let i = 0; i < str.length; i++) {
|
283
|
+
const char = str.charCodeAt(i);
|
284
|
+
hash = (hash << 5) - hash + char;
|
285
|
+
hash = hash & hash; // Convert to 32-bit integer
|
286
|
+
}
|
287
|
+
const hashStr = Math.abs(hash).toString(36).substring(0, 6);
|
288
|
+
|
289
|
+
return `${projectName}-${cleanBranch}-${hashStr}`;
|
290
|
+
}
|
291
|
+
|
292
|
+
/**
|
293
|
+
* Get the current project path
|
294
|
+
*/
|
295
|
+
private getProjectPath(): string {
|
296
|
+
return this.projectPath;
|
297
|
+
}
|
298
|
+
|
299
|
+
/**
|
300
|
+
* Get the current git branch for a project
|
301
|
+
*
|
302
|
+
* @param projectPath - Path to the project
|
303
|
+
*/
|
304
|
+
private getGitBranch(projectPath: string): string {
|
305
|
+
try {
|
306
|
+
// Check if this is a git repository
|
307
|
+
if (!existsSync(`${projectPath}/.git`)) {
|
308
|
+
logger.debug('Not a git repository, using "default" as branch name', {
|
309
|
+
projectPath,
|
310
|
+
});
|
311
|
+
return 'default';
|
312
|
+
}
|
313
|
+
|
314
|
+
// Get current branch name
|
315
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
316
|
+
cwd: projectPath,
|
317
|
+
encoding: 'utf-8',
|
318
|
+
stdio: ['ignore', 'pipe', 'ignore'], // Suppress stderr to avoid "fatal: not a git repository" warnings
|
319
|
+
}).trim();
|
320
|
+
|
321
|
+
logger.debug('Detected git branch', { projectPath, branch });
|
322
|
+
|
323
|
+
return branch;
|
324
|
+
} catch (_error) {
|
325
|
+
logger.debug('Failed to get git branch, using "default" as branch name', {
|
326
|
+
projectPath,
|
327
|
+
});
|
328
|
+
return 'default';
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
/**
|
333
|
+
* Check if a conversation has any previous interactions
|
334
|
+
* Used to determine if this is the first interaction in a conversation
|
335
|
+
*/
|
336
|
+
async hasInteractions(conversationId: string): Promise<boolean> {
|
337
|
+
try {
|
338
|
+
// Get all interactions for this conversation
|
339
|
+
const interactions =
|
340
|
+
await this.database.getInteractionsByConversationId(conversationId);
|
341
|
+
const count = interactions.length;
|
342
|
+
|
343
|
+
logger.debug('Checked interaction count for conversation', {
|
344
|
+
conversationId,
|
345
|
+
count,
|
346
|
+
});
|
347
|
+
|
348
|
+
return count > 0;
|
349
|
+
} catch (error) {
|
350
|
+
logger.error('Failed to check interaction count', error as Error, {
|
351
|
+
conversationId,
|
352
|
+
});
|
353
|
+
// If we can't check, assume this is the first interaction to be safe
|
354
|
+
return false;
|
355
|
+
}
|
356
|
+
}
|
357
|
+
|
358
|
+
/**
|
359
|
+
* Reset conversation data (hybrid approach)
|
360
|
+
*/
|
361
|
+
async resetConversation(
|
362
|
+
confirm: boolean,
|
363
|
+
reason?: string
|
364
|
+
): Promise<{
|
365
|
+
success: boolean;
|
366
|
+
resetItems: string[];
|
367
|
+
conversationId: string;
|
368
|
+
message: string;
|
369
|
+
}> {
|
370
|
+
logger.info('Starting conversation reset', { confirm, reason });
|
371
|
+
|
372
|
+
// Validate reset request
|
373
|
+
this.validateResetRequest(confirm);
|
374
|
+
|
375
|
+
const context = await this.getConversationContext();
|
376
|
+
const resetItems: string[] = [];
|
377
|
+
|
378
|
+
try {
|
379
|
+
// Step 1: Soft delete interaction logs
|
380
|
+
await this.database.softDeleteInteractionLogs(
|
381
|
+
context.conversationId,
|
382
|
+
reason
|
383
|
+
);
|
384
|
+
resetItems.push('interaction_logs');
|
385
|
+
logger.debug('Interaction logs soft deleted');
|
386
|
+
|
387
|
+
// Step 2: Hard delete conversation state
|
388
|
+
await this.database.deleteConversationState(context.conversationId);
|
389
|
+
resetItems.push('conversation_state');
|
390
|
+
logger.debug('Conversation state hard deleted');
|
391
|
+
|
392
|
+
// Step 3: Hard delete plan file
|
393
|
+
const planManager = new PlanManager();
|
394
|
+
await planManager.deletePlanFile(context.planFilePath);
|
395
|
+
resetItems.push('plan_file');
|
396
|
+
logger.debug('Plan file deleted');
|
397
|
+
|
398
|
+
// Verify cleanup
|
399
|
+
await this.verifyResetCleanup(
|
400
|
+
context.conversationId,
|
401
|
+
context.planFilePath
|
402
|
+
);
|
403
|
+
|
404
|
+
const message = `Successfully reset conversation ${context.conversationId}. Reset items: ${resetItems.join(', ')}${reason ? `. Reason: ${reason}` : ''}`;
|
405
|
+
|
406
|
+
logger.info('Conversation reset completed successfully', {
|
407
|
+
conversationId: context.conversationId,
|
408
|
+
resetItems,
|
409
|
+
reason,
|
410
|
+
});
|
411
|
+
|
412
|
+
return {
|
413
|
+
success: true,
|
414
|
+
resetItems,
|
415
|
+
conversationId: context.conversationId,
|
416
|
+
message,
|
417
|
+
};
|
418
|
+
} catch (error) {
|
419
|
+
logger.error('Failed to reset conversation', error as Error, {
|
420
|
+
conversationId: context.conversationId,
|
421
|
+
resetItems,
|
422
|
+
reason,
|
423
|
+
});
|
424
|
+
|
425
|
+
throw new Error(
|
426
|
+
`Reset failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
427
|
+
);
|
428
|
+
}
|
429
|
+
}
|
430
|
+
|
431
|
+
/**
|
432
|
+
* Validate reset request parameters
|
433
|
+
*/
|
434
|
+
private validateResetRequest(confirm: boolean): void {
|
435
|
+
if (!confirm) {
|
436
|
+
throw new Error(
|
437
|
+
'Reset operation requires explicit confirmation. Set confirm parameter to true.'
|
438
|
+
);
|
439
|
+
}
|
440
|
+
}
|
441
|
+
|
442
|
+
/**
|
443
|
+
* Verify that reset cleanup was successful
|
444
|
+
*/
|
445
|
+
private async verifyResetCleanup(
|
446
|
+
conversationId: string,
|
447
|
+
planFilePath: string
|
448
|
+
): Promise<void> {
|
449
|
+
logger.debug('Verifying reset cleanup', { conversationId, planFilePath });
|
450
|
+
|
451
|
+
try {
|
452
|
+
// Check that conversation state is deleted
|
453
|
+
const state = await this.database.getConversationState(conversationId);
|
454
|
+
if (state) {
|
455
|
+
throw new Error('Conversation state was not properly deleted');
|
456
|
+
}
|
457
|
+
|
458
|
+
// Check that plan file is deleted
|
459
|
+
const planManager = new PlanManager();
|
460
|
+
const isDeleted = await planManager.ensurePlanFileDeleted(planFilePath);
|
461
|
+
if (!isDeleted) {
|
462
|
+
throw new Error('Plan file was not properly deleted');
|
463
|
+
}
|
464
|
+
|
465
|
+
logger.debug('Reset cleanup verification successful');
|
466
|
+
} catch (error) {
|
467
|
+
logger.error('Reset cleanup verification failed', error as Error);
|
468
|
+
throw error;
|
469
|
+
}
|
470
|
+
}
|
471
|
+
|
472
|
+
/**
|
473
|
+
* Clean up conversation data (used internally)
|
474
|
+
*/
|
475
|
+
async cleanupConversationData(conversationId: string): Promise<void> {
|
476
|
+
logger.debug('Cleaning up conversation data', { conversationId });
|
477
|
+
|
478
|
+
try {
|
479
|
+
// This method can be used for additional cleanup if needed
|
480
|
+
// Currently, the main cleanup is handled by resetConversation
|
481
|
+
await this.database.softDeleteInteractionLogs(conversationId);
|
482
|
+
await this.database.deleteConversationState(conversationId);
|
483
|
+
|
484
|
+
logger.debug('Conversation data cleanup completed', { conversationId });
|
485
|
+
} catch (error) {
|
486
|
+
logger.error('Failed to cleanup conversation data', error as Error, {
|
487
|
+
conversationId,
|
488
|
+
});
|
489
|
+
throw error;
|
490
|
+
}
|
491
|
+
}
|
492
|
+
}
|