@ottocode/server 0.1.267 → 0.1.269
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/bus.ts +28 -1
- package/src/events/types.ts +31 -0
- package/src/index.ts +24 -3
- package/src/routes/client-events.ts +139 -0
- package/src/routes/terminals/service.ts +5 -1
- package/src/runtime/agent/runner-reminders.ts +2 -2
- package/src/runtime/agent/runner-setup-prompt.ts +10 -0
- package/src/runtime/agent/runner-setup.ts +4 -0
- package/src/runtime/session/queue.ts +21 -1
- package/src/runtime/stream/error-handler.ts +24 -1
- package/src/runtime/stream/finish-handler.ts +28 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.269",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"typecheck": "tsc --noEmit"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@ottocode/database": "0.1.
|
|
65
|
-
"@ottocode/sdk": "0.1.
|
|
64
|
+
"@ottocode/database": "0.1.269",
|
|
65
|
+
"@ottocode/sdk": "0.1.269",
|
|
66
66
|
"@hono/zod-openapi": "^1.1.5",
|
|
67
67
|
"ai-sdk-ollama": "^3.8.3",
|
|
68
68
|
"drizzle-orm": "^0.44.5",
|
package/src/events/bus.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { OttoEvent } from './types.ts';
|
|
1
|
+
import type { ClientEvent, NotificationEvent, OttoEvent } from './types.ts';
|
|
2
2
|
|
|
3
3
|
type Subscriber = (evt: OttoEvent) => void;
|
|
4
|
+
type ClientSubscriber = (evt: ClientEvent) => void;
|
|
4
5
|
|
|
5
6
|
const subscribers = new Map<string, Set<Subscriber>>(); // sessionId -> subs
|
|
7
|
+
const clientSubscribers = new Set<ClientSubscriber>();
|
|
6
8
|
|
|
7
9
|
function sanitizeBigInt<T>(obj: T): T {
|
|
8
10
|
if (obj === null || obj === undefined) return obj;
|
|
@@ -34,6 +36,24 @@ export function publish(event: OttoEvent) {
|
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
export function publishClientEvent(event: ClientEvent) {
|
|
40
|
+
const sanitizedEvent = sanitizeBigInt(event);
|
|
41
|
+
for (const sub of clientSubscribers) {
|
|
42
|
+
try {
|
|
43
|
+
sub(sanitizedEvent);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(
|
|
46
|
+
`[bus] Client subscriber threw on event ${event.type}:`,
|
|
47
|
+
err instanceof Error ? err.message : String(err),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function publishNotification(payload: NotificationEvent) {
|
|
54
|
+
publishClientEvent({ type: 'notification', payload });
|
|
55
|
+
}
|
|
56
|
+
|
|
37
57
|
export function subscribe(sessionId: string, handler: Subscriber) {
|
|
38
58
|
let set = subscribers.get(sessionId);
|
|
39
59
|
if (!set) {
|
|
@@ -46,3 +66,10 @@ export function subscribe(sessionId: string, handler: Subscriber) {
|
|
|
46
66
|
if (set && set.size === 0) subscribers.delete(sessionId);
|
|
47
67
|
};
|
|
48
68
|
}
|
|
69
|
+
|
|
70
|
+
export function subscribeClientEvents(handler: ClientSubscriber) {
|
|
71
|
+
clientSubscribers.add(handler);
|
|
72
|
+
return () => {
|
|
73
|
+
clientSubscribers.delete(handler);
|
|
74
|
+
};
|
|
75
|
+
}
|
package/src/events/types.ts
CHANGED
|
@@ -34,3 +34,34 @@ export interface OttoEvent<T = unknown> {
|
|
|
34
34
|
sessionId: string;
|
|
35
35
|
payload?: T;
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
export type NotificationLevel = 'info' | 'success' | 'warning' | 'error';
|
|
39
|
+
|
|
40
|
+
export interface NotificationAction {
|
|
41
|
+
label: string;
|
|
42
|
+
href: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface NotificationEvent {
|
|
46
|
+
id: string;
|
|
47
|
+
level: NotificationLevel;
|
|
48
|
+
title: string;
|
|
49
|
+
body?: string;
|
|
50
|
+
action?: NotificationAction;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
expiresAt?: string;
|
|
53
|
+
source?: 'agent' | 'system' | 'session' | 'auth' | 'billing';
|
|
54
|
+
sessionId?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SessionStatusEvent {
|
|
58
|
+
sessionId: string;
|
|
59
|
+
status: 'running' | 'completed' | 'failed' | 'needs_attention';
|
|
60
|
+
messageId?: string;
|
|
61
|
+
createdAt: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type ClientEvent =
|
|
65
|
+
| { type: 'notification'; payload: NotificationEvent }
|
|
66
|
+
| { type: 'session.status'; payload: SessionStatusEvent }
|
|
67
|
+
| { type: 'heartbeat'; payload: { createdAt: string } };
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { registerOpenApiRoute } from './routes/openapi.ts';
|
|
|
9
9
|
import { registerSessionsRoutes } from './routes/sessions.ts';
|
|
10
10
|
import { registerSessionMessagesRoutes } from './routes/session-messages.ts';
|
|
11
11
|
import { registerSessionStreamRoute } from './routes/session-stream.ts';
|
|
12
|
+
import { registerClientEventsRoute } from './routes/client-events.ts';
|
|
12
13
|
import { registerAskRoutes } from './routes/ask.ts';
|
|
13
14
|
import { registerConfigRoutes } from './routes/config/index.ts';
|
|
14
15
|
import { registerFilesRoutes } from './routes/files.ts';
|
|
@@ -34,8 +35,23 @@ setTerminalManager(globalTerminalManager);
|
|
|
34
35
|
// Suppress noisy AI SDK provider warnings unless debug mode is enabled.
|
|
35
36
|
installAiSdkWarningHandler();
|
|
36
37
|
|
|
38
|
+
const corsAllowHeaders = [
|
|
39
|
+
'Content-Type',
|
|
40
|
+
'Authorization',
|
|
41
|
+
'X-Requested-With',
|
|
42
|
+
'Access-Control-Request-Private-Network',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function applyPrivateNetworkAccessHeaders(app: OpenAPIHono<BlankEnv>) {
|
|
46
|
+
app.use('*', async (c, next) => {
|
|
47
|
+
c.header('Access-Control-Allow-Private-Network', 'true');
|
|
48
|
+
await next();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
function initApp() {
|
|
38
53
|
const app = new OpenAPIHono<BlankEnv>();
|
|
54
|
+
applyPrivateNetworkAccessHeaders(app);
|
|
39
55
|
|
|
40
56
|
// Enable CORS for localhost and local network access
|
|
41
57
|
app.use(
|
|
@@ -61,7 +77,7 @@ function initApp() {
|
|
|
61
77
|
return origin;
|
|
62
78
|
},
|
|
63
79
|
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
64
|
-
allowHeaders:
|
|
80
|
+
allowHeaders: corsAllowHeaders,
|
|
65
81
|
exposeHeaders: ['Content-Length', 'X-Request-Id'],
|
|
66
82
|
credentials: true,
|
|
67
83
|
maxAge: 600,
|
|
@@ -74,6 +90,7 @@ function initApp() {
|
|
|
74
90
|
registerSessionApprovalRoute(app);
|
|
75
91
|
registerSessionMessagesRoutes(app);
|
|
76
92
|
registerSessionStreamRoute(app);
|
|
93
|
+
registerClientEventsRoute(app);
|
|
77
94
|
registerAskRoutes(app);
|
|
78
95
|
registerConfigRoutes(app);
|
|
79
96
|
registerFilesRoutes(app);
|
|
@@ -112,6 +129,7 @@ export type StandaloneAppConfig = {
|
|
|
112
129
|
|
|
113
130
|
export function createStandaloneApp(_config?: StandaloneAppConfig) {
|
|
114
131
|
const honoApp = new OpenAPIHono<BlankEnv>();
|
|
132
|
+
applyPrivateNetworkAccessHeaders(honoApp);
|
|
115
133
|
|
|
116
134
|
honoApp.use(
|
|
117
135
|
'*',
|
|
@@ -136,7 +154,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
|
|
|
136
154
|
return origin;
|
|
137
155
|
},
|
|
138
156
|
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
139
|
-
allowHeaders:
|
|
157
|
+
allowHeaders: corsAllowHeaders,
|
|
140
158
|
exposeHeaders: ['Content-Length', 'X-Request-Id'],
|
|
141
159
|
credentials: true,
|
|
142
160
|
maxAge: 600,
|
|
@@ -149,6 +167,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
|
|
|
149
167
|
registerSessionApprovalRoute(honoApp);
|
|
150
168
|
registerSessionMessagesRoutes(honoApp);
|
|
151
169
|
registerSessionStreamRoute(honoApp);
|
|
170
|
+
registerClientEventsRoute(honoApp);
|
|
152
171
|
registerAskRoutes(honoApp);
|
|
153
172
|
registerConfigRoutes(honoApp);
|
|
154
173
|
registerFilesRoutes(honoApp);
|
|
@@ -205,6 +224,7 @@ export type EmbeddedAppConfig = {
|
|
|
205
224
|
|
|
206
225
|
export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
|
|
207
226
|
const honoApp = new OpenAPIHono<BlankEnv>();
|
|
227
|
+
applyPrivateNetworkAccessHeaders(honoApp);
|
|
208
228
|
|
|
209
229
|
// Store injected config in Hono context for routes to access
|
|
210
230
|
// Config can be empty - routes will fall back to files/env
|
|
@@ -247,7 +267,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
|
|
|
247
267
|
return origin;
|
|
248
268
|
},
|
|
249
269
|
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
250
|
-
allowHeaders:
|
|
270
|
+
allowHeaders: corsAllowHeaders,
|
|
251
271
|
exposeHeaders: ['Content-Length', 'X-Request-Id'],
|
|
252
272
|
credentials: true,
|
|
253
273
|
maxAge: 600,
|
|
@@ -260,6 +280,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
|
|
|
260
280
|
registerSessionApprovalRoute(honoApp);
|
|
261
281
|
registerSessionMessagesRoutes(honoApp);
|
|
262
282
|
registerSessionStreamRoute(honoApp);
|
|
283
|
+
registerClientEventsRoute(honoApp);
|
|
263
284
|
registerAskRoutes(honoApp);
|
|
264
285
|
registerConfigRoutes(honoApp);
|
|
265
286
|
registerFilesRoutes(honoApp);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { Hono } from 'hono';
|
|
3
|
+
import { subscribeClientEvents } from '../events/bus.ts';
|
|
4
|
+
import type { ClientEvent } from '../events/types.ts';
|
|
5
|
+
import { openApiRoute } from '../openapi/route.ts';
|
|
6
|
+
|
|
7
|
+
const STREAM_DESCRIPTION =
|
|
8
|
+
'SSE event stream. Events include notification, session.status, and heartbeat.';
|
|
9
|
+
|
|
10
|
+
function safeStringify(obj: unknown): string {
|
|
11
|
+
return JSON.stringify(obj, (_key, value) =>
|
|
12
|
+
typeof value === 'bigint' ? Number(value) : value,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function handleClientEventsStream(c: Context) {
|
|
17
|
+
const headers = new Headers({
|
|
18
|
+
'Content-Type': 'text/event-stream',
|
|
19
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
20
|
+
Connection: 'keep-alive',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const encoder = new TextEncoder();
|
|
24
|
+
|
|
25
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
26
|
+
start(controller) {
|
|
27
|
+
const write = (evt: ClientEvent) => {
|
|
28
|
+
let line: string;
|
|
29
|
+
try {
|
|
30
|
+
line =
|
|
31
|
+
`event: ${evt.type}\n` +
|
|
32
|
+
`data: ${safeStringify(evt.payload ?? {})}\n\n`;
|
|
33
|
+
} catch {
|
|
34
|
+
line = `event: ${evt.type}\ndata: {}\n\n`;
|
|
35
|
+
}
|
|
36
|
+
controller.enqueue(encoder.encode(line));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const unsubscribe = subscribeClientEvents(write);
|
|
40
|
+
controller.enqueue(encoder.encode(': connected client-events\n\n'));
|
|
41
|
+
const hb = setInterval(() => {
|
|
42
|
+
try {
|
|
43
|
+
write({
|
|
44
|
+
type: 'heartbeat',
|
|
45
|
+
payload: { createdAt: new Date().toISOString() },
|
|
46
|
+
});
|
|
47
|
+
} catch {
|
|
48
|
+
clearInterval(hb);
|
|
49
|
+
}
|
|
50
|
+
}, 5000);
|
|
51
|
+
|
|
52
|
+
const signal = c.req.raw?.signal as AbortSignal | undefined;
|
|
53
|
+
signal?.addEventListener('abort', () => {
|
|
54
|
+
clearInterval(hb);
|
|
55
|
+
unsubscribe();
|
|
56
|
+
try {
|
|
57
|
+
controller.close();
|
|
58
|
+
} catch {}
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return new Response(stream, { headers });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function registerClientEventsRoute(app: Hono) {
|
|
67
|
+
openApiRoute(
|
|
68
|
+
app,
|
|
69
|
+
{
|
|
70
|
+
method: 'get',
|
|
71
|
+
path: '/v1/events/stream',
|
|
72
|
+
operationId: 'subscribeClientEventsStream',
|
|
73
|
+
tags: ['stream'],
|
|
74
|
+
summary: 'Subscribe to global client event stream (SSE)',
|
|
75
|
+
description:
|
|
76
|
+
'App-level SSE stream for notifications and lightweight cross-session status updates.',
|
|
77
|
+
parameters: [
|
|
78
|
+
{
|
|
79
|
+
in: 'query',
|
|
80
|
+
name: 'project',
|
|
81
|
+
required: false,
|
|
82
|
+
schema: { type: 'string' },
|
|
83
|
+
description:
|
|
84
|
+
'Project root override (defaults to current working directory).',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
responses: {
|
|
88
|
+
'200': {
|
|
89
|
+
description: 'text/event-stream',
|
|
90
|
+
content: {
|
|
91
|
+
'text/event-stream': {
|
|
92
|
+
schema: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: STREAM_DESCRIPTION,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
handleClientEventsStream,
|
|
102
|
+
);
|
|
103
|
+
openApiRoute(
|
|
104
|
+
app,
|
|
105
|
+
{
|
|
106
|
+
method: 'post',
|
|
107
|
+
path: '/v1/events/stream',
|
|
108
|
+
operationId: 'subscribeClientEventsStreamPost',
|
|
109
|
+
tags: ['stream'],
|
|
110
|
+
summary: 'Subscribe to global client event stream (SSE) using POST',
|
|
111
|
+
description:
|
|
112
|
+
'Compatibility alias for app-level SSE over tunnels/proxies that do not support GET streams.',
|
|
113
|
+
parameters: [
|
|
114
|
+
{
|
|
115
|
+
in: 'query',
|
|
116
|
+
name: 'project',
|
|
117
|
+
required: false,
|
|
118
|
+
schema: { type: 'string' },
|
|
119
|
+
description:
|
|
120
|
+
'Project root override (defaults to current working directory).',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
responses: {
|
|
124
|
+
'200': {
|
|
125
|
+
description: 'text/event-stream',
|
|
126
|
+
content: {
|
|
127
|
+
'text/event-stream': {
|
|
128
|
+
schema: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
description: STREAM_DESCRIPTION,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
handleClientEventsStream,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -24,17 +24,21 @@ export async function createTerminal(
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
let resolvedCommand = command;
|
|
27
|
+
let resolvedArgs = args || [];
|
|
27
28
|
if (command === 'bash' || command === 'sh' || command === 'shell') {
|
|
28
29
|
resolvedCommand =
|
|
29
30
|
process.platform === 'win32'
|
|
30
31
|
? process.env.COMSPEC || 'cmd.exe'
|
|
31
32
|
: process.env.SHELL || '/bin/bash';
|
|
33
|
+
if (resolvedArgs.length === 0 && process.platform !== 'win32') {
|
|
34
|
+
resolvedArgs = process.platform === 'darwin' ? ['-il'] : ['-i'];
|
|
35
|
+
}
|
|
32
36
|
}
|
|
33
37
|
const resolvedCwd = cwd || process.cwd();
|
|
34
38
|
|
|
35
39
|
const terminal = terminalManager.create({
|
|
36
40
|
command: resolvedCommand,
|
|
37
|
-
args:
|
|
41
|
+
args: resolvedArgs,
|
|
38
42
|
purpose,
|
|
39
43
|
cwd: resolvedCwd,
|
|
40
44
|
createdBy: 'user',
|
|
@@ -15,7 +15,7 @@ export function appendRunnerReminderMessages(args: {
|
|
|
15
15
|
messages.push(
|
|
16
16
|
isOpenAIOAuth
|
|
17
17
|
? {
|
|
18
|
-
role: '
|
|
18
|
+
role: 'user',
|
|
19
19
|
content:
|
|
20
20
|
'[system-reminder] Continuing an existing session. Execute directly, use tools as needed, and call `finish` at the end. For simple questions, your answer IS the response — do not add a "Summary:" recap.',
|
|
21
21
|
}
|
|
@@ -32,7 +32,7 @@ export function appendRunnerReminderMessages(args: {
|
|
|
32
32
|
messages.push(
|
|
33
33
|
isOpenAIOAuth
|
|
34
34
|
? {
|
|
35
|
-
role: '
|
|
35
|
+
role: 'user',
|
|
36
36
|
content:
|
|
37
37
|
'[system-reminder] Your previous response stopped mid-task. Resume from where you left off and complete the actual work — not a plan-only update.',
|
|
38
38
|
}
|
|
@@ -183,3 +183,13 @@ export function appendRunnerPromptMessages(args: {
|
|
|
183
183
|
additionalSystemMessages.push(...opts.additionalPromptMessages);
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
+
|
|
187
|
+
export function moveSystemMessagesToUserForOpenAIOAuth(
|
|
188
|
+
messages: Array<{ role: 'system' | 'user'; content: string }>,
|
|
189
|
+
): void {
|
|
190
|
+
for (const message of messages) {
|
|
191
|
+
if (message.role !== 'system') continue;
|
|
192
|
+
message.role = 'user';
|
|
193
|
+
message.content = `<system-message>${message.content}</system-message>`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -14,6 +14,7 @@ import { resolveAgentConfig } from './registry.ts';
|
|
|
14
14
|
import {
|
|
15
15
|
appendRunnerPromptMessages,
|
|
16
16
|
buildRunnerPrompt,
|
|
17
|
+
moveSystemMessagesToUserForOpenAIOAuth,
|
|
17
18
|
} from './runner-setup-prompt.ts';
|
|
18
19
|
import {
|
|
19
20
|
buildAllowedTools,
|
|
@@ -134,6 +135,9 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
134
135
|
opts,
|
|
135
136
|
additionalSystemMessages: prompt.additionalSystemMessages,
|
|
136
137
|
});
|
|
138
|
+
if (prompt.isOpenAIOAuth) {
|
|
139
|
+
moveSystemMessagesToUserForOpenAIOAuth(prompt.additionalSystemMessages);
|
|
140
|
+
}
|
|
137
141
|
|
|
138
142
|
const gated = buildAllowedTools({
|
|
139
143
|
agentName: agentCfg.name,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProviderName } from '../provider/index.ts';
|
|
2
|
-
import { publish } from '../../events/bus.ts';
|
|
2
|
+
import { publish, publishClientEvent } from '../../events/bus.ts';
|
|
3
3
|
import type { ToolApprovalMode } from '../tools/approval.ts';
|
|
4
4
|
import type { ReasoningLevel } from '@ottocode/sdk';
|
|
5
5
|
|
|
@@ -234,6 +234,17 @@ export function setCurrentMessage(sessionId: string, messageId: string | null) {
|
|
|
234
234
|
if (state) {
|
|
235
235
|
state.currentMessageId = messageId;
|
|
236
236
|
publishQueueState(sessionId);
|
|
237
|
+
if (messageId) {
|
|
238
|
+
publishClientEvent({
|
|
239
|
+
type: 'session.status',
|
|
240
|
+
payload: {
|
|
241
|
+
sessionId,
|
|
242
|
+
status: 'running',
|
|
243
|
+
messageId,
|
|
244
|
+
createdAt: new Date().toISOString(),
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
237
248
|
}
|
|
238
249
|
}
|
|
239
250
|
|
|
@@ -243,6 +254,15 @@ export function dequeueJob(sessionId: string): RunOpts | undefined {
|
|
|
243
254
|
if (job && state) {
|
|
244
255
|
state.currentMessageId = job.assistantMessageId;
|
|
245
256
|
publishQueueState(sessionId);
|
|
257
|
+
publishClientEvent({
|
|
258
|
+
type: 'session.status',
|
|
259
|
+
payload: {
|
|
260
|
+
sessionId,
|
|
261
|
+
status: 'running',
|
|
262
|
+
messageId: job.assistantMessageId,
|
|
263
|
+
createdAt: new Date().toISOString(),
|
|
264
|
+
},
|
|
265
|
+
});
|
|
246
266
|
}
|
|
247
267
|
return job;
|
|
248
268
|
}
|
|
@@ -2,7 +2,7 @@ import type { getDb } from '@ottocode/database';
|
|
|
2
2
|
import { messages, messageParts } from '@ottocode/database/schema';
|
|
3
3
|
import { eq } from 'drizzle-orm';
|
|
4
4
|
import { APICallError } from 'ai';
|
|
5
|
-
import { publish } from '../../events/bus.ts';
|
|
5
|
+
import { publish, publishClientEvent } from '../../events/bus.ts';
|
|
6
6
|
import { toErrorPayload } from '../errors/handling.ts';
|
|
7
7
|
import type { RunOpts } from '../session/queue.ts';
|
|
8
8
|
import type { ToolAdapterContext } from '../../tools/adapter.ts';
|
|
@@ -349,5 +349,28 @@ export function createErrorHandler(
|
|
|
349
349
|
autoCompacted: isPromptTooLong && !opts.isCompactCommand,
|
|
350
350
|
},
|
|
351
351
|
});
|
|
352
|
+
|
|
353
|
+
const createdAt = new Date().toISOString();
|
|
354
|
+
publishClientEvent({
|
|
355
|
+
type: 'session.status',
|
|
356
|
+
payload: {
|
|
357
|
+
sessionId: opts.sessionId,
|
|
358
|
+
status: 'failed',
|
|
359
|
+
messageId: opts.assistantMessageId,
|
|
360
|
+
createdAt,
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
publishClientEvent({
|
|
364
|
+
type: 'notification',
|
|
365
|
+
payload: {
|
|
366
|
+
id: crypto.randomUUID(),
|
|
367
|
+
level: 'error',
|
|
368
|
+
title: 'Session failed',
|
|
369
|
+
body: displayMessage,
|
|
370
|
+
source: 'session',
|
|
371
|
+
sessionId: opts.sessionId,
|
|
372
|
+
createdAt,
|
|
373
|
+
},
|
|
374
|
+
});
|
|
352
375
|
};
|
|
353
376
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { getDb } from '@ottocode/database';
|
|
2
2
|
import { messages, messageParts } from '@ottocode/database/schema';
|
|
3
3
|
import { eq } from 'drizzle-orm';
|
|
4
|
-
import { publish } from '../../events/bus.ts';
|
|
4
|
+
import { publish, publishClientEvent } from '../../events/bus.ts';
|
|
5
5
|
import { estimateModelCostUsd } from '@ottocode/sdk';
|
|
6
6
|
import type { RunOpts } from '../session/queue.ts';
|
|
7
7
|
import { markSessionCompacted } from '../message/compaction.ts';
|
|
@@ -84,5 +84,32 @@ export function createFinishHandler(
|
|
|
84
84
|
finishReason: fin.finishReason,
|
|
85
85
|
},
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
const createdAt = new Date().toISOString();
|
|
89
|
+
const status = fin.finishReason === 'error' ? 'failed' : 'completed';
|
|
90
|
+
publishClientEvent({
|
|
91
|
+
type: 'session.status',
|
|
92
|
+
payload: {
|
|
93
|
+
sessionId: opts.sessionId,
|
|
94
|
+
status,
|
|
95
|
+
messageId: opts.assistantMessageId,
|
|
96
|
+
createdAt,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
publishClientEvent({
|
|
100
|
+
type: 'notification',
|
|
101
|
+
payload: {
|
|
102
|
+
id: crypto.randomUUID(),
|
|
103
|
+
level: status === 'failed' ? 'error' : 'success',
|
|
104
|
+
title: status === 'failed' ? 'Session failed' : 'Session completed',
|
|
105
|
+
body:
|
|
106
|
+
status === 'failed'
|
|
107
|
+
? 'An assistant run ended with an error.'
|
|
108
|
+
: 'An assistant run finished successfully.',
|
|
109
|
+
source: 'session',
|
|
110
|
+
sessionId: opts.sessionId,
|
|
111
|
+
createdAt,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
87
114
|
};
|
|
88
115
|
}
|