@ottocode/server 0.1.173

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.
Files changed (111) hide show
  1. package/package.json +42 -0
  2. package/src/events/bus.ts +43 -0
  3. package/src/events/types.ts +32 -0
  4. package/src/index.ts +281 -0
  5. package/src/openapi/helpers.ts +64 -0
  6. package/src/openapi/paths/ask.ts +70 -0
  7. package/src/openapi/paths/config.ts +218 -0
  8. package/src/openapi/paths/files.ts +72 -0
  9. package/src/openapi/paths/git.ts +457 -0
  10. package/src/openapi/paths/messages.ts +92 -0
  11. package/src/openapi/paths/sessions.ts +90 -0
  12. package/src/openapi/paths/setu.ts +154 -0
  13. package/src/openapi/paths/stream.ts +26 -0
  14. package/src/openapi/paths/terminals.ts +226 -0
  15. package/src/openapi/schemas.ts +345 -0
  16. package/src/openapi/spec.ts +49 -0
  17. package/src/presets.ts +85 -0
  18. package/src/routes/ask.ts +113 -0
  19. package/src/routes/auth.ts +592 -0
  20. package/src/routes/branch.ts +106 -0
  21. package/src/routes/config/agents.ts +44 -0
  22. package/src/routes/config/cwd.ts +21 -0
  23. package/src/routes/config/defaults.ts +45 -0
  24. package/src/routes/config/index.ts +16 -0
  25. package/src/routes/config/main.ts +73 -0
  26. package/src/routes/config/models.ts +139 -0
  27. package/src/routes/config/providers.ts +46 -0
  28. package/src/routes/config/utils.ts +120 -0
  29. package/src/routes/files.ts +218 -0
  30. package/src/routes/git/branch.ts +75 -0
  31. package/src/routes/git/commit.ts +209 -0
  32. package/src/routes/git/diff.ts +137 -0
  33. package/src/routes/git/index.ts +18 -0
  34. package/src/routes/git/push.ts +160 -0
  35. package/src/routes/git/schemas.ts +48 -0
  36. package/src/routes/git/staging.ts +208 -0
  37. package/src/routes/git/status.ts +83 -0
  38. package/src/routes/git/types.ts +31 -0
  39. package/src/routes/git/utils.ts +249 -0
  40. package/src/routes/openapi.ts +6 -0
  41. package/src/routes/research.ts +392 -0
  42. package/src/routes/root.ts +5 -0
  43. package/src/routes/session-approval.ts +63 -0
  44. package/src/routes/session-files.ts +387 -0
  45. package/src/routes/session-messages.ts +170 -0
  46. package/src/routes/session-stream.ts +61 -0
  47. package/src/routes/sessions.ts +814 -0
  48. package/src/routes/setu.ts +346 -0
  49. package/src/routes/terminals.ts +227 -0
  50. package/src/runtime/agent/registry.ts +351 -0
  51. package/src/runtime/agent/runner-reasoning.ts +108 -0
  52. package/src/runtime/agent/runner-setup.ts +257 -0
  53. package/src/runtime/agent/runner.ts +375 -0
  54. package/src/runtime/agent-registry.ts +6 -0
  55. package/src/runtime/ask/service.ts +369 -0
  56. package/src/runtime/context/environment.ts +202 -0
  57. package/src/runtime/debug/index.ts +117 -0
  58. package/src/runtime/debug/state.ts +140 -0
  59. package/src/runtime/errors/api-error.ts +192 -0
  60. package/src/runtime/errors/handling.ts +199 -0
  61. package/src/runtime/message/compaction-auto.ts +154 -0
  62. package/src/runtime/message/compaction-context.ts +101 -0
  63. package/src/runtime/message/compaction-detect.ts +26 -0
  64. package/src/runtime/message/compaction-limits.ts +37 -0
  65. package/src/runtime/message/compaction-mark.ts +111 -0
  66. package/src/runtime/message/compaction-prune.ts +75 -0
  67. package/src/runtime/message/compaction.ts +21 -0
  68. package/src/runtime/message/history-builder.ts +266 -0
  69. package/src/runtime/message/service.ts +468 -0
  70. package/src/runtime/message/tool-history-tracker.ts +204 -0
  71. package/src/runtime/prompt/builder.ts +167 -0
  72. package/src/runtime/provider/anthropic.ts +50 -0
  73. package/src/runtime/provider/copilot.ts +12 -0
  74. package/src/runtime/provider/google.ts +8 -0
  75. package/src/runtime/provider/index.ts +60 -0
  76. package/src/runtime/provider/moonshot.ts +8 -0
  77. package/src/runtime/provider/oauth-adapter.ts +237 -0
  78. package/src/runtime/provider/openai.ts +18 -0
  79. package/src/runtime/provider/opencode.ts +7 -0
  80. package/src/runtime/provider/openrouter.ts +7 -0
  81. package/src/runtime/provider/selection.ts +118 -0
  82. package/src/runtime/provider/setu.ts +126 -0
  83. package/src/runtime/provider/zai.ts +16 -0
  84. package/src/runtime/session/branch.ts +280 -0
  85. package/src/runtime/session/db-operations.ts +285 -0
  86. package/src/runtime/session/manager.ts +99 -0
  87. package/src/runtime/session/queue.ts +243 -0
  88. package/src/runtime/stream/abort-handler.ts +65 -0
  89. package/src/runtime/stream/error-handler.ts +371 -0
  90. package/src/runtime/stream/finish-handler.ts +101 -0
  91. package/src/runtime/stream/handlers.ts +5 -0
  92. package/src/runtime/stream/step-finish.ts +93 -0
  93. package/src/runtime/stream/types.ts +25 -0
  94. package/src/runtime/tools/approval.ts +180 -0
  95. package/src/runtime/tools/context.ts +83 -0
  96. package/src/runtime/tools/mapping.ts +154 -0
  97. package/src/runtime/tools/setup.ts +44 -0
  98. package/src/runtime/topup/manager.ts +110 -0
  99. package/src/runtime/utils/cwd.ts +69 -0
  100. package/src/runtime/utils/token.ts +35 -0
  101. package/src/tools/adapter.ts +634 -0
  102. package/src/tools/database/get-parent-session.ts +183 -0
  103. package/src/tools/database/get-session-context.ts +161 -0
  104. package/src/tools/database/index.ts +42 -0
  105. package/src/tools/database/present-session-links.ts +47 -0
  106. package/src/tools/database/query-messages.ts +160 -0
  107. package/src/tools/database/query-sessions.ts +126 -0
  108. package/src/tools/database/search-history.ts +135 -0
  109. package/src/types/sql-imports.d.ts +5 -0
  110. package/sst-env.d.ts +8 -0
  111. package/tsconfig.json +7 -0
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Runtime debug state management
3
+ *
4
+ * Centralizes debug flag state that can be set either via:
5
+ * - Environment variables (OTTO_DEBUG, DEBUG_OTTO)
6
+ * - Runtime configuration (CLI --debug flag)
7
+ */
8
+
9
+ const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
10
+
11
+ type DebugState = {
12
+ enabled: boolean;
13
+ traceEnabled: boolean;
14
+ runtimeOverride: boolean | null;
15
+ runtimeTraceOverride: boolean | null;
16
+ };
17
+
18
+ // Global state
19
+ const state: DebugState = {
20
+ enabled: false,
21
+ traceEnabled: false,
22
+ runtimeOverride: null,
23
+ runtimeTraceOverride: null,
24
+ };
25
+
26
+ type GlobalDebugFlags = {
27
+ __OTTO_DEBUG_ENABLED__?: boolean;
28
+ __OTTO_TRACE_ENABLED__?: boolean;
29
+ };
30
+
31
+ const globalFlags = globalThis as GlobalDebugFlags;
32
+
33
+ function syncGlobalFlags() {
34
+ globalFlags.__OTTO_DEBUG_ENABLED__ = state.enabled;
35
+ globalFlags.__OTTO_TRACE_ENABLED__ = state.traceEnabled;
36
+ }
37
+
38
+ /**
39
+ * Check if environment variables indicate debug mode
40
+ */
41
+ function checkEnvDebug(): boolean {
42
+ const sources = [process.env.OTTO_DEBUG, process.env.DEBUG_OTTO];
43
+ for (const value of sources) {
44
+ if (!value) continue;
45
+ const trimmed = value.trim().toLowerCase();
46
+ if (TRUTHY.has(trimmed) || trimmed === 'all') {
47
+ return true;
48
+ }
49
+ }
50
+ return false;
51
+ }
52
+
53
+ /**
54
+ * Check if environment variables indicate trace mode
55
+ */
56
+ function checkEnvTrace(): boolean {
57
+ const sources = [process.env.OTTO_TRACE, process.env.TRACE_OTTO];
58
+ for (const value of sources) {
59
+ if (!value) continue;
60
+ const trimmed = value.trim().toLowerCase();
61
+ if (TRUTHY.has(trimmed)) {
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+
68
+ /**
69
+ * Initialize debug state from environment
70
+ */
71
+ function initialize() {
72
+ if (state.runtimeOverride === null) {
73
+ state.enabled = checkEnvDebug();
74
+ }
75
+ if (state.runtimeTraceOverride === null) {
76
+ state.traceEnabled = checkEnvTrace();
77
+ }
78
+ syncGlobalFlags();
79
+ }
80
+
81
+ /**
82
+ * Check if debug mode is enabled
83
+ * Considers both runtime override and environment variables
84
+ */
85
+ export function isDebugEnabled(): boolean {
86
+ initialize();
87
+ return state.enabled;
88
+ }
89
+
90
+ /**
91
+ * Check if trace mode is enabled (shows stack traces)
92
+ * Trace mode requires debug mode to be enabled
93
+ */
94
+ export function isTraceEnabled(): boolean {
95
+ initialize();
96
+ return state.enabled && state.traceEnabled;
97
+ }
98
+
99
+ /**
100
+ * Enable or disable debug mode at runtime
101
+ * Overrides environment variable settings
102
+ *
103
+ * @param enabled - true to enable debug mode, false to disable
104
+ */
105
+ export function setDebugEnabled(enabled: boolean): void {
106
+ state.enabled = enabled;
107
+ state.runtimeOverride = enabled;
108
+ syncGlobalFlags();
109
+ }
110
+
111
+ /**
112
+ * Enable or disable trace mode at runtime
113
+ * Trace mode shows full stack traces in error logs
114
+ *
115
+ * @param enabled - true to enable trace mode, false to disable
116
+ */
117
+ export function setTraceEnabled(enabled: boolean): void {
118
+ state.traceEnabled = enabled;
119
+ state.runtimeTraceOverride = enabled;
120
+ syncGlobalFlags();
121
+ }
122
+
123
+ /**
124
+ * Reset debug state to environment defaults
125
+ */
126
+ export function resetDebugState(): void {
127
+ state.runtimeOverride = null;
128
+ state.runtimeTraceOverride = null;
129
+ state.enabled = checkEnvDebug();
130
+ state.traceEnabled = checkEnvTrace();
131
+ syncGlobalFlags();
132
+ }
133
+
134
+ /**
135
+ * Get current debug state (for testing/diagnostics)
136
+ */
137
+ export function getDebugState(): Readonly<DebugState> {
138
+ initialize();
139
+ return { ...state };
140
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Unified API error handling
3
+ *
4
+ * Provides consistent error serialization and response formatting
5
+ * across all API endpoints.
6
+ */
7
+
8
+ import { isDebugEnabled } from '../debug/state.ts';
9
+ import { toErrorPayload } from './handling.ts';
10
+
11
+ /**
12
+ * Standard API error response format
13
+ */
14
+ export type APIErrorResponse = {
15
+ error: {
16
+ message: string;
17
+ type: string;
18
+ code?: string;
19
+ status?: number;
20
+ details?: Record<string, unknown>;
21
+ stack?: string;
22
+ };
23
+ };
24
+
25
+ /**
26
+ * Custom API Error class
27
+ */
28
+ export class APIError extends Error {
29
+ public readonly code?: string;
30
+ public readonly status: number;
31
+ public readonly type: string;
32
+ public readonly details?: Record<string, unknown>;
33
+
34
+ constructor(
35
+ message: string,
36
+ options?: {
37
+ code?: string;
38
+ status?: number;
39
+ type?: string;
40
+ details?: Record<string, unknown>;
41
+ cause?: unknown;
42
+ },
43
+ ) {
44
+ super(message);
45
+ this.name = 'APIError';
46
+ this.code = options?.code;
47
+ this.status = options?.status ?? 500;
48
+ this.type = options?.type ?? 'api_error';
49
+ this.details = options?.details;
50
+
51
+ if (options?.cause) {
52
+ this.cause = options.cause;
53
+ }
54
+
55
+ // Maintain proper stack trace
56
+ if (Error.captureStackTrace) {
57
+ Error.captureStackTrace(this, APIError);
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Serialize any error into a consistent API error response
64
+ *
65
+ * @param err - The error to serialize
66
+ * @returns A properly formatted API error response
67
+ */
68
+ export function serializeError(err: unknown): APIErrorResponse {
69
+ // Use existing error payload logic
70
+ const payload = toErrorPayload(err);
71
+
72
+ // Determine HTTP status code
73
+ // Default to 400 for generic errors (client errors)
74
+ // Only use 500 if explicitly set or for APIError instances without a status
75
+ let status = 400;
76
+
77
+ // Handle APIError instances first
78
+ if (err instanceof APIError) {
79
+ status = err.status;
80
+ } else if (err && typeof err === 'object') {
81
+ const errObj = err as Record<string, unknown>;
82
+ if (typeof errObj.status === 'number') {
83
+ status = errObj.status;
84
+ } else if (typeof errObj.statusCode === 'number') {
85
+ status = errObj.statusCode;
86
+ } else if (
87
+ errObj.details &&
88
+ typeof errObj.details === 'object' &&
89
+ typeof (errObj.details as Record<string, unknown>).statusCode === 'number'
90
+ ) {
91
+ status = (errObj.details as Record<string, unknown>).statusCode as number;
92
+ }
93
+ }
94
+
95
+ // Extract code if available
96
+ let code: string | undefined;
97
+ if (err && typeof err === 'object') {
98
+ const errObj = err as Record<string, unknown>;
99
+ if (typeof errObj.code === 'string') {
100
+ code = errObj.code;
101
+ }
102
+ }
103
+
104
+ if (err instanceof APIError && err.code) {
105
+ code = err.code;
106
+ }
107
+
108
+ // Build response
109
+ const response: APIErrorResponse = {
110
+ error: {
111
+ message: payload.message || 'An error occurred',
112
+ type: payload.type || 'unknown',
113
+ status,
114
+ ...(code ? { code } : {}),
115
+ ...(payload.details ? { details: payload.details } : {}),
116
+ },
117
+ };
118
+
119
+ // Include stack trace in debug mode
120
+ if (isDebugEnabled() && err instanceof Error && err.stack) {
121
+ response.error.stack = err.stack;
122
+ }
123
+
124
+ return response;
125
+ }
126
+
127
+ /**
128
+ * Create an error response with proper HTTP status code
129
+ *
130
+ * @param err - The error to convert
131
+ * @returns Tuple of [APIErrorResponse, HTTP status code]
132
+ */
133
+ export function createErrorResponse(err: unknown): [APIErrorResponse, number] {
134
+ const response = serializeError(err);
135
+ return [response, response.error.status ?? 500];
136
+ }
137
+
138
+ /**
139
+ * Normalize error to ensure it's an Error instance
140
+ *
141
+ * @param err - The error to normalize
142
+ * @returns An Error instance
143
+ */
144
+ export function normalizeError(err: unknown): Error {
145
+ if (err instanceof Error) {
146
+ return err;
147
+ }
148
+
149
+ if (typeof err === 'string') {
150
+ return new Error(err);
151
+ }
152
+
153
+ if (err && typeof err === 'object') {
154
+ const errObj = err as Record<string, unknown>;
155
+ if (typeof errObj.message === 'string') {
156
+ return new Error(errObj.message);
157
+ }
158
+ }
159
+
160
+ return new Error('An unknown error occurred');
161
+ }
162
+
163
+ /**
164
+ * Extract error message from any error type
165
+ *
166
+ * @param err - The error to extract message from
167
+ * @returns The error message string
168
+ */
169
+ export function getErrorMessage(err: unknown): string {
170
+ if (typeof err === 'string') {
171
+ return err;
172
+ }
173
+
174
+ if (err instanceof Error) {
175
+ return err.message;
176
+ }
177
+
178
+ if (err && typeof err === 'object') {
179
+ const errObj = err as Record<string, unknown>;
180
+ if (typeof errObj.message === 'string') {
181
+ return errObj.message;
182
+ }
183
+ if (typeof errObj.error === 'string') {
184
+ return errObj.error;
185
+ }
186
+ }
187
+
188
+ return 'An unknown error occurred';
189
+ }
190
+
191
+ // Legacy compatibility - AskServiceError alias
192
+ export { APIError as AskServiceError };
@@ -0,0 +1,199 @@
1
+ import { APICallError } from 'ai';
2
+
3
+ export type ErrorPayload = {
4
+ message: string;
5
+ type: string;
6
+ details?: Record<string, unknown>;
7
+ };
8
+
9
+ export function toErrorPayload(err: unknown): ErrorPayload {
10
+ let actualError = err;
11
+ if (
12
+ err &&
13
+ typeof err === 'object' &&
14
+ 'error' in err &&
15
+ Object.keys(err).length === 1
16
+ ) {
17
+ actualError = (err as { error: unknown }).error;
18
+ }
19
+
20
+ const asObj =
21
+ actualError && typeof actualError === 'object'
22
+ ? (actualError as Record<string, unknown>)
23
+ : undefined;
24
+ let message = '';
25
+ let errorType = 'unknown';
26
+ const details: Record<string, unknown> = {};
27
+
28
+ if (APICallError.isInstance(actualError)) {
29
+ errorType = 'api_error';
30
+ message = actualError.message || 'API call failed';
31
+
32
+ details.name = actualError.name;
33
+ details.statusCode = actualError.statusCode;
34
+ details.url = actualError.url;
35
+ details.isRetryable = actualError.isRetryable;
36
+
37
+ if (actualError.responseBody) {
38
+ details.responseBody = actualError.responseBody;
39
+ try {
40
+ const parsed = JSON.parse(actualError.responseBody);
41
+ if (parsed.error) {
42
+ if (typeof parsed.error === 'string') {
43
+ message = parsed.error;
44
+ } else if (parsed.error.message) {
45
+ message = parsed.error.message;
46
+ }
47
+ }
48
+ if (parsed.error?.type) {
49
+ details.apiErrorType = parsed.error.type;
50
+ }
51
+ if (parsed.error?.code) {
52
+ details.apiErrorCode = parsed.error.code;
53
+ }
54
+ } catch {}
55
+ }
56
+
57
+ if (actualError.requestBodyValues) {
58
+ details.requestBodyValues = actualError.requestBodyValues;
59
+ }
60
+
61
+ if (actualError.responseHeaders) {
62
+ details.responseHeaders = actualError.responseHeaders;
63
+ }
64
+
65
+ if (actualError.cause) {
66
+ const cause = actualError.cause as Record<string, unknown> | undefined;
67
+ details.cause = {
68
+ message: typeof cause?.message === 'string' ? cause.message : undefined,
69
+ code: cause?.code,
70
+ status: cause?.status ?? cause?.statusCode,
71
+ };
72
+ }
73
+
74
+ return { message, type: errorType, details };
75
+ }
76
+
77
+ if (
78
+ asObj &&
79
+ 'type' in asObj &&
80
+ asObj.type === 'error' &&
81
+ 'error' in asObj &&
82
+ typeof asObj.error === 'object' &&
83
+ asObj.error
84
+ ) {
85
+ const errorObj = asObj.error as Record<string, unknown>;
86
+
87
+ if (typeof errorObj.message === 'string') {
88
+ message = errorObj.message;
89
+ }
90
+ if (typeof errorObj.type === 'string') {
91
+ errorType = errorObj.type;
92
+ details.errorType = errorObj.type;
93
+ }
94
+ if (typeof errorObj.code === 'string') {
95
+ details.code = errorObj.code;
96
+ }
97
+ if ('param' in errorObj) {
98
+ details.param = errorObj.param;
99
+ }
100
+
101
+ return { message, type: errorType, details };
102
+ }
103
+
104
+ if (asObj) {
105
+ if ('name' in asObj && typeof asObj.name === 'string') {
106
+ errorType = asObj.name;
107
+ details.name = asObj.name;
108
+ }
109
+
110
+ if ('type' in asObj && typeof asObj.type === 'string') {
111
+ errorType = asObj.type;
112
+ details.type = asObj.type;
113
+ }
114
+
115
+ if ('code' in asObj && asObj.code != null) {
116
+ details.code = asObj.code;
117
+ }
118
+
119
+ if ('status' in asObj && asObj.status != null) {
120
+ details.status = asObj.status;
121
+ }
122
+
123
+ if ('statusCode' in asObj && asObj.statusCode != null) {
124
+ details.statusCode = asObj.statusCode;
125
+ }
126
+ }
127
+
128
+ if (asObj && typeof asObj.message === 'string' && asObj.message) {
129
+ message = asObj.message;
130
+ } else if (typeof actualError === 'string') {
131
+ message = actualError;
132
+ } else if (asObj && typeof asObj.error === 'string' && asObj.error) {
133
+ message = asObj.error;
134
+ } else if (
135
+ asObj &&
136
+ typeof asObj.responseBody === 'string' &&
137
+ asObj.responseBody
138
+ ) {
139
+ details.responseBody = asObj.responseBody;
140
+ try {
141
+ const parsed = JSON.parse(asObj.responseBody);
142
+ if (parsed.error) {
143
+ if (typeof parsed.error === 'string') {
144
+ message = parsed.error;
145
+ } else if (typeof parsed.error.message === 'string') {
146
+ message = parsed.error.message;
147
+ } else {
148
+ message = asObj.responseBody;
149
+ }
150
+ } else {
151
+ message = asObj.responseBody;
152
+ }
153
+ } catch {
154
+ message = asObj.responseBody;
155
+ }
156
+ } else if (asObj?.statusCode && asObj.url) {
157
+ message = `HTTP ${String(asObj.statusCode)} error at ${String(asObj.url)}`;
158
+ details.url = asObj.url;
159
+ } else if (asObj?.name) {
160
+ message = String(asObj.name);
161
+ } else {
162
+ message = 'An error occurred';
163
+ }
164
+
165
+ if (asObj) {
166
+ if ('url' in asObj) details.url = asObj.url;
167
+ if ('isRetryable' in asObj) details.isRetryable = asObj.isRetryable;
168
+ if ('data' in asObj) details.data = asObj.data;
169
+
170
+ if (asObj.cause) {
171
+ const c = asObj.cause as Record<string, unknown> | undefined;
172
+ details.cause = {
173
+ message: typeof c?.message === 'string' ? c.message : undefined,
174
+ code: c?.code,
175
+ status: c?.status ?? c?.statusCode,
176
+ };
177
+ }
178
+
179
+ if (
180
+ (asObj as { response?: { status?: unknown; statusText?: unknown } })
181
+ ?.response?.status
182
+ ) {
183
+ details.response = {
184
+ status: (
185
+ asObj as { response?: { status?: unknown; statusText?: unknown } }
186
+ ).response?.status,
187
+ statusText: (
188
+ asObj as { response?: { status?: unknown; statusText?: unknown } }
189
+ ).response?.statusText,
190
+ };
191
+ }
192
+ }
193
+
194
+ return {
195
+ message,
196
+ type: errorType,
197
+ details: Object.keys(details).length ? details : undefined,
198
+ };
199
+ }
@@ -0,0 +1,154 @@
1
+ import type { getDb } from '@ottocode/database';
2
+ import { messageParts } from '@ottocode/database/schema';
3
+ import { eq } from 'drizzle-orm';
4
+ import { streamText } from 'ai';
5
+ import { resolveModel } from '../provider/index.ts';
6
+ import { getAuth } from '@ottocode/sdk';
7
+ import { loadConfig } from '@ottocode/sdk';
8
+ import { debugLog } from '../debug/index.ts';
9
+ import { getModelLimits } from './compaction-limits.ts';
10
+ import { buildCompactionContext } from './compaction-context.ts';
11
+ import { getCompactionSystemPrompt } from './compaction-detect.ts';
12
+ import { markSessionCompacted } from './compaction-mark.ts';
13
+ import { detectOAuth, adaptSimpleCall } from '../provider/oauth-adapter.ts';
14
+
15
+ export async function performAutoCompaction(
16
+ db: Awaited<ReturnType<typeof getDb>>,
17
+ sessionId: string,
18
+ assistantMessageId: string,
19
+ publishFn: (event: {
20
+ type: string;
21
+ sessionId: string;
22
+ payload: Record<string, unknown>;
23
+ }) => void,
24
+ provider: string,
25
+ modelId: string,
26
+ ): Promise<{
27
+ success: boolean;
28
+ summary?: string;
29
+ error?: string;
30
+ compactMessageId?: string;
31
+ }> {
32
+ debugLog(`[compaction] Starting auto-compaction for session ${sessionId}`);
33
+
34
+ try {
35
+ const limits = getModelLimits(provider, modelId);
36
+ const contextTokenLimit = limits
37
+ ? Math.max(Math.floor(limits.context * 0.5), 15000)
38
+ : 15000;
39
+ debugLog(
40
+ `[compaction] Model ${modelId} context limit: ${limits?.context ?? 'unknown'}, using ${contextTokenLimit} tokens for compaction`,
41
+ );
42
+
43
+ const context = await buildCompactionContext(
44
+ db,
45
+ sessionId,
46
+ contextTokenLimit,
47
+ );
48
+ if (!context || context.length < 100) {
49
+ debugLog('[compaction] Not enough context to compact');
50
+ return { success: false, error: 'Not enough context to compact' };
51
+ }
52
+
53
+ const cfg = await loadConfig();
54
+ debugLog(
55
+ `[compaction] Using session model ${provider}/${modelId} for auto-compaction`,
56
+ );
57
+
58
+ const auth = await getAuth(
59
+ provider as Parameters<typeof getAuth>[0],
60
+ cfg.projectRoot,
61
+ );
62
+ const oauth = detectOAuth(provider, auth);
63
+
64
+ debugLog(
65
+ `[compaction] OAuth: needsSpoof=${oauth.needsSpoof}, isOpenAIOAuth=${oauth.isOpenAIOAuth}`,
66
+ );
67
+
68
+ const model = await resolveModel(
69
+ provider as Parameters<typeof resolveModel>[0],
70
+ modelId,
71
+ cfg,
72
+ );
73
+
74
+ const compactionPrompt = getCompactionSystemPrompt();
75
+ const userContent = `IMPORTANT: Generate a comprehensive summary. This will replace the detailed conversation history.\n\nPlease summarize this conversation:\n\n<conversation-to-summarize>\n${context}\n</conversation-to-summarize>`;
76
+
77
+ const adapted = adaptSimpleCall(oauth, {
78
+ instructions: compactionPrompt,
79
+ userContent,
80
+ maxOutputTokens: 2000,
81
+ });
82
+
83
+ const compactPartId = crypto.randomUUID();
84
+ const now = Date.now();
85
+
86
+ await db.insert(messageParts).values({
87
+ id: compactPartId,
88
+ messageId: assistantMessageId,
89
+ index: 0,
90
+ stepIndex: 0,
91
+ type: 'text',
92
+ content: JSON.stringify({ text: '' }),
93
+ agent: 'system',
94
+ provider: provider,
95
+ model: modelId,
96
+ startedAt: now,
97
+ });
98
+
99
+ const result = streamText({
100
+ model,
101
+ system: adapted.system,
102
+ messages: adapted.messages,
103
+ maxOutputTokens: adapted.maxOutputTokens,
104
+ providerOptions: adapted.providerOptions,
105
+ });
106
+
107
+ let summary = '';
108
+ for await (const chunk of result.textStream) {
109
+ summary += chunk;
110
+
111
+ publishFn({
112
+ type: 'message.part.delta',
113
+ sessionId,
114
+ payload: {
115
+ messageId: assistantMessageId,
116
+ partId: compactPartId,
117
+ stepIndex: 0,
118
+ type: 'text',
119
+ delta: chunk,
120
+ },
121
+ });
122
+ }
123
+
124
+ await db
125
+ .update(messageParts)
126
+ .set({
127
+ content: JSON.stringify({ text: summary }),
128
+ completedAt: Date.now(),
129
+ })
130
+ .where(eq(messageParts.id, compactPartId));
131
+
132
+ if (!summary || summary.length < 50) {
133
+ debugLog('[compaction] Failed to generate summary');
134
+ return { success: false, error: 'Failed to generate summary' };
135
+ }
136
+
137
+ debugLog(`[compaction] Generated summary: ${summary.slice(0, 100)}...`);
138
+
139
+ const compactResult = await markSessionCompacted(
140
+ db,
141
+ sessionId,
142
+ assistantMessageId,
143
+ );
144
+ debugLog(
145
+ `[compaction] Marked ${compactResult.compacted} parts as compacted, saved ~${compactResult.saved} tokens`,
146
+ );
147
+
148
+ return { success: true, summary, compactMessageId: assistantMessageId };
149
+ } catch (err) {
150
+ const errorMsg = err instanceof Error ? err.message : String(err);
151
+ debugLog(`[compaction] Auto-compaction failed: ${errorMsg}`);
152
+ return { success: false, error: errorMsg };
153
+ }
154
+ }