@ottocode/server 0.1.220 → 0.1.222

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.220",
3
+ "version": "0.1.222",
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.220",
53
- "@ottocode/database": "0.1.220",
52
+ "@ottocode/sdk": "0.1.222",
53
+ "@ottocode/database": "0.1.222",
54
54
  "drizzle-orm": "^0.44.5",
55
55
  "hono": "^4.9.9",
56
56
  "zod": "^4.3.6"
@@ -78,6 +78,31 @@ export const sessionsPaths = {
78
78
  },
79
79
  },
80
80
  '/v1/sessions/{sessionId}': {
81
+ get: {
82
+ tags: ['sessions'],
83
+ operationId: 'getSession',
84
+ summary: 'Get a single session by ID',
85
+ parameters: [
86
+ {
87
+ in: 'path',
88
+ name: 'sessionId',
89
+ required: true,
90
+ schema: { type: 'string' },
91
+ },
92
+ projectQueryParam(),
93
+ ],
94
+ responses: {
95
+ 200: {
96
+ description: 'OK',
97
+ content: {
98
+ 'application/json': {
99
+ schema: { $ref: '#/components/schemas/Session' },
100
+ },
101
+ },
102
+ },
103
+ 404: errorResponse(),
104
+ },
105
+ },
81
106
  patch: {
82
107
  tags: ['sessions'],
83
108
  operationId: 'updateSession',
@@ -66,6 +66,7 @@ export const schemas = {
66
66
  totalOutputTokens: { type: 'integer', nullable: true },
67
67
  totalCachedTokens: { type: 'integer', nullable: true },
68
68
  totalCacheCreationTokens: { type: 'integer', nullable: true },
69
+ currentContextTokens: { type: 'integer', nullable: true },
69
70
  totalToolTimeMs: { type: 'integer', nullable: true },
70
71
  toolCounts: {
71
72
  type: 'object',
@@ -104,6 +104,43 @@ export function registerSessionsRoutes(app: Hono) {
104
104
  }
105
105
  });
106
106
 
107
+ // Get single session
108
+ app.get('/v1/sessions/:sessionId', async (c) => {
109
+ try {
110
+ const sessionId = c.req.param('sessionId');
111
+ const projectRoot = c.req.query('project') || process.cwd();
112
+ const cfg = await loadConfig(projectRoot);
113
+ const db = await getDb(cfg.projectRoot);
114
+ const rows = await db
115
+ .select()
116
+ .from(sessions)
117
+ .where(eq(sessions.id, sessionId))
118
+ .limit(1);
119
+ if (!rows.length) {
120
+ return c.json(
121
+ { error: { message: 'Session not found', status: 404 } },
122
+ 404,
123
+ );
124
+ }
125
+ const r = rows[0];
126
+ let counts: Record<string, unknown> | undefined;
127
+ if (r.toolCountsJson) {
128
+ try {
129
+ const parsed = JSON.parse(r.toolCountsJson);
130
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
131
+ counts = parsed as Record<string, unknown>;
132
+ }
133
+ } catch {}
134
+ }
135
+ const { toolCountsJson: _toolCountsJson, ...rest } = r;
136
+ return c.json(counts ? { ...rest, toolCounts: counts } : rest);
137
+ } catch (err) {
138
+ logger.error('Failed to get session', err);
139
+ const errorResponse = serializeError(err);
140
+ return c.json(errorResponse, errorResponse.error.status || 500);
141
+ }
142
+ });
143
+
107
144
  // Update session preferences
108
145
  app.patch('/v1/sessions/:sessionId', async (c) => {
109
146
  try {
@@ -24,10 +24,6 @@ const INTERMEDIATE_PROGRESS_PATTERNS: RegExp[] = [
24
24
  /\b(and|then)\s+continue\b/i,
25
25
  ];
26
26
 
27
- /**
28
- * Detects whether assistant text looks like an intermediate progress update
29
- * (e.g. "Next I'll inspect...") rather than a final user-facing completion.
30
- */
31
27
  export function looksLikeIntermediateProgressText(text: string): boolean {
32
28
  const trimmed = text.trim();
33
29
  if (!trimmed) return false;
@@ -44,11 +40,15 @@ function isTruncatedResponse(
44
40
  return rawFinishReason === 'max_output_tokens';
45
41
  }
46
42
 
47
- /**
48
- * Decides whether an OpenAI OAuth Codex turn should auto-continue to recover
49
- * only from hard truncation. Other completion behavior is handled by
50
- * stream step limits and prompt alignment, not synthetic continuation turns.
51
- */
43
+ const MAX_UNCLEAN_EOF_RETRIES = 1;
44
+
45
+ function isUncleanEof(input: OauthCodexContinuationInput): boolean {
46
+ if (input.finishReason && input.finishReason !== 'unknown') return false;
47
+ if (input.firstToolSeen) return true;
48
+ if (looksLikeIntermediateProgressText(input.lastAssistantText)) return true;
49
+ return false;
50
+ }
51
+
52
52
  export function decideOauthCodexContinuation(
53
53
  input: OauthCodexContinuationInput,
54
54
  ): OauthCodexContinuationDecision {
@@ -68,5 +68,12 @@ export function decideOauthCodexContinuation(
68
68
  return { shouldContinue: true, reason: 'truncated' };
69
69
  }
70
70
 
71
+ if (
72
+ isUncleanEof(input) &&
73
+ input.continuationCount < MAX_UNCLEAN_EOF_RETRIES
74
+ ) {
75
+ return { shouldContinue: true, reason: 'unclean-eof' };
76
+ }
77
+
71
78
  return { shouldContinue: false };
72
79
  }
@@ -1,5 +1,5 @@
1
1
  import { hasToolCall, stepCountIs, streamText } from 'ai';
2
- import { messages, messageParts } from '@ottocode/database/schema';
2
+ import { messages, messageParts, sessions } from '@ottocode/database/schema';
3
3
  import { eq } from 'drizzle-orm';
4
4
  import { publish, subscribe } from '../../events/bus.ts';
5
5
  import { debugLog, time } from '../debug/index.ts';
@@ -243,7 +243,7 @@ async function runAssistant(opts: RunOpts) {
243
243
 
244
244
  const onFinish = createFinishHandler(opts, db, completeAssistantMessage);
245
245
  const stopWhenCondition = isOpenAIOAuth
246
- ? stepCountIs(48)
246
+ ? stepCountIs(20)
247
247
  : hasToolCall('finish');
248
248
 
249
249
  try {
@@ -423,55 +423,68 @@ async function runAssistant(opts: RunOpts) {
423
423
  });
424
424
 
425
425
  if (continuationDecision.shouldContinue) {
426
- debugLog(
427
- `[RUNNER] WARNING: Stream ended without finish. reason=${continuationDecision.reason ?? 'unknown'}, finishReason=${streamFinishReason}, rawFinishReason=${streamRawFinishReason}, firstToolSeen=${fs}. Auto-continuing.`,
428
- );
429
-
430
- debugLog(
431
- `[RUNNER] Auto-continuing (${continuationCount + 1}/${MAX_CONTINUATIONS})...`,
432
- );
426
+ const sessRows = await db
427
+ .select()
428
+ .from(sessions)
429
+ .where(eq(sessions.id, opts.sessionId))
430
+ .limit(1);
431
+ const sessionInputTokens = Number(sessRows[0]?.totalInputTokens ?? 0);
432
+ const MAX_SESSION_INPUT_TOKENS = 800_000;
433
+ if (sessionInputTokens > MAX_SESSION_INPUT_TOKENS) {
434
+ debugLog(
435
+ `[RUNNER] Token budget exceeded (${sessionInputTokens} > ${MAX_SESSION_INPUT_TOKENS}), stopping continuation.`,
436
+ );
437
+ } else {
438
+ debugLog(
439
+ `[RUNNER] WARNING: Stream ended without finish. reason=${continuationDecision.reason ?? 'unknown'}, finishReason=${streamFinishReason}, rawFinishReason=${streamRawFinishReason}, firstToolSeen=${fs}. Auto-continuing.`,
440
+ );
433
441
 
434
- try {
435
- await completeAssistantMessage({}, opts, db);
436
- } catch (err) {
437
442
  debugLog(
438
- `[RUNNER] completeAssistantMessage failed before continuation: ${err instanceof Error ? err.message : String(err)}`,
443
+ `[RUNNER] Auto-continuing (${continuationCount + 1}/${MAX_CONTINUATIONS})...`,
439
444
  );
440
- }
441
445
 
442
- const continuationMessageId = crypto.randomUUID();
443
- await db.insert(messages).values({
444
- id: continuationMessageId,
445
- sessionId: opts.sessionId,
446
- role: 'assistant',
447
- status: 'pending',
448
- agent: opts.agent,
449
- provider: opts.provider,
450
- model: opts.model,
451
- createdAt: Date.now(),
452
- });
446
+ try {
447
+ await completeAssistantMessage({}, opts, db);
448
+ } catch (err) {
449
+ debugLog(
450
+ `[RUNNER] completeAssistantMessage failed before continuation: ${err instanceof Error ? err.message : String(err)}`,
451
+ );
452
+ }
453
453
 
454
- publish({
455
- type: 'message.created',
456
- sessionId: opts.sessionId,
457
- payload: {
454
+ const continuationMessageId = crypto.randomUUID();
455
+ await db.insert(messages).values({
458
456
  id: continuationMessageId,
457
+ sessionId: opts.sessionId,
459
458
  role: 'assistant',
459
+ status: 'pending',
460
460
  agent: opts.agent,
461
461
  provider: opts.provider,
462
462
  model: opts.model,
463
- },
464
- });
463
+ createdAt: Date.now(),
464
+ });
465
465
 
466
- enqueueAssistantRun(
467
- {
468
- ...opts,
469
- assistantMessageId: continuationMessageId,
470
- continuationCount: continuationCount + 1,
471
- },
472
- runSessionLoop,
473
- );
474
- return;
466
+ publish({
467
+ type: 'message.created',
468
+ sessionId: opts.sessionId,
469
+ payload: {
470
+ id: continuationMessageId,
471
+ role: 'assistant',
472
+ agent: opts.agent,
473
+ provider: opts.provider,
474
+ model: opts.model,
475
+ },
476
+ });
477
+
478
+ enqueueAssistantRun(
479
+ {
480
+ ...opts,
481
+ assistantMessageId: continuationMessageId,
482
+ continuationCount: continuationCount + 1,
483
+ },
484
+ runSessionLoop,
485
+ );
486
+ return;
487
+ }
475
488
  }
476
489
  if (
477
490
  continuationDecision.reason === 'max-continuations-reached' &&
@@ -25,7 +25,7 @@ export async function resolveModel(
25
25
  },
26
26
  ) {
27
27
  if (provider === 'openai') {
28
- return resolveOpenAIModel(model, cfg);
28
+ return resolveOpenAIModel(model, cfg, options?.sessionId);
29
29
  }
30
30
  if (provider === 'anthropic') {
31
31
  const instance = await getAnthropicInstance(cfg);
@@ -2,12 +2,17 @@ import type { OttoConfig } from '@ottocode/sdk';
2
2
  import { getAuth, createOpenAIOAuthModel } from '@ottocode/sdk';
3
3
  import { openai, createOpenAI } from '@ai-sdk/openai';
4
4
 
5
- export async function resolveOpenAIModel(model: string, cfg: OttoConfig) {
5
+ export async function resolveOpenAIModel(
6
+ model: string,
7
+ cfg: OttoConfig,
8
+ sessionId?: string,
9
+ ) {
6
10
  const auth = await getAuth('openai', cfg.projectRoot);
7
11
  if (auth?.type === 'oauth') {
8
12
  return createOpenAIOAuthModel(model, {
9
13
  oauth: auth,
10
14
  projectRoot: cfg.projectRoot,
15
+ sessionId,
11
16
  });
12
17
  }
13
18
  if (auth?.type === 'api' && auth.key) {