codeep 1.2.36 → 1.2.38

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.
@@ -31,6 +31,15 @@ export declare function initWorkspace(workspaceRoot: string): {
31
31
  history: Message[];
32
32
  welcomeText: string;
33
33
  };
34
+ /**
35
+ * Restore a previously saved ACP session by its Zed sessionId.
36
+ * Falls back to initWorkspace if the session cannot be found on disk.
37
+ */
38
+ export declare function loadWorkspace(workspaceRoot: string, acpSessionId: string): {
39
+ codeepSessionId: string;
40
+ history: Message[];
41
+ welcomeText: string;
42
+ };
34
43
  /**
35
44
  * Try to handle a slash command. Async because skills and diff/review
36
45
  * need to call the AI API or run shell commands.
@@ -74,6 +74,40 @@ export function initWorkspace(workspaceRoot) {
74
74
  }
75
75
  return { codeepSessionId, history, welcomeText: lines.join('\n') };
76
76
  }
77
+ /**
78
+ * Restore a previously saved ACP session by its Zed sessionId.
79
+ * Falls back to initWorkspace if the session cannot be found on disk.
80
+ */
81
+ export function loadWorkspace(workspaceRoot, acpSessionId) {
82
+ // Ensure workspace is set up
83
+ const codeepDir = join(workspaceRoot, '.codeep');
84
+ if (!existsSync(codeepDir)) {
85
+ mkdirSync(codeepDir, { recursive: true });
86
+ }
87
+ if (!isManuallyInitializedProject(workspaceRoot)) {
88
+ initializeAsProject(workspaceRoot);
89
+ }
90
+ if (!hasReadPermission(workspaceRoot)) {
91
+ setProjectPermission(workspaceRoot, true, true);
92
+ }
93
+ // Try to load the session that was saved under this ACP session ID
94
+ const loaded = loadSession(acpSessionId, workspaceRoot);
95
+ if (loaded) {
96
+ const history = loaded;
97
+ const provider = getCurrentProvider();
98
+ const model = config.get('model');
99
+ const lines = [
100
+ `**Codeep** • ${provider.name} • \`${model}\``,
101
+ '',
102
+ `**Session restored:** ${acpSessionId} (${history.length} messages)`,
103
+ '',
104
+ ...formatSessionPreviewLines(history),
105
+ ];
106
+ return { codeepSessionId: acpSessionId, history, welcomeText: lines.join('\n') };
107
+ }
108
+ // Session not found — fall back to initWorkspace behaviour
109
+ return initWorkspace(workspaceRoot);
110
+ }
77
111
  // ─── Command dispatch ─────────────────────────────────────────────────────────
78
112
  /**
79
113
  * Try to handle a slash command. Async because skills and diff/review
@@ -19,15 +19,23 @@ export interface JsonRpcNotification {
19
19
  params?: unknown;
20
20
  }
21
21
  export interface InitializeParams {
22
- capabilities?: Record<string, unknown>;
23
- workspaceFolders?: {
24
- uri: string;
22
+ protocolVersion?: number;
23
+ clientCapabilities?: {
24
+ fs?: {
25
+ readTextFile?: boolean;
26
+ writeTextFile?: boolean;
27
+ };
28
+ terminal?: boolean;
29
+ };
30
+ clientInfo?: {
25
31
  name: string;
26
- }[];
32
+ version: string;
33
+ };
27
34
  }
28
35
  export interface InitializeResult {
29
36
  protocolVersion: number;
30
37
  agentCapabilities: {
38
+ loadSession?: boolean;
31
39
  streaming?: boolean;
32
40
  fileEditing?: boolean;
33
41
  };
@@ -37,21 +45,154 @@ export interface InitializeResult {
37
45
  };
38
46
  authMethods: unknown[];
39
47
  }
40
- export interface SessionNewParams {
41
- cwd: string;
42
- mcpServers?: {
48
+ export interface McpServer {
49
+ name: string;
50
+ command: string;
51
+ args: string[];
52
+ env?: Record<string, string>;
53
+ }
54
+ export interface SessionMode {
55
+ id: string;
56
+ name: string;
57
+ description?: string | null;
58
+ }
59
+ export interface SessionModeState {
60
+ availableModes: SessionMode[];
61
+ currentModeId: string;
62
+ }
63
+ export interface SessionConfigOption {
64
+ id: string;
65
+ name: string;
66
+ description?: string | null;
67
+ category?: 'mode' | 'model' | 'thought_level' | null;
68
+ type: 'select';
69
+ options?: {
70
+ id: string;
43
71
  name: string;
44
- command: string;
45
- args: string[];
46
- env?: Record<string, string>;
47
72
  }[];
73
+ currentValue?: string;
74
+ }
75
+ export interface SessionNewParams {
76
+ cwd: string;
77
+ mcpServers?: McpServer[];
78
+ }
79
+ export interface SessionNewResult {
80
+ sessionId: string;
81
+ modes?: SessionModeState | null;
82
+ configOptions?: SessionConfigOption[] | null;
83
+ }
84
+ export interface SessionLoadParams {
85
+ sessionId: string;
86
+ cwd: string;
87
+ mcpServers?: McpServer[];
88
+ }
89
+ export interface SessionLoadResult {
90
+ modes?: SessionModeState | null;
91
+ configOptions?: SessionConfigOption[] | null;
48
92
  }
49
93
  export interface ContentBlock {
50
- type: 'text';
51
- text: string;
94
+ type: 'text' | 'image' | 'audio' | 'resource_link' | 'resource';
95
+ text?: string;
96
+ data?: string;
97
+ mimeType?: string;
98
+ uri?: string;
99
+ name?: string;
52
100
  }
53
101
  export interface SessionPromptParams {
54
102
  sessionId: string;
55
103
  prompt: ContentBlock[];
56
104
  }
105
+ export interface SessionPromptResult {
106
+ stopReason: 'end_turn' | 'cancelled';
107
+ }
108
+ export interface SessionCancelParams {
109
+ sessionId: string;
110
+ }
111
+ export interface SetSessionModeParams {
112
+ sessionId: string;
113
+ modeId: string;
114
+ }
115
+ export interface SetSessionConfigOptionParams {
116
+ sessionId: string;
117
+ configId: string;
118
+ value: unknown;
119
+ }
120
+ export interface SessionUpdateAgentMessageChunk {
121
+ sessionUpdate: 'agent_message_chunk';
122
+ content: ContentBlock;
123
+ }
124
+ export interface SessionUpdateAgentThoughtChunk {
125
+ sessionUpdate: 'agent_thought_chunk';
126
+ content: ContentBlock;
127
+ }
128
+ export interface SessionUpdateToolCall {
129
+ sessionUpdate: 'tool_call';
130
+ toolCallId: string;
131
+ status: 'pending';
132
+ rawInput: unknown;
133
+ }
134
+ export interface SessionUpdateToolCallUpdate {
135
+ sessionUpdate: 'tool_call_update';
136
+ toolCallId: string;
137
+ status: 'completed' | 'failed';
138
+ rawOutput?: string;
139
+ }
140
+ export interface SessionUpdateAvailableCommands {
141
+ sessionUpdate: 'available_commands_update';
142
+ availableCommands: {
143
+ name: string;
144
+ description: string;
145
+ input?: {
146
+ hint: string;
147
+ } | null;
148
+ }[];
149
+ }
150
+ export interface SessionUpdateCurrentMode {
151
+ sessionUpdate: 'current_mode_update';
152
+ currentModeId: string;
153
+ }
154
+ export type SessionUpdateInner = SessionUpdateAgentMessageChunk | SessionUpdateAgentThoughtChunk | SessionUpdateToolCall | SessionUpdateToolCallUpdate | SessionUpdateAvailableCommands | SessionUpdateCurrentMode;
155
+ export interface SessionUpdateParams {
156
+ sessionId: string;
157
+ update: SessionUpdateInner;
158
+ }
159
+ export type PermissionOptionKind = 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
160
+ export interface PermissionOption {
161
+ optionId: string;
162
+ name: string;
163
+ kind: PermissionOptionKind;
164
+ }
165
+ export interface RequestPermissionParams {
166
+ sessionId: string;
167
+ toolCall: {
168
+ toolCallId: string;
169
+ toolName: string;
170
+ toolInput: unknown;
171
+ status: 'pending' | 'completed' | 'failed';
172
+ content: unknown[];
173
+ };
174
+ options: PermissionOption[];
175
+ }
176
+ export interface RequestPermissionResult {
177
+ outcome: {
178
+ type: 'cancelled';
179
+ } | {
180
+ type: 'selected';
181
+ optionId: string;
182
+ };
183
+ }
184
+ export interface FsReadTextFileParams {
185
+ sessionId: string;
186
+ path: string;
187
+ line?: number;
188
+ limit?: number;
189
+ }
190
+ export interface FsReadTextFileResult {
191
+ content: string;
192
+ }
193
+ export interface FsWriteTextFileParams {
194
+ sessionId: string;
195
+ path: string;
196
+ content: string;
197
+ }
57
198
  export type AcpMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
@@ -1,3 +1,3 @@
1
1
  // acp/protocol.ts
2
- // ACP JSON-RPC message types for Zed Agent Client Protocol
2
+ // ACP JSON-RPC message types Agent Client Protocol spec
3
3
  export {};
@@ -3,10 +3,10 @@
3
3
  import { randomUUID } from 'crypto';
4
4
  import { StdioTransport } from './transport.js';
5
5
  import { runAgentSession } from './session.js';
6
- import { initWorkspace, handleCommand } from './commands.js';
7
- import { autoSaveSession } from '../config/index.js';
6
+ import { initWorkspace, loadWorkspace, handleCommand } from './commands.js';
7
+ import { autoSaveSession, config } from '../config/index.js';
8
8
  import { getCurrentVersion } from '../utils/update.js';
9
- // All advertised slash commands (shown in Zed autocomplete)
9
+ // ─── Slash commands advertised to Zed ────────────────────────────────────────
10
10
  const AVAILABLE_COMMANDS = [
11
11
  // Configuration
12
12
  { name: 'help', description: 'Show available commands' },
@@ -49,36 +49,77 @@ const AVAILABLE_COMMANDS = [
49
49
  { name: 'build', description: 'Build the project' },
50
50
  { name: 'deploy', description: 'Deploy the project' },
51
51
  ];
52
+ // ─── Mode definitions ─────────────────────────────────────────────────────────
53
+ const AGENT_MODES = {
54
+ currentModeId: 'auto',
55
+ availableModes: [
56
+ { id: 'auto', name: 'Auto', description: 'Agent runs automatically without confirmation' },
57
+ { id: 'manual', name: 'Manual', description: 'Confirm dangerous operations before running' },
58
+ ],
59
+ };
60
+ // ─── Config options ───────────────────────────────────────────────────────────
61
+ function buildConfigOptions() {
62
+ return [
63
+ {
64
+ id: 'model',
65
+ name: 'Model',
66
+ description: 'AI model to use',
67
+ category: 'model',
68
+ type: 'select',
69
+ currentValue: config.get('model'),
70
+ },
71
+ ];
72
+ }
73
+ // ─── Server ───────────────────────────────────────────────────────────────────
52
74
  export function startAcpServer() {
53
75
  const transport = new StdioTransport();
54
76
  // ACP sessionId → full AcpSession (includes history + codeep session tracking)
55
77
  const sessions = new Map();
56
78
  transport.start((msg) => {
57
- switch (msg.method) {
79
+ // Notifications have no id — handle separately
80
+ if (!('id' in msg)) {
81
+ handleNotification(msg);
82
+ return;
83
+ }
84
+ const req = msg;
85
+ switch (req.method) {
58
86
  case 'initialize':
59
- handleInitialize(msg);
60
- break;
61
- case 'initialized':
62
- // no-op acknowledgment
87
+ handleInitialize(req);
63
88
  break;
89
+ case 'initialized': /* no-op acknowledgment */ break;
64
90
  case 'session/new':
65
- handleSessionNew(msg);
91
+ handleSessionNew(req);
92
+ break;
93
+ case 'session/load':
94
+ handleSessionLoad(req);
66
95
  break;
67
96
  case 'session/prompt':
68
- handleSessionPrompt(msg);
97
+ handleSessionPrompt(req);
98
+ break;
99
+ case 'session/set_mode':
100
+ handleSetMode(req);
69
101
  break;
70
- case 'session/cancel':
71
- handleSessionCancel(msg);
102
+ case 'session/set_config_option':
103
+ handleSetConfigOption(req);
72
104
  break;
73
105
  default:
74
- transport.error(msg.id, -32601, `Method not found: ${msg.method}`);
106
+ transport.error(req.id, -32601, `Method not found: ${req.method}`);
75
107
  }
76
108
  });
109
+ // ── Notification handler (no id, no response) ──────────────────────────────
110
+ function handleNotification(msg) {
111
+ if (msg.method === 'session/cancel') {
112
+ const { sessionId } = (msg.params ?? {});
113
+ sessions.get(sessionId)?.abortController?.abort();
114
+ }
115
+ }
116
+ // ── initialize ──────────────────────────────────────────────────────────────
77
117
  function handleInitialize(msg) {
78
118
  const _params = msg.params;
79
119
  const result = {
80
120
  protocolVersion: 1,
81
121
  agentCapabilities: {
122
+ loadSession: true,
82
123
  streaming: true,
83
124
  fileEditing: true,
84
125
  },
@@ -90,10 +131,10 @@ export function startAcpServer() {
90
131
  };
91
132
  transport.respond(msg.id, result);
92
133
  }
134
+ // ── session/new ─────────────────────────────────────────────────────────────
93
135
  function handleSessionNew(msg) {
94
136
  const params = msg.params;
95
137
  const acpSessionId = randomUUID();
96
- // Initialise workspace: create .codeep folder, load/create codeep session
97
138
  const { codeepSessionId, history, welcomeText } = initWorkspace(params.cwd);
98
139
  sessions.set(acpSessionId, {
99
140
  sessionId: acpSessionId,
@@ -102,9 +143,15 @@ export function startAcpServer() {
102
143
  codeepSessionId,
103
144
  addedFiles: new Map(),
104
145
  abortController: null,
146
+ currentModeId: 'auto',
105
147
  });
106
- transport.respond(msg.id, { sessionId: acpSessionId });
107
- // Advertise all available slash commands to Zed
148
+ const result = {
149
+ sessionId: acpSessionId,
150
+ modes: AGENT_MODES,
151
+ configOptions: buildConfigOptions(),
152
+ };
153
+ transport.respond(msg.id, result);
154
+ // Advertise slash commands
108
155
  transport.notify('session/update', {
109
156
  sessionId: acpSessionId,
110
157
  update: {
@@ -112,7 +159,7 @@ export function startAcpServer() {
112
159
  availableCommands: AVAILABLE_COMMANDS,
113
160
  },
114
161
  });
115
- // Stream welcome message
162
+ // Send welcome message
116
163
  transport.notify('session/update', {
117
164
  sessionId: acpSessionId,
118
165
  update: {
@@ -121,6 +168,86 @@ export function startAcpServer() {
121
168
  },
122
169
  });
123
170
  }
171
+ // ── session/load ────────────────────────────────────────────────────────────
172
+ function handleSessionLoad(msg) {
173
+ const params = msg.params;
174
+ // Try to restore existing Codeep session or fall back to fresh workspace
175
+ const existing = sessions.get(params.sessionId);
176
+ if (existing) {
177
+ // Session already in memory — update cwd if changed
178
+ existing.workspaceRoot = params.cwd;
179
+ const result = {
180
+ modes: AGENT_MODES,
181
+ configOptions: buildConfigOptions(),
182
+ };
183
+ transport.respond(msg.id, result);
184
+ return;
185
+ }
186
+ // Session not in memory — try to load from disk
187
+ const { codeepSessionId, history, welcomeText } = loadWorkspace(params.cwd, params.sessionId);
188
+ sessions.set(params.sessionId, {
189
+ sessionId: params.sessionId,
190
+ workspaceRoot: params.cwd,
191
+ history,
192
+ codeepSessionId,
193
+ addedFiles: new Map(),
194
+ abortController: null,
195
+ currentModeId: 'auto',
196
+ });
197
+ const result = {
198
+ modes: AGENT_MODES,
199
+ configOptions: buildConfigOptions(),
200
+ };
201
+ transport.respond(msg.id, result);
202
+ // Send restored session welcome
203
+ transport.notify('session/update', {
204
+ sessionId: params.sessionId,
205
+ update: {
206
+ sessionUpdate: 'agent_message_chunk',
207
+ content: { type: 'text', text: welcomeText },
208
+ },
209
+ });
210
+ }
211
+ // ── session/set_mode ────────────────────────────────────────────────────────
212
+ function handleSetMode(msg) {
213
+ const { sessionId, modeId } = msg.params;
214
+ const session = sessions.get(sessionId);
215
+ if (!session) {
216
+ transport.error(msg.id, -32602, `Unknown sessionId: ${sessionId}`);
217
+ return;
218
+ }
219
+ const validMode = AGENT_MODES.availableModes.find(m => m.id === modeId);
220
+ if (!validMode) {
221
+ transport.error(msg.id, -32602, `Unknown modeId: ${modeId}`);
222
+ return;
223
+ }
224
+ session.currentModeId = modeId;
225
+ // Map ACP mode to Codeep agentConfirmation setting
226
+ config.set('agentConfirmation', modeId === 'manual' ? 'dangerous' : 'never');
227
+ transport.respond(msg.id, {});
228
+ // Notify Zed of the mode change
229
+ transport.notify('session/update', {
230
+ sessionId,
231
+ update: {
232
+ sessionUpdate: 'current_mode_update',
233
+ currentModeId: modeId,
234
+ },
235
+ });
236
+ }
237
+ // ── session/set_config_option ───────────────────────────────────────────────
238
+ function handleSetConfigOption(msg) {
239
+ const { sessionId, configId, value } = msg.params;
240
+ const session = sessions.get(sessionId);
241
+ if (!session) {
242
+ transport.error(msg.id, -32602, `Unknown sessionId: ${sessionId}`);
243
+ return;
244
+ }
245
+ if (configId === 'model' && typeof value === 'string') {
246
+ config.set('model', value);
247
+ }
248
+ transport.respond(msg.id, {});
249
+ }
250
+ // ── session/prompt ──────────────────────────────────────────────────────────
124
251
  function handleSessionPrompt(msg) {
125
252
  const params = msg.params;
126
253
  const session = sessions.get(params.sessionId);
@@ -131,7 +258,7 @@ export function startAcpServer() {
131
258
  // Extract text from ContentBlock[]
132
259
  const prompt = params.prompt
133
260
  .filter((b) => b.type === 'text')
134
- .map((b) => b.text)
261
+ .map((b) => b.text ?? '')
135
262
  .join('\n');
136
263
  const abortController = new AbortController();
137
264
  session.abortController = abortController;
@@ -146,19 +273,16 @@ export function startAcpServer() {
146
273
  },
147
274
  });
148
275
  };
149
- // Try slash commands first (async — skills, diff, scan, etc.)
276
+ // Try slash commands first
150
277
  handleCommand(prompt, session, sendChunk, abortController.signal)
151
278
  .then((cmd) => {
152
279
  if (cmd.handled) {
153
- // For streaming commands (skills, diff), chunks were already sent via onChunk.
154
- // For simple commands, send the response now.
155
280
  if (cmd.response)
156
281
  sendChunk(cmd.response);
157
282
  transport.respond(msg.id, { stopReason: 'end_turn' });
158
283
  return;
159
284
  }
160
285
  // Not a command — run agent loop
161
- // Prepend any added-files context to the prompt
162
286
  let enrichedPrompt = prompt;
163
287
  if (session.addedFiles.size > 0) {
164
288
  const parts = ['[Attached files]'];
@@ -182,21 +306,31 @@ export function startAcpServer() {
182
306
  },
183
307
  });
184
308
  },
185
- onToolCall: (toolCallId, _toolName, kind, title, status, locations) => {
186
- transport.notify('session/update', {
187
- sessionId: params.sessionId,
188
- update: {
189
- sessionUpdate: 'tool_call',
190
- toolCallId,
191
- title,
192
- kind,
193
- status,
194
- ...(locations?.length ? { locations: locations.map(uri => ({ uri })) } : {}),
195
- },
196
- });
309
+ onToolCall: (toolCallId, toolName, _kind, _title, status, _locations) => {
310
+ if (status === 'running') {
311
+ transport.notify('session/update', {
312
+ sessionId: params.sessionId,
313
+ update: {
314
+ sessionUpdate: 'tool_call',
315
+ toolCallId,
316
+ status: 'pending',
317
+ rawInput: { toolName },
318
+ },
319
+ });
320
+ }
321
+ else {
322
+ transport.notify('session/update', {
323
+ sessionId: params.sessionId,
324
+ update: {
325
+ sessionUpdate: 'tool_call_update',
326
+ toolCallId,
327
+ status: status === 'finished' ? 'completed' : 'failed',
328
+ rawOutput: '',
329
+ },
330
+ });
331
+ }
197
332
  },
198
333
  onFileEdit: (uri, newText) => {
199
- // ACP structured file/edit notification — lets the editor apply changes
200
334
  transport.notify('file/edit', {
201
335
  uri,
202
336
  textChanges: newText
@@ -230,11 +364,6 @@ export function startAcpServer() {
230
364
  session.abortController = null;
231
365
  });
232
366
  }
233
- function handleSessionCancel(msg) {
234
- const { sessionId } = msg.params;
235
- sessions.get(sessionId)?.abortController?.abort();
236
- transport.respond(msg.id, {});
237
- }
238
367
  // Keep process alive until stdin closes (Zed terminates us)
239
368
  return new Promise((resolve) => {
240
369
  process.stdin.on('end', resolve);
@@ -87,13 +87,23 @@ export async function runAgentSession(opts) {
87
87
  }
88
88
  },
89
89
  onToolResult: (toolResult, toolCall) => {
90
- // Find the tracked entry by tool call id or by matching tool name in insertion order
91
- const mapKey = toolCall.id
92
- ? [...toolCallIdMap.keys()].find(k => k === toolCall.id)
93
- : [...toolCallIdMap.keys()].find(k => k.startsWith(`${toolCall.tool}_`));
94
- if (mapKey) {
90
+ // Find the tracked entry: prefer exact id match, then first FIFO entry for same tool name
91
+ let mapKey;
92
+ if (toolCall.id && toolCallIdMap.has(toolCall.id)) {
93
+ mapKey = toolCall.id;
94
+ }
95
+ else {
96
+ // FIFO: find oldest pending entry for this tool name
97
+ for (const [k, v] of toolCallIdMap) {
98
+ if (k.startsWith(`${toolCall.tool}_`) && v.toolCallId) {
99
+ mapKey = k;
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ if (mapKey !== undefined) {
95
105
  const tracked = toolCallIdMap.get(mapKey);
96
- if (opts.onToolCall) {
106
+ if (tracked && opts.onToolCall) {
97
107
  const status = toolResult.success ? 'finished' : 'error';
98
108
  opts.onToolCall(tracked.toolCallId, toolCall.tool, tracked.kind, '', status, tracked.locations);
99
109
  }
@@ -1,5 +1,5 @@
1
1
  import { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification } from './protocol.js';
2
- type MessageHandler = (msg: JsonRpcRequest) => void;
2
+ type MessageHandler = (msg: JsonRpcRequest | JsonRpcNotification) => void;
3
3
  export declare class StdioTransport {
4
4
  private buffer;
5
5
  private handler;
@@ -1,6 +1,7 @@
1
1
  import Conf from 'conf';
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
+ import { randomUUID } from 'crypto';
4
5
  import { PROVIDERS, getProvider } from './providers.js';
5
6
  import { logSession } from '../utils/logger.js';
6
7
  // We'll initialize GLOBAL_SESSIONS_DIR after config is created (to use config.path)
@@ -389,8 +390,17 @@ export function isConfigured(providerId) {
389
390
  }
390
391
  // Get current provider info
391
392
  export function getCurrentProvider() {
392
- const providerId = config.get('provider');
393
- const provider = getProvider(providerId);
393
+ let providerId = config.get('provider');
394
+ let provider = getProvider(providerId);
395
+ // If stored provider no longer exists in registry, fall back to first available
396
+ if (!provider) {
397
+ const fallback = Object.keys(PROVIDERS)[0];
398
+ if (fallback) {
399
+ providerId = fallback;
400
+ provider = getProvider(fallback);
401
+ config.set('provider', fallback);
402
+ }
403
+ }
394
404
  return {
395
405
  id: providerId,
396
406
  name: provider?.name || providerId,
@@ -425,10 +435,8 @@ export function getModelsForCurrentProvider() {
425
435
  export { PROVIDERS } from './providers.js';
426
436
  // Generate unique session ID
427
437
  function generateSessionId() {
428
- const now = new Date();
429
- const date = now.toISOString().split('T')[0];
430
- const time = now.toTimeString().split(' ')[0].replace(/:/g, '-');
431
- return `session-${date}-${time}`;
438
+ const date = new Date().toISOString().split('T')[0];
439
+ return `session-${date}-${randomUUID().slice(0, 8)}`;
432
440
  }
433
441
  // Get or create current session ID
434
442
  export function getCurrentSessionId() {
@@ -636,7 +644,7 @@ export function getProjectPermission(projectPath) {
636
644
  if (configPath && existsSync(configPath)) {
637
645
  try {
638
646
  const data = JSON.parse(readFileSync(configPath, 'utf-8'));
639
- if (data.permission)
647
+ if (data.permission && typeof data.permission === 'object')
640
648
  return data.permission;
641
649
  }
642
650
  catch {
@@ -124,6 +124,8 @@ export async function runAgent(prompt, projectContext, options = {}) {
124
124
  let finalResponse = '';
125
125
  let result;
126
126
  let consecutiveTimeouts = 0;
127
+ let incompleteWorkRetries = 0;
128
+ const maxIncompleteWorkRetries = 2;
127
129
  const maxTimeoutRetries = 3;
128
130
  const maxConsecutiveTimeouts = 9; // Allow more consecutive timeouts before giving up
129
131
  const baseTimeout = config.get('agentApiTimeout');
@@ -248,9 +250,11 @@ export async function runAgent(prompt, projectContext, options = {}) {
248
250
  const wantsToContinue = continueIndicators.some(indicator => lowerResponse.includes(indicator));
249
251
  // Also check if there were tool call parsing failures in this iteration
250
252
  // by looking for incomplete actions (e.g., write_file without content)
251
- const hasIncompleteWork = iteration < 10 && wantsToContinue && finalResponse.length < 500;
253
+ const hasIncompleteWork = wantsToContinue && finalResponse.length < 500
254
+ && incompleteWorkRetries < maxIncompleteWorkRetries;
252
255
  if (hasIncompleteWork) {
253
256
  debug('Model wants to continue, prompting for next action');
257
+ incompleteWorkRetries++;
254
258
  messages.push({ role: 'assistant', content });
255
259
  messages.push({
256
260
  role: 'user',
@@ -258,6 +262,8 @@ export async function runAgent(prompt, projectContext, options = {}) {
258
262
  });
259
263
  continue;
260
264
  }
265
+ // Reset counter once model produces real output or we give up
266
+ incompleteWorkRetries = 0;
261
267
  // Model is done
262
268
  debug(`Agent finished at iteration ${iteration}`);
263
269
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "1.2.36",
3
+ "version": "1.2.38",
4
4
  "description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",