@ottocode/server 0.1.210 → 0.1.211

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/server",
3
- "version": "0.1.210",
3
+ "version": "0.1.211",
4
4
  "description": "HTTP API server for ottocode",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -49,8 +49,8 @@
49
49
  "typecheck": "tsc --noEmit"
50
50
  },
51
51
  "dependencies": {
52
- "@ottocode/sdk": "0.1.210",
53
- "@ottocode/database": "0.1.210",
52
+ "@ottocode/sdk": "0.1.211",
53
+ "@ottocode/database": "0.1.211",
54
54
  "drizzle-orm": "^0.44.5",
55
55
  "hono": "^4.9.9",
56
56
  "zod": "^4.1.8"
@@ -13,6 +13,7 @@ export type OttoEventType =
13
13
  | 'session.created'
14
14
  | 'session.updated'
15
15
  | 'message.created'
16
+ | 'message.updated'
16
17
  | 'message.part.delta'
17
18
  | 'reasoning.delta'
18
19
  | 'message.completed'
@@ -48,11 +48,15 @@ function shouldExcludeDir(name: string): boolean {
48
48
  async function listFilesWithRg(
49
49
  projectRoot: string,
50
50
  limit: number,
51
+ includeIgnored = false,
51
52
  ): Promise<{ files: string[]; truncated: boolean }> {
52
53
  const rgBin = await resolveBinary('rg');
53
54
 
54
55
  return new Promise((resolve) => {
55
56
  const args = ['--files', '--hidden', '--glob', '!.git/', '--sort', 'path'];
57
+ if (includeIgnored) {
58
+ args.push('--no-ignore');
59
+ }
56
60
 
57
61
  const proc = spawn(rgBin, args, { cwd: projectRoot });
58
62
  let stdout = '';
@@ -232,6 +236,34 @@ async function getChangedFiles(
232
236
  }
233
237
  }
234
238
 
239
+ async function getGitIgnoredFiles(
240
+ projectRoot: string,
241
+ files: string[],
242
+ ): Promise<Set<string>> {
243
+ if (files.length === 0) return new Set();
244
+ try {
245
+ return new Promise((resolve) => {
246
+ const proc = spawn('git', ['check-ignore', '--stdin'], {
247
+ cwd: projectRoot,
248
+ });
249
+ let stdout = '';
250
+ proc.stdout.on('data', (data) => {
251
+ stdout += data.toString();
252
+ });
253
+ proc.on('close', () => {
254
+ resolve(new Set(stdout.split('\n').filter(Boolean)));
255
+ });
256
+ proc.on('error', () => {
257
+ resolve(new Set());
258
+ });
259
+ proc.stdin.write(files.join('\n'));
260
+ proc.stdin.end();
261
+ });
262
+ } catch (_err) {
263
+ return new Set();
264
+ }
265
+ }
266
+
235
267
  export function registerFilesRoutes(app: Hono) {
236
268
  app.get('/v1/files', async (c) => {
237
269
  try {
@@ -239,7 +271,7 @@ export function registerFilesRoutes(app: Hono) {
239
271
  const maxDepth = Number.parseInt(c.req.query('maxDepth') || '10', 10);
240
272
  const limit = Number.parseInt(c.req.query('limit') || '1000', 10);
241
273
 
242
- let result = await listFilesWithRg(projectRoot, limit);
274
+ let result = await listFilesWithRg(projectRoot, limit, true);
243
275
 
244
276
  if (result.files.length === 0) {
245
277
  const gitignorePatterns = await parseGitignore(projectRoot);
@@ -254,9 +286,15 @@ export function registerFilesRoutes(app: Hono) {
254
286
  );
255
287
  }
256
288
 
257
- const changedFiles = await getChangedFiles(projectRoot);
289
+ const [changedFiles, ignoredFiles] = await Promise.all([
290
+ getChangedFiles(projectRoot),
291
+ getGitIgnoredFiles(projectRoot, result.files),
292
+ ]);
258
293
 
259
294
  result.files.sort((a, b) => {
295
+ const aIgnored = ignoredFiles.has(a);
296
+ const bIgnored = ignoredFiles.has(b);
297
+ if (aIgnored !== bIgnored) return aIgnored ? 1 : -1;
260
298
  const aChanged = changedFiles.has(a);
261
299
  const bChanged = changedFiles.has(b);
262
300
  if (aChanged && !bChanged) return -1;
@@ -266,6 +304,7 @@ export function registerFilesRoutes(app: Hono) {
266
304
 
267
305
  return c.json({
268
306
  files: result.files,
307
+ ignoredFiles: Array.from(ignoredFiles),
269
308
  changedFiles: Array.from(changedFiles.entries()).map(
270
309
  ([path, status]) => ({
271
310
  path,
@@ -293,6 +332,7 @@ export function registerFilesRoutes(app: Hono) {
293
332
  name: string;
294
333
  path: string;
295
334
  type: 'file' | 'directory';
335
+ gitignored?: boolean;
296
336
  }> = [];
297
337
 
298
338
  for (const entry of entries) {
@@ -301,17 +341,30 @@ export function registerFilesRoutes(app: Hono) {
301
341
 
302
342
  if (entry.isDirectory()) {
303
343
  if (shouldExcludeDir(entry.name)) continue;
304
- if (matchesGitignorePattern(relPath, gitignorePatterns)) continue;
305
- items.push({ name: entry.name, path: relPath, type: 'directory' });
344
+ const ignored = matchesGitignorePattern(relPath, gitignorePatterns);
345
+ items.push({
346
+ name: entry.name,
347
+ path: relPath,
348
+ type: 'directory',
349
+ gitignored: ignored || undefined,
350
+ });
306
351
  } else if (entry.isFile()) {
307
352
  if (shouldExcludeFile(entry.name)) continue;
308
- if (matchesGitignorePattern(relPath, gitignorePatterns)) continue;
309
- items.push({ name: entry.name, path: relPath, type: 'file' });
353
+ const ignored = matchesGitignorePattern(relPath, gitignorePatterns);
354
+ items.push({
355
+ name: entry.name,
356
+ path: relPath,
357
+ type: 'file',
358
+ gitignored: ignored || undefined,
359
+ });
310
360
  }
311
361
  }
312
362
 
313
363
  items.sort((a, b) => {
314
364
  if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
365
+ const aIgnored = a.gitignored ?? false;
366
+ const bIgnored = b.gitignored ?? false;
367
+ if (aIgnored !== bIgnored) return aIgnored ? 1 : -1;
315
368
  return a.name.localeCompare(b.name);
316
369
  });
317
370
 
@@ -15,6 +15,7 @@ import {
15
15
  detectOAuth,
16
16
  adaptSimpleCall,
17
17
  } from '../../runtime/provider/oauth-adapter.ts';
18
+ import { appendCoAuthorTrailer } from '@ottocode/sdk';
18
19
 
19
20
  const execFileAsync = promisify(execFile);
20
21
 
@@ -36,9 +37,14 @@ export function registerCommitRoutes(app: Hono) {
36
37
 
37
38
  const { gitRoot } = validation;
38
39
 
39
- const { stdout } = await execFileAsync('git', ['commit', '-m', message], {
40
- cwd: gitRoot,
41
- });
40
+ const fullMessage = appendCoAuthorTrailer(message);
41
+ const { stdout } = await execFileAsync(
42
+ 'git',
43
+ ['commit', '-m', fullMessage],
44
+ {
45
+ cwd: gitRoot,
46
+ },
47
+ );
42
48
 
43
49
  return c.json({
44
50
  status: 'ok',
@@ -383,7 +383,18 @@ export function registerSessionsRoutes(app: Hono) {
383
383
  // Delete message parts first (foreign key constraint)
384
384
  await db
385
385
  .delete(messageParts)
386
- .where(eq(messageParts.messageId, messageId));
386
+ .where(
387
+ and(
388
+ eq(messageParts.messageId, messageId),
389
+ or(
390
+ eq(messageParts.type, 'error'),
391
+ and(
392
+ eq(messageParts.type, 'tool_call'),
393
+ eq(messageParts.toolName, 'finish'),
394
+ ),
395
+ ),
396
+ ),
397
+ );
387
398
  // Delete message
388
399
  await db.delete(messages).where(eq(messages.id, messageId));
389
400
 
@@ -758,7 +769,6 @@ export function registerSessionsRoutes(app: Hono) {
758
769
  return c.json({ error: 'Session not found' }, 404);
759
770
  }
760
771
 
761
- // Delete only error parts - preserve valid text/tool content
762
772
  await db
763
773
  .delete(messageParts)
764
774
  .where(
@@ -63,7 +63,11 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
63
63
  debugLog('[RUNNER] Using minimal history for /compact command');
64
64
  history = [];
65
65
  } else {
66
- history = await buildHistoryMessages(db, opts.sessionId);
66
+ history = await buildHistoryMessages(
67
+ db,
68
+ opts.sessionId,
69
+ opts.assistantMessageId,
70
+ );
67
71
  }
68
72
  historyTimer.end({ messages: history.length });
69
73
 
@@ -454,7 +454,13 @@ async function runAssistant(opts: RunOpts) {
454
454
  publish({
455
455
  type: 'message.created',
456
456
  sessionId: opts.sessionId,
457
- payload: { id: continuationMessageId, role: 'assistant' },
457
+ payload: {
458
+ id: continuationMessageId,
459
+ role: 'assistant',
460
+ agent: opts.agent,
461
+ provider: opts.provider,
462
+ model: opts.model,
463
+ },
458
464
  });
459
465
 
460
466
  enqueueAssistantRun(
@@ -12,6 +12,7 @@ import { ToolHistoryTracker } from './tool-history-tracker.ts';
12
12
  export async function buildHistoryMessages(
13
13
  db: Awaited<ReturnType<typeof getDb>>,
14
14
  sessionId: string,
15
+ currentMessageId?: string,
15
16
  ): Promise<ModelMessage[]> {
16
17
  const rows = await db
17
18
  .select()
@@ -26,10 +27,12 @@ export async function buildHistoryMessages(
26
27
  if (
27
28
  m.role === 'assistant' &&
28
29
  m.status !== 'complete' &&
29
- m.status !== 'completed'
30
+ m.status !== 'completed' &&
31
+ m.status !== 'error' &&
32
+ m.id !== currentMessageId
30
33
  ) {
31
34
  debugLog(
32
- `[buildHistoryMessages] Skipping assistant message ${m.id} with status ${m.status} (current turn still in progress)`,
35
+ `[buildHistoryMessages] Skipping assistant message ${m.id} with status ${m.status}`,
33
36
  );
34
37
  logPendingToolParts(db, m.id);
35
38
  continue;
@@ -124,7 +124,7 @@ export async function dispatchAssistantMessage(
124
124
  publish({
125
125
  type: 'message.created',
126
126
  sessionId,
127
- payload: { id: userMessageId, role: 'user' },
127
+ payload: { id: userMessageId, role: 'user', agent, provider, model },
128
128
  });
129
129
 
130
130
  const assistantMessageId = crypto.randomUUID();
@@ -141,7 +141,13 @@ export async function dispatchAssistantMessage(
141
141
  publish({
142
142
  type: 'message.created',
143
143
  sessionId,
144
- payload: { id: assistantMessageId, role: 'assistant' },
144
+ payload: {
145
+ id: assistantMessageId,
146
+ role: 'assistant',
147
+ agent,
148
+ provider,
149
+ model,
150
+ },
145
151
  });
146
152
 
147
153
  debugLog(
@@ -216,7 +216,13 @@ export function createErrorHandler(
216
216
  publish({
217
217
  type: 'message.created',
218
218
  sessionId: opts.sessionId,
219
- payload: { id: compactMessageId, role: 'assistant' },
219
+ payload: {
220
+ id: compactMessageId,
221
+ role: 'assistant',
222
+ agent: opts.agent,
223
+ provider: opts.provider,
224
+ model: opts.model,
225
+ },
220
226
  });
221
227
 
222
228
  let compactionSucceeded = false;
@@ -288,7 +294,13 @@ export function createErrorHandler(
288
294
  publish({
289
295
  type: 'message.created',
290
296
  sessionId: opts.sessionId,
291
- payload: { id: retryMessageId, role: 'assistant' },
297
+ payload: {
298
+ id: retryMessageId,
299
+ role: 'assistant',
300
+ agent: opts.agent,
301
+ provider: opts.provider,
302
+ model: opts.model,
303
+ },
292
304
  });
293
305
 
294
306
  enqueueAssistantRun(
@@ -1,4 +1,6 @@
1
1
  import type { getDb } from '@ottocode/database';
2
+ import { messageParts } from '@ottocode/database/schema';
3
+ import { eq, desc } from 'drizzle-orm';
2
4
  import { time } from '../debug/index.ts';
3
5
  import type { ToolAdapterContext } from '../../tools/adapter.ts';
4
6
  import type { RunOpts } from '../session/queue.ts';
@@ -16,8 +18,13 @@ export async function setupToolContext(
16
18
  const firstToolTimer = time('runner:first-tool-call');
17
19
  let firstToolSeen = false;
18
20
 
19
- // Simple counter starting at 0 - first event gets 0, second gets 1, etc.
20
- let currentIndex = 0;
21
+ const existingParts = await db
22
+ .select({ index: messageParts.index })
23
+ .from(messageParts)
24
+ .where(eq(messageParts.messageId, opts.assistantMessageId))
25
+ .orderBy(desc(messageParts.index))
26
+ .limit(1);
27
+ let currentIndex = existingParts.length > 0 ? existingParts[0].index + 1 : 0;
21
28
  const nextIndex = () => currentIndex++;
22
29
 
23
30
  const sharedCtx: RunnerToolContext = {