@ottocode/server 0.1.237 → 0.1.242

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.
@@ -0,0 +1,84 @@
1
+ import type { DB } from '@ottocode/database';
2
+ import type { OttoConfig, ProviderId } from '@ottocode/sdk';
3
+ import {
4
+ buildCompactionContext,
5
+ getModelLimits,
6
+ isCompactCommand,
7
+ } from '../message/compaction.ts';
8
+ import {
9
+ buildInitCommandUserPrompt,
10
+ buildInitProjectSnapshot,
11
+ getInitCommandSystemPrompt,
12
+ isInitCommand,
13
+ } from './init.ts';
14
+
15
+ export type BuiltinCommandPromptMessage = {
16
+ role: 'system' | 'user';
17
+ content: string;
18
+ };
19
+
20
+ export type BuiltinCommandSpec = {
21
+ id: 'compact' | 'init';
22
+ agent?: string;
23
+ oneShot?: boolean;
24
+ /**
25
+ * Controls prompt construction only. The run still executes in the current
26
+ * session and persists messages/tool activity through the normal pipeline.
27
+ */
28
+ omitHistory?: boolean;
29
+ isCompactCommand?: boolean;
30
+ compactionContext?: string;
31
+ additionalPromptMessages?: BuiltinCommandPromptMessage[];
32
+ };
33
+
34
+ /**
35
+ * Returns a prepared built-in slash command spec when the message matches one.
36
+ *
37
+ * These commands still run through the normal current-session agent pipeline;
38
+ * this only customizes the prompt setup and agent selection.
39
+ */
40
+ export async function prepareBuiltinCommand(args: {
41
+ cfg: OttoConfig;
42
+ db: DB;
43
+ sessionId: string;
44
+ provider: ProviderId;
45
+ model: string;
46
+ content: string;
47
+ }): Promise<BuiltinCommandSpec | null> {
48
+ if (isCompactCommand(args.content)) {
49
+ const limits = getModelLimits(args.provider, args.model);
50
+ const contextTokenLimit = limits
51
+ ? Math.max(Math.floor(limits.context * 0.5), 15000)
52
+ : 15000;
53
+ const compactionContext = await buildCompactionContext(
54
+ args.db,
55
+ args.sessionId,
56
+ contextTokenLimit,
57
+ );
58
+ return {
59
+ id: 'compact',
60
+ isCompactCommand: true,
61
+ omitHistory: true,
62
+ compactionContext,
63
+ };
64
+ }
65
+
66
+ if (isInitCommand(args.content)) {
67
+ const snapshot = await buildInitProjectSnapshot(args.cfg.projectRoot);
68
+ return {
69
+ id: 'init',
70
+ agent: 'init',
71
+ oneShot: true,
72
+ omitHistory: true,
73
+ additionalPromptMessages: [
74
+ { role: 'system', content: getInitCommandSystemPrompt() },
75
+ {
76
+ role: 'user',
77
+ content: buildInitCommandUserPrompt(args.cfg.projectRoot, snapshot),
78
+ },
79
+ ],
80
+ };
81
+ }
82
+
83
+ return null;
84
+ }
@@ -0,0 +1,358 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { basename, join, relative } from 'node:path';
3
+
4
+ const IGNORED_NAMES = new Set([
5
+ '.git',
6
+ '.idea',
7
+ '.next',
8
+ '.turbo',
9
+ '.venv',
10
+ '.vscode',
11
+ 'dist',
12
+ 'build',
13
+ 'coverage',
14
+ 'node_modules',
15
+ 'target',
16
+ ]);
17
+
18
+ const KEY_DIRECTORY_LIMIT = 24;
19
+ const WORKSPACE_LIMIT = 24;
20
+
21
+ /** Returns true when the message is the built-in /init command. */
22
+ export function isInitCommand(content: string): boolean {
23
+ return content.trim().toLowerCase() === '/init';
24
+ }
25
+
26
+ /** System instructions for the focused /init repo-analysis run. */
27
+ export function getInitCommandSystemPrompt(): string {
28
+ return [
29
+ 'You are generating durable agent instructions for this repository.',
30
+ '',
31
+ 'Your job is to inspect the real project structure and source code, then create or refresh AGENTS.md documentation for future coding agents.',
32
+ '',
33
+ 'Hard rules:',
34
+ '- Do not trust existing markdown as the source of truth; code, config, and filesystem structure win when there is a conflict.',
35
+ '- Inspect actual package manifests, source directories, route wiring, schemas, app entrypoints, and build configuration before writing.',
36
+ '- Prefer a small number of strong docs over many tiny docs.',
37
+ '- If this is a monorepo, create a root AGENTS.md that acts as the routing/index doc and points to a small set of focused docs under .agents/.',
38
+ '- If this is not a monorepo, root AGENTS.md may stay mostly self-contained with only minimal supporting docs.',
39
+ '- Root AGENTS.md must tell future agents which .agents doc to read for mobile, server/api, web/tui clients, database, and cross-cutting changes when applicable.',
40
+ '- Avoid oversplitting. Only create supporting docs when a domain is meaningfully distinct. Aim for roughly 3-6 supporting docs max when splitting is needed.',
41
+ '- Keep docs actionable for coding agents: architecture, key file paths, workflow rules, and when to consult related docs.',
42
+ '- Reuse and update existing AGENTS.md or .agents docs when appropriate instead of duplicating content.',
43
+ '',
44
+ 'Expected process:',
45
+ '1. Scan the repository structure and key code/config files with tools.',
46
+ '2. Decide the minimum useful documentation split.',
47
+ '3. Write or update AGENTS.md and any needed .agents/*.md files.',
48
+ '4. Finish with a concise summary of what you generated and why.',
49
+ ].join('\n');
50
+ }
51
+
52
+ /** Builds a concise filesystem-grounded snapshot to seed the /init run. */
53
+ export async function buildInitProjectSnapshot(
54
+ projectRoot: string,
55
+ ): Promise<string> {
56
+ const rootManifest = await readJson<Record<string, unknown>>(
57
+ join(projectRoot, 'package.json'),
58
+ );
59
+ const workspacePatterns = extractWorkspacePatterns(rootManifest?.workspaces);
60
+ const workspaceDirs = await collectWorkspaceDirs(
61
+ projectRoot,
62
+ workspacePatterns,
63
+ );
64
+ const topLevelDirs = await listDirectoryNames(
65
+ projectRoot,
66
+ true,
67
+ KEY_DIRECTORY_LIMIT,
68
+ );
69
+ const topLevelFiles = await listDirectoryNames(
70
+ projectRoot,
71
+ false,
72
+ KEY_DIRECTORY_LIMIT,
73
+ );
74
+ const appDirs = await listDirectoryNames(
75
+ join(projectRoot, 'apps'),
76
+ true,
77
+ KEY_DIRECTORY_LIMIT,
78
+ );
79
+ const packageDirs = await listDirectoryNames(
80
+ join(projectRoot, 'packages'),
81
+ true,
82
+ KEY_DIRECTORY_LIMIT,
83
+ );
84
+ const docsDirs = await listDirectoryNames(
85
+ join(projectRoot, 'docs'),
86
+ false,
87
+ KEY_DIRECTORY_LIMIT,
88
+ );
89
+ const routeFiles = await listDirectoryNames(
90
+ join(projectRoot, 'packages/server/src/routes'),
91
+ false,
92
+ KEY_DIRECTORY_LIMIT,
93
+ );
94
+ const schemaFiles = await listDirectoryNames(
95
+ join(projectRoot, 'packages/database/src/schema'),
96
+ false,
97
+ KEY_DIRECTORY_LIMIT,
98
+ );
99
+ const sdkDirs = await listDirectoryNames(
100
+ join(projectRoot, 'packages/sdk/src'),
101
+ true,
102
+ KEY_DIRECTORY_LIMIT,
103
+ );
104
+ const webSdkDirs = await listDirectoryNames(
105
+ join(projectRoot, 'packages/web-sdk/src'),
106
+ true,
107
+ KEY_DIRECTORY_LIMIT,
108
+ );
109
+ const existingAgentDocs = await summarizeExistingAgentDocs(projectRoot);
110
+ const workspaceSummaries = await Promise.all(
111
+ workspaceDirs.slice(0, WORKSPACE_LIMIT).map(async (workspaceDir) => {
112
+ const rel = toProjectRelative(projectRoot, workspaceDir);
113
+ const manifest = await readJson<Record<string, unknown>>(
114
+ join(workspaceDir, 'package.json'),
115
+ );
116
+ return summarizeWorkspace(rel, manifest);
117
+ }),
118
+ );
119
+
120
+ const runtimeSignals = [
121
+ (await exists(join(projectRoot, 'bun.lock'))) ? 'bun.lock' : null,
122
+ (await exists(join(projectRoot, 'bunfig.toml'))) ? 'bunfig.toml' : null,
123
+ (await exists(join(projectRoot, 'biome.json'))) ? 'biome.json' : null,
124
+ (await exists(join(projectRoot, 'tsconfig.base.json')))
125
+ ? 'tsconfig.base.json'
126
+ : null,
127
+ (await exists(join(projectRoot, 'docker'))) ? 'docker/' : null,
128
+ ].filter((value): value is string => Boolean(value));
129
+
130
+ const lines = [
131
+ `Project root: ${projectRoot}`,
132
+ `Repo shape: ${workspacePatterns.length > 0 || workspaceDirs.length > 1 ? 'monorepo/workspace' : 'single-project or minimal workspace'}`,
133
+ rootManifest?.name ? `Root package: ${String(rootManifest.name)}` : null,
134
+ workspacePatterns.length
135
+ ? `Workspace globs: ${workspacePatterns.join(', ')}`
136
+ : 'Workspace globs: none declared',
137
+ runtimeSignals.length
138
+ ? `Tooling signals: ${runtimeSignals.join(', ')}`
139
+ : null,
140
+ topLevelDirs.length
141
+ ? `Top-level directories: ${topLevelDirs.join(', ')}`
142
+ : null,
143
+ topLevelFiles.length
144
+ ? `Top-level files: ${topLevelFiles.join(', ')}`
145
+ : null,
146
+ appDirs.length ? `apps/: ${appDirs.join(', ')}` : null,
147
+ packageDirs.length ? `packages/: ${packageDirs.join(', ')}` : null,
148
+ docsDirs.length ? `docs/: ${docsDirs.join(', ')}` : null,
149
+ routeFiles.length
150
+ ? `packages/server/src/routes: ${routeFiles.join(', ')}`
151
+ : null,
152
+ schemaFiles.length
153
+ ? `packages/database/src/schema: ${schemaFiles.join(', ')}`
154
+ : null,
155
+ sdkDirs.length ? `packages/sdk/src dirs: ${sdkDirs.join(', ')}` : null,
156
+ webSdkDirs.length
157
+ ? `packages/web-sdk/src dirs: ${webSdkDirs.join(', ')}`
158
+ : null,
159
+ existingAgentDocs,
160
+ workspaceSummaries.length ? 'Workspace packages:' : null,
161
+ ...workspaceSummaries.map((summary) => `- ${summary}`),
162
+ ]
163
+ .filter((line): line is string => Boolean(line))
164
+ .join('\n');
165
+
166
+ return lines;
167
+ }
168
+
169
+ /** Builds the primary user prompt for /init after the repo snapshot is collected. */
170
+ export function buildInitCommandUserPrompt(
171
+ projectRoot: string,
172
+ snapshot: string,
173
+ ): string {
174
+ return [
175
+ 'Generate or refresh the repository agent documentation for this project.',
176
+ '',
177
+ 'Deliverables:',
178
+ '- Root AGENTS.md at the repository root.',
179
+ '- Supporting docs under .agents/ only when needed.',
180
+ '',
181
+ 'What to analyze before writing:',
182
+ '- monorepo boundaries and workspace responsibilities',
183
+ '- how server/API routes are wired',
184
+ '- how web and TUI clients are wired',
185
+ '- whether mobile deserves its own doc',
186
+ '- database/schema/migration locations',
187
+ '- SDK/shared packages and build/test tooling',
188
+ '',
189
+ 'Document design rules:',
190
+ '- Root AGENTS.md should be the primary entrypoint and routing guide for future LLM agents.',
191
+ '- Root AGENTS.md should point to the exact .agents docs to read for different task types.',
192
+ '- If a task touches multiple layers, root AGENTS.md should say to read multiple docs.',
193
+ '- Prefer fewer, stronger docs. Do not scatter tiny markdown files.',
194
+ '- Keep instructions concrete and file-path-aware.',
195
+ '- Mention repository-specific norms that matter for safe changes.',
196
+ '',
197
+ 'Repository snapshot (filesystem-grounded, still verify with tools):',
198
+ '<project-snapshot>',
199
+ snapshot,
200
+ '</project-snapshot>',
201
+ '',
202
+ `Project root for file writes: ${projectRoot}`,
203
+ '',
204
+ 'Now inspect the real code/config with tools, decide the right doc split, and write the docs.',
205
+ ].join('\n');
206
+ }
207
+
208
+ async function summarizeExistingAgentDocs(
209
+ projectRoot: string,
210
+ ): Promise<string | null> {
211
+ const rootAgents = (await exists(join(projectRoot, 'AGENTS.md')))
212
+ ? 'AGENTS.md'
213
+ : null;
214
+ const subdocs = await listDirectoryNames(
215
+ join(projectRoot, '.agents'),
216
+ false,
217
+ KEY_DIRECTORY_LIMIT,
218
+ );
219
+ if (!rootAgents && subdocs.length === 0) return null;
220
+ return `Existing agent docs: ${[rootAgents, ...subdocs].filter(Boolean).join(', ')}`;
221
+ }
222
+
223
+ async function collectWorkspaceDirs(
224
+ projectRoot: string,
225
+ patterns: string[],
226
+ ): Promise<string[]> {
227
+ const found = new Set<string>();
228
+ for (const pattern of patterns) {
229
+ for (const match of await expandWorkspacePattern(projectRoot, pattern)) {
230
+ found.add(match);
231
+ }
232
+ }
233
+
234
+ if (found.size === 0) {
235
+ for (const fallbackRoot of ['apps', 'packages', 'examples']) {
236
+ for (const dir of await listSubdirectories(
237
+ join(projectRoot, fallbackRoot),
238
+ )) {
239
+ found.add(dir);
240
+ }
241
+ }
242
+ }
243
+
244
+ return Array.from(found).sort((a, b) => a.localeCompare(b));
245
+ }
246
+
247
+ async function expandWorkspacePattern(
248
+ projectRoot: string,
249
+ pattern: string,
250
+ ): Promise<string[]> {
251
+ const normalized = pattern.replace(/\\/g, '/').replace(/\/$/, '');
252
+ if (!normalized.endsWith('/*')) return [];
253
+ const base = normalized.slice(0, -2);
254
+ if (!base || base.includes('*')) return [];
255
+ return listSubdirectories(join(projectRoot, base));
256
+ }
257
+
258
+ function extractWorkspacePatterns(workspaces: unknown): string[] {
259
+ if (Array.isArray(workspaces)) {
260
+ return workspaces.filter(
261
+ (value): value is string => typeof value === 'string',
262
+ );
263
+ }
264
+ if (
265
+ workspaces &&
266
+ typeof workspaces === 'object' &&
267
+ Array.isArray((workspaces as { packages?: unknown }).packages)
268
+ ) {
269
+ return (workspaces as { packages: unknown[] }).packages.filter(
270
+ (value): value is string => typeof value === 'string',
271
+ );
272
+ }
273
+ return [];
274
+ }
275
+
276
+ function summarizeWorkspace(
277
+ relPath: string,
278
+ manifest?: Record<string, unknown>,
279
+ ): string {
280
+ const name =
281
+ typeof manifest?.name === 'string'
282
+ ? manifest.name
283
+ : `(${basename(relPath)})`;
284
+ const scripts =
285
+ manifest?.scripts && typeof manifest.scripts === 'object'
286
+ ? Object.keys(manifest.scripts as Record<string, unknown>).slice(0, 6)
287
+ : [];
288
+ const flags = [
289
+ typeof manifest?.private === 'boolean'
290
+ ? manifest.private
291
+ ? 'private'
292
+ : 'public'
293
+ : null,
294
+ scripts.length ? `scripts: ${scripts.join(', ')}` : null,
295
+ ]
296
+ .filter((value): value is string => Boolean(value))
297
+ .join(' | ');
298
+ return flags ? `${relPath} → ${name} (${flags})` : `${relPath} → ${name}`;
299
+ }
300
+
301
+ async function listSubdirectories(path: string): Promise<string[]> {
302
+ try {
303
+ const entries = await readdir(path, { withFileTypes: true });
304
+ return entries
305
+ .filter((entry) => entry.isDirectory() && !shouldIgnore(entry.name))
306
+ .map((entry) => join(path, entry.name))
307
+ .sort((a, b) => a.localeCompare(b));
308
+ } catch {
309
+ return [];
310
+ }
311
+ }
312
+
313
+ async function listDirectoryNames(
314
+ path: string,
315
+ directoriesOnly: boolean,
316
+ limit: number,
317
+ ): Promise<string[]> {
318
+ try {
319
+ const entries = await readdir(path, { withFileTypes: true });
320
+ return entries
321
+ .filter((entry) => !shouldIgnore(entry.name))
322
+ .filter((entry) =>
323
+ directoriesOnly ? entry.isDirectory() : !entry.isDirectory(),
324
+ )
325
+ .map((entry) => (entry.isDirectory() ? `${entry.name}/` : entry.name))
326
+ .sort((a, b) => a.localeCompare(b))
327
+ .slice(0, limit);
328
+ } catch {
329
+ return [];
330
+ }
331
+ }
332
+
333
+ function shouldIgnore(name: string): boolean {
334
+ return IGNORED_NAMES.has(name) || name.startsWith('.DS_Store');
335
+ }
336
+
337
+ async function readJson<T>(path: string): Promise<T | undefined> {
338
+ try {
339
+ const file = Bun.file(path);
340
+ if (!(await file.exists())) return undefined;
341
+ return (await file.json()) as T;
342
+ } catch {
343
+ return undefined;
344
+ }
345
+ }
346
+
347
+ async function exists(path: string): Promise<boolean> {
348
+ try {
349
+ return await Bun.file(path).exists();
350
+ } catch {
351
+ return false;
352
+ }
353
+ }
354
+
355
+ function toProjectRelative(projectRoot: string, absPath: string): string {
356
+ const rel = relative(projectRoot, absPath).replace(/\\/g, '/');
357
+ return rel.length > 0 ? rel : '.';
358
+ }
@@ -12,6 +12,46 @@ export interface ModelLimits {
12
12
  output: number;
13
13
  }
14
14
 
15
+ export function shouldAutoCompactBeforeOverflow(args: {
16
+ autoCompactThresholdTokens?: number | null;
17
+ modelContextWindow?: number | null;
18
+ currentContextTokens?: number | null;
19
+ estimatedInputTokens?: number | null;
20
+ isCompactCommand?: boolean;
21
+ compactionRetries?: number;
22
+ }): boolean {
23
+ const threshold = Number(args.autoCompactThresholdTokens ?? 0);
24
+ if (!Number.isFinite(threshold) || threshold <= 0) {
25
+ return false;
26
+ }
27
+ if (args.isCompactCommand) {
28
+ return false;
29
+ }
30
+ if ((args.compactionRetries ?? 0) > 0) {
31
+ return false;
32
+ }
33
+
34
+ const modelContextWindow = Number(args.modelContextWindow ?? 0);
35
+ if (!Number.isFinite(modelContextWindow) || modelContextWindow <= threshold) {
36
+ return false;
37
+ }
38
+
39
+ const currentContextTokens = Math.max(
40
+ 0,
41
+ Math.floor(Number(args.currentContextTokens ?? 0)),
42
+ );
43
+ if (currentContextTokens <= 0) {
44
+ return false;
45
+ }
46
+
47
+ const estimatedInputTokens = Math.max(
48
+ 0,
49
+ Math.floor(Number(args.estimatedInputTokens ?? 0)),
50
+ );
51
+
52
+ return currentContextTokens + estimatedInputTokens >= threshold;
53
+ }
54
+
15
55
  export function getModelLimits(
16
56
  provider: string,
17
57
  model: string,
@@ -3,6 +3,7 @@ export {
3
3
  estimateTokens,
4
4
  type ModelLimits,
5
5
  getModelLimits,
6
+ shouldAutoCompactBeforeOverflow,
6
7
  isCompacted,
7
8
  COMPACTED_PLACEHOLDER,
8
9
  } from './compaction-limits.ts';
@@ -13,8 +13,9 @@ import {
13
13
  type ProviderId,
14
14
  type ReasoningLevel,
15
15
  } from '@ottocode/sdk';
16
- import { isCompactCommand, buildCompactionContext } from './compaction.ts';
16
+ import { estimateTokens } from './compaction.ts';
17
17
  import { detectOAuth, adaptSimpleCall } from '../provider/oauth-adapter.ts';
18
+ import { prepareBuiltinCommand } from '../commands/builtins.ts';
18
19
 
19
20
  type SessionRow = typeof sessions.$inferSelect;
20
21
 
@@ -61,14 +62,25 @@ export async function dispatchAssistantMessage(
61
62
 
62
63
  const sessionId = session.id;
63
64
  const now = Date.now();
65
+ const builtinCommand = await prepareBuiltinCommand({
66
+ cfg,
67
+ db,
68
+ sessionId,
69
+ provider,
70
+ model,
71
+ content,
72
+ });
73
+ const effectiveAgent = builtinCommand?.agent ?? agent;
74
+ const effectiveOneShot = builtinCommand?.oneShot ?? oneShot;
64
75
  const userMessageId = crypto.randomUUID();
65
76
  logger.debug('[agent] dispatching assistant message', {
66
77
  sessionId,
67
- agent,
78
+ agent: effectiveAgent,
68
79
  provider,
69
80
  model,
70
- oneShot: Boolean(oneShot),
81
+ oneShot: Boolean(effectiveOneShot),
71
82
  hasUserContext: Boolean(userContext),
83
+ builtinCommand: builtinCommand?.id,
72
84
  });
73
85
 
74
86
  await db.insert(messages).values({
@@ -76,7 +88,7 @@ export async function dispatchAssistantMessage(
76
88
  sessionId,
77
89
  role: 'user',
78
90
  status: 'complete',
79
- agent,
91
+ agent: effectiveAgent,
80
92
  provider,
81
93
  model,
82
94
  createdAt: now,
@@ -87,7 +99,7 @@ export async function dispatchAssistantMessage(
87
99
  index: 0,
88
100
  type: 'text',
89
101
  content: JSON.stringify({ text: String(content) }),
90
- agent,
102
+ agent: effectiveAgent,
91
103
  provider,
92
104
  model,
93
105
  });
@@ -101,7 +113,7 @@ export async function dispatchAssistantMessage(
101
113
  index: i + 1,
102
114
  type: 'image',
103
115
  content: JSON.stringify({ data: img.data, mediaType: img.mediaType }),
104
- agent,
116
+ agent: effectiveAgent,
105
117
  provider,
106
118
  model,
107
119
  });
@@ -124,7 +136,7 @@ export async function dispatchAssistantMessage(
124
136
  mediaType: file.mediaType,
125
137
  textContent: file.textContent,
126
138
  }),
127
- agent,
139
+ agent: effectiveAgent,
128
140
  provider,
129
141
  model,
130
142
  });
@@ -137,7 +149,7 @@ export async function dispatchAssistantMessage(
137
149
  payload: {
138
150
  id: userMessageId,
139
151
  role: 'user',
140
- agent,
152
+ agent: effectiveAgent,
141
153
  provider,
142
154
  model,
143
155
  content: String(content),
@@ -150,7 +162,7 @@ export async function dispatchAssistantMessage(
150
162
  sessionId,
151
163
  role: 'assistant',
152
164
  status: 'pending',
153
- agent,
165
+ agent: effectiveAgent,
154
166
  provider,
155
167
  model,
156
168
  createdAt: Date.now(),
@@ -161,27 +173,23 @@ export async function dispatchAssistantMessage(
161
173
  payload: {
162
174
  id: assistantMessageId,
163
175
  role: 'assistant',
164
- agent,
176
+ agent: effectiveAgent,
165
177
  provider,
166
178
  model,
167
179
  },
168
180
  });
169
181
 
170
- const isCompact = isCompactCommand(content);
171
- let compactionContext: string | undefined;
172
-
173
- if (isCompact) {
174
- const { getModelLimits } = await import('./compaction.ts');
175
- const limits = getModelLimits(provider, model);
176
- const contextTokenLimit = limits
177
- ? Math.max(Math.floor(limits.context * 0.5), 15000)
178
- : 15000;
179
- compactionContext = await buildCompactionContext(
180
- db,
181
- sessionId,
182
- contextTokenLimit,
183
- );
184
- }
182
+ const commandPromptText =
183
+ builtinCommand?.additionalPromptMessages
184
+ ?.map((message) => message.content)
185
+ .join('\n\n') ?? content;
186
+ const estimatedInputTokens =
187
+ estimateTokens(commandPromptText) +
188
+ estimateTokens(userContext ?? '') +
189
+ (files?.reduce(
190
+ (total, file) => total + estimateTokens(file.textContent ?? ''),
191
+ 0,
192
+ ) ?? 0);
185
193
 
186
194
  const toolApprovalMode = cfg.defaults.toolApproval ?? 'dangerous';
187
195
 
@@ -189,16 +197,19 @@ export async function dispatchAssistantMessage(
189
197
  {
190
198
  sessionId,
191
199
  assistantMessageId,
192
- agent,
200
+ agent: effectiveAgent,
193
201
  provider,
194
202
  model,
195
203
  projectRoot: cfg.projectRoot,
196
- oneShot: Boolean(oneShot),
204
+ oneShot: Boolean(effectiveOneShot),
197
205
  userContext,
206
+ estimatedInputTokens,
198
207
  reasoningText,
199
208
  reasoningLevel,
200
- isCompactCommand: isCompact,
201
- compactionContext,
209
+ omitHistory: builtinCommand?.omitHistory,
210
+ isCompactCommand: builtinCommand?.isCompactCommand,
211
+ compactionContext: builtinCommand?.compactionContext,
212
+ additionalPromptMessages: builtinCommand?.additionalPromptMessages,
202
213
  toolApprovalMode,
203
214
  },
204
215
  runSessionLoop,
@@ -206,10 +217,11 @@ export async function dispatchAssistantMessage(
206
217
  logger.debug('[agent] assistant run enqueued', {
207
218
  sessionId,
208
219
  assistantMessageId,
209
- agent,
220
+ agent: effectiveAgent,
210
221
  provider,
211
222
  model,
212
- isCompactCommand: isCompact,
223
+ builtinCommand: builtinCommand?.id,
224
+ isCompactCommand: builtinCommand?.isCompactCommand,
213
225
  });
214
226
 
215
227
  void touchSessionLastActive({ db, sessionId });
@@ -134,6 +134,8 @@ function buildSharedProviderOptions(
134
134
  function usesAdaptiveAnthropicThinking(model: string): boolean {
135
135
  const lower = model.toLowerCase();
136
136
  return (
137
+ lower.includes('claude-opus-4-7') ||
138
+ lower.includes('claude-opus-4.7') ||
137
139
  lower.includes('claude-opus-4-6') ||
138
140
  lower.includes('claude-opus-4.6') ||
139
141
  lower.includes('claude-sonnet-4-6') ||
@@ -12,11 +12,21 @@ export type RunOpts = {
12
12
  projectRoot: string;
13
13
  oneShot?: boolean;
14
14
  userContext?: string;
15
+ estimatedInputTokens?: number;
15
16
  reasoningText?: boolean;
16
17
  reasoningLevel?: ReasoningLevel;
17
18
  abortSignal?: AbortSignal;
19
+ /**
20
+ * Omits prior session history from prompt assembly only. The run still emits
21
+ * events, tool calls, and persisted message parts in the current session.
22
+ */
23
+ omitHistory?: boolean;
18
24
  isCompactCommand?: boolean;
19
25
  compactionContext?: string;
26
+ additionalPromptMessages?: Array<{
27
+ role: 'system' | 'user';
28
+ content: string;
29
+ }>;
20
30
  toolApprovalMode?: ToolApprovalMode;
21
31
  compactionRetries?: number;
22
32
  continuationCount?: number;