@ottocode/server 0.1.192 → 0.1.194
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/index.ts +4 -0
- package/src/openapi/paths/git.ts +46 -0
- package/src/routes/auth.ts +21 -0
- package/src/routes/git/index.ts +2 -0
- package/src/routes/git/pull.ts +98 -0
- package/src/routes/git/schemas.ts +4 -0
- package/src/routes/provider-usage.ts +199 -0
- package/src/runtime/agent/runner.ts +67 -4
- package/src/runtime/session/queue.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.194",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@ottocode/sdk": "0.1.
|
|
33
|
-
"@ottocode/database": "0.1.
|
|
32
|
+
"@ottocode/sdk": "0.1.194",
|
|
33
|
+
"@ottocode/database": "0.1.194",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { registerSessionApprovalRoute } from './routes/session-approval.ts';
|
|
|
20
20
|
import { registerSetuRoutes } from './routes/setu.ts';
|
|
21
21
|
import { registerAuthRoutes } from './routes/auth.ts';
|
|
22
22
|
import { registerTunnelRoutes } from './routes/tunnel.ts';
|
|
23
|
+
import { registerProviderUsageRoutes } from './routes/provider-usage.ts';
|
|
23
24
|
import type { AgentConfigEntry } from './runtime/agent/registry.ts';
|
|
24
25
|
|
|
25
26
|
const globalTerminalManager = new TerminalManager();
|
|
@@ -76,6 +77,7 @@ function initApp() {
|
|
|
76
77
|
registerSetuRoutes(app);
|
|
77
78
|
registerAuthRoutes(app);
|
|
78
79
|
registerTunnelRoutes(app);
|
|
80
|
+
registerProviderUsageRoutes(app);
|
|
79
81
|
|
|
80
82
|
return app;
|
|
81
83
|
}
|
|
@@ -148,6 +150,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
|
|
|
148
150
|
registerSetuRoutes(honoApp);
|
|
149
151
|
registerAuthRoutes(honoApp);
|
|
150
152
|
registerTunnelRoutes(honoApp);
|
|
153
|
+
registerProviderUsageRoutes(honoApp);
|
|
151
154
|
|
|
152
155
|
return honoApp;
|
|
153
156
|
}
|
|
@@ -248,6 +251,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
|
|
|
248
251
|
registerSetuRoutes(honoApp);
|
|
249
252
|
registerAuthRoutes(honoApp);
|
|
250
253
|
registerTunnelRoutes(honoApp);
|
|
254
|
+
registerProviderUsageRoutes(honoApp);
|
|
251
255
|
|
|
252
256
|
return honoApp;
|
|
253
257
|
}
|
package/src/openapi/paths/git.ts
CHANGED
|
@@ -350,6 +350,52 @@ export const gitPaths = {
|
|
|
350
350
|
},
|
|
351
351
|
},
|
|
352
352
|
},
|
|
353
|
+
'/v1/git/pull': {
|
|
354
|
+
post: {
|
|
355
|
+
tags: ['git'],
|
|
356
|
+
operationId: 'pullChanges',
|
|
357
|
+
summary: 'Pull changes from remote',
|
|
358
|
+
description: 'Pulls changes from the configured remote repository',
|
|
359
|
+
requestBody: {
|
|
360
|
+
required: false,
|
|
361
|
+
content: {
|
|
362
|
+
'application/json': {
|
|
363
|
+
schema: {
|
|
364
|
+
type: 'object',
|
|
365
|
+
properties: {
|
|
366
|
+
project: { type: 'string' },
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
responses: {
|
|
373
|
+
200: {
|
|
374
|
+
description: 'OK',
|
|
375
|
+
content: {
|
|
376
|
+
'application/json': {
|
|
377
|
+
schema: {
|
|
378
|
+
type: 'object',
|
|
379
|
+
properties: {
|
|
380
|
+
status: { type: 'string', enum: ['ok'] },
|
|
381
|
+
data: {
|
|
382
|
+
type: 'object',
|
|
383
|
+
properties: {
|
|
384
|
+
output: { type: 'string' },
|
|
385
|
+
},
|
|
386
|
+
required: ['output'],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
required: ['status', 'data'],
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
400: gitErrorResponse(),
|
|
395
|
+
500: gitErrorResponse(),
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
353
399
|
'/v1/git/restore': {
|
|
354
400
|
post: {
|
|
355
401
|
tags: ['git'],
|
package/src/routes/auth.ts
CHANGED
|
@@ -161,6 +161,27 @@ export function registerAuthRoutes(app: Hono) {
|
|
|
161
161
|
}
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
app.get('/v1/auth/setu/export', async (c) => {
|
|
165
|
+
try {
|
|
166
|
+
const projectRoot = process.cwd();
|
|
167
|
+
const wallet = await getSetuWallet(projectRoot);
|
|
168
|
+
|
|
169
|
+
if (!wallet) {
|
|
170
|
+
return c.json({ error: 'Setu wallet not configured' }, 404);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return c.json({
|
|
174
|
+
success: true,
|
|
175
|
+
publicKey: wallet.publicKey,
|
|
176
|
+
privateKey: wallet.privateKey,
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
logger.error('Failed to export Setu wallet', error);
|
|
180
|
+
const errorResponse = serializeError(error);
|
|
181
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
164
185
|
app.post('/v1/auth/:provider', async (c) => {
|
|
165
186
|
try {
|
|
166
187
|
const provider = c.req.param('provider') as ProviderId;
|
package/src/routes/git/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { registerDiffRoute } from './diff.ts';
|
|
|
5
5
|
import { registerStagingRoutes } from './staging.ts';
|
|
6
6
|
import { registerCommitRoutes } from './commit.ts';
|
|
7
7
|
import { registerPushRoute } from './push.ts';
|
|
8
|
+
import { registerPullRoute } from './pull.ts';
|
|
8
9
|
|
|
9
10
|
export type { GitFile } from './types.ts';
|
|
10
11
|
|
|
@@ -15,4 +16,5 @@ export function registerGitRoutes(app: Hono) {
|
|
|
15
16
|
registerStagingRoutes(app);
|
|
16
17
|
registerCommitRoutes(app);
|
|
17
18
|
registerPushRoute(app);
|
|
19
|
+
registerPullRoute(app);
|
|
18
20
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { gitPullSchema } from './schemas.ts';
|
|
5
|
+
import { validateAndGetGitRoot } from './utils.ts';
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
export function registerPullRoute(app: Hono) {
|
|
10
|
+
app.post('/v1/git/pull', async (c) => {
|
|
11
|
+
try {
|
|
12
|
+
let body = {};
|
|
13
|
+
try {
|
|
14
|
+
body = await c.req.json();
|
|
15
|
+
} catch {
|
|
16
|
+
body = {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { project } = gitPullSchema.parse(body);
|
|
20
|
+
|
|
21
|
+
const requestedPath = project || process.cwd();
|
|
22
|
+
|
|
23
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
24
|
+
if ('error' in validation) {
|
|
25
|
+
return c.json(
|
|
26
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
27
|
+
400,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { gitRoot } = validation;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const result = await execFileAsync('git', ['pull'], {
|
|
35
|
+
cwd: gitRoot,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return c.json({
|
|
39
|
+
status: 'ok',
|
|
40
|
+
data: {
|
|
41
|
+
output: result.stdout.trim() || result.stderr.trim(),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
} catch (pullErr: unknown) {
|
|
45
|
+
const error = pullErr as {
|
|
46
|
+
message?: string;
|
|
47
|
+
stderr?: string;
|
|
48
|
+
};
|
|
49
|
+
const errorMessage = error.stderr || error.message || 'Failed to pull';
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
errorMessage.includes('CONFLICT') ||
|
|
53
|
+
errorMessage.includes('merge conflict')
|
|
54
|
+
) {
|
|
55
|
+
return c.json(
|
|
56
|
+
{
|
|
57
|
+
status: 'error',
|
|
58
|
+
error: 'Merge conflicts detected. Resolve conflicts manually',
|
|
59
|
+
details: errorMessage,
|
|
60
|
+
},
|
|
61
|
+
400,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
errorMessage.includes('Permission denied') ||
|
|
67
|
+
errorMessage.includes('authentication')
|
|
68
|
+
) {
|
|
69
|
+
return c.json(
|
|
70
|
+
{
|
|
71
|
+
status: 'error',
|
|
72
|
+
error: 'Authentication failed. Check your git credentials',
|
|
73
|
+
details: errorMessage,
|
|
74
|
+
},
|
|
75
|
+
401,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return c.json(
|
|
80
|
+
{
|
|
81
|
+
status: 'error',
|
|
82
|
+
error: 'Failed to pull changes',
|
|
83
|
+
details: errorMessage,
|
|
84
|
+
},
|
|
85
|
+
500,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return c.json(
|
|
90
|
+
{
|
|
91
|
+
status: 'error',
|
|
92
|
+
error: error instanceof Error ? error.message : 'Failed to pull',
|
|
93
|
+
},
|
|
94
|
+
500,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import {
|
|
3
|
+
getAuth,
|
|
4
|
+
refreshToken,
|
|
5
|
+
refreshOpenAIToken,
|
|
6
|
+
type ProviderId,
|
|
7
|
+
} from '@ottocode/sdk';
|
|
8
|
+
import { logger } from '@ottocode/sdk';
|
|
9
|
+
import { setAuth } from '@ottocode/sdk';
|
|
10
|
+
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
11
|
+
import type { OAuth } from '@ottocode/sdk';
|
|
12
|
+
|
|
13
|
+
async function ensureValidOAuth(
|
|
14
|
+
provider: ProviderId,
|
|
15
|
+
): Promise<{ access: string; oauth: OAuth } | null> {
|
|
16
|
+
const projectRoot = process.cwd();
|
|
17
|
+
const auth = await getAuth(provider, projectRoot);
|
|
18
|
+
if (!auth || auth.type !== 'oauth') return null;
|
|
19
|
+
|
|
20
|
+
if (auth.access && auth.expires > Date.now()) {
|
|
21
|
+
return { access: auth.access, oauth: auth };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const refreshFn = provider === 'openai' ? refreshOpenAIToken : refreshToken;
|
|
26
|
+
const newTokens = await refreshFn(auth.refresh);
|
|
27
|
+
const updated: OAuth = {
|
|
28
|
+
...auth,
|
|
29
|
+
access: newTokens.access,
|
|
30
|
+
refresh: newTokens.refresh,
|
|
31
|
+
expires: newTokens.expires,
|
|
32
|
+
};
|
|
33
|
+
await setAuth(provider, updated, projectRoot, 'global');
|
|
34
|
+
return { access: updated.access, oauth: updated };
|
|
35
|
+
} catch {
|
|
36
|
+
return { access: auth.access, oauth: auth };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function fetchAnthropicUsage(access: string) {
|
|
41
|
+
const response = await fetch('https://api.anthropic.com/api/oauth/usage', {
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${access}`,
|
|
44
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
45
|
+
Accept: 'application/json',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`Anthropic usage API returned ${response.status}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = (await response.json()) as {
|
|
54
|
+
five_hour?: { utilization: number; resets_at: string | null };
|
|
55
|
+
seven_day?: { utilization: number; resets_at: string | null };
|
|
56
|
+
seven_day_sonnet?: { utilization: number; resets_at: string | null };
|
|
57
|
+
extra_usage?: {
|
|
58
|
+
is_enabled: boolean;
|
|
59
|
+
monthly_limit: number;
|
|
60
|
+
used_credits: number;
|
|
61
|
+
utilization: number | null;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
provider: 'anthropic' as const,
|
|
67
|
+
primaryWindow: data.five_hour
|
|
68
|
+
? {
|
|
69
|
+
usedPercent: data.five_hour.utilization,
|
|
70
|
+
windowSeconds: 18000,
|
|
71
|
+
resetsAt: data.five_hour.resets_at,
|
|
72
|
+
}
|
|
73
|
+
: null,
|
|
74
|
+
secondaryWindow: data.seven_day
|
|
75
|
+
? {
|
|
76
|
+
usedPercent: data.seven_day.utilization,
|
|
77
|
+
windowSeconds: 604800,
|
|
78
|
+
resetsAt: data.seven_day.resets_at,
|
|
79
|
+
}
|
|
80
|
+
: null,
|
|
81
|
+
sonnetWindow: data.seven_day_sonnet
|
|
82
|
+
? {
|
|
83
|
+
usedPercent: data.seven_day_sonnet.utilization,
|
|
84
|
+
resetsAt: data.seven_day_sonnet.resets_at,
|
|
85
|
+
}
|
|
86
|
+
: null,
|
|
87
|
+
extraUsage: data.extra_usage ?? null,
|
|
88
|
+
limitReached: (data.five_hour?.utilization ?? 0) >= 100,
|
|
89
|
+
raw: data,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function fetchOpenAIUsage(access: string, accountId?: string) {
|
|
94
|
+
const headers: Record<string, string> = {
|
|
95
|
+
Authorization: `Bearer ${access}`,
|
|
96
|
+
Accept: '*/*',
|
|
97
|
+
};
|
|
98
|
+
if (accountId) {
|
|
99
|
+
headers['ChatGPT-Account-Id'] = accountId;
|
|
100
|
+
}
|
|
101
|
+
const response = await fetch('https://chatgpt.com/backend-api/wham/usage', {
|
|
102
|
+
headers,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`OpenAI usage API returned ${response.status}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const data = (await response.json()) as {
|
|
110
|
+
plan_type?: string;
|
|
111
|
+
rate_limit?: {
|
|
112
|
+
allowed: boolean;
|
|
113
|
+
limit_reached: boolean;
|
|
114
|
+
primary_window?: {
|
|
115
|
+
used_percent: number;
|
|
116
|
+
limit_window_seconds: number;
|
|
117
|
+
reset_after_seconds: number;
|
|
118
|
+
reset_at: number;
|
|
119
|
+
};
|
|
120
|
+
secondary_window?: {
|
|
121
|
+
used_percent: number;
|
|
122
|
+
limit_window_seconds: number;
|
|
123
|
+
reset_after_seconds: number;
|
|
124
|
+
reset_at: number;
|
|
125
|
+
} | null;
|
|
126
|
+
};
|
|
127
|
+
credits?: {
|
|
128
|
+
has_credits: boolean;
|
|
129
|
+
balance: number | null;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const rl = data.rate_limit;
|
|
134
|
+
return {
|
|
135
|
+
provider: 'openai' as const,
|
|
136
|
+
planType: data.plan_type ?? null,
|
|
137
|
+
primaryWindow: rl?.primary_window
|
|
138
|
+
? {
|
|
139
|
+
usedPercent: rl.primary_window.used_percent,
|
|
140
|
+
windowSeconds: rl.primary_window.limit_window_seconds,
|
|
141
|
+
resetsAt: new Date(rl.primary_window.reset_at * 1000).toISOString(),
|
|
142
|
+
resetAfterSeconds: rl.primary_window.reset_after_seconds,
|
|
143
|
+
}
|
|
144
|
+
: null,
|
|
145
|
+
secondaryWindow: rl?.secondary_window
|
|
146
|
+
? {
|
|
147
|
+
usedPercent: rl.secondary_window.used_percent,
|
|
148
|
+
windowSeconds: rl.secondary_window.limit_window_seconds,
|
|
149
|
+
resetsAt: new Date(rl.secondary_window.reset_at * 1000).toISOString(),
|
|
150
|
+
resetAfterSeconds: rl.secondary_window.reset_after_seconds,
|
|
151
|
+
}
|
|
152
|
+
: null,
|
|
153
|
+
credits: data.credits ?? null,
|
|
154
|
+
limitReached: rl?.limit_reached ?? false,
|
|
155
|
+
raw: data,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function registerProviderUsageRoutes(app: Hono) {
|
|
160
|
+
app.get('/v1/provider-usage/:provider', async (c) => {
|
|
161
|
+
try {
|
|
162
|
+
const provider = c.req.param('provider') as ProviderId;
|
|
163
|
+
|
|
164
|
+
if (provider !== 'anthropic' && provider !== 'openai') {
|
|
165
|
+
return c.json(
|
|
166
|
+
{ error: { message: 'Usage not supported for this provider' } },
|
|
167
|
+
400,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const tokenResult = await ensureValidOAuth(provider);
|
|
172
|
+
if (!tokenResult) {
|
|
173
|
+
return c.json(
|
|
174
|
+
{
|
|
175
|
+
error: {
|
|
176
|
+
message: `No OAuth credentials for ${provider}. Usage is only available for OAuth-authenticated providers.`,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
404,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const usage =
|
|
184
|
+
provider === 'anthropic'
|
|
185
|
+
? await fetchAnthropicUsage(tokenResult.access)
|
|
186
|
+
: await fetchOpenAIUsage(
|
|
187
|
+
tokenResult.access,
|
|
188
|
+
tokenResult.oauth.accountId,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
return c.json(usage);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger.error('Failed to fetch provider usage', error);
|
|
194
|
+
const errorResponse = serializeError(error);
|
|
195
|
+
const status = (errorResponse.error.status || 500) as 500;
|
|
196
|
+
return c.json(errorResponse, status);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { hasToolCall, streamText } from 'ai';
|
|
2
|
-
import { messageParts } from '@ottocode/database/schema';
|
|
2
|
+
import { messages, messageParts } 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';
|
|
6
6
|
import { toErrorPayload } from '../errors/handling.ts';
|
|
7
7
|
import {
|
|
8
8
|
type RunOpts,
|
|
9
|
+
enqueueAssistantRun,
|
|
9
10
|
setRunning,
|
|
10
11
|
dequeueJob,
|
|
11
12
|
cleanupSession,
|
|
@@ -293,13 +294,75 @@ async function runAssistant(opts: RunOpts) {
|
|
|
293
294
|
await cleanupEmptyTextParts(opts, db);
|
|
294
295
|
firstToolTimer.end({ seen: firstToolSeen() });
|
|
295
296
|
|
|
297
|
+
let streamFinishReason: string | undefined;
|
|
298
|
+
try {
|
|
299
|
+
streamFinishReason = await result.finishReason;
|
|
300
|
+
} catch {
|
|
301
|
+
streamFinishReason = undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
296
304
|
debugLog(
|
|
297
|
-
`[RUNNER] Stream finished. finishSeen=${_finishObserved}, firstToolSeen=${fs}`,
|
|
305
|
+
`[RUNNER] Stream finished. finishSeen=${_finishObserved}, firstToolSeen=${fs}, finishReason=${streamFinishReason}`,
|
|
298
306
|
);
|
|
299
307
|
|
|
300
|
-
|
|
308
|
+
const wasTruncated = streamFinishReason === 'length';
|
|
309
|
+
|
|
310
|
+
const shouldContinue =
|
|
311
|
+
opts.provider === 'openai' &&
|
|
312
|
+
isOpenAIOAuth &&
|
|
313
|
+
!_finishObserved &&
|
|
314
|
+
(wasTruncated || fs);
|
|
315
|
+
|
|
316
|
+
if (shouldContinue) {
|
|
317
|
+
debugLog(
|
|
318
|
+
`[RUNNER] WARNING: Stream ended without finish. finishReason=${streamFinishReason}, firstToolSeen=${fs}. Auto-continuing.`,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const MAX_CONTINUATIONS = 10;
|
|
322
|
+
const count = opts.continuationCount ?? 0;
|
|
323
|
+
if (count < MAX_CONTINUATIONS) {
|
|
324
|
+
debugLog(
|
|
325
|
+
`[RUNNER] Auto-continuing (${count + 1}/${MAX_CONTINUATIONS})...`,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
await completeAssistantMessage({}, opts, db);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
debugLog(
|
|
332
|
+
`[RUNNER] completeAssistantMessage failed before continuation: ${err instanceof Error ? err.message : String(err)}`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const continuationMessageId = crypto.randomUUID();
|
|
337
|
+
await db.insert(messages).values({
|
|
338
|
+
id: continuationMessageId,
|
|
339
|
+
sessionId: opts.sessionId,
|
|
340
|
+
role: 'assistant',
|
|
341
|
+
status: 'pending',
|
|
342
|
+
agent: opts.agent,
|
|
343
|
+
provider: opts.provider,
|
|
344
|
+
model: opts.model,
|
|
345
|
+
createdAt: Date.now(),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
publish({
|
|
349
|
+
type: 'message.created',
|
|
350
|
+
sessionId: opts.sessionId,
|
|
351
|
+
payload: { id: continuationMessageId, role: 'assistant' },
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
enqueueAssistantRun(
|
|
355
|
+
{
|
|
356
|
+
...opts,
|
|
357
|
+
assistantMessageId: continuationMessageId,
|
|
358
|
+
continuationCount: count + 1,
|
|
359
|
+
},
|
|
360
|
+
runSessionLoop,
|
|
361
|
+
);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
301
364
|
debugLog(
|
|
302
|
-
`[RUNNER]
|
|
365
|
+
`[RUNNER] Max continuations (${MAX_CONTINUATIONS}) reached, stopping.`,
|
|
303
366
|
);
|
|
304
367
|
}
|
|
305
368
|
} catch (err) {
|