@ottocode/server 0.1.260 → 0.1.262
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 +4 -3
- package/src/index.ts +5 -4
- package/src/openapi/register.ts +92 -0
- package/src/openapi/route.ts +22 -0
- package/src/routes/ask.ts +210 -99
- package/src/routes/auth.ts +1701 -626
- package/src/routes/branch.ts +281 -90
- package/src/routes/config/agents.ts +79 -32
- package/src/routes/config/cwd.ts +46 -14
- package/src/routes/config/debug.ts +159 -30
- package/src/routes/config/defaults.ts +182 -64
- package/src/routes/config/main.ts +109 -73
- package/src/routes/config/models.ts +304 -137
- package/src/routes/config/providers.ts +462 -166
- package/src/routes/config/utils.ts +2 -2
- package/src/routes/doctor.ts +395 -161
- package/src/routes/files.ts +650 -260
- package/src/routes/git/branch.ts +143 -52
- package/src/routes/git/commit.ts +347 -141
- package/src/routes/git/diff.ts +239 -116
- package/src/routes/git/init.ts +103 -23
- package/src/routes/git/pull.ts +167 -65
- package/src/routes/git/push.ts +222 -117
- package/src/routes/git/remote.ts +401 -100
- package/src/routes/git/staging.ts +502 -141
- package/src/routes/git/status.ts +171 -78
- package/src/routes/mcp.ts +1129 -404
- package/src/routes/openapi.ts +27 -4
- package/src/routes/ottorouter.ts +1221 -389
- package/src/routes/provider-usage.ts +153 -36
- package/src/routes/research.ts +817 -370
- package/src/routes/root.ts +50 -6
- package/src/routes/session-approval.ts +228 -54
- package/src/routes/session-files.ts +265 -134
- package/src/routes/session-messages.ts +330 -150
- package/src/routes/session-stream.ts +83 -2
- package/src/routes/sessions.ts +1830 -780
- package/src/routes/skills.ts +849 -161
- package/src/routes/terminals.ts +469 -103
- package/src/routes/tunnel.ts +394 -118
- package/src/runtime/ask/service.ts +1 -0
- package/src/runtime/message/compaction-limits.ts +3 -3
- package/src/runtime/provider/reasoning.ts +2 -1
- package/src/runtime/session/db-operations.ts +4 -3
- package/src/runtime/utils/token.ts +7 -2
- package/src/tools/adapter.ts +21 -0
- package/src/openapi/paths/ask.ts +0 -81
- package/src/openapi/paths/auth.ts +0 -687
- package/src/openapi/paths/branch.ts +0 -102
- package/src/openapi/paths/config.ts +0 -485
- package/src/openapi/paths/doctor.ts +0 -165
- package/src/openapi/paths/files.ts +0 -236
- package/src/openapi/paths/git.ts +0 -690
- package/src/openapi/paths/mcp.ts +0 -339
- package/src/openapi/paths/messages.ts +0 -103
- package/src/openapi/paths/ottorouter.ts +0 -594
- package/src/openapi/paths/provider-usage.ts +0 -59
- package/src/openapi/paths/research.ts +0 -227
- package/src/openapi/paths/session-approval.ts +0 -93
- package/src/openapi/paths/session-extras.ts +0 -336
- package/src/openapi/paths/session-files.ts +0 -91
- package/src/openapi/paths/sessions.ts +0 -210
- package/src/openapi/paths/skills.ts +0 -377
- package/src/openapi/paths/stream.ts +0 -26
- package/src/openapi/paths/terminals.ts +0 -226
- package/src/openapi/paths/tunnel.ts +0 -163
- package/src/openapi/spec.ts +0 -73
package/src/routes/sessions.ts
CHANGED
|
@@ -16,480 +16,1133 @@ import { createSession as createSessionRow } from '../runtime/session/manager.ts
|
|
|
16
16
|
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
17
17
|
import { logger } from '@ottocode/sdk';
|
|
18
18
|
import { getRunnerState } from '../runtime/session/queue.ts';
|
|
19
|
+
import { openApiRoute } from '../openapi/route.ts';
|
|
19
20
|
|
|
20
21
|
export function registerSessionsRoutes(app: Hono) {
|
|
21
22
|
// List sessions
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const row = await createSessionRow({
|
|
95
|
-
db,
|
|
96
|
-
cfg,
|
|
97
|
-
agent,
|
|
98
|
-
provider,
|
|
99
|
-
model,
|
|
100
|
-
title: (body.title as string | null | undefined) ?? null,
|
|
101
|
-
});
|
|
102
|
-
return c.json(row, 201);
|
|
103
|
-
} catch (err) {
|
|
104
|
-
logger.error('Failed to create session', err);
|
|
105
|
-
const errorResponse = serializeError(err);
|
|
106
|
-
return c.json(errorResponse, errorResponse.error.status || 400);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Get single session
|
|
111
|
-
app.get('/v1/sessions/:sessionId', async (c) => {
|
|
112
|
-
try {
|
|
113
|
-
const sessionId = c.req.param('sessionId');
|
|
23
|
+
openApiRoute(
|
|
24
|
+
app,
|
|
25
|
+
{
|
|
26
|
+
method: 'get',
|
|
27
|
+
path: '/v1/sessions',
|
|
28
|
+
tags: ['sessions'],
|
|
29
|
+
operationId: 'listSessions',
|
|
30
|
+
summary: 'List sessions',
|
|
31
|
+
parameters: [
|
|
32
|
+
{
|
|
33
|
+
in: 'query',
|
|
34
|
+
name: 'project',
|
|
35
|
+
required: false,
|
|
36
|
+
schema: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
},
|
|
39
|
+
description:
|
|
40
|
+
'Project root override (defaults to current working directory).',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
in: 'query',
|
|
44
|
+
name: 'limit',
|
|
45
|
+
schema: {
|
|
46
|
+
type: 'integer',
|
|
47
|
+
default: 50,
|
|
48
|
+
minimum: 1,
|
|
49
|
+
maximum: 200,
|
|
50
|
+
},
|
|
51
|
+
description: 'Maximum number of sessions to return',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
in: 'query',
|
|
55
|
+
name: 'offset',
|
|
56
|
+
schema: {
|
|
57
|
+
type: 'integer',
|
|
58
|
+
default: 0,
|
|
59
|
+
minimum: 0,
|
|
60
|
+
},
|
|
61
|
+
description: 'Offset for pagination',
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
responses: {
|
|
65
|
+
'200': {
|
|
66
|
+
description: 'OK',
|
|
67
|
+
content: {
|
|
68
|
+
'application/json': {
|
|
69
|
+
schema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
items: {
|
|
73
|
+
type: 'array',
|
|
74
|
+
items: {
|
|
75
|
+
$ref: '#/components/schemas/Session',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
hasMore: {
|
|
79
|
+
type: 'boolean',
|
|
80
|
+
},
|
|
81
|
+
nextOffset: {
|
|
82
|
+
type: 'integer',
|
|
83
|
+
nullable: true,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: ['items', 'hasMore', 'nextOffset'],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
async (c) => {
|
|
114
94
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
95
|
+
const limit = Math.min(
|
|
96
|
+
Math.max(parseInt(c.req.query('limit') || '50', 10) || 50, 1),
|
|
97
|
+
200,
|
|
98
|
+
);
|
|
99
|
+
const offset = Math.max(
|
|
100
|
+
parseInt(c.req.query('offset') || '0', 10) || 0,
|
|
101
|
+
0,
|
|
102
|
+
);
|
|
115
103
|
const cfg = await loadConfig(projectRoot);
|
|
116
104
|
const db = await getDb(cfg.projectRoot);
|
|
105
|
+
// Only return sessions for this project, excluding research sessions
|
|
117
106
|
const rows = await db
|
|
118
107
|
.select()
|
|
119
108
|
.from(sessions)
|
|
120
|
-
.where(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
109
|
+
.where(
|
|
110
|
+
and(
|
|
111
|
+
eq(sessions.projectPath, cfg.projectRoot),
|
|
112
|
+
ne(sessions.sessionType, 'research'),
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
.orderBy(desc(sessions.lastActiveAt), desc(sessions.createdAt))
|
|
116
|
+
.limit(limit + 1)
|
|
117
|
+
.offset(offset);
|
|
118
|
+
const hasMore = rows.length > limit;
|
|
119
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
120
|
+
const normalized = page.map((r) => {
|
|
121
|
+
let counts: Record<string, unknown> | undefined;
|
|
122
|
+
if (r.toolCountsJson) {
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(r.toolCountsJson);
|
|
125
|
+
if (
|
|
126
|
+
parsed &&
|
|
127
|
+
typeof parsed === 'object' &&
|
|
128
|
+
!Array.isArray(parsed)
|
|
129
|
+
) {
|
|
130
|
+
counts = parsed as Record<string, unknown>;
|
|
131
|
+
}
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
const { toolCountsJson: _toolCountsJson, ...rest } = r;
|
|
135
|
+
const isRunning = getRunnerState(r.id)?.running ?? false;
|
|
136
|
+
const base = counts ? { ...rest, toolCounts: counts } : rest;
|
|
137
|
+
return { ...base, isRunning };
|
|
138
|
+
});
|
|
139
|
+
return c.json({
|
|
140
|
+
items: normalized,
|
|
141
|
+
hasMore,
|
|
142
|
+
nextOffset: hasMore ? offset + limit : null,
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
146
|
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
// Create session
|
|
148
|
+
openApiRoute(
|
|
149
|
+
app,
|
|
150
|
+
{
|
|
151
|
+
method: 'post',
|
|
152
|
+
path: '/v1/sessions',
|
|
153
|
+
tags: ['sessions'],
|
|
154
|
+
operationId: 'createSession',
|
|
155
|
+
summary: 'Create a new session',
|
|
156
|
+
parameters: [
|
|
157
|
+
{
|
|
158
|
+
in: 'query',
|
|
159
|
+
name: 'project',
|
|
160
|
+
required: false,
|
|
161
|
+
schema: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
},
|
|
164
|
+
description:
|
|
165
|
+
'Project root override (defaults to current working directory).',
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
requestBody: {
|
|
169
|
+
required: false,
|
|
170
|
+
content: {
|
|
171
|
+
'application/json': {
|
|
172
|
+
schema: {
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: {
|
|
175
|
+
title: {
|
|
176
|
+
type: 'string',
|
|
177
|
+
nullable: true,
|
|
178
|
+
},
|
|
179
|
+
agent: {
|
|
180
|
+
type: 'string',
|
|
181
|
+
},
|
|
182
|
+
provider: {
|
|
183
|
+
$ref: '#/components/schemas/Provider',
|
|
184
|
+
},
|
|
185
|
+
model: {
|
|
186
|
+
type: 'string',
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
responses: {
|
|
194
|
+
'201': {
|
|
195
|
+
description: 'Created',
|
|
196
|
+
content: {
|
|
197
|
+
'application/json': {
|
|
198
|
+
schema: {
|
|
199
|
+
$ref: '#/components/schemas/Session',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
'400': {
|
|
205
|
+
description: 'Bad Request',
|
|
206
|
+
content: {
|
|
207
|
+
'application/json': {
|
|
208
|
+
schema: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
error: {
|
|
212
|
+
type: 'string',
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
required: ['error'],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
async (c) => {
|
|
151
223
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
152
224
|
const cfg = await loadConfig(projectRoot);
|
|
153
225
|
const db = await getDb(cfg.projectRoot);
|
|
154
|
-
|
|
155
226
|
const body = (await c.req.json().catch(() => ({}))) as Record<
|
|
156
227
|
string,
|
|
157
228
|
unknown
|
|
158
229
|
>;
|
|
230
|
+
const agent = (body.agent as string | undefined) ?? cfg.defaults.agent;
|
|
231
|
+
const agentCfg = await resolveAgentConfig(cfg.projectRoot, agent);
|
|
232
|
+
const providerCandidate =
|
|
233
|
+
typeof body.provider === 'string' ? body.provider : undefined;
|
|
234
|
+
const provider: ProviderId = (() => {
|
|
235
|
+
if (providerCandidate && hasConfiguredProvider(cfg, providerCandidate))
|
|
236
|
+
return providerCandidate;
|
|
237
|
+
if (hasConfiguredProvider(cfg, agentCfg.provider))
|
|
238
|
+
return agentCfg.provider;
|
|
239
|
+
return cfg.defaults.provider;
|
|
240
|
+
})();
|
|
241
|
+
const modelCandidate =
|
|
242
|
+
typeof body.model === 'string' ? body.model.trim() : undefined;
|
|
243
|
+
const model = modelCandidate?.length
|
|
244
|
+
? modelCandidate
|
|
245
|
+
: (agentCfg.model ?? cfg.defaults.model);
|
|
246
|
+
try {
|
|
247
|
+
const row = await createSessionRow({
|
|
248
|
+
db,
|
|
249
|
+
cfg,
|
|
250
|
+
agent,
|
|
251
|
+
provider,
|
|
252
|
+
model,
|
|
253
|
+
title: (body.title as string | null | undefined) ?? null,
|
|
254
|
+
});
|
|
255
|
+
return c.json(row, 201);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
logger.error('Failed to create session', err);
|
|
258
|
+
const errorResponse = serializeError(err);
|
|
259
|
+
return c.json(errorResponse, errorResponse.error.status || 400);
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
);
|
|
159
263
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
264
|
+
// Get single session
|
|
265
|
+
openApiRoute(
|
|
266
|
+
app,
|
|
267
|
+
{
|
|
268
|
+
method: 'get',
|
|
269
|
+
path: '/v1/sessions/{sessionId}',
|
|
270
|
+
tags: ['sessions'],
|
|
271
|
+
operationId: 'getSession',
|
|
272
|
+
summary: 'Get a single session by ID',
|
|
273
|
+
parameters: [
|
|
274
|
+
{
|
|
275
|
+
in: 'path',
|
|
276
|
+
name: 'sessionId',
|
|
277
|
+
required: true,
|
|
278
|
+
schema: {
|
|
279
|
+
type: 'string',
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
in: 'query',
|
|
284
|
+
name: 'project',
|
|
285
|
+
required: false,
|
|
286
|
+
schema: {
|
|
287
|
+
type: 'string',
|
|
288
|
+
},
|
|
289
|
+
description:
|
|
290
|
+
'Project root override (defaults to current working directory).',
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
responses: {
|
|
294
|
+
'200': {
|
|
295
|
+
description: 'OK',
|
|
296
|
+
content: {
|
|
297
|
+
'application/json': {
|
|
298
|
+
schema: {
|
|
299
|
+
$ref: '#/components/schemas/Session',
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
'404': {
|
|
305
|
+
description: 'Bad Request',
|
|
306
|
+
content: {
|
|
307
|
+
'application/json': {
|
|
308
|
+
schema: {
|
|
309
|
+
type: 'object',
|
|
310
|
+
properties: {
|
|
311
|
+
error: {
|
|
312
|
+
type: 'string',
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
required: ['error'],
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
async (c) => {
|
|
323
|
+
try {
|
|
324
|
+
const sessionId = c.req.param('sessionId');
|
|
325
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
326
|
+
const cfg = await loadConfig(projectRoot);
|
|
327
|
+
const db = await getDb(cfg.projectRoot);
|
|
328
|
+
const rows = await db
|
|
329
|
+
.select()
|
|
330
|
+
.from(sessions)
|
|
331
|
+
.where(eq(sessions.id, sessionId))
|
|
332
|
+
.limit(1);
|
|
333
|
+
if (!rows.length) {
|
|
334
|
+
return c.json(
|
|
335
|
+
{ error: { message: 'Session not found', status: 404 } },
|
|
336
|
+
404,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const r = rows[0];
|
|
340
|
+
let counts: Record<string, unknown> | undefined;
|
|
341
|
+
if (r.toolCountsJson) {
|
|
342
|
+
try {
|
|
343
|
+
const parsed = JSON.parse(r.toolCountsJson);
|
|
344
|
+
if (
|
|
345
|
+
parsed &&
|
|
346
|
+
typeof parsed === 'object' &&
|
|
347
|
+
!Array.isArray(parsed)
|
|
348
|
+
) {
|
|
349
|
+
counts = parsed as Record<string, unknown>;
|
|
350
|
+
}
|
|
351
|
+
} catch {}
|
|
352
|
+
}
|
|
353
|
+
const { toolCountsJson: _toolCountsJson, ...rest } = r;
|
|
354
|
+
return c.json(counts ? { ...rest, toolCounts: counts } : rest);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
logger.error('Failed to get session', err);
|
|
357
|
+
const errorResponse = serializeError(err);
|
|
358
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
169
359
|
}
|
|
360
|
+
},
|
|
361
|
+
);
|
|
170
362
|
|
|
171
|
-
|
|
363
|
+
// Update session preferences
|
|
364
|
+
openApiRoute(
|
|
365
|
+
app,
|
|
366
|
+
{
|
|
367
|
+
method: 'patch',
|
|
368
|
+
path: '/v1/sessions/{sessionId}',
|
|
369
|
+
tags: ['sessions'],
|
|
370
|
+
operationId: 'updateSession',
|
|
371
|
+
summary: 'Update session preferences',
|
|
372
|
+
parameters: [
|
|
373
|
+
{
|
|
374
|
+
in: 'path',
|
|
375
|
+
name: 'sessionId',
|
|
376
|
+
required: true,
|
|
377
|
+
schema: {
|
|
378
|
+
type: 'string',
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
in: 'query',
|
|
383
|
+
name: 'project',
|
|
384
|
+
required: false,
|
|
385
|
+
schema: {
|
|
386
|
+
type: 'string',
|
|
387
|
+
},
|
|
388
|
+
description:
|
|
389
|
+
'Project root override (defaults to current working directory).',
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
requestBody: {
|
|
393
|
+
required: true,
|
|
394
|
+
content: {
|
|
395
|
+
'application/json': {
|
|
396
|
+
schema: {
|
|
397
|
+
type: 'object',
|
|
398
|
+
properties: {
|
|
399
|
+
title: {
|
|
400
|
+
type: 'string',
|
|
401
|
+
},
|
|
402
|
+
agent: {
|
|
403
|
+
type: 'string',
|
|
404
|
+
},
|
|
405
|
+
provider: {
|
|
406
|
+
$ref: '#/components/schemas/Provider',
|
|
407
|
+
},
|
|
408
|
+
model: {
|
|
409
|
+
type: 'string',
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
responses: {
|
|
417
|
+
'200': {
|
|
418
|
+
description: 'OK',
|
|
419
|
+
content: {
|
|
420
|
+
'application/json': {
|
|
421
|
+
schema: {
|
|
422
|
+
$ref: '#/components/schemas/Session',
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
'400': {
|
|
428
|
+
description: 'Bad Request',
|
|
429
|
+
content: {
|
|
430
|
+
'application/json': {
|
|
431
|
+
schema: {
|
|
432
|
+
type: 'object',
|
|
433
|
+
properties: {
|
|
434
|
+
error: {
|
|
435
|
+
type: 'string',
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
required: ['error'],
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
'404': {
|
|
444
|
+
description: 'Bad Request',
|
|
445
|
+
content: {
|
|
446
|
+
'application/json': {
|
|
447
|
+
schema: {
|
|
448
|
+
type: 'object',
|
|
449
|
+
properties: {
|
|
450
|
+
error: {
|
|
451
|
+
type: 'string',
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
required: ['error'],
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
async (c) => {
|
|
462
|
+
try {
|
|
463
|
+
const sessionId = c.req.param('sessionId');
|
|
464
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
465
|
+
const cfg = await loadConfig(projectRoot);
|
|
466
|
+
const db = await getDb(cfg.projectRoot);
|
|
467
|
+
|
|
468
|
+
const body = (await c.req.json().catch(() => ({}))) as Record<
|
|
469
|
+
string,
|
|
470
|
+
unknown
|
|
471
|
+
>;
|
|
472
|
+
|
|
473
|
+
// Fetch existing session
|
|
474
|
+
const existingRows = await db
|
|
475
|
+
.select()
|
|
476
|
+
.from(sessions)
|
|
477
|
+
.where(eq(sessions.id, sessionId))
|
|
478
|
+
.limit(1);
|
|
172
479
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
480
|
+
if (!existingRows.length) {
|
|
481
|
+
return c.json({ error: 'Session not found' }, 404);
|
|
482
|
+
}
|
|
177
483
|
|
|
178
|
-
|
|
179
|
-
const updates: {
|
|
180
|
-
agent?: string;
|
|
181
|
-
provider?: string;
|
|
182
|
-
model?: string;
|
|
183
|
-
title?: string | null;
|
|
184
|
-
lastActiveAt?: number;
|
|
185
|
-
} = {
|
|
186
|
-
lastActiveAt: Date.now(),
|
|
187
|
-
};
|
|
484
|
+
const existingSession = existingRows[0];
|
|
188
485
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
486
|
+
// Verify session belongs to current project
|
|
487
|
+
if (existingSession.projectPath !== cfg.projectRoot) {
|
|
488
|
+
return c.json({ error: 'Session not found in this project' }, 404);
|
|
489
|
+
}
|
|
192
490
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
491
|
+
// Prepare update data
|
|
492
|
+
const updates: {
|
|
493
|
+
agent?: string;
|
|
494
|
+
provider?: string;
|
|
495
|
+
model?: string;
|
|
496
|
+
title?: string | null;
|
|
497
|
+
lastActiveAt?: number;
|
|
498
|
+
} = {
|
|
499
|
+
lastActiveAt: Date.now(),
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
if (typeof body.title === 'string') {
|
|
503
|
+
updates.title = body.title.trim() || null;
|
|
205
504
|
}
|
|
206
|
-
}
|
|
207
505
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
506
|
+
// Validate agent if provided
|
|
507
|
+
if (typeof body.agent === 'string') {
|
|
508
|
+
const agentName = body.agent.trim();
|
|
509
|
+
if (agentName) {
|
|
510
|
+
// Agent validation: check if it exists via resolveAgentConfig
|
|
511
|
+
try {
|
|
512
|
+
await resolveAgentConfig(cfg.projectRoot, agentName);
|
|
513
|
+
updates.agent = agentName;
|
|
514
|
+
} catch (err) {
|
|
515
|
+
logger.warn('Invalid agent provided', { agent: agentName, err });
|
|
516
|
+
return c.json({ error: `Invalid agent: ${agentName}` }, 400);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
215
519
|
}
|
|
216
|
-
}
|
|
217
520
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
validateProviderModel(targetProvider, modelName, cfg);
|
|
226
|
-
} catch {
|
|
227
|
-
return c.json(
|
|
228
|
-
{
|
|
229
|
-
error: `Model "${modelName}" not found for provider "${targetProvider}"`,
|
|
230
|
-
},
|
|
231
|
-
400,
|
|
232
|
-
);
|
|
521
|
+
// Validate provider if provided
|
|
522
|
+
if (typeof body.provider === 'string') {
|
|
523
|
+
const providerName = body.provider.trim();
|
|
524
|
+
if (providerName && hasConfiguredProvider(cfg, providerName)) {
|
|
525
|
+
updates.provider = providerName;
|
|
526
|
+
} else if (providerName) {
|
|
527
|
+
return c.json({ error: `Invalid provider: ${providerName}` }, 400);
|
|
233
528
|
}
|
|
529
|
+
}
|
|
234
530
|
|
|
235
|
-
|
|
531
|
+
// Validate model if provided (and optionally verify it belongs to provider)
|
|
532
|
+
if (typeof body.model === 'string') {
|
|
533
|
+
const modelName = body.model.trim();
|
|
534
|
+
if (modelName) {
|
|
535
|
+
const targetProvider = (updates.provider ||
|
|
536
|
+
existingSession.provider) as ProviderId;
|
|
537
|
+
try {
|
|
538
|
+
validateProviderModel(targetProvider, modelName, cfg);
|
|
539
|
+
} catch {
|
|
540
|
+
return c.json(
|
|
541
|
+
{
|
|
542
|
+
error: `Model "${modelName}" not found for provider "${targetProvider}"`,
|
|
543
|
+
},
|
|
544
|
+
400,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
updates.model = modelName;
|
|
549
|
+
}
|
|
236
550
|
}
|
|
237
|
-
}
|
|
238
551
|
|
|
239
|
-
|
|
240
|
-
|
|
552
|
+
// Perform update
|
|
553
|
+
await db
|
|
554
|
+
.update(sessions)
|
|
555
|
+
.set(updates)
|
|
556
|
+
.where(eq(sessions.id, sessionId));
|
|
241
557
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
558
|
+
// Return updated session
|
|
559
|
+
const updatedRows = await db
|
|
560
|
+
.select()
|
|
561
|
+
.from(sessions)
|
|
562
|
+
.where(eq(sessions.id, sessionId))
|
|
563
|
+
.limit(1);
|
|
248
564
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
565
|
+
return c.json(updatedRows[0]);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
logger.error('Failed to update session', err);
|
|
568
|
+
const errorResponse = serializeError(err);
|
|
569
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
);
|
|
256
573
|
|
|
257
574
|
// Delete session
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
575
|
+
openApiRoute(
|
|
576
|
+
app,
|
|
577
|
+
{
|
|
578
|
+
method: 'delete',
|
|
579
|
+
path: '/v1/sessions/{sessionId}',
|
|
580
|
+
tags: ['sessions'],
|
|
581
|
+
operationId: 'deleteSession',
|
|
582
|
+
summary: 'Delete a session',
|
|
583
|
+
parameters: [
|
|
584
|
+
{
|
|
585
|
+
in: 'path',
|
|
586
|
+
name: 'sessionId',
|
|
587
|
+
required: true,
|
|
588
|
+
schema: {
|
|
589
|
+
type: 'string',
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
in: 'query',
|
|
594
|
+
name: 'project',
|
|
595
|
+
required: false,
|
|
596
|
+
schema: {
|
|
597
|
+
type: 'string',
|
|
598
|
+
},
|
|
599
|
+
description:
|
|
600
|
+
'Project root override (defaults to current working directory).',
|
|
601
|
+
},
|
|
602
|
+
],
|
|
603
|
+
responses: {
|
|
604
|
+
'200': {
|
|
605
|
+
description: 'OK',
|
|
606
|
+
content: {
|
|
607
|
+
'application/json': {
|
|
608
|
+
schema: {
|
|
609
|
+
type: 'object',
|
|
610
|
+
properties: {
|
|
611
|
+
success: {
|
|
612
|
+
type: 'boolean',
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
required: ['success'],
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
'404': {
|
|
621
|
+
description: 'Bad Request',
|
|
622
|
+
content: {
|
|
623
|
+
'application/json': {
|
|
624
|
+
schema: {
|
|
625
|
+
type: 'object',
|
|
626
|
+
properties: {
|
|
627
|
+
error: {
|
|
628
|
+
type: 'string',
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
required: ['error'],
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
async (c) => {
|
|
639
|
+
try {
|
|
640
|
+
const sessionId = c.req.param('sessionId');
|
|
641
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
642
|
+
const cfg = await loadConfig(projectRoot);
|
|
643
|
+
const db = await getDb(cfg.projectRoot);
|
|
264
644
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
645
|
+
const existingRows = await db
|
|
646
|
+
.select()
|
|
647
|
+
.from(sessions)
|
|
648
|
+
.where(eq(sessions.id, sessionId))
|
|
649
|
+
.limit(1);
|
|
270
650
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
651
|
+
if (!existingRows.length) {
|
|
652
|
+
return c.json({ error: 'Session not found' }, 404);
|
|
653
|
+
}
|
|
274
654
|
|
|
275
|
-
|
|
655
|
+
const existingSession = existingRows[0];
|
|
276
656
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
657
|
+
if (existingSession.projectPath !== cfg.projectRoot) {
|
|
658
|
+
return c.json({ error: 'Session not found in this project' }, 404);
|
|
659
|
+
}
|
|
280
660
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
661
|
+
await db
|
|
662
|
+
.delete(messageParts)
|
|
663
|
+
.where(
|
|
664
|
+
inArray(
|
|
665
|
+
messageParts.messageId,
|
|
666
|
+
db
|
|
667
|
+
.select({ id: messages.id })
|
|
668
|
+
.from(messages)
|
|
669
|
+
.where(eq(messages.sessionId, sessionId)),
|
|
670
|
+
),
|
|
671
|
+
);
|
|
672
|
+
await db.delete(messages).where(eq(messages.sessionId, sessionId));
|
|
673
|
+
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
|
294
674
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
675
|
+
return c.json({ success: true });
|
|
676
|
+
} catch (err) {
|
|
677
|
+
logger.error('Failed to delete session', err);
|
|
678
|
+
const errorResponse = serializeError(err);
|
|
679
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
);
|
|
302
683
|
|
|
303
684
|
// Abort session stream
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
685
|
+
openApiRoute(
|
|
686
|
+
app,
|
|
687
|
+
{
|
|
688
|
+
method: 'delete',
|
|
689
|
+
path: '/v1/sessions/{sessionId}/abort',
|
|
690
|
+
tags: ['sessions'],
|
|
691
|
+
operationId: 'abortSession',
|
|
692
|
+
summary: 'Abort a running session',
|
|
693
|
+
description:
|
|
694
|
+
'Aborts any currently running assistant generation for the session',
|
|
695
|
+
parameters: [
|
|
696
|
+
{
|
|
697
|
+
in: 'path',
|
|
698
|
+
name: 'sessionId',
|
|
699
|
+
required: true,
|
|
700
|
+
schema: {
|
|
701
|
+
type: 'string',
|
|
702
|
+
},
|
|
703
|
+
description: 'Session ID to abort',
|
|
704
|
+
},
|
|
705
|
+
],
|
|
706
|
+
responses: {
|
|
707
|
+
'200': {
|
|
708
|
+
description: 'OK',
|
|
709
|
+
content: {
|
|
710
|
+
'application/json': {
|
|
711
|
+
schema: {
|
|
712
|
+
type: 'object',
|
|
713
|
+
properties: {
|
|
714
|
+
success: {
|
|
715
|
+
type: 'boolean',
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
required: ['success'],
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
async (c) => {
|
|
726
|
+
const sessionId = c.req.param('sessionId');
|
|
727
|
+
const body = (await c.req.json().catch(() => ({}))) as Record<
|
|
728
|
+
string,
|
|
729
|
+
unknown
|
|
730
|
+
>;
|
|
731
|
+
const messageId =
|
|
732
|
+
typeof body.messageId === 'string' ? body.messageId : undefined;
|
|
733
|
+
const clearQueue = body.clearQueue === true;
|
|
326
734
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
735
|
+
const { abortSession, abortMessage } = await import(
|
|
736
|
+
'../runtime/agent/runner.ts'
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
if (messageId) {
|
|
740
|
+
const result = abortMessage(sessionId, messageId);
|
|
741
|
+
return c.json({
|
|
742
|
+
success: result.removed,
|
|
743
|
+
wasRunning: result.wasRunning,
|
|
744
|
+
messageId,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
abortSession(sessionId, clearQueue);
|
|
749
|
+
return c.json({ success: true });
|
|
750
|
+
},
|
|
751
|
+
);
|
|
330
752
|
|
|
331
753
|
// Get queue state for a session
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
754
|
+
openApiRoute(
|
|
755
|
+
app,
|
|
756
|
+
{
|
|
757
|
+
method: 'get',
|
|
758
|
+
path: '/v1/sessions/{sessionId}/queue',
|
|
759
|
+
tags: ['sessions'],
|
|
760
|
+
operationId: 'getSessionQueue',
|
|
761
|
+
summary: 'Get queue state for a session',
|
|
762
|
+
parameters: [
|
|
763
|
+
{
|
|
764
|
+
in: 'path',
|
|
765
|
+
name: 'sessionId',
|
|
766
|
+
required: true,
|
|
767
|
+
schema: {
|
|
768
|
+
type: 'string',
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
],
|
|
772
|
+
responses: {
|
|
773
|
+
'200': {
|
|
774
|
+
description: 'OK',
|
|
775
|
+
content: {
|
|
776
|
+
'application/json': {
|
|
777
|
+
schema: {
|
|
778
|
+
type: 'object',
|
|
779
|
+
properties: {
|
|
780
|
+
currentMessageId: {
|
|
781
|
+
type: 'string',
|
|
782
|
+
nullable: true,
|
|
783
|
+
},
|
|
784
|
+
queuedMessages: {
|
|
785
|
+
type: 'array',
|
|
786
|
+
items: {
|
|
787
|
+
type: 'object',
|
|
788
|
+
properties: {
|
|
789
|
+
assistantMessageId: {
|
|
790
|
+
type: 'string',
|
|
791
|
+
},
|
|
792
|
+
agent: {
|
|
793
|
+
type: 'string',
|
|
794
|
+
},
|
|
795
|
+
provider: {
|
|
796
|
+
type: 'string',
|
|
797
|
+
},
|
|
798
|
+
model: {
|
|
799
|
+
type: 'string',
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
isRunning: {
|
|
805
|
+
type: 'boolean',
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
required: ['currentMessageId', 'queuedMessages', 'isRunning'],
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
},
|
|
341
813
|
},
|
|
342
|
-
|
|
343
|
-
|
|
814
|
+
},
|
|
815
|
+
async (c) => {
|
|
816
|
+
const sessionId = c.req.param('sessionId');
|
|
817
|
+
const { getQueueState } = await import('../runtime/session/queue.ts');
|
|
818
|
+
const state = getQueueState(sessionId);
|
|
819
|
+
return c.json(
|
|
820
|
+
state ?? {
|
|
821
|
+
currentMessageId: null,
|
|
822
|
+
queuedMessages: [],
|
|
823
|
+
isRunning: false,
|
|
824
|
+
},
|
|
825
|
+
);
|
|
826
|
+
},
|
|
827
|
+
);
|
|
344
828
|
|
|
345
829
|
// Remove a message from the queue
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
'
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
830
|
+
openApiRoute(
|
|
831
|
+
app,
|
|
832
|
+
{
|
|
833
|
+
method: 'delete',
|
|
834
|
+
path: '/v1/sessions/{sessionId}/queue/{messageId}',
|
|
835
|
+
tags: ['sessions'],
|
|
836
|
+
operationId: 'removeFromQueue',
|
|
837
|
+
summary: 'Remove a message from session queue',
|
|
838
|
+
parameters: [
|
|
839
|
+
{
|
|
840
|
+
in: 'path',
|
|
841
|
+
name: 'sessionId',
|
|
842
|
+
required: true,
|
|
843
|
+
schema: {
|
|
844
|
+
type: 'string',
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
in: 'path',
|
|
849
|
+
name: 'messageId',
|
|
850
|
+
required: true,
|
|
851
|
+
schema: {
|
|
852
|
+
type: 'string',
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
in: 'query',
|
|
857
|
+
name: 'project',
|
|
858
|
+
required: false,
|
|
859
|
+
schema: {
|
|
860
|
+
type: 'string',
|
|
861
|
+
},
|
|
862
|
+
description:
|
|
863
|
+
'Project root override (defaults to current working directory).',
|
|
864
|
+
},
|
|
865
|
+
],
|
|
866
|
+
responses: {
|
|
867
|
+
'200': {
|
|
868
|
+
description: 'OK',
|
|
869
|
+
content: {
|
|
870
|
+
'application/json': {
|
|
871
|
+
schema: {
|
|
872
|
+
type: 'object',
|
|
873
|
+
properties: {
|
|
874
|
+
success: {
|
|
875
|
+
type: 'boolean',
|
|
876
|
+
},
|
|
877
|
+
removed: {
|
|
878
|
+
type: 'boolean',
|
|
879
|
+
},
|
|
880
|
+
wasQueued: {
|
|
881
|
+
type: 'boolean',
|
|
882
|
+
},
|
|
883
|
+
wasRunning: {
|
|
884
|
+
type: 'boolean',
|
|
885
|
+
},
|
|
886
|
+
wasStored: {
|
|
887
|
+
type: 'boolean',
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
required: ['success'],
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
'404': {
|
|
896
|
+
description: 'Bad Request',
|
|
897
|
+
content: {
|
|
898
|
+
'application/json': {
|
|
899
|
+
schema: {
|
|
900
|
+
type: 'object',
|
|
901
|
+
properties: {
|
|
902
|
+
error: {
|
|
903
|
+
type: 'string',
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
required: ['error'],
|
|
907
|
+
},
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
},
|
|
913
|
+
async (c) => {
|
|
914
|
+
const sessionId = c.req.param('sessionId');
|
|
915
|
+
const messageId = c.req.param('messageId');
|
|
916
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
917
|
+
const cfg = await loadConfig(projectRoot);
|
|
918
|
+
const db = await getDb(cfg.projectRoot);
|
|
919
|
+
const { removeFromQueue, abortMessage } = await import(
|
|
920
|
+
'../runtime/session/queue.ts'
|
|
921
|
+
);
|
|
367
922
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
923
|
+
// First try to remove from queue (queued messages)
|
|
924
|
+
const removed = removeFromQueue(sessionId, messageId);
|
|
925
|
+
if (removed) {
|
|
926
|
+
// Delete messages from database
|
|
927
|
+
try {
|
|
928
|
+
// Find the assistant message to get its creation time
|
|
929
|
+
const assistantMsg = await db
|
|
371
930
|
.select()
|
|
372
931
|
.from(messages)
|
|
373
|
-
.where(
|
|
374
|
-
and(eq(messages.sessionId, sessionId), eq(messages.role, 'user')),
|
|
375
|
-
)
|
|
376
|
-
.orderBy(desc(messages.createdAt))
|
|
932
|
+
.where(eq(messages.id, messageId))
|
|
377
933
|
.limit(1);
|
|
378
934
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
935
|
+
if (assistantMsg.length > 0) {
|
|
936
|
+
// Find the user message that came right before (same session, created just before)
|
|
937
|
+
const userMsg = await db
|
|
938
|
+
.select()
|
|
939
|
+
.from(messages)
|
|
940
|
+
.where(
|
|
941
|
+
and(
|
|
942
|
+
eq(messages.sessionId, sessionId),
|
|
943
|
+
eq(messages.role, 'user'),
|
|
944
|
+
),
|
|
945
|
+
)
|
|
946
|
+
.orderBy(desc(messages.createdAt))
|
|
947
|
+
.limit(1);
|
|
948
|
+
|
|
949
|
+
const messageIdsToDelete = [messageId];
|
|
950
|
+
if (userMsg.length > 0) {
|
|
951
|
+
messageIdsToDelete.push(userMsg[0].id);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Delete message parts first (foreign key constraint)
|
|
955
|
+
await db
|
|
956
|
+
.delete(messageParts)
|
|
957
|
+
.where(inArray(messageParts.messageId, messageIdsToDelete));
|
|
958
|
+
// Delete messages
|
|
959
|
+
await db
|
|
960
|
+
.delete(messages)
|
|
961
|
+
.where(inArray(messages.id, messageIdsToDelete));
|
|
382
962
|
}
|
|
963
|
+
} catch (err) {
|
|
964
|
+
logger.error('Failed to delete queued messages from DB', err);
|
|
965
|
+
}
|
|
966
|
+
return c.json({ success: true, removed: true, wasQueued: true });
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// If not in queue, try to abort (might be running)
|
|
970
|
+
const result = abortMessage(sessionId, messageId);
|
|
971
|
+
if (result.removed) {
|
|
972
|
+
return c.json({
|
|
973
|
+
success: true,
|
|
974
|
+
removed: true,
|
|
975
|
+
wasQueued: false,
|
|
976
|
+
wasRunning: result.wasRunning,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// If not queued or running, try to delete directly from database
|
|
981
|
+
// This handles system messages (like injected research context)
|
|
982
|
+
try {
|
|
983
|
+
const existingMsg = await db
|
|
984
|
+
.select()
|
|
985
|
+
.from(messages)
|
|
986
|
+
.where(
|
|
987
|
+
and(eq(messages.id, messageId), eq(messages.sessionId, sessionId)),
|
|
988
|
+
)
|
|
989
|
+
.limit(1);
|
|
383
990
|
|
|
991
|
+
if (existingMsg.length > 0) {
|
|
384
992
|
// Delete message parts first (foreign key constraint)
|
|
385
993
|
await db
|
|
386
994
|
.delete(messageParts)
|
|
387
|
-
.where(
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
995
|
+
.where(
|
|
996
|
+
and(
|
|
997
|
+
eq(messageParts.messageId, messageId),
|
|
998
|
+
or(
|
|
999
|
+
eq(messageParts.type, 'error'),
|
|
1000
|
+
and(
|
|
1001
|
+
eq(messageParts.type, 'tool_call'),
|
|
1002
|
+
eq(messageParts.toolName, 'finish'),
|
|
1003
|
+
),
|
|
1004
|
+
),
|
|
1005
|
+
),
|
|
1006
|
+
);
|
|
1007
|
+
// Delete message
|
|
1008
|
+
await db.delete(messages).where(eq(messages.id, messageId));
|
|
1009
|
+
|
|
1010
|
+
return c.json({ success: true, removed: true, wasStored: true });
|
|
392
1011
|
}
|
|
393
1012
|
} catch (err) {
|
|
394
|
-
logger.error('Failed to delete
|
|
1013
|
+
logger.error('Failed to delete message from DB', err);
|
|
1014
|
+
return c.json(
|
|
1015
|
+
{ success: false, error: 'Failed to delete message' },
|
|
1016
|
+
500,
|
|
1017
|
+
);
|
|
395
1018
|
}
|
|
396
|
-
return c.json({ success: true, removed: true, wasQueued: true });
|
|
397
|
-
}
|
|
398
1019
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
1020
|
+
return c.json({ success: false, removed: false }, 404);
|
|
1021
|
+
},
|
|
1022
|
+
);
|
|
1023
|
+
|
|
1024
|
+
openApiRoute(
|
|
1025
|
+
app,
|
|
1026
|
+
{
|
|
1027
|
+
method: 'get',
|
|
1028
|
+
path: '/v1/sessions/{sessionId}/share',
|
|
1029
|
+
tags: ['sessions'],
|
|
1030
|
+
operationId: 'getShareStatus',
|
|
1031
|
+
summary: 'Get share status for a session',
|
|
1032
|
+
parameters: [
|
|
1033
|
+
{
|
|
1034
|
+
in: 'path',
|
|
1035
|
+
name: 'sessionId',
|
|
1036
|
+
required: true,
|
|
1037
|
+
schema: {
|
|
1038
|
+
type: 'string',
|
|
1039
|
+
},
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
in: 'query',
|
|
1043
|
+
name: 'project',
|
|
1044
|
+
required: false,
|
|
1045
|
+
schema: {
|
|
1046
|
+
type: 'string',
|
|
1047
|
+
},
|
|
1048
|
+
description:
|
|
1049
|
+
'Project root override (defaults to current working directory).',
|
|
1050
|
+
},
|
|
1051
|
+
],
|
|
1052
|
+
responses: {
|
|
1053
|
+
'200': {
|
|
1054
|
+
description: 'OK',
|
|
1055
|
+
content: {
|
|
1056
|
+
'application/json': {
|
|
1057
|
+
schema: {
|
|
1058
|
+
type: 'object',
|
|
1059
|
+
properties: {
|
|
1060
|
+
shared: {
|
|
1061
|
+
type: 'boolean',
|
|
1062
|
+
},
|
|
1063
|
+
shareId: {
|
|
1064
|
+
type: 'string',
|
|
1065
|
+
},
|
|
1066
|
+
url: {
|
|
1067
|
+
type: 'string',
|
|
1068
|
+
},
|
|
1069
|
+
title: {
|
|
1070
|
+
type: 'string',
|
|
1071
|
+
nullable: true,
|
|
1072
|
+
},
|
|
1073
|
+
createdAt: {
|
|
1074
|
+
type: 'integer',
|
|
1075
|
+
},
|
|
1076
|
+
lastSyncedAt: {
|
|
1077
|
+
type: 'integer',
|
|
1078
|
+
},
|
|
1079
|
+
lastSyncedMessageId: {
|
|
1080
|
+
type: 'string',
|
|
1081
|
+
},
|
|
1082
|
+
syncedMessages: {
|
|
1083
|
+
type: 'integer',
|
|
1084
|
+
},
|
|
1085
|
+
totalMessages: {
|
|
1086
|
+
type: 'integer',
|
|
1087
|
+
},
|
|
1088
|
+
pendingMessages: {
|
|
1089
|
+
type: 'integer',
|
|
1090
|
+
},
|
|
1091
|
+
isSynced: {
|
|
1092
|
+
type: 'boolean',
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
required: ['shared'],
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
async (c) => {
|
|
1103
|
+
const sessionId = c.req.param('sessionId');
|
|
1104
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
1105
|
+
const cfg = await loadConfig(projectRoot);
|
|
1106
|
+
const db = await getDb(cfg.projectRoot);
|
|
409
1107
|
|
|
410
|
-
|
|
411
|
-
// This handles system messages (like injected research context)
|
|
412
|
-
try {
|
|
413
|
-
const existingMsg = await db
|
|
1108
|
+
const share = await db
|
|
414
1109
|
.select()
|
|
415
|
-
.from(
|
|
416
|
-
.where(
|
|
417
|
-
and(eq(messages.id, messageId), eq(messages.sessionId, sessionId)),
|
|
418
|
-
)
|
|
1110
|
+
.from(shares)
|
|
1111
|
+
.where(eq(shares.sessionId, sessionId))
|
|
419
1112
|
.limit(1);
|
|
420
1113
|
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
await db
|
|
424
|
-
.delete(messageParts)
|
|
425
|
-
.where(
|
|
426
|
-
and(
|
|
427
|
-
eq(messageParts.messageId, messageId),
|
|
428
|
-
or(
|
|
429
|
-
eq(messageParts.type, 'error'),
|
|
430
|
-
and(
|
|
431
|
-
eq(messageParts.type, 'tool_call'),
|
|
432
|
-
eq(messageParts.toolName, 'finish'),
|
|
433
|
-
),
|
|
434
|
-
),
|
|
435
|
-
),
|
|
436
|
-
);
|
|
437
|
-
// Delete message
|
|
438
|
-
await db.delete(messages).where(eq(messages.id, messageId));
|
|
439
|
-
|
|
440
|
-
return c.json({ success: true, removed: true, wasStored: true });
|
|
1114
|
+
if (!share.length) {
|
|
1115
|
+
return c.json({ shared: false });
|
|
441
1116
|
}
|
|
442
|
-
} catch (err) {
|
|
443
|
-
logger.error('Failed to delete message from DB', err);
|
|
444
|
-
return c.json({ success: false, error: 'Failed to delete message' }, 500);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return c.json({ success: false, removed: false }, 404);
|
|
448
|
-
});
|
|
449
1117
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const share = await db
|
|
457
|
-
.select()
|
|
458
|
-
.from(shares)
|
|
459
|
-
.where(eq(shares.sessionId, sessionId))
|
|
460
|
-
.limit(1);
|
|
1118
|
+
const allMessages = await db
|
|
1119
|
+
.select({ id: messages.id })
|
|
1120
|
+
.from(messages)
|
|
1121
|
+
.where(eq(messages.sessionId, sessionId))
|
|
1122
|
+
.orderBy(messages.createdAt);
|
|
461
1123
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
1124
|
+
const totalMessages = allMessages.length;
|
|
1125
|
+
const syncedIdx = allMessages.findIndex(
|
|
1126
|
+
(m) => m.id === share[0].lastSyncedMessageId,
|
|
1127
|
+
);
|
|
1128
|
+
const syncedMessages = syncedIdx === -1 ? 0 : syncedIdx + 1;
|
|
1129
|
+
const pendingMessages = totalMessages - syncedMessages;
|
|
465
1130
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
shareId: share[0].shareId,
|
|
482
|
-
url: share[0].url,
|
|
483
|
-
title: share[0].title,
|
|
484
|
-
createdAt: share[0].createdAt,
|
|
485
|
-
lastSyncedAt: share[0].lastSyncedAt,
|
|
486
|
-
lastSyncedMessageId: share[0].lastSyncedMessageId,
|
|
487
|
-
syncedMessages,
|
|
488
|
-
totalMessages,
|
|
489
|
-
pendingMessages,
|
|
490
|
-
isSynced: pendingMessages === 0,
|
|
491
|
-
});
|
|
492
|
-
});
|
|
1131
|
+
return c.json({
|
|
1132
|
+
shared: true,
|
|
1133
|
+
shareId: share[0].shareId,
|
|
1134
|
+
url: share[0].url,
|
|
1135
|
+
title: share[0].title,
|
|
1136
|
+
createdAt: share[0].createdAt,
|
|
1137
|
+
lastSyncedAt: share[0].lastSyncedAt,
|
|
1138
|
+
lastSyncedMessageId: share[0].lastSyncedMessageId,
|
|
1139
|
+
syncedMessages,
|
|
1140
|
+
totalMessages,
|
|
1141
|
+
pendingMessages,
|
|
1142
|
+
isSynced: pendingMessages === 0,
|
|
1143
|
+
});
|
|
1144
|
+
},
|
|
1145
|
+
);
|
|
493
1146
|
|
|
494
1147
|
const SHARE_API_URL =
|
|
495
1148
|
process.env.OTTO_SHARE_API_URL || 'https://api.share.ottocode.io';
|
|
@@ -502,430 +1155,827 @@ export function registerSessionsRoutes(app: Hono) {
|
|
|
502
1155
|
}
|
|
503
1156
|
}
|
|
504
1157
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1158
|
+
openApiRoute(
|
|
1159
|
+
app,
|
|
1160
|
+
{
|
|
1161
|
+
method: 'post',
|
|
1162
|
+
path: '/v1/sessions/{sessionId}/share',
|
|
1163
|
+
tags: ['sessions'],
|
|
1164
|
+
operationId: 'shareSession',
|
|
1165
|
+
summary: 'Share a session',
|
|
1166
|
+
parameters: [
|
|
1167
|
+
{
|
|
1168
|
+
in: 'path',
|
|
1169
|
+
name: 'sessionId',
|
|
1170
|
+
required: true,
|
|
1171
|
+
schema: {
|
|
1172
|
+
type: 'string',
|
|
1173
|
+
},
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
in: 'query',
|
|
1177
|
+
name: 'project',
|
|
1178
|
+
required: false,
|
|
1179
|
+
schema: {
|
|
1180
|
+
type: 'string',
|
|
1181
|
+
},
|
|
1182
|
+
description:
|
|
1183
|
+
'Project root override (defaults to current working directory).',
|
|
1184
|
+
},
|
|
1185
|
+
],
|
|
1186
|
+
responses: {
|
|
1187
|
+
'200': {
|
|
1188
|
+
description: 'OK',
|
|
1189
|
+
content: {
|
|
1190
|
+
'application/json': {
|
|
1191
|
+
schema: {
|
|
1192
|
+
type: 'object',
|
|
1193
|
+
properties: {
|
|
1194
|
+
shared: {
|
|
1195
|
+
type: 'boolean',
|
|
1196
|
+
},
|
|
1197
|
+
shareId: {
|
|
1198
|
+
type: 'string',
|
|
1199
|
+
},
|
|
1200
|
+
url: {
|
|
1201
|
+
type: 'string',
|
|
1202
|
+
},
|
|
1203
|
+
message: {
|
|
1204
|
+
type: 'string',
|
|
1205
|
+
},
|
|
1206
|
+
},
|
|
1207
|
+
required: ['shared'],
|
|
1208
|
+
},
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
},
|
|
1212
|
+
'400': {
|
|
1213
|
+
description: 'Bad Request',
|
|
1214
|
+
content: {
|
|
1215
|
+
'application/json': {
|
|
1216
|
+
schema: {
|
|
1217
|
+
type: 'object',
|
|
1218
|
+
properties: {
|
|
1219
|
+
error: {
|
|
1220
|
+
type: 'string',
|
|
1221
|
+
},
|
|
1222
|
+
},
|
|
1223
|
+
required: ['error'],
|
|
1224
|
+
},
|
|
1225
|
+
},
|
|
1226
|
+
},
|
|
1227
|
+
},
|
|
1228
|
+
'404': {
|
|
1229
|
+
description: 'Bad Request',
|
|
1230
|
+
content: {
|
|
1231
|
+
'application/json': {
|
|
1232
|
+
schema: {
|
|
1233
|
+
type: 'object',
|
|
1234
|
+
properties: {
|
|
1235
|
+
error: {
|
|
1236
|
+
type: 'string',
|
|
1237
|
+
},
|
|
1238
|
+
},
|
|
1239
|
+
required: ['error'],
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1242
|
+
},
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
async (c) => {
|
|
1247
|
+
const sessionId = c.req.param('sessionId');
|
|
1248
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
1249
|
+
const cfg = await loadConfig(projectRoot);
|
|
1250
|
+
const db = await getDb(cfg.projectRoot);
|
|
539
1251
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1252
|
+
const session = await db
|
|
1253
|
+
.select()
|
|
1254
|
+
.from(sessions)
|
|
1255
|
+
.where(eq(sessions.id, sessionId))
|
|
1256
|
+
.limit(1);
|
|
1257
|
+
if (!session.length) {
|
|
1258
|
+
return c.json({ error: 'Session not found' }, 404);
|
|
1259
|
+
}
|
|
543
1260
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const list = partsByMessage.get(part.messageId) || [];
|
|
558
|
-
list.push(part);
|
|
559
|
-
partsByMessage.set(part.messageId, list);
|
|
560
|
-
}
|
|
1261
|
+
const existingShare = await db
|
|
1262
|
+
.select()
|
|
1263
|
+
.from(shares)
|
|
1264
|
+
.where(eq(shares.sessionId, sessionId))
|
|
1265
|
+
.limit(1);
|
|
1266
|
+
if (existingShare.length) {
|
|
1267
|
+
return c.json({
|
|
1268
|
+
shared: true,
|
|
1269
|
+
shareId: existingShare[0].shareId,
|
|
1270
|
+
url: existingShare[0].url,
|
|
1271
|
+
message: 'Already shared',
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
561
1274
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
username: getUsername(),
|
|
568
|
-
agent: sess.agent,
|
|
569
|
-
provider: sess.provider,
|
|
570
|
-
model: sess.model,
|
|
571
|
-
createdAt: sess.createdAt,
|
|
572
|
-
stats: {
|
|
573
|
-
inputTokens: sess.totalInputTokens ?? 0,
|
|
574
|
-
outputTokens: sess.totalOutputTokens ?? 0,
|
|
575
|
-
cachedTokens: sess.totalCachedTokens ?? 0,
|
|
576
|
-
cacheCreationTokens: sess.totalCacheCreationTokens ?? 0,
|
|
577
|
-
reasoningTokens: sess.totalReasoningTokens ?? 0,
|
|
578
|
-
toolTimeMs: sess.totalToolTimeMs ?? 0,
|
|
579
|
-
toolCounts: sess.toolCountsJson ? JSON.parse(sess.toolCountsJson) : {},
|
|
580
|
-
},
|
|
581
|
-
messages: allMessages.map((m) => ({
|
|
582
|
-
id: m.id,
|
|
583
|
-
role: m.role,
|
|
584
|
-
createdAt: m.createdAt,
|
|
585
|
-
parts: (partsByMessage.get(m.id) || []).map((p) => ({
|
|
586
|
-
type: p.type,
|
|
587
|
-
content: p.content,
|
|
588
|
-
toolName: p.toolName,
|
|
589
|
-
toolCallId: p.toolCallId,
|
|
590
|
-
})),
|
|
591
|
-
})),
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
const res = await fetch(`${SHARE_API_URL}/share`, {
|
|
595
|
-
method: 'POST',
|
|
596
|
-
headers: { 'Content-Type': 'application/json' },
|
|
597
|
-
body: JSON.stringify({
|
|
598
|
-
sessionData,
|
|
599
|
-
title: sess.title,
|
|
600
|
-
lastMessageId,
|
|
601
|
-
}),
|
|
602
|
-
});
|
|
1275
|
+
const allMessages = await db
|
|
1276
|
+
.select()
|
|
1277
|
+
.from(messages)
|
|
1278
|
+
.where(eq(messages.sessionId, sessionId))
|
|
1279
|
+
.orderBy(messages.createdAt);
|
|
603
1280
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
}
|
|
1281
|
+
if (!allMessages.length) {
|
|
1282
|
+
return c.json({ error: 'Session has no messages' }, 400);
|
|
1283
|
+
}
|
|
608
1284
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
url: data.url,
|
|
620
|
-
title: sess.title,
|
|
621
|
-
description: null,
|
|
622
|
-
createdAt: Date.now(),
|
|
623
|
-
lastSyncedAt: Date.now(),
|
|
624
|
-
lastSyncedMessageId: lastMessageId,
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
return c.json({
|
|
628
|
-
shared: true,
|
|
629
|
-
shareId: data.shareId,
|
|
630
|
-
url: data.url,
|
|
631
|
-
});
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
app.put('/v1/sessions/:sessionId/share', async (c) => {
|
|
635
|
-
const sessionId = c.req.param('sessionId');
|
|
636
|
-
const projectRoot = c.req.query('project') || process.cwd();
|
|
637
|
-
const cfg = await loadConfig(projectRoot);
|
|
638
|
-
const db = await getDb(cfg.projectRoot);
|
|
639
|
-
|
|
640
|
-
const share = await db
|
|
641
|
-
.select()
|
|
642
|
-
.from(shares)
|
|
643
|
-
.where(eq(shares.sessionId, sessionId))
|
|
644
|
-
.limit(1);
|
|
645
|
-
if (!share.length) {
|
|
646
|
-
return c.json({ error: 'Session not shared. Use share first.' }, 400);
|
|
647
|
-
}
|
|
1285
|
+
const msgParts = await db
|
|
1286
|
+
.select()
|
|
1287
|
+
.from(messageParts)
|
|
1288
|
+
.where(
|
|
1289
|
+
inArray(
|
|
1290
|
+
messageParts.messageId,
|
|
1291
|
+
allMessages.map((m) => m.id),
|
|
1292
|
+
),
|
|
1293
|
+
)
|
|
1294
|
+
.orderBy(messageParts.index);
|
|
648
1295
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
return c.json({ error: 'Session not found' }, 404);
|
|
656
|
-
}
|
|
1296
|
+
const partsByMessage = new Map<string, typeof msgParts>();
|
|
1297
|
+
for (const part of msgParts) {
|
|
1298
|
+
const list = partsByMessage.get(part.messageId) || [];
|
|
1299
|
+
list.push(part);
|
|
1300
|
+
partsByMessage.set(part.messageId, list);
|
|
1301
|
+
}
|
|
657
1302
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
.from(messages)
|
|
661
|
-
.where(eq(messages.sessionId, sessionId))
|
|
662
|
-
.orderBy(messages.createdAt);
|
|
663
|
-
|
|
664
|
-
const msgParts = await db
|
|
665
|
-
.select()
|
|
666
|
-
.from(messageParts)
|
|
667
|
-
.where(
|
|
668
|
-
inArray(
|
|
669
|
-
messageParts.messageId,
|
|
670
|
-
allMessages.map((m) => m.id),
|
|
671
|
-
),
|
|
672
|
-
)
|
|
673
|
-
.orderBy(messageParts.index);
|
|
674
|
-
|
|
675
|
-
const partsByMessage = new Map<string, typeof msgParts>();
|
|
676
|
-
for (const part of msgParts) {
|
|
677
|
-
const list = partsByMessage.get(part.messageId) || [];
|
|
678
|
-
list.push(part);
|
|
679
|
-
partsByMessage.set(part.messageId, list);
|
|
680
|
-
}
|
|
1303
|
+
const lastMessageId = allMessages[allMessages.length - 1].id;
|
|
1304
|
+
const sess = session[0];
|
|
681
1305
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1306
|
+
const sessionData = {
|
|
1307
|
+
title: sess.title,
|
|
1308
|
+
username: getUsername(),
|
|
1309
|
+
agent: sess.agent,
|
|
1310
|
+
provider: sess.provider,
|
|
1311
|
+
model: sess.model,
|
|
1312
|
+
createdAt: sess.createdAt,
|
|
1313
|
+
stats: {
|
|
1314
|
+
inputTokens: sess.totalInputTokens ?? 0,
|
|
1315
|
+
outputTokens: sess.totalOutputTokens ?? 0,
|
|
1316
|
+
cachedTokens: sess.totalCachedTokens ?? 0,
|
|
1317
|
+
cacheCreationTokens: sess.totalCacheCreationTokens ?? 0,
|
|
1318
|
+
reasoningTokens: sess.totalReasoningTokens ?? 0,
|
|
1319
|
+
toolTimeMs: sess.totalToolTimeMs ?? 0,
|
|
1320
|
+
toolCounts: sess.toolCountsJson
|
|
1321
|
+
? JSON.parse(sess.toolCountsJson)
|
|
1322
|
+
: {},
|
|
1323
|
+
},
|
|
1324
|
+
messages: allMessages.map((m) => ({
|
|
1325
|
+
id: m.id,
|
|
1326
|
+
role: m.role,
|
|
1327
|
+
createdAt: m.createdAt,
|
|
1328
|
+
parts: (partsByMessage.get(m.id) || []).map((p) => ({
|
|
1329
|
+
type: p.type,
|
|
1330
|
+
content: p.content,
|
|
1331
|
+
toolName: p.toolName,
|
|
1332
|
+
toolCallId: p.toolCallId,
|
|
1333
|
+
})),
|
|
1334
|
+
})),
|
|
1335
|
+
};
|
|
689
1336
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1337
|
+
const res = await fetch(`${SHARE_API_URL}/share`, {
|
|
1338
|
+
method: 'POST',
|
|
1339
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1340
|
+
body: JSON.stringify({
|
|
1341
|
+
sessionData,
|
|
1342
|
+
title: sess.title,
|
|
1343
|
+
lastMessageId,
|
|
1344
|
+
}),
|
|
696
1345
|
});
|
|
697
|
-
}
|
|
698
1346
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
agent: sess.agent,
|
|
704
|
-
provider: sess.provider,
|
|
705
|
-
model: sess.model,
|
|
706
|
-
createdAt: sess.createdAt,
|
|
707
|
-
stats: {
|
|
708
|
-
inputTokens: sess.totalInputTokens ?? 0,
|
|
709
|
-
outputTokens: sess.totalOutputTokens ?? 0,
|
|
710
|
-
cachedTokens: sess.totalCachedTokens ?? 0,
|
|
711
|
-
cacheCreationTokens: sess.totalCacheCreationTokens ?? 0,
|
|
712
|
-
reasoningTokens: sess.totalReasoningTokens ?? 0,
|
|
713
|
-
toolTimeMs: sess.totalToolTimeMs ?? 0,
|
|
714
|
-
toolCounts: sess.toolCountsJson ? JSON.parse(sess.toolCountsJson) : {},
|
|
715
|
-
},
|
|
716
|
-
messages: allMessages.map((m) => ({
|
|
717
|
-
id: m.id,
|
|
718
|
-
role: m.role,
|
|
719
|
-
createdAt: m.createdAt,
|
|
720
|
-
parts: (partsByMessage.get(m.id) || []).map((p) => ({
|
|
721
|
-
type: p.type,
|
|
722
|
-
content: p.content,
|
|
723
|
-
toolName: p.toolName,
|
|
724
|
-
toolCallId: p.toolCallId,
|
|
725
|
-
})),
|
|
726
|
-
})),
|
|
727
|
-
};
|
|
728
|
-
|
|
729
|
-
const res = await fetch(`${SHARE_API_URL}/share/${share[0].shareId}`, {
|
|
730
|
-
method: 'PUT',
|
|
731
|
-
headers: {
|
|
732
|
-
'Content-Type': 'application/json',
|
|
733
|
-
'X-Share-Secret': share[0].secret,
|
|
734
|
-
},
|
|
735
|
-
body: JSON.stringify({
|
|
736
|
-
sessionData,
|
|
737
|
-
title: sess.title,
|
|
738
|
-
lastMessageId,
|
|
739
|
-
}),
|
|
740
|
-
});
|
|
1347
|
+
if (!res.ok) {
|
|
1348
|
+
const err = await res.text();
|
|
1349
|
+
return c.json({ error: `Failed to create share: ${err}` }, 500);
|
|
1350
|
+
}
|
|
741
1351
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1352
|
+
const data = (await res.json()) as {
|
|
1353
|
+
shareId: string;
|
|
1354
|
+
secret: string;
|
|
1355
|
+
url: string;
|
|
1356
|
+
};
|
|
746
1357
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1358
|
+
await db.insert(shares).values({
|
|
1359
|
+
sessionId,
|
|
1360
|
+
shareId: data.shareId,
|
|
1361
|
+
secret: data.secret,
|
|
1362
|
+
url: data.url,
|
|
750
1363
|
title: sess.title,
|
|
1364
|
+
description: null,
|
|
1365
|
+
createdAt: Date.now(),
|
|
751
1366
|
lastSyncedAt: Date.now(),
|
|
752
1367
|
lastSyncedMessageId: lastMessageId,
|
|
753
|
-
})
|
|
754
|
-
.where(eq(shares.sessionId, sessionId));
|
|
755
|
-
|
|
756
|
-
return c.json({
|
|
757
|
-
synced: true,
|
|
758
|
-
url: share[0].url,
|
|
759
|
-
newMessages: newMessages.length,
|
|
760
|
-
});
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
app.delete('/v1/sessions/:sessionId/share', async (c) => {
|
|
764
|
-
const sessionId = c.req.param('sessionId');
|
|
765
|
-
const projectRoot = c.req.query('project') || process.cwd();
|
|
766
|
-
const cfg = await loadConfig(projectRoot);
|
|
767
|
-
const db = await getDb(cfg.projectRoot);
|
|
768
|
-
|
|
769
|
-
const share = await db
|
|
770
|
-
.select()
|
|
771
|
-
.from(shares)
|
|
772
|
-
.where(eq(shares.sessionId, sessionId))
|
|
773
|
-
.limit(1);
|
|
774
|
-
|
|
775
|
-
if (!share.length) {
|
|
776
|
-
return c.json({ error: 'Session is not shared' }, 404);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
try {
|
|
780
|
-
const res = await fetch(`${SHARE_API_URL}/share/${share[0].shareId}`, {
|
|
781
|
-
method: 'DELETE',
|
|
782
|
-
headers: { 'X-Share-Secret': share[0].secret },
|
|
783
1368
|
});
|
|
784
1369
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1370
|
+
return c.json({
|
|
1371
|
+
shared: true,
|
|
1372
|
+
shareId: data.shareId,
|
|
1373
|
+
url: data.url,
|
|
1374
|
+
});
|
|
1375
|
+
},
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
openApiRoute(
|
|
1379
|
+
app,
|
|
1380
|
+
{
|
|
1381
|
+
method: 'put',
|
|
1382
|
+
path: '/v1/sessions/{sessionId}/share',
|
|
1383
|
+
tags: ['sessions'],
|
|
1384
|
+
operationId: 'syncShare',
|
|
1385
|
+
summary: 'Sync shared session with new messages',
|
|
1386
|
+
parameters: [
|
|
1387
|
+
{
|
|
1388
|
+
in: 'path',
|
|
1389
|
+
name: 'sessionId',
|
|
1390
|
+
required: true,
|
|
1391
|
+
schema: {
|
|
1392
|
+
type: 'string',
|
|
1393
|
+
},
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
in: 'query',
|
|
1397
|
+
name: 'project',
|
|
1398
|
+
required: false,
|
|
1399
|
+
schema: {
|
|
1400
|
+
type: 'string',
|
|
1401
|
+
},
|
|
1402
|
+
description:
|
|
1403
|
+
'Project root override (defaults to current working directory).',
|
|
1404
|
+
},
|
|
1405
|
+
],
|
|
1406
|
+
responses: {
|
|
1407
|
+
'200': {
|
|
1408
|
+
description: 'OK',
|
|
1409
|
+
content: {
|
|
1410
|
+
'application/json': {
|
|
1411
|
+
schema: {
|
|
1412
|
+
type: 'object',
|
|
1413
|
+
properties: {
|
|
1414
|
+
synced: {
|
|
1415
|
+
type: 'boolean',
|
|
1416
|
+
},
|
|
1417
|
+
url: {
|
|
1418
|
+
type: 'string',
|
|
1419
|
+
},
|
|
1420
|
+
newMessages: {
|
|
1421
|
+
type: 'integer',
|
|
1422
|
+
},
|
|
1423
|
+
message: {
|
|
1424
|
+
type: 'string',
|
|
1425
|
+
},
|
|
1426
|
+
},
|
|
1427
|
+
required: ['synced'],
|
|
1428
|
+
},
|
|
1429
|
+
},
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
'400': {
|
|
1433
|
+
description: 'Bad Request',
|
|
1434
|
+
content: {
|
|
1435
|
+
'application/json': {
|
|
1436
|
+
schema: {
|
|
1437
|
+
type: 'object',
|
|
1438
|
+
properties: {
|
|
1439
|
+
error: {
|
|
1440
|
+
type: 'string',
|
|
1441
|
+
},
|
|
1442
|
+
},
|
|
1443
|
+
required: ['error'],
|
|
1444
|
+
},
|
|
1445
|
+
},
|
|
1446
|
+
},
|
|
1447
|
+
},
|
|
1448
|
+
},
|
|
1449
|
+
},
|
|
1450
|
+
async (c) => {
|
|
821
1451
|
const sessionId = c.req.param('sessionId');
|
|
822
|
-
const messageId = c.req.param('messageId');
|
|
823
1452
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
824
1453
|
const cfg = await loadConfig(projectRoot);
|
|
825
1454
|
const db = await getDb(cfg.projectRoot);
|
|
826
1455
|
|
|
827
|
-
|
|
828
|
-
const [assistantMsg] = await db
|
|
1456
|
+
const share = await db
|
|
829
1457
|
.select()
|
|
830
|
-
.from(
|
|
831
|
-
.where(
|
|
832
|
-
and(
|
|
833
|
-
eq(messages.id, messageId),
|
|
834
|
-
eq(messages.sessionId, sessionId),
|
|
835
|
-
eq(messages.role, 'assistant'),
|
|
836
|
-
),
|
|
837
|
-
)
|
|
1458
|
+
.from(shares)
|
|
1459
|
+
.where(eq(shares.sessionId, sessionId))
|
|
838
1460
|
.limit(1);
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
return c.json({ error: 'Message not found' }, 404);
|
|
1461
|
+
if (!share.length) {
|
|
1462
|
+
return c.json({ error: 'Session not shared. Use share first.' }, 400);
|
|
842
1463
|
}
|
|
843
1464
|
|
|
844
|
-
|
|
845
|
-
if (
|
|
846
|
-
assistantMsg.status !== 'error' &&
|
|
847
|
-
assistantMsg.status !== 'complete'
|
|
848
|
-
) {
|
|
849
|
-
return c.json(
|
|
850
|
-
{ error: 'Can only retry error or complete messages' },
|
|
851
|
-
400,
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// Get session for context
|
|
856
|
-
const [session] = await db
|
|
1465
|
+
const session = await db
|
|
857
1466
|
.select()
|
|
858
1467
|
.from(sessions)
|
|
859
1468
|
.where(eq(sessions.id, sessionId))
|
|
860
1469
|
.limit(1);
|
|
861
|
-
|
|
862
|
-
if (!session) {
|
|
1470
|
+
if (!session.length) {
|
|
863
1471
|
return c.json({ error: 'Session not found' }, 404);
|
|
864
1472
|
}
|
|
865
1473
|
|
|
866
|
-
await db
|
|
867
|
-
.
|
|
1474
|
+
const allMessages = await db
|
|
1475
|
+
.select()
|
|
1476
|
+
.from(messages)
|
|
1477
|
+
.where(eq(messages.sessionId, sessionId))
|
|
1478
|
+
.orderBy(messages.createdAt);
|
|
1479
|
+
|
|
1480
|
+
const msgParts = await db
|
|
1481
|
+
.select()
|
|
1482
|
+
.from(messageParts)
|
|
868
1483
|
.where(
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
eq(messageParts.type, 'error'),
|
|
873
|
-
and(
|
|
874
|
-
eq(messageParts.type, 'tool_call'),
|
|
875
|
-
eq(messageParts.toolName, 'finish'),
|
|
876
|
-
),
|
|
877
|
-
),
|
|
1484
|
+
inArray(
|
|
1485
|
+
messageParts.messageId,
|
|
1486
|
+
allMessages.map((m) => m.id),
|
|
878
1487
|
),
|
|
879
|
-
)
|
|
1488
|
+
)
|
|
1489
|
+
.orderBy(messageParts.index);
|
|
1490
|
+
|
|
1491
|
+
const partsByMessage = new Map<string, typeof msgParts>();
|
|
1492
|
+
for (const part of msgParts) {
|
|
1493
|
+
const list = partsByMessage.get(part.messageId) || [];
|
|
1494
|
+
list.push(part);
|
|
1495
|
+
partsByMessage.set(part.messageId, list);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const lastSyncedIdx = allMessages.findIndex(
|
|
1499
|
+
(m) => m.id === share[0].lastSyncedMessageId,
|
|
1500
|
+
);
|
|
1501
|
+
const newMessages =
|
|
1502
|
+
lastSyncedIdx === -1
|
|
1503
|
+
? allMessages
|
|
1504
|
+
: allMessages.slice(lastSyncedIdx + 1);
|
|
1505
|
+
const lastMessageId =
|
|
1506
|
+
allMessages[allMessages.length - 1]?.id ?? share[0].lastSyncedMessageId;
|
|
1507
|
+
|
|
1508
|
+
if (newMessages.length === 0) {
|
|
1509
|
+
return c.json({
|
|
1510
|
+
synced: true,
|
|
1511
|
+
url: share[0].url,
|
|
1512
|
+
newMessages: 0,
|
|
1513
|
+
message: 'Already synced',
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
const sess = session[0];
|
|
1518
|
+
const sessionData = {
|
|
1519
|
+
title: sess.title,
|
|
1520
|
+
username: getUsername(),
|
|
1521
|
+
agent: sess.agent,
|
|
1522
|
+
provider: sess.provider,
|
|
1523
|
+
model: sess.model,
|
|
1524
|
+
createdAt: sess.createdAt,
|
|
1525
|
+
stats: {
|
|
1526
|
+
inputTokens: sess.totalInputTokens ?? 0,
|
|
1527
|
+
outputTokens: sess.totalOutputTokens ?? 0,
|
|
1528
|
+
cachedTokens: sess.totalCachedTokens ?? 0,
|
|
1529
|
+
cacheCreationTokens: sess.totalCacheCreationTokens ?? 0,
|
|
1530
|
+
reasoningTokens: sess.totalReasoningTokens ?? 0,
|
|
1531
|
+
toolTimeMs: sess.totalToolTimeMs ?? 0,
|
|
1532
|
+
toolCounts: sess.toolCountsJson
|
|
1533
|
+
? JSON.parse(sess.toolCountsJson)
|
|
1534
|
+
: {},
|
|
1535
|
+
},
|
|
1536
|
+
messages: allMessages.map((m) => ({
|
|
1537
|
+
id: m.id,
|
|
1538
|
+
role: m.role,
|
|
1539
|
+
createdAt: m.createdAt,
|
|
1540
|
+
parts: (partsByMessage.get(m.id) || []).map((p) => ({
|
|
1541
|
+
type: p.type,
|
|
1542
|
+
content: p.content,
|
|
1543
|
+
toolName: p.toolName,
|
|
1544
|
+
toolCallId: p.toolCallId,
|
|
1545
|
+
})),
|
|
1546
|
+
})),
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
const res = await fetch(`${SHARE_API_URL}/share/${share[0].shareId}`, {
|
|
1550
|
+
method: 'PUT',
|
|
1551
|
+
headers: {
|
|
1552
|
+
'Content-Type': 'application/json',
|
|
1553
|
+
'X-Share-Secret': share[0].secret,
|
|
1554
|
+
},
|
|
1555
|
+
body: JSON.stringify({
|
|
1556
|
+
sessionData,
|
|
1557
|
+
title: sess.title,
|
|
1558
|
+
lastMessageId,
|
|
1559
|
+
}),
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
if (!res.ok) {
|
|
1563
|
+
const err = await res.text();
|
|
1564
|
+
return c.json({ error: `Failed to sync share: ${err}` }, 500);
|
|
1565
|
+
}
|
|
880
1566
|
|
|
881
|
-
// Reset message status to pending
|
|
882
1567
|
await db
|
|
883
|
-
.update(
|
|
1568
|
+
.update(shares)
|
|
884
1569
|
.set({
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
errorDetails: null,
|
|
889
|
-
completedAt: null,
|
|
1570
|
+
title: sess.title,
|
|
1571
|
+
lastSyncedAt: Date.now(),
|
|
1572
|
+
lastSyncedMessageId: lastMessageId,
|
|
890
1573
|
})
|
|
891
|
-
.where(eq(
|
|
1574
|
+
.where(eq(shares.sessionId, sessionId));
|
|
892
1575
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
sessionId,
|
|
898
|
-
payload: { id: messageId, status: 'pending' },
|
|
1576
|
+
return c.json({
|
|
1577
|
+
synced: true,
|
|
1578
|
+
url: share[0].url,
|
|
1579
|
+
newMessages: newMessages.length,
|
|
899
1580
|
});
|
|
1581
|
+
},
|
|
1582
|
+
);
|
|
1583
|
+
|
|
1584
|
+
openApiRoute(
|
|
1585
|
+
app,
|
|
1586
|
+
{
|
|
1587
|
+
method: 'delete',
|
|
1588
|
+
path: '/v1/sessions/{sessionId}/share',
|
|
1589
|
+
tags: ['sessions'],
|
|
1590
|
+
operationId: 'deleteShare',
|
|
1591
|
+
summary: 'Delete a shared session',
|
|
1592
|
+
parameters: [
|
|
1593
|
+
{
|
|
1594
|
+
in: 'path',
|
|
1595
|
+
name: 'sessionId',
|
|
1596
|
+
required: true,
|
|
1597
|
+
schema: {
|
|
1598
|
+
type: 'string',
|
|
1599
|
+
},
|
|
1600
|
+
},
|
|
1601
|
+
{
|
|
1602
|
+
in: 'query',
|
|
1603
|
+
name: 'project',
|
|
1604
|
+
required: false,
|
|
1605
|
+
schema: {
|
|
1606
|
+
type: 'string',
|
|
1607
|
+
},
|
|
1608
|
+
description:
|
|
1609
|
+
'Project root override (defaults to current working directory).',
|
|
1610
|
+
},
|
|
1611
|
+
],
|
|
1612
|
+
responses: {
|
|
1613
|
+
'200': {
|
|
1614
|
+
description: 'OK',
|
|
1615
|
+
content: {
|
|
1616
|
+
'application/json': {
|
|
1617
|
+
schema: {
|
|
1618
|
+
type: 'object',
|
|
1619
|
+
properties: {
|
|
1620
|
+
deleted: {
|
|
1621
|
+
type: 'boolean',
|
|
1622
|
+
},
|
|
1623
|
+
sessionId: {
|
|
1624
|
+
type: 'string',
|
|
1625
|
+
},
|
|
1626
|
+
},
|
|
1627
|
+
required: ['deleted', 'sessionId'],
|
|
1628
|
+
},
|
|
1629
|
+
},
|
|
1630
|
+
},
|
|
1631
|
+
},
|
|
1632
|
+
'404': {
|
|
1633
|
+
description: 'Bad Request',
|
|
1634
|
+
content: {
|
|
1635
|
+
'application/json': {
|
|
1636
|
+
schema: {
|
|
1637
|
+
type: 'object',
|
|
1638
|
+
properties: {
|
|
1639
|
+
error: {
|
|
1640
|
+
type: 'string',
|
|
1641
|
+
},
|
|
1642
|
+
},
|
|
1643
|
+
required: ['error'],
|
|
1644
|
+
},
|
|
1645
|
+
},
|
|
1646
|
+
},
|
|
1647
|
+
},
|
|
1648
|
+
},
|
|
1649
|
+
},
|
|
1650
|
+
async (c) => {
|
|
1651
|
+
const sessionId = c.req.param('sessionId');
|
|
1652
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
1653
|
+
const cfg = await loadConfig(projectRoot);
|
|
1654
|
+
const db = await getDb(cfg.projectRoot);
|
|
900
1655
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1656
|
+
const share = await db
|
|
1657
|
+
.select()
|
|
1658
|
+
.from(shares)
|
|
1659
|
+
.where(eq(shares.sessionId, sessionId))
|
|
1660
|
+
.limit(1);
|
|
1661
|
+
|
|
1662
|
+
if (!share.length) {
|
|
1663
|
+
return c.json({ error: 'Session is not shared' }, 404);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
try {
|
|
1667
|
+
const res = await fetch(`${SHARE_API_URL}/share/${share[0].shareId}`, {
|
|
1668
|
+
method: 'DELETE',
|
|
1669
|
+
headers: { 'X-Share-Secret': share[0].secret },
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
if (!res.ok && res.status !== 404) {
|
|
1673
|
+
const err = await res.text();
|
|
1674
|
+
return c.json({ error: `Failed to delete share: ${err}` }, 500);
|
|
1675
|
+
}
|
|
1676
|
+
} catch {}
|
|
1677
|
+
|
|
1678
|
+
await db.delete(shares).where(eq(shares.sessionId, sessionId));
|
|
1679
|
+
|
|
1680
|
+
return c.json({ deleted: true, sessionId });
|
|
1681
|
+
},
|
|
1682
|
+
);
|
|
1683
|
+
|
|
1684
|
+
openApiRoute(
|
|
1685
|
+
app,
|
|
1686
|
+
{
|
|
1687
|
+
method: 'get',
|
|
1688
|
+
path: '/v1/shares',
|
|
1689
|
+
tags: ['sessions'],
|
|
1690
|
+
operationId: 'listShares',
|
|
1691
|
+
summary: 'List all shared sessions for a project',
|
|
1692
|
+
parameters: [
|
|
1693
|
+
{
|
|
1694
|
+
in: 'query',
|
|
1695
|
+
name: 'project',
|
|
1696
|
+
required: false,
|
|
1697
|
+
schema: {
|
|
1698
|
+
type: 'string',
|
|
1699
|
+
},
|
|
1700
|
+
description:
|
|
1701
|
+
'Project root override (defaults to current working directory).',
|
|
1702
|
+
},
|
|
1703
|
+
],
|
|
1704
|
+
responses: {
|
|
1705
|
+
'200': {
|
|
1706
|
+
description: 'OK',
|
|
1707
|
+
content: {
|
|
1708
|
+
'application/json': {
|
|
1709
|
+
schema: {
|
|
1710
|
+
type: 'object',
|
|
1711
|
+
properties: {
|
|
1712
|
+
shares: {
|
|
1713
|
+
type: 'array',
|
|
1714
|
+
items: {
|
|
1715
|
+
type: 'object',
|
|
1716
|
+
properties: {
|
|
1717
|
+
sessionId: {
|
|
1718
|
+
type: 'string',
|
|
1719
|
+
},
|
|
1720
|
+
shareId: {
|
|
1721
|
+
type: 'string',
|
|
1722
|
+
},
|
|
1723
|
+
url: {
|
|
1724
|
+
type: 'string',
|
|
1725
|
+
},
|
|
1726
|
+
title: {
|
|
1727
|
+
type: 'string',
|
|
1728
|
+
nullable: true,
|
|
1729
|
+
},
|
|
1730
|
+
createdAt: {
|
|
1731
|
+
type: 'integer',
|
|
1732
|
+
},
|
|
1733
|
+
lastSyncedAt: {
|
|
1734
|
+
type: 'integer',
|
|
1735
|
+
},
|
|
1736
|
+
},
|
|
1737
|
+
required: [
|
|
1738
|
+
'sessionId',
|
|
1739
|
+
'shareId',
|
|
1740
|
+
'url',
|
|
1741
|
+
'createdAt',
|
|
1742
|
+
'lastSyncedAt',
|
|
1743
|
+
],
|
|
1744
|
+
},
|
|
1745
|
+
},
|
|
1746
|
+
},
|
|
1747
|
+
required: ['shares'],
|
|
1748
|
+
},
|
|
1749
|
+
},
|
|
1750
|
+
},
|
|
1751
|
+
},
|
|
1752
|
+
},
|
|
1753
|
+
},
|
|
1754
|
+
async (c) => {
|
|
1755
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
1756
|
+
const cfg = await loadConfig(projectRoot);
|
|
1757
|
+
const db = await getDb(cfg.projectRoot);
|
|
1758
|
+
|
|
1759
|
+
const rows = await db
|
|
1760
|
+
.select({
|
|
1761
|
+
sessionId: shares.sessionId,
|
|
1762
|
+
shareId: shares.shareId,
|
|
1763
|
+
url: shares.url,
|
|
1764
|
+
title: shares.title,
|
|
1765
|
+
createdAt: shares.createdAt,
|
|
1766
|
+
lastSyncedAt: shares.lastSyncedAt,
|
|
1767
|
+
})
|
|
1768
|
+
.from(shares)
|
|
1769
|
+
.innerJoin(sessions, eq(shares.sessionId, sessions.id))
|
|
1770
|
+
.where(eq(sessions.projectPath, cfg.projectRoot))
|
|
1771
|
+
.orderBy(desc(shares.lastSyncedAt));
|
|
906
1772
|
|
|
907
|
-
|
|
1773
|
+
return c.json({ shares: rows });
|
|
1774
|
+
},
|
|
1775
|
+
);
|
|
908
1776
|
|
|
909
|
-
|
|
1777
|
+
// Retry a failed assistant message
|
|
1778
|
+
openApiRoute(
|
|
1779
|
+
app,
|
|
1780
|
+
{
|
|
1781
|
+
method: 'post',
|
|
1782
|
+
path: '/v1/sessions/{sessionId}/messages/{messageId}/retry',
|
|
1783
|
+
tags: ['sessions'],
|
|
1784
|
+
operationId: 'retryMessage',
|
|
1785
|
+
summary: 'Retry a failed assistant message',
|
|
1786
|
+
parameters: [
|
|
1787
|
+
{
|
|
1788
|
+
in: 'path',
|
|
1789
|
+
name: 'sessionId',
|
|
1790
|
+
required: true,
|
|
1791
|
+
schema: {
|
|
1792
|
+
type: 'string',
|
|
1793
|
+
},
|
|
1794
|
+
},
|
|
910
1795
|
{
|
|
1796
|
+
in: 'path',
|
|
1797
|
+
name: 'messageId',
|
|
1798
|
+
required: true,
|
|
1799
|
+
schema: {
|
|
1800
|
+
type: 'string',
|
|
1801
|
+
},
|
|
1802
|
+
},
|
|
1803
|
+
{
|
|
1804
|
+
in: 'query',
|
|
1805
|
+
name: 'project',
|
|
1806
|
+
required: false,
|
|
1807
|
+
schema: {
|
|
1808
|
+
type: 'string',
|
|
1809
|
+
},
|
|
1810
|
+
description:
|
|
1811
|
+
'Project root override (defaults to current working directory).',
|
|
1812
|
+
},
|
|
1813
|
+
],
|
|
1814
|
+
responses: {
|
|
1815
|
+
'200': {
|
|
1816
|
+
description: 'OK',
|
|
1817
|
+
content: {
|
|
1818
|
+
'application/json': {
|
|
1819
|
+
schema: {
|
|
1820
|
+
type: 'object',
|
|
1821
|
+
properties: {
|
|
1822
|
+
success: {
|
|
1823
|
+
type: 'boolean',
|
|
1824
|
+
},
|
|
1825
|
+
messageId: {
|
|
1826
|
+
type: 'string',
|
|
1827
|
+
},
|
|
1828
|
+
},
|
|
1829
|
+
required: ['success', 'messageId'],
|
|
1830
|
+
},
|
|
1831
|
+
},
|
|
1832
|
+
},
|
|
1833
|
+
},
|
|
1834
|
+
'400': {
|
|
1835
|
+
description: 'Bad Request',
|
|
1836
|
+
content: {
|
|
1837
|
+
'application/json': {
|
|
1838
|
+
schema: {
|
|
1839
|
+
type: 'object',
|
|
1840
|
+
properties: {
|
|
1841
|
+
error: {
|
|
1842
|
+
type: 'string',
|
|
1843
|
+
},
|
|
1844
|
+
},
|
|
1845
|
+
required: ['error'],
|
|
1846
|
+
},
|
|
1847
|
+
},
|
|
1848
|
+
},
|
|
1849
|
+
},
|
|
1850
|
+
'404': {
|
|
1851
|
+
description: 'Bad Request',
|
|
1852
|
+
content: {
|
|
1853
|
+
'application/json': {
|
|
1854
|
+
schema: {
|
|
1855
|
+
type: 'object',
|
|
1856
|
+
properties: {
|
|
1857
|
+
error: {
|
|
1858
|
+
type: 'string',
|
|
1859
|
+
},
|
|
1860
|
+
},
|
|
1861
|
+
required: ['error'],
|
|
1862
|
+
},
|
|
1863
|
+
},
|
|
1864
|
+
},
|
|
1865
|
+
},
|
|
1866
|
+
},
|
|
1867
|
+
},
|
|
1868
|
+
async (c) => {
|
|
1869
|
+
try {
|
|
1870
|
+
const sessionId = c.req.param('sessionId');
|
|
1871
|
+
const messageId = c.req.param('messageId');
|
|
1872
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
1873
|
+
const cfg = await loadConfig(projectRoot);
|
|
1874
|
+
const db = await getDb(cfg.projectRoot);
|
|
1875
|
+
|
|
1876
|
+
// Get the assistant message
|
|
1877
|
+
const [assistantMsg] = await db
|
|
1878
|
+
.select()
|
|
1879
|
+
.from(messages)
|
|
1880
|
+
.where(
|
|
1881
|
+
and(
|
|
1882
|
+
eq(messages.id, messageId),
|
|
1883
|
+
eq(messages.sessionId, sessionId),
|
|
1884
|
+
eq(messages.role, 'assistant'),
|
|
1885
|
+
),
|
|
1886
|
+
)
|
|
1887
|
+
.limit(1);
|
|
1888
|
+
|
|
1889
|
+
if (!assistantMsg) {
|
|
1890
|
+
return c.json({ error: 'Message not found' }, 404);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// Only allow retry on error or complete messages
|
|
1894
|
+
if (
|
|
1895
|
+
assistantMsg.status !== 'error' &&
|
|
1896
|
+
assistantMsg.status !== 'complete'
|
|
1897
|
+
) {
|
|
1898
|
+
return c.json(
|
|
1899
|
+
{ error: 'Can only retry error or complete messages' },
|
|
1900
|
+
400,
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// Get session for context
|
|
1905
|
+
const [session] = await db
|
|
1906
|
+
.select()
|
|
1907
|
+
.from(sessions)
|
|
1908
|
+
.where(eq(sessions.id, sessionId))
|
|
1909
|
+
.limit(1);
|
|
1910
|
+
|
|
1911
|
+
if (!session) {
|
|
1912
|
+
return c.json({ error: 'Session not found' }, 404);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
await db
|
|
1916
|
+
.delete(messageParts)
|
|
1917
|
+
.where(
|
|
1918
|
+
and(
|
|
1919
|
+
eq(messageParts.messageId, messageId),
|
|
1920
|
+
or(
|
|
1921
|
+
eq(messageParts.type, 'error'),
|
|
1922
|
+
and(
|
|
1923
|
+
eq(messageParts.type, 'tool_call'),
|
|
1924
|
+
eq(messageParts.toolName, 'finish'),
|
|
1925
|
+
),
|
|
1926
|
+
),
|
|
1927
|
+
),
|
|
1928
|
+
);
|
|
1929
|
+
|
|
1930
|
+
// Reset message status to pending
|
|
1931
|
+
await db
|
|
1932
|
+
.update(messages)
|
|
1933
|
+
.set({
|
|
1934
|
+
status: 'pending',
|
|
1935
|
+
error: null,
|
|
1936
|
+
errorType: null,
|
|
1937
|
+
errorDetails: null,
|
|
1938
|
+
completedAt: null,
|
|
1939
|
+
})
|
|
1940
|
+
.where(eq(messages.id, messageId));
|
|
1941
|
+
|
|
1942
|
+
// Emit event so UI updates
|
|
1943
|
+
const { publish } = await import('../events/bus.ts');
|
|
1944
|
+
publish({
|
|
1945
|
+
type: 'message.updated',
|
|
911
1946
|
sessionId,
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
provider: (assistantMsg.provider ??
|
|
915
|
-
cfg.defaults.provider) as ProviderId,
|
|
916
|
-
model: assistantMsg.model ?? cfg.defaults.model,
|
|
917
|
-
projectRoot: cfg.projectRoot,
|
|
918
|
-
oneShot: false,
|
|
919
|
-
toolApprovalMode,
|
|
920
|
-
},
|
|
921
|
-
runSessionLoop,
|
|
922
|
-
);
|
|
1947
|
+
payload: { id: messageId, status: 'pending' },
|
|
1948
|
+
});
|
|
923
1949
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1950
|
+
// Re-enqueue the assistant run
|
|
1951
|
+
const { enqueueAssistantRun } = await import(
|
|
1952
|
+
'../runtime/session/queue.ts'
|
|
1953
|
+
);
|
|
1954
|
+
const { runSessionLoop } = await import('../runtime/agent/runner.ts');
|
|
1955
|
+
|
|
1956
|
+
const toolApprovalMode = cfg.defaults.toolApproval ?? 'dangerous';
|
|
1957
|
+
|
|
1958
|
+
enqueueAssistantRun(
|
|
1959
|
+
{
|
|
1960
|
+
sessionId,
|
|
1961
|
+
assistantMessageId: messageId,
|
|
1962
|
+
agent: assistantMsg.agent ?? 'build',
|
|
1963
|
+
provider: (assistantMsg.provider ??
|
|
1964
|
+
cfg.defaults.provider) as ProviderId,
|
|
1965
|
+
model: assistantMsg.model ?? cfg.defaults.model,
|
|
1966
|
+
projectRoot: cfg.projectRoot,
|
|
1967
|
+
oneShot: false,
|
|
1968
|
+
toolApprovalMode,
|
|
1969
|
+
},
|
|
1970
|
+
runSessionLoop,
|
|
1971
|
+
);
|
|
1972
|
+
|
|
1973
|
+
return c.json({ success: true, messageId });
|
|
1974
|
+
} catch (err) {
|
|
1975
|
+
logger.error('Failed to retry message', err);
|
|
1976
|
+
const errorResponse = serializeError(err);
|
|
1977
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
1978
|
+
}
|
|
1979
|
+
},
|
|
1980
|
+
);
|
|
931
1981
|
}
|