@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 +3 -3
- package/src/events/types.ts +1 -0
- package/src/routes/files.ts +59 -6
- package/src/routes/git/commit.ts +9 -3
- package/src/routes/sessions.ts +12 -2
- package/src/runtime/agent/runner-setup.ts +5 -1
- package/src/runtime/agent/runner.ts +7 -1
- package/src/runtime/message/history-builder.ts +5 -2
- package/src/runtime/message/service.ts +8 -2
- package/src/runtime/stream/error-handler.ts +14 -2
- package/src/runtime/tools/setup.ts +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
53
|
-
"@ottocode/database": "0.1.
|
|
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"
|
package/src/events/types.ts
CHANGED
package/src/routes/files.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
305
|
-
items.push({
|
|
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
|
-
|
|
309
|
-
items.push({
|
|
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
|
|
package/src/routes/git/commit.ts
CHANGED
|
@@ -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
|
|
40
|
-
|
|
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',
|
package/src/routes/sessions.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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: {
|
|
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}
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
20
|
-
|
|
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 = {
|