@kmugalkhod/lightcode 0.1.3 → 0.3.0

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 (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -0
  3. package/cli.js +1540 -542
  4. package/package.json +4 -2
  5. package/server.js +1141 -194
package/server.js CHANGED
@@ -17,6 +17,19 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  var __require = import.meta.require;
18
18
 
19
19
  // packages/shared/src/data-dir.ts
20
+ import { homedir, platform } from "os";
21
+ import path from "path";
22
+ function getLightcodeDataDir(env = process.env) {
23
+ const configuredHome = env.LIGHTCODE_HOME?.trim();
24
+ if (configuredHome) {
25
+ return path.resolve(configuredHome);
26
+ }
27
+ if (platform() === "win32") {
28
+ const appData = env.APPDATA || env.LOCALAPPDATA;
29
+ return appData ? path.join(appData, "lightcode") : path.join(homedir(), "AppData", "Roaming", "lightcode");
30
+ }
31
+ return path.join(homedir(), ".lightcode");
32
+ }
20
33
  var init_data_dir = () => {};
21
34
 
22
35
  // packages/shared/src/error-utils.ts
@@ -31,6 +44,14 @@ function getErrorMessage(error, fallback = "Unknown error") {
31
44
  }
32
45
 
33
46
  // packages/shared/src/logger.ts
47
+ import {
48
+ appendFileSync,
49
+ existsSync,
50
+ mkdirSync,
51
+ readdirSync,
52
+ unlinkSync
53
+ } from "fs";
54
+ import path2 from "path";
34
55
  function resolveConfiguredLevel() {
35
56
  const raw = process.env.LIGHTCODE_LOG_LEVEL?.trim().toLowerCase();
36
57
  if (raw === "debug" || raw === "info" || raw === "warn" || raw === "error") {
@@ -38,6 +59,49 @@ function resolveConfiguredLevel() {
38
59
  }
39
60
  return "info";
40
61
  }
62
+ function getLogDirectory(env = process.env) {
63
+ return path2.join(getLightcodeDataDir(env), "logs");
64
+ }
65
+ function currentDateStamp() {
66
+ return new Date().toISOString().slice(0, 10);
67
+ }
68
+ function pruneOldLogFiles(directory) {
69
+ try {
70
+ const logFiles = readdirSync(directory).filter((name) => logFilePattern.test(name)).sort();
71
+ for (const name of logFiles.slice(0, -maxLogFilesKept)) {
72
+ unlinkSync(path2.join(directory, name));
73
+ }
74
+ } catch {}
75
+ }
76
+ function enableFileLogSink(env = process.env) {
77
+ try {
78
+ const directory = getLogDirectory(env);
79
+ mkdirSync(directory, { recursive: true });
80
+ fileSinkPrefix = directory;
81
+ pruneOldLogFiles(directory);
82
+ return directory;
83
+ } catch {
84
+ fileSinkPrefix = null;
85
+ return null;
86
+ }
87
+ }
88
+ function writeToFileSink(line) {
89
+ if (!fileSinkPrefix) {
90
+ return;
91
+ }
92
+ try {
93
+ const date = currentDateStamp();
94
+ if (!fileSinkState || fileSinkState.date !== date) {
95
+ fileSinkState = {
96
+ date,
97
+ filePath: path2.join(fileSinkPrefix, `server-${date}.log`)
98
+ };
99
+ pruneOldLogFiles(fileSinkPrefix);
100
+ }
101
+ appendFileSync(fileSinkState.filePath, `${line}
102
+ `);
103
+ } catch {}
104
+ }
41
105
  function createLogger(name) {
42
106
  const write = (level, event, fields) => {
43
107
  if (levelOrder[level] < levelOrder[resolveConfiguredLevel()]) {
@@ -52,6 +116,7 @@ function createLogger(name) {
52
116
  });
53
117
  process.stderr.write(`${line}
54
118
  `);
119
+ writeToFileSink(line);
55
120
  };
56
121
  return {
57
122
  debug: (event, fields) => write("debug", event, fields),
@@ -60,14 +125,16 @@ function createLogger(name) {
60
125
  error: (event, fields) => write("error", event, fields)
61
126
  };
62
127
  }
63
- var levelOrder;
128
+ var levelOrder, maxLogFilesKept = 5, logFilePattern, fileSinkPrefix = null, fileSinkState = null;
64
129
  var init_logger = __esm(() => {
130
+ init_data_dir();
65
131
  levelOrder = {
66
132
  debug: 10,
67
133
  info: 20,
68
134
  warn: 30,
69
135
  error: 40
70
136
  };
137
+ logFilePattern = /^server-\d{4}-\d{2}-\d{2}\.log$/;
71
138
  });
72
139
  // packages/shared/src/index.ts
73
140
  function createGreeting(target) {
@@ -1486,6 +1553,29 @@ var init_agent_tools = __esm(() => {
1486
1553
  });
1487
1554
  });
1488
1555
 
1556
+ // packages/ai/src/chat-error.ts
1557
+ function formatChatStreamError(info) {
1558
+ const payload = {
1559
+ ...info,
1560
+ message: info.message.slice(0, maxTransportMessageLength)
1561
+ };
1562
+ return `${chatStreamErrorPrefix}${JSON.stringify(payload)}`;
1563
+ }
1564
+ var chatStreamErrorPrefix = "LIGHTCODE_ERROR:", maxTransportMessageLength = 500, chatErrorKinds;
1565
+ var init_chat_error = __esm(() => {
1566
+ chatErrorKinds = new Set([
1567
+ "auth",
1568
+ "billing",
1569
+ "rate_limit",
1570
+ "invalid_request",
1571
+ "context_overflow",
1572
+ "provider_unavailable",
1573
+ "network",
1574
+ "aborted",
1575
+ "unknown"
1576
+ ]);
1577
+ });
1578
+
1489
1579
  // packages/ai/src/context/context-state.ts
1490
1580
  import { z as z22 } from "zod";
1491
1581
  var contextStateTierSchema, sessionContextStateSchema;
@@ -1811,7 +1901,9 @@ var init_coding_agent_prompt = __esm(() => {
1811
1901
  init_coding_agent_modes();
1812
1902
  defaultCodingAgentSystemPrompt = "You are a basic coding agent. Use tools for filesystem and codebase tasks instead of guessing. " + "Respect the user's intent, explain changes clearly, and prefer incremental, auditable actions. " + "You can only interact with files under this working directory: {cwd}. " + "Use glob_search for path discovery, grep for text search, structured git tools for repository inspection, and bash only when dedicated tools are not enough. " + "Use todo_write to keep multi-step implementation work visible and current. " + "For risky or uncertain operations, inspect context first and be explicit about assumptions. " + "While working, emit brief progress notes in natural language before major tool actions and after important findings. " + `Keep them short, human, and concrete. Vary wording naturally and avoid repetitive templates or rigid labels.
1813
1903
 
1814
- ` + "You are an agent \u2014 keep working until the user's request is fully resolved before ending your turn. " + "Never stop with a partial answer. If you say you will do something, do it in the same turn by calling a tool. " + "Use todo_write to track multi-step work and do not finish while any todo is pending or in_progress. " + "Only end your turn when the task is complete or you are blocked on input that only the user can provide.";
1904
+ ` + "You are an agent \u2014 keep working until the user's request is fully resolved before ending your turn. " + "Never stop with a partial answer. If you say you will do something, do it in the same turn by calling a tool. " + "Use todo_write to track multi-step work and do not finish while any todo is pending or in_progress. " + `Only end your turn when the task is complete or you are blocked on input that only the user can provide.
1905
+
1906
+ ` + "Long-running processes: never run dev servers, watchers, or other non-exiting commands as plain foreground bash \u2014 " + "the bash tool enforces a timeout and the command will be killed. Start them detached with output redirected to a " + 'log file (e.g. on Windows: start /B cmd /c "npm run dev > dev.log 2>&1"; on Unix: nohup npm run dev > dev.log 2>&1 &), ' + "wait briefly, then verify by reading the log file or with a bounded probe like curl --max-time 5. " + "Dev servers do not always get their default port \u2014 read the actual port from the startup log instead of assuming it. " + "Never kill processes or free ports you did not open yourself; other tools (including this agent's own backend) may be using them.";
1815
1907
  toolCallingDisciplineAddendum = "Invoke tools only through the function-calling interface. " + "Never write tool calls as XML or plain text such as <tool_call>, <function_call>, <invoke>, or bare JSON in your message. " + "Tool arguments must be valid JSON matching the tool's schema. " + "Call one tool at a time and wait for its result before deciding the next step.";
1816
1908
  });
1817
1909
 
@@ -2456,6 +2548,7 @@ function createCodingAgent({
2456
2548
  promptOverride,
2457
2549
  maxOutputTokens = 16384,
2458
2550
  maxSteps = 30,
2551
+ maxRetries = 5,
2459
2552
  providerOptions,
2460
2553
  includeToolDiscipline = false
2461
2554
  }) {
@@ -2465,6 +2558,7 @@ function createCodingAgent({
2465
2558
  tools,
2466
2559
  stopWhen: stepCountIs(maxSteps),
2467
2560
  maxOutputTokens,
2561
+ maxRetries,
2468
2562
  providerOptions,
2469
2563
  experimental_repairToolCall: repairCodingAgentToolCall,
2470
2564
  callOptionsSchema: codingAgentCallOptionsSchema,
@@ -2518,20 +2612,20 @@ var init_coding_agent = __esm(() => {
2518
2612
  });
2519
2613
 
2520
2614
  // packages/ai/src/config/credentials.ts
2521
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2615
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
2522
2616
  import os from "os";
2523
- import path from "path";
2617
+ import path3 from "path";
2524
2618
  import { z as z25 } from "zod";
2525
2619
  function getCredentialsPath(env = process.env) {
2526
2620
  const configuredPath = env.LIGHTCODE_CREDENTIALS?.trim();
2527
2621
  if (configuredPath) {
2528
- return path.resolve(configuredPath);
2622
+ return path3.resolve(configuredPath);
2529
2623
  }
2530
- return path.join(os.homedir(), ".lightcode", "credentials.json");
2624
+ return path3.join(os.homedir(), ".lightcode", "credentials.json");
2531
2625
  }
2532
2626
  function readStoredCredentials(env = process.env) {
2533
2627
  const credentialsPath = getCredentialsPath(env);
2534
- if (!existsSync(credentialsPath)) {
2628
+ if (!existsSync2(credentialsPath)) {
2535
2629
  return {};
2536
2630
  }
2537
2631
  try {
@@ -2912,11 +3006,11 @@ function normalizeReadPathKey(input) {
2912
3006
  if (!isRecord3(input)) {
2913
3007
  return null;
2914
3008
  }
2915
- const path2 = input.path;
2916
- if (typeof path2 !== "string" || !path2.trim()) {
3009
+ const path4 = input.path;
3010
+ if (typeof path4 !== "string" || !path4.trim()) {
2917
3011
  return null;
2918
3012
  }
2919
- return path2.trim().replaceAll("\\", "/").toLowerCase();
3013
+ return path4.trim().replaceAll("\\", "/").toLowerCase();
2920
3014
  }
2921
3015
  function getPartState(part) {
2922
3016
  const state = isRecord3(part) ? Reflect.get(part, "state") : undefined;
@@ -3107,6 +3201,40 @@ var init_build_provider_view = __esm(() => {
3107
3201
  init_tier1_prune();
3108
3202
  });
3109
3203
 
3204
+ // packages/ai/src/context/normalize-provider-messages.ts
3205
+ import { isToolUIPart } from "ai";
3206
+ function resolveDanglingToolParts(messages, errorText = interruptedToolErrorText) {
3207
+ return messages.map((message) => {
3208
+ if (message.role !== "assistant") {
3209
+ return message;
3210
+ }
3211
+ let changed = false;
3212
+ const parts = message.parts.map((part) => {
3213
+ if (!isToolUIPart(part) || terminalToolStates.has(part.state)) {
3214
+ return part;
3215
+ }
3216
+ changed = true;
3217
+ return {
3218
+ ...part,
3219
+ state: "output-error",
3220
+ errorText
3221
+ };
3222
+ });
3223
+ return changed ? { ...message, parts } : message;
3224
+ });
3225
+ }
3226
+ function normalizeProviderMessages(messages) {
3227
+ return resolveDanglingToolParts(messages);
3228
+ }
3229
+ var terminalToolStates, interruptedToolErrorText = "Tool call was interrupted by a connection retry. Re-issue the tool call if it is still needed.";
3230
+ var init_normalize_provider_messages = __esm(() => {
3231
+ terminalToolStates = new Set([
3232
+ "output-available",
3233
+ "output-error",
3234
+ "output-denied"
3235
+ ]);
3236
+ });
3237
+
3110
3238
  // packages/ai/src/context/index.ts
3111
3239
  var init_context = __esm(() => {
3112
3240
  init_build_provider_view();
@@ -3115,22 +3243,23 @@ var init_context = __esm(() => {
3115
3243
  init_estimate();
3116
3244
  init_heuristic_summary();
3117
3245
  init_message_parts();
3246
+ init_normalize_provider_messages();
3118
3247
  init_tier1_prune();
3119
3248
  });
3120
3249
 
3121
3250
  // packages/ai/src/config/lightcode-config.ts
3122
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
3251
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
3123
3252
  import os2 from "os";
3124
- import path2 from "path";
3253
+ import path4 from "path";
3125
3254
  import { z as z28 } from "zod";
3126
3255
  function getDefaultUserConfigPath(env) {
3127
3256
  if (env.LIGHTCODE_USER_CONFIG) {
3128
3257
  return env.LIGHTCODE_USER_CONFIG;
3129
3258
  }
3130
- return path2.join(os2.homedir(), ".lightcode", "settings.json");
3259
+ return path4.join(os2.homedir(), ".lightcode", "settings.json");
3131
3260
  }
3132
3261
  function readConfigFile(scope, configPath) {
3133
- if (!existsSync2(configPath)) {
3262
+ if (!existsSync3(configPath)) {
3134
3263
  return {
3135
3264
  config: {},
3136
3265
  file: {
@@ -3213,6 +3342,12 @@ function readEnvConfig(env) {
3213
3342
  summaryMaxChars: parseNumberEnv(env.LIGHTCODE_CONTEXT_SUMMARY_MAX_CHARS, "LIGHTCODE_CONTEXT_SUMMARY_MAX_CHARS")
3214
3343
  };
3215
3344
  const compactContextConfig = Object.fromEntries(Object.entries(contextConfig).filter(([, value]) => value !== undefined));
3345
+ const headroomConfig = {
3346
+ enabled: parseBooleanEnv(env.LIGHTCODE_HEADROOM_ENABLED, "LIGHTCODE_HEADROOM_ENABLED"),
3347
+ proxyUrl: env.LIGHTCODE_HEADROOM_PROXY_URL?.trim() || undefined,
3348
+ failOpen: parseBooleanEnv(env.LIGHTCODE_HEADROOM_FAIL_OPEN, "LIGHTCODE_HEADROOM_FAIL_OPEN")
3349
+ };
3350
+ const compactHeadroomConfig = Object.fromEntries(Object.entries(headroomConfig).filter(([, value]) => value !== undefined));
3216
3351
  const rawConfig = {
3217
3352
  provider: env.LIGHTCODE_PROVIDER,
3218
3353
  model: env.LIGHTCODE_CHAT_MODEL,
@@ -3221,7 +3356,9 @@ function readEnvConfig(env) {
3221
3356
  permissionMode: env.LIGHTCODE_PERMISSION_MODE,
3222
3357
  maxOutputTokens: parseNumberEnv(env.LIGHTCODE_MAX_OUTPUT_TOKENS, "LIGHTCODE_MAX_OUTPUT_TOKENS"),
3223
3358
  maxSteps: parseNumberEnv(env.LIGHTCODE_MAX_STEPS, "LIGHTCODE_MAX_STEPS"),
3224
- context: Object.keys(compactContextConfig).length > 0 ? compactContextConfig : undefined
3359
+ maxRetries: parseNumberEnv(env.LIGHTCODE_MAX_RETRIES, "LIGHTCODE_MAX_RETRIES"),
3360
+ context: Object.keys(compactContextConfig).length > 0 ? compactContextConfig : undefined,
3361
+ headroom: Object.keys(compactHeadroomConfig).length > 0 ? compactHeadroomConfig : undefined
3225
3362
  };
3226
3363
  const compactConfig = Object.fromEntries(Object.entries(rawConfig).filter(([, value]) => value !== undefined));
3227
3364
  const parsedConfig = lightcodeConfigSchema.safeParse(compactConfig);
@@ -3245,13 +3382,37 @@ function mergeConfig(...configs) {
3245
3382
  ...mergedConfig.autoContinue,
3246
3383
  ...config2.autoContinue
3247
3384
  } : mergedConfig.autoContinue,
3385
+ headroom: config2.headroom ? {
3386
+ ...mergedConfig.headroom,
3387
+ ...config2.headroom
3388
+ } : mergedConfig.headroom,
3248
3389
  allowedTools: config2.allowedTools ?? mergedConfig.allowedTools
3249
3390
  }), {});
3250
3391
  }
3392
+ function updateUserSettings(update, env = process.env) {
3393
+ const configPath = getDefaultUserConfigPath(env);
3394
+ const existing = readConfigFile("user", configPath).config;
3395
+ const merged = { ...existing };
3396
+ for (const [key, value] of Object.entries(update)) {
3397
+ if (value === undefined || value === null) {
3398
+ delete merged[key];
3399
+ } else {
3400
+ Reflect.set(merged, key, value);
3401
+ }
3402
+ }
3403
+ const parsed = lightcodeConfigSchema.safeParse(merged);
3404
+ if (!parsed.success) {
3405
+ throw new LightcodeConfigError(`Invalid settings update: ${parsed.error.message}`);
3406
+ }
3407
+ mkdirSync3(path4.dirname(configPath), { recursive: true });
3408
+ writeFileSync2(configPath, `${JSON.stringify(parsed.data, null, 2)}
3409
+ `, "utf8");
3410
+ return { path: configPath, config: parsed.data };
3411
+ }
3251
3412
  function loadLightcodeConfig({
3252
3413
  cwd = process.cwd(),
3253
3414
  env = process.env,
3254
- projectConfigPath = path2.join(cwd, ".lightcode", "settings.json"),
3415
+ projectConfigPath = path4.join(cwd, ".lightcode", "settings.json"),
3255
3416
  userConfigPath = getDefaultUserConfigPath(env)
3256
3417
  } = {}) {
3257
3418
  const userConfig = readConfigFile("user", userConfigPath);
@@ -3265,6 +3426,10 @@ function loadLightcodeConfig({
3265
3426
  autoContinue: {
3266
3427
  ...defaultAutoContinueConfig,
3267
3428
  ...mergedConfig.autoContinue
3429
+ },
3430
+ headroom: {
3431
+ ...defaultHeadroomConfig,
3432
+ ...mergedConfig.headroom
3268
3433
  }
3269
3434
  };
3270
3435
  return lightcodeConfigLoadResultSchema.parse({
@@ -3272,7 +3437,7 @@ function loadLightcodeConfig({
3272
3437
  loadedFiles: [userConfig.file, projectConfig.file]
3273
3438
  });
3274
3439
  }
3275
- var lightcodeProviderSchema, autoContinueConfigSchema, resolvedAutoContinueConfigSchema, defaultAutoContinueConfig, lightcodeConfigSchema, lightcodeConfigDefaults, loadedConfigFileSchema, lightcodeResolvedConfigSchema, lightcodeConfigLoadResultSchema, lightcodeConfigStatusSchema, LightcodeConfigError;
3440
+ var lightcodeProviderSchema, autoContinueConfigSchema, resolvedAutoContinueConfigSchema, defaultAutoContinueConfig, headroomConfigSchema, resolvedHeadroomConfigSchema, defaultHeadroomConfig, lightcodeConfigSchema, lightcodeConfigDefaults, loadedConfigFileSchema, lightcodeResolvedConfigSchema, lightcodeConfigLoadResultSchema, lightcodeConfigStatusSchema, LightcodeConfigError;
3276
3441
  var init_lightcode_config = __esm(() => {
3277
3442
  init_coding_agent_modes();
3278
3443
  init_coding_agent_modes();
@@ -3302,7 +3467,25 @@ var init_lightcode_config = __esm(() => {
3302
3467
  enabled: true,
3303
3468
  maxAutoContinues: 50,
3304
3469
  maxErrorRetries: 5,
3305
- stallTimeoutSeconds: 120
3470
+ stallTimeoutSeconds: 180
3471
+ };
3472
+ headroomConfigSchema = z28.object({
3473
+ enabled: z28.boolean().optional(),
3474
+ proxyUrl: z28.string().min(1).max(4096).optional(),
3475
+ providers: z28.array(lightcodeProviderSchema).optional(),
3476
+ failOpen: z28.boolean().optional()
3477
+ }).strict();
3478
+ resolvedHeadroomConfigSchema = z28.object({
3479
+ enabled: z28.boolean(),
3480
+ proxyUrl: z28.string().min(1).max(4096),
3481
+ providers: z28.array(lightcodeProviderSchema).nullable(),
3482
+ failOpen: z28.boolean()
3483
+ });
3484
+ defaultHeadroomConfig = {
3485
+ enabled: false,
3486
+ proxyUrl: "http://127.0.0.1:8787",
3487
+ providers: null,
3488
+ failOpen: true
3306
3489
  };
3307
3490
  lightcodeConfigSchema = z28.object({
3308
3491
  provider: lightcodeProviderSchema.optional(),
@@ -3315,9 +3498,11 @@ var init_lightcode_config = __esm(() => {
3315
3498
  sandbox: sandboxConfigSchema.optional(),
3316
3499
  mcp: mcpConfigSchema.optional(),
3317
3500
  context: contextOptimizerConfigSchema.optional(),
3318
- maxOutputTokens: z28.number().int().min(1).max(200000).optional(),
3501
+ maxOutputTokens: z28.number().int().min(1).max(1e6).optional(),
3319
3502
  maxSteps: z28.number().int().min(1).max(100).optional(),
3320
- autoContinue: autoContinueConfigSchema.optional()
3503
+ maxRetries: z28.number().int().min(0).max(10).optional(),
3504
+ autoContinue: autoContinueConfigSchema.optional(),
3505
+ headroom: headroomConfigSchema.optional()
3321
3506
  }).strict();
3322
3507
  lightcodeConfigDefaults = {
3323
3508
  provider: "anthropic",
@@ -3325,7 +3510,9 @@ var init_lightcode_config = __esm(() => {
3325
3510
  context: defaultContextOptimizerConfig,
3326
3511
  maxOutputTokens: 32768,
3327
3512
  maxSteps: 30,
3328
- autoContinue: defaultAutoContinueConfig
3513
+ maxRetries: 5,
3514
+ autoContinue: defaultAutoContinueConfig,
3515
+ headroom: defaultHeadroomConfig
3329
3516
  };
3330
3517
  loadedConfigFileSchema = z28.object({
3331
3518
  scope: z28.enum(["user", "project"]),
@@ -3337,9 +3524,11 @@ var init_lightcode_config = __esm(() => {
3337
3524
  provider: lightcodeProviderSchema,
3338
3525
  defaultMode: codingAgentModeSchema,
3339
3526
  context: resolvedContextOptimizerConfigSchema,
3340
- maxOutputTokens: z28.number().int().min(1).max(200000),
3527
+ maxOutputTokens: z28.number().int().min(1).max(1e6),
3341
3528
  maxSteps: z28.number().int().min(1).max(100),
3342
- autoContinue: resolvedAutoContinueConfigSchema
3529
+ maxRetries: z28.number().int().min(0).max(10),
3530
+ autoContinue: resolvedAutoContinueConfigSchema,
3531
+ headroom: resolvedHeadroomConfigSchema.optional()
3343
3532
  });
3344
3533
  lightcodeConfigLoadResultSchema = z28.object({
3345
3534
  config: lightcodeResolvedConfigSchema,
@@ -3353,10 +3542,15 @@ var init_lightcode_config = __esm(() => {
3353
3542
  baseUrl: z28.string().nullable(),
3354
3543
  defaultMode: codingAgentModeSchema,
3355
3544
  permissionMode: permissionModeSchema.nullable(),
3356
- maxOutputTokens: z28.number().int().min(1).max(200000),
3545
+ maxOutputTokens: z28.number().int().min(1).max(1e6),
3357
3546
  maxSteps: z28.number().int().min(1).max(100),
3358
3547
  autoContinue: resolvedAutoContinueConfigSchema,
3359
3548
  contextWindow: z28.number().int().positive(),
3549
+ headroom: z28.object({
3550
+ enabled: z28.boolean(),
3551
+ proxyUrl: z28.string().nullable(),
3552
+ routed: z28.boolean()
3553
+ }),
3360
3554
  missingCredentialHints: z28.array(z28.string())
3361
3555
  });
3362
3556
  LightcodeConfigError = class LightcodeConfigError extends Error {
@@ -3596,6 +3790,21 @@ var init_runtime = __esm(() => {
3596
3790
  init_schema13();
3597
3791
  });
3598
3792
 
3793
+ // packages/ai/src/providers/clamp-output-tokens.ts
3794
+ function clampMaxOutputTokens(configured, modelMaxCompletionTokens) {
3795
+ if (typeof modelMaxCompletionTokens === "number" && Number.isFinite(modelMaxCompletionTokens) && modelMaxCompletionTokens > 0) {
3796
+ return Math.min(configured, modelMaxCompletionTokens);
3797
+ }
3798
+ return configured;
3799
+ }
3800
+ function resolveMaxOutputTokens(configured, defaultValue, modelMaxCompletionTokens) {
3801
+ const hasModelCap = typeof modelMaxCompletionTokens === "number" && Number.isFinite(modelMaxCompletionTokens) && modelMaxCompletionTokens > 0;
3802
+ if (configured === defaultValue) {
3803
+ return hasModelCap ? modelMaxCompletionTokens : configured;
3804
+ }
3805
+ return clampMaxOutputTokens(configured, modelMaxCompletionTokens);
3806
+ }
3807
+
3599
3808
  // packages/ai/src/providers/model-metadata.ts
3600
3809
  function getModelContextWindow(provider, modelId) {
3601
3810
  if (provider === "anthropic") {
@@ -4227,27 +4436,27 @@ var init_schema16 = __esm(() => {
4227
4436
  });
4228
4437
 
4229
4438
  // packages/ai/src/plugins/runtime.ts
4230
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
4439
+ import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
4231
4440
  import os3 from "os";
4232
- import path3 from "path";
4441
+ import path5 from "path";
4233
4442
  function pluginRoots(cwd) {
4234
4443
  return [
4235
- path3.join(cwd, ".lightcode", "plugins"),
4236
- path3.join(os3.homedir(), ".lightcode", "plugins")
4444
+ path5.join(cwd, ".lightcode", "plugins"),
4445
+ path5.join(os3.homedir(), ".lightcode", "plugins")
4237
4446
  ];
4238
4447
  }
4239
4448
  function listPlugins({ cwd = process.cwd() } = {}) {
4240
4449
  const plugins = [];
4241
4450
  for (const root of pluginRoots(cwd)) {
4242
- if (!existsSync3(root)) {
4451
+ if (!existsSync4(root)) {
4243
4452
  continue;
4244
4453
  }
4245
- for (const entry of readdirSync(root, { withFileTypes: true })) {
4454
+ for (const entry of readdirSync2(root, { withFileTypes: true })) {
4246
4455
  if (!entry.isDirectory()) {
4247
4456
  continue;
4248
4457
  }
4249
- const manifestPath = path3.join(root, entry.name, "plugin.json");
4250
- if (!existsSync3(manifestPath)) {
4458
+ const manifestPath = path5.join(root, entry.name, "plugin.json");
4459
+ if (!existsSync4(manifestPath)) {
4251
4460
  continue;
4252
4461
  }
4253
4462
  const manifest = pluginManifestSchema.parse(JSON.parse(readFileSync3(manifestPath, "utf8")));
@@ -4269,14 +4478,14 @@ var init_runtime2 = __esm(() => {
4269
4478
  });
4270
4479
 
4271
4480
  // packages/ai/src/skills/runtime.ts
4272
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync } from "fs";
4481
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync } from "fs";
4273
4482
  import os4 from "os";
4274
- import path4 from "path";
4483
+ import path6 from "path";
4275
4484
  function defaultSkillRoots(cwd) {
4276
4485
  return [
4277
- { source: "project", path: path4.join(cwd, ".lightcode", "skills") },
4278
- { source: "project", path: path4.join(cwd, ".agents", "skills") },
4279
- { source: "user", path: path4.join(os4.homedir(), ".lightcode", "skills") }
4486
+ { source: "project", path: path6.join(cwd, ".lightcode", "skills") },
4487
+ { source: "project", path: path6.join(cwd, ".agents", "skills") },
4488
+ { source: "user", path: path6.join(os4.homedir(), ".lightcode", "skills") }
4280
4489
  ];
4281
4490
  }
4282
4491
  function normalizeSkillName(value) {
@@ -4298,7 +4507,7 @@ function parseBasicFrontmatter(content) {
4298
4507
  ]));
4299
4508
  }
4300
4509
  function findSkillFiles(root) {
4301
- if (!existsSync4(root.path)) {
4510
+ if (!existsSync5(root.path)) {
4302
4511
  return [];
4303
4512
  }
4304
4513
  const files = [];
@@ -4306,8 +4515,8 @@ function findSkillFiles(root) {
4306
4515
  if (depth > 4) {
4307
4516
  return;
4308
4517
  }
4309
- for (const entry of readdirSync2(directory)) {
4310
- const entryPath = path4.join(directory, entry);
4518
+ for (const entry of readdirSync3(directory)) {
4519
+ const entryPath = path6.join(directory, entry);
4311
4520
  const stat = statSync(entryPath);
4312
4521
  if (stat.isDirectory()) {
4313
4522
  visit(entryPath, depth + 1);
@@ -4328,7 +4537,7 @@ function listSkills({
4328
4537
  for (const skillPath of findSkillFiles(root)) {
4329
4538
  const content = readFileSync4(skillPath, "utf8");
4330
4539
  const frontmatter = parseBasicFrontmatter(content);
4331
- const fallbackName = path4.basename(path4.dirname(skillPath));
4540
+ const fallbackName = path6.basename(path6.dirname(skillPath));
4332
4541
  const name = normalizeSkillName(frontmatter.name ?? fallbackName);
4333
4542
  if (!name || seen.has(name)) {
4334
4543
  continue;
@@ -4403,6 +4612,7 @@ var init_subagent_schemas = __esm(() => {
4403
4612
  // packages/ai/src/index.ts
4404
4613
  var init_src2 = __esm(() => {
4405
4614
  init_agent_tools();
4615
+ init_chat_error();
4406
4616
  init_chat_interaction_schemas();
4407
4617
  init_plan_block();
4408
4618
  init_chat_schemas();
@@ -4433,20 +4643,20 @@ var init_src2 = __esm(() => {
4433
4643
 
4434
4644
  // packages/db/src/local-database.ts
4435
4645
  import { Database } from "bun:sqlite";
4436
- import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
4646
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
4437
4647
  import { dirname, isAbsolute, join, resolve } from "path";
4438
4648
  import { fileURLToPath, pathToFileURL } from "url";
4439
- import { homedir, platform } from "os";
4649
+ import { homedir as homedir2, platform as platform2 } from "os";
4440
4650
  function getDefaultLightcodeDataDir() {
4441
4651
  const configuredHome = process.env.LIGHTCODE_HOME?.trim();
4442
4652
  if (configuredHome) {
4443
4653
  return resolve(configuredHome);
4444
4654
  }
4445
- if (platform() === "win32") {
4655
+ if (platform2() === "win32") {
4446
4656
  const appData = process.env.APPDATA || process.env.LOCALAPPDATA;
4447
- return appData ? join(appData, appDirectoryName) : join(homedir(), "AppData", "Roaming", appDirectoryName);
4657
+ return appData ? join(appData, appDirectoryName) : join(homedir2(), "AppData", "Roaming", appDirectoryName);
4448
4658
  }
4449
- return join(homedir(), `.${appDirectoryName}`);
4659
+ return join(homedir2(), `.${appDirectoryName}`);
4450
4660
  }
4451
4661
  function getDefaultDatabasePath() {
4452
4662
  const configuredPath = process.env.LIGHTCODE_DATABASE_PATH?.trim();
@@ -4456,11 +4666,9 @@ function getDefaultDatabaseUrl() {
4456
4666
  return pathToFileURL(getDefaultDatabasePath()).href;
4457
4667
  }
4458
4668
  function resolveDatabaseUrl() {
4459
- const configuredUrl = process.env.DATABASE_URL?.trim();
4460
- const supportedConfiguredUrl = configuredUrl?.startsWith("file:") || configuredUrl?.startsWith("libsql:") || configuredUrl?.startsWith("http://") || configuredUrl?.startsWith("https://") ? configuredUrl : null;
4461
- const databaseUrl = supportedConfiguredUrl && supportedConfiguredUrl.length > 0 ? supportedConfiguredUrl : getDefaultDatabaseUrl();
4462
- process.env.DATABASE_URL = databaseUrl;
4463
- return databaseUrl;
4669
+ const configuredUrl = process.env.LIGHTCODE_DATABASE_URL?.trim();
4670
+ const supportedConfiguredUrl = configuredUrl && (configuredUrl.startsWith("file:") || configuredUrl.startsWith("libsql:") || configuredUrl.startsWith("http://") || configuredUrl.startsWith("https://")) ? configuredUrl : null;
4671
+ return supportedConfiguredUrl ?? getDefaultDatabaseUrl();
4464
4672
  }
4465
4673
  function filePathFromDatabaseUrl(databaseUrl) {
4466
4674
  if (!databaseUrl.startsWith("file:")) {
@@ -4474,8 +4682,8 @@ function filePathFromDatabaseUrl(databaseUrl) {
4474
4682
  }
4475
4683
  function ensureParentDirectory(filePath) {
4476
4684
  const parentDirectory = dirname(filePath);
4477
- if (!existsSync5(parentDirectory)) {
4478
- mkdirSync2(parentDirectory, { recursive: true });
4685
+ if (!existsSync6(parentDirectory)) {
4686
+ mkdirSync4(parentDirectory, { recursive: true });
4479
4687
  }
4480
4688
  }
4481
4689
  function initializeLocalDatabase(databaseUrl) {
@@ -4611,14 +4819,10 @@ var init_local_database = () => {};
4611
4819
  // apps/server/src/lib/chat-interaction-store.ts
4612
4820
  import { Database as Database2 } from "bun:sqlite";
4613
4821
  function getDatabase() {
4614
- if (!_initialized) {
4822
+ if (!_db) {
4615
4823
  const dbUrl = resolveDatabaseUrl();
4616
4824
  initializeLocalDatabase(dbUrl);
4617
- _initialized = true;
4618
- }
4619
- if (!_db) {
4620
- const dbUrl = process.env.DATABASE_URL ?? getDefaultDatabasePath();
4621
- const dbPath = filePathFromDatabaseUrl(dbUrl) ?? (dbUrl.startsWith("file:") ? dbUrl.slice(5) : dbUrl);
4825
+ const dbPath = filePathFromDatabaseUrl(dbUrl) ?? getDefaultDatabasePath();
4622
4826
  _db = new Database2(dbPath);
4623
4827
  }
4624
4828
  return _db;
@@ -4769,7 +4973,7 @@ async function summarizePendingChatInteractions({
4769
4973
  recoverable: total
4770
4974
  };
4771
4975
  }
4772
- var ChatInteractionNotFoundError, _db = null, _initialized = false;
4976
+ var ChatInteractionNotFoundError, _db = null;
4773
4977
  var init_chat_interaction_store = __esm(() => {
4774
4978
  init_src2();
4775
4979
  init_local_database();
@@ -4786,6 +4990,28 @@ var init_chat_interaction_store = __esm(() => {
4786
4990
  });
4787
4991
 
4788
4992
  // apps/server/src/lib/chat-observability.ts
4993
+ import { APICallError, RetryError } from "ai";
4994
+ function chatFailureClassForErrorKind(kind) {
4995
+ switch (kind) {
4996
+ case "auth":
4997
+ return "provider_auth";
4998
+ case "billing":
4999
+ return "provider_billing_quota";
5000
+ case "rate_limit":
5001
+ return "provider_rate_limit";
5002
+ case "invalid_request":
5003
+ return "provider_invalid_request";
5004
+ case "context_overflow":
5005
+ return "context_overflow";
5006
+ case "provider_unavailable":
5007
+ return "provider_unavailable";
5008
+ case "network":
5009
+ case "aborted":
5010
+ return "timeout_disconnect";
5011
+ default:
5012
+ return "provider_unknown";
5013
+ }
5014
+ }
4789
5015
  function normalizeErrorMessage(message) {
4790
5016
  return message.toLowerCase();
4791
5017
  }
@@ -4797,6 +5023,76 @@ function isProviderBillingOrQuotaError(error) {
4797
5023
  const message = normalizeErrorMessage(getErrorMessage(error));
4798
5024
  return message.includes("credit balance") || message.includes("insufficient credit") || message.includes("insufficient funds") || message.includes("out of credits") || message.includes("quota") || message.includes("billing") || message.includes("payment required");
4799
5025
  }
5026
+ function isContextOverflowMessage(message) {
5027
+ return contextOverflowPattern.test(message);
5028
+ }
5029
+ function isAbortError(error) {
5030
+ return error instanceof Error && error.name === "AbortError" || normalizeErrorMessage(getErrorMessage(error)).includes("aborted");
5031
+ }
5032
+ function classifyByStatusCode(statusCode, message) {
5033
+ if (statusCode === 401 || statusCode === 403) {
5034
+ return { kind: "auth", retryable: false };
5035
+ }
5036
+ if (statusCode === 402) {
5037
+ return { kind: "billing", retryable: false };
5038
+ }
5039
+ if (statusCode === 429) {
5040
+ return { kind: "rate_limit", retryable: true };
5041
+ }
5042
+ if (statusCode === 400 || statusCode === 422 || statusCode === 413) {
5043
+ if (isContextOverflowMessage(message)) {
5044
+ return { kind: "context_overflow", retryable: true };
5045
+ }
5046
+ if (isProviderBillingOrQuotaError(message)) {
5047
+ return { kind: "billing", retryable: false };
5048
+ }
5049
+ return { kind: "invalid_request", retryable: false };
5050
+ }
5051
+ if (retryableStatusCodes.has(statusCode) || statusCode >= 500) {
5052
+ return { kind: "provider_unavailable", retryable: true };
5053
+ }
5054
+ return { kind: "unknown", retryable: false };
5055
+ }
5056
+ function classifyChatError(error) {
5057
+ if (RetryError.isInstance(error) && error.lastError) {
5058
+ return classifyChatError(error.lastError);
5059
+ }
5060
+ if (APICallError.isInstance(error)) {
5061
+ const responseBody = error.responseBody ?? "";
5062
+ const message2 = responseBody.length > 0 ? responseBody : error.message;
5063
+ if (typeof error.statusCode === "number") {
5064
+ const { kind, retryable } = classifyByStatusCode(error.statusCode, message2);
5065
+ return {
5066
+ kind,
5067
+ statusCode: error.statusCode,
5068
+ retryable,
5069
+ message: message2
5070
+ };
5071
+ }
5072
+ return {
5073
+ kind: error.isRetryable ? "network" : "unknown",
5074
+ retryable: error.isRetryable,
5075
+ message: message2
5076
+ };
5077
+ }
5078
+ const message = getErrorMessage(error);
5079
+ if (isAbortError(error)) {
5080
+ return { kind: "aborted", retryable: false, message };
5081
+ }
5082
+ if (isContextOverflowMessage(message)) {
5083
+ return { kind: "context_overflow", retryable: true, message };
5084
+ }
5085
+ if (isProviderBillingOrQuotaError(error)) {
5086
+ return { kind: "billing", retryable: false, message };
5087
+ }
5088
+ if (isProviderSchemaRejectionError(error)) {
5089
+ return { kind: "invalid_request", retryable: false, message };
5090
+ }
5091
+ if (isDisconnectOrTimeoutError(error)) {
5092
+ return { kind: "network", retryable: true, message };
5093
+ }
5094
+ return { kind: "unknown", retryable: false, message };
5095
+ }
4800
5096
  function isDisconnectOrTimeoutError(error) {
4801
5097
  const message = normalizeErrorMessage(getErrorMessage(error));
4802
5098
  return message.includes("timed out") || message.includes("timeout") || message.includes("socket") || message.includes("connection") || message.includes("aborted") || message.includes("broken pipe") || message.includes("econnreset");
@@ -4822,6 +5118,22 @@ function incrementChatFailureCounter(failureClass, context2 = {}) {
4822
5118
  ...context2
4823
5119
  });
4824
5120
  }
5121
+ function logChatStreamErrorEvent({
5122
+ sessionId,
5123
+ phase,
5124
+ classified,
5125
+ error
5126
+ }) {
5127
+ logger2.error("chat_stream_error", {
5128
+ sessionId,
5129
+ phase,
5130
+ kind: classified.kind,
5131
+ statusCode: classified.statusCode,
5132
+ retryable: classified.retryable,
5133
+ message: classified.message,
5134
+ rawMessage: getErrorMessage(error)
5135
+ });
5136
+ }
4825
5137
  function logChatDisconnectEvent({
4826
5138
  sessionId,
4827
5139
  phase,
@@ -4834,17 +5146,25 @@ function logChatDisconnectEvent({
4834
5146
  message: getErrorMessage(error)
4835
5147
  });
4836
5148
  }
4837
- var chatFailureCounters, logger2;
5149
+ var chatFailureCounters, logger2, retryableStatusCodes, contextOverflowPattern;
4838
5150
  var init_chat_observability = __esm(() => {
4839
5151
  init_src();
4840
5152
  chatFailureCounters = {
4841
5153
  provider_schema_rejection: 0,
4842
5154
  provider_billing_quota: 0,
5155
+ provider_auth: 0,
5156
+ provider_rate_limit: 0,
5157
+ provider_invalid_request: 0,
5158
+ provider_unavailable: 0,
5159
+ context_overflow: 0,
5160
+ provider_unknown: 0,
4843
5161
  timeout_disconnect: 0,
4844
5162
  stale_finish_skip: 0,
4845
5163
  db_unique_conflict: 0
4846
5164
  };
4847
5165
  logger2 = createLogger("chat");
5166
+ retryableStatusCodes = new Set([408, 409, 429, 500, 502, 503, 504]);
5167
+ contextOverflowPattern = /(context window|context length|context_length|maximum context|prompt is too long|input is too long|too many tokens|exceeds? the (?:maximum|model'?s) (?:context|token)|max(?:imum)? input tokens)/i;
4848
5168
  });
4849
5169
 
4850
5170
  // packages/db/generated/prisma/internal/class.ts
@@ -5057,7 +5377,7 @@ var init_prismaNamespace = __esm(() => {
5057
5377
  var init_enums = () => {};
5058
5378
 
5059
5379
  // packages/db/generated/prisma/client.ts
5060
- import * as path5 from "path";
5380
+ import * as path7 from "path";
5061
5381
  import { fileURLToPath as fileURLToPath2 } from "url";
5062
5382
  var PrismaClient;
5063
5383
  var init_client = __esm(() => {
@@ -5065,7 +5385,7 @@ var init_client = __esm(() => {
5065
5385
  init_prismaNamespace();
5066
5386
  init_enums();
5067
5387
  init_enums();
5068
- globalThis["__dirname"] = path5.dirname(fileURLToPath2(import.meta.url));
5388
+ globalThis["__dirname"] = path7.dirname(fileURLToPath2(import.meta.url));
5069
5389
  PrismaClient = getPrismaClientClass();
5070
5390
  });
5071
5391
 
@@ -5643,28 +5963,28 @@ function mergeFinishedMessagesIntoFullHistory({
5643
5963
  }
5644
5964
 
5645
5965
  // packages/ai/src/common/resolve-within-workspace.ts
5646
- import path6 from "path";
5966
+ import path8 from "path";
5647
5967
  import {
5648
- existsSync as existsSync6,
5968
+ existsSync as existsSync7,
5649
5969
  lstatSync,
5650
5970
  realpathSync,
5651
5971
  statSync as statSync2
5652
5972
  } from "fs";
5653
5973
  function stripTrailingSeparator(value) {
5654
- const parsed = path6.parse(value);
5974
+ const parsed = path8.parse(value);
5655
5975
  if (value === parsed.root) {
5656
5976
  return value;
5657
5977
  }
5658
5978
  return value.replace(/[\\/]+$/, "");
5659
5979
  }
5660
5980
  function normalizeForComparison(value) {
5661
- const normalized = stripTrailingSeparator(path6.resolve(value));
5981
+ const normalized = stripTrailingSeparator(path8.resolve(value));
5662
5982
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
5663
5983
  }
5664
5984
  function isWithinWorkspace(absolutePath, context2) {
5665
5985
  const workspace = normalizeForComparison(context2.root);
5666
5986
  const target = normalizeForComparison(absolutePath);
5667
- return target === workspace || target.startsWith(`${workspace}${path6.sep}`);
5987
+ return target === workspace || target.startsWith(`${workspace}${path8.sep}`);
5668
5988
  }
5669
5989
  function assertWithinWorkspace(absolutePath, context2, input) {
5670
5990
  if (!isWithinWorkspace(absolutePath, context2)) {
@@ -5679,17 +5999,17 @@ function getExistingRealPath(absolutePath) {
5679
5999
  }
5680
6000
  }
5681
6001
  function findNearestExistingParent(absolutePath, context2, input) {
5682
- let currentPath = path6.dirname(absolutePath);
6002
+ let currentPath = path8.dirname(absolutePath);
5683
6003
  while (true) {
5684
6004
  assertWithinWorkspace(currentPath, context2, input);
5685
- if (existsSync6(currentPath)) {
6005
+ if (existsSync7(currentPath)) {
5686
6006
  const realParentPath = getExistingRealPath(currentPath);
5687
6007
  if (!realParentPath) {
5688
6008
  throw new Error(`Unable to resolve existing parent for path: ${input}`);
5689
6009
  }
5690
6010
  return realParentPath;
5691
6011
  }
5692
- const parentPath = path6.dirname(currentPath);
6012
+ const parentPath = path8.dirname(currentPath);
5693
6013
  if (parentPath === currentPath) {
5694
6014
  throw new Error(`Unable to find an existing workspace parent for path: ${input}`);
5695
6015
  }
@@ -5709,7 +6029,7 @@ function getDefaultWorkspaceContext() {
5709
6029
  }
5710
6030
  function resolveWithinWorkspace(input, options = {}) {
5711
6031
  const context2 = options.context ?? defaultWorkspaceContext;
5712
- const resolved = path6.resolve(context2.root, input);
6032
+ const resolved = path8.resolve(context2.root, input);
5713
6033
  assertWithinWorkspace(resolved, context2, input);
5714
6034
  const realPath = getExistingRealPath(resolved);
5715
6035
  if (realPath) {
@@ -5852,7 +6172,7 @@ var init_runtime11 = __esm(() => {
5852
6172
 
5853
6173
  // packages/ai/src/todo-write/runtime.ts
5854
6174
  import { mkdir, readFile, writeFile } from "fs/promises";
5855
- import path7 from "path";
6175
+ import path9 from "path";
5856
6176
  import { z as z33 } from "zod";
5857
6177
  function safeTodoSessionId(sessionId) {
5858
6178
  const trimmed = sessionId?.trim();
@@ -5862,7 +6182,7 @@ function safeTodoSessionId(sessionId) {
5862
6182
  return trimmed.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 120);
5863
6183
  }
5864
6184
  function getTodoFilePath(sessionId, workspaceContext) {
5865
- return resolveWithinWorkspace(path7.join(".lightcode", "todos", `${safeTodoSessionId(sessionId)}.json`), {
6185
+ return resolveWithinWorkspace(path9.join(".lightcode", "todos", `${safeTodoSessionId(sessionId)}.json`), {
5866
6186
  context: workspaceContext,
5867
6187
  allowMissing: true
5868
6188
  });
@@ -6066,6 +6386,7 @@ ${transcript}`
6066
6386
  system: COMPACTION_SYSTEM_PROMPT,
6067
6387
  prompt,
6068
6388
  maxOutputTokens: COMPACTION_MAX_OUTPUT_TOKENS,
6389
+ maxRetries: 2,
6069
6390
  abortSignal: AbortSignal.timeout(COMPACTION_TIMEOUT_MS)
6070
6391
  });
6071
6392
  const text = result.text.trim();
@@ -6114,7 +6435,7 @@ function composeSummary({
6114
6435
  `).trim();
6115
6436
  const bodyBudget = Math.max(MIN_SUMMARY_BODY_CHARS, summaryMaxChars - (pinnedText ? pinnedText.length + 2 : 0));
6116
6437
  const compressedBody = compressSummary(body, bodyBudget);
6117
- return [compressedBody, pinnedText].filter(Boolean).join(`
6438
+ return [compressedBody, pinnedText, compactionDirectResumeInstruction].filter(Boolean).join(`
6118
6439
 
6119
6440
  `);
6120
6441
  }
@@ -6192,13 +6513,227 @@ Unfinished work, known follow-ups, and the immediate next step.
6192
6513
  ## Tools and commands that mattered
6193
6514
  Important commands run and their relevant outcomes (test results, errors, etc.).
6194
6515
 
6195
- Rules: be specific (real file paths, function names, error messages); never invent details; omit pleasantries; if a "Previously compacted context" section is provided, fold its still-relevant facts into your summary instead of referencing it.`;
6516
+ Rules: be specific (real file paths, function names, error messages); never invent details; omit pleasantries; if a "Previously compacted context" section is provided, fold its still-relevant facts into your summary instead of referencing it.`, compactionDirectResumeInstruction;
6196
6517
  var init_context_compaction = __esm(() => {
6197
6518
  init_src2();
6198
6519
  init_runtime17();
6199
6520
  init_src();
6200
6521
  init_context_state_store();
6201
6522
  logger3 = createLogger("context-compaction");
6523
+ compactionDirectResumeInstruction = "Continue the task from where it left off using the summary above. " + "Do not ask for confirmation, do not re-state the plan, and do not repeat completed work.";
6524
+ });
6525
+
6526
+ // apps/server/src/lib/overflow-recovery.ts
6527
+ function noteContextOverflow(sessionId) {
6528
+ overflowRecoveryRounds.set(sessionId, (overflowRecoveryRounds.get(sessionId) ?? 0) + 1);
6529
+ }
6530
+ function clearContextOverflowRounds(sessionId) {
6531
+ overflowRecoveryRounds.delete(sessionId);
6532
+ }
6533
+ function getOverflowPreserveRecentMessages(sessionId) {
6534
+ const rounds = overflowRecoveryRounds.get(sessionId) ?? 0;
6535
+ if (rounds === 0) {
6536
+ return null;
6537
+ }
6538
+ return overflowPreserveSchedule[Math.min(rounds, overflowPreserveSchedule.length) - 1];
6539
+ }
6540
+ var overflowPreserveSchedule, overflowRecoveryRounds;
6541
+ var init_overflow_recovery = __esm(() => {
6542
+ overflowPreserveSchedule = [4, 2, 1, 0];
6543
+ overflowRecoveryRounds = new Map;
6544
+ });
6545
+
6546
+ // apps/server/src/lib/sse-heartbeat.ts
6547
+ function endsWithEventBoundary(chunk) {
6548
+ const length = chunk.length;
6549
+ if (length >= 2 && chunk[length - 1] === 10 && chunk[length - 2] === 10) {
6550
+ return true;
6551
+ }
6552
+ return length >= 4 && chunk[length - 1] === 10 && chunk[length - 2] === 13 && chunk[length - 3] === 10 && chunk[length - 4] === 13;
6553
+ }
6554
+ function withSseHeartbeat(response, intervalMs = defaultHeartbeatIntervalMs) {
6555
+ const body = response.body;
6556
+ if (!body) {
6557
+ return response;
6558
+ }
6559
+ let atEventBoundary = true;
6560
+ let heartbeatTimer = null;
6561
+ let lastChunkAt = Date.now();
6562
+ const stopHeartbeat = () => {
6563
+ if (heartbeatTimer) {
6564
+ clearInterval(heartbeatTimer);
6565
+ heartbeatTimer = null;
6566
+ }
6567
+ };
6568
+ const transform = new TransformStream({
6569
+ start(controller) {
6570
+ heartbeatTimer = setInterval(() => {
6571
+ if (!atEventBoundary || Date.now() - lastChunkAt < intervalMs) {
6572
+ return;
6573
+ }
6574
+ try {
6575
+ controller.enqueue(heartbeatFrame);
6576
+ lastChunkAt = Date.now();
6577
+ } catch {
6578
+ stopHeartbeat();
6579
+ }
6580
+ }, Math.min(intervalMs, 5000));
6581
+ },
6582
+ transform(chunk, controller) {
6583
+ atEventBoundary = endsWithEventBoundary(chunk);
6584
+ lastChunkAt = Date.now();
6585
+ controller.enqueue(chunk);
6586
+ },
6587
+ flush() {
6588
+ stopHeartbeat();
6589
+ }
6590
+ });
6591
+ const heartbeatBody = body.pipeThrough(transform);
6592
+ return new Response(heartbeatBody, {
6593
+ status: response.status,
6594
+ statusText: response.statusText,
6595
+ headers: response.headers
6596
+ });
6597
+ }
6598
+ var encoder, heartbeatFrame, defaultHeartbeatIntervalMs = 15000;
6599
+ var init_sse_heartbeat = __esm(() => {
6600
+ encoder = new TextEncoder;
6601
+ heartbeatFrame = encoder.encode(`: heartbeat
6602
+
6603
+ `);
6604
+ });
6605
+
6606
+ // apps/server/src/lib/openrouter-models.ts
6607
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
6608
+ import path10 from "path";
6609
+ import { z as z34 } from "zod";
6610
+ function getCacheFilePath() {
6611
+ return path10.join(getLightcodeDataDir(), "cache", "openrouter-models.json");
6612
+ }
6613
+ function readCache() {
6614
+ try {
6615
+ const cachePath = getCacheFilePath();
6616
+ if (!existsSync8(cachePath)) {
6617
+ return null;
6618
+ }
6619
+ return modelsCacheSchema.parse(JSON.parse(readFileSync5(cachePath, "utf8")));
6620
+ } catch {
6621
+ return null;
6622
+ }
6623
+ }
6624
+ function writeCache(cache) {
6625
+ try {
6626
+ const cachePath = getCacheFilePath();
6627
+ mkdirSync5(path10.dirname(cachePath), { recursive: true });
6628
+ writeFileSync3(cachePath, JSON.stringify(cache), "utf8");
6629
+ } catch (error) {
6630
+ logger4.warn("openrouter_models_cache_write_failed", {
6631
+ error: getErrorMessage(error)
6632
+ });
6633
+ }
6634
+ }
6635
+ function summarizeRawModel(raw2) {
6636
+ if (typeof raw2 !== "object" || raw2 === null) {
6637
+ return null;
6638
+ }
6639
+ const id = Reflect.get(raw2, "id");
6640
+ if (typeof id !== "string" || id.length === 0) {
6641
+ return null;
6642
+ }
6643
+ const name = Reflect.get(raw2, "name");
6644
+ const contextLength = Reflect.get(raw2, "context_length");
6645
+ const topProvider = Reflect.get(raw2, "top_provider");
6646
+ const topProviderMaxCompletion = typeof topProvider === "object" && topProvider !== null ? Reflect.get(topProvider, "max_completion_tokens") : undefined;
6647
+ const rawMaxCompletion = typeof topProviderMaxCompletion === "number" ? topProviderMaxCompletion : Reflect.get(raw2, "max_completion_tokens");
6648
+ const supportedParameters = Reflect.get(raw2, "supported_parameters");
6649
+ const parameterList = Array.isArray(supportedParameters) ? supportedParameters.filter((entry) => typeof entry === "string") : [];
6650
+ const pricingRaw = Reflect.get(raw2, "pricing");
6651
+ const pricing = typeof pricingRaw === "object" && pricingRaw !== null ? {
6652
+ prompt: typeof Reflect.get(pricingRaw, "prompt") === "string" ? Reflect.get(pricingRaw, "prompt") : null,
6653
+ completion: typeof Reflect.get(pricingRaw, "completion") === "string" ? Reflect.get(pricingRaw, "completion") : null
6654
+ } : null;
6655
+ return {
6656
+ id,
6657
+ name: typeof name === "string" && name.length > 0 ? name : id,
6658
+ contextLength: typeof contextLength === "number" && contextLength > 0 ? Math.round(contextLength) : null,
6659
+ maxCompletionTokens: typeof rawMaxCompletion === "number" && rawMaxCompletion > 0 ? Math.round(rawMaxCompletion) : null,
6660
+ supportsTools: parameterList.includes("tools"),
6661
+ supportsReasoning: parameterList.includes("reasoning"),
6662
+ pricing
6663
+ };
6664
+ }
6665
+ async function fetchModelsFromApi() {
6666
+ const response = await fetch(openRouterModelsUrl, {
6667
+ signal: AbortSignal.timeout(fetchTimeoutMs)
6668
+ });
6669
+ if (!response.ok) {
6670
+ throw new Error(`OpenRouter models API returned HTTP ${response.status}.`);
6671
+ }
6672
+ const payload = await response.json();
6673
+ const models = (payload.data ?? []).map(summarizeRawModel).filter((model) => model !== null);
6674
+ if (models.length === 0) {
6675
+ throw new Error("OpenRouter models API returned no models.");
6676
+ }
6677
+ return models;
6678
+ }
6679
+ async function listOpenRouterModels({
6680
+ forceRefresh = false
6681
+ } = {}) {
6682
+ const cache = readCache();
6683
+ if (!forceRefresh && cache && Date.now() - cache.fetchedAt < cacheTtlMs) {
6684
+ return { models: cache.models, fromCache: true };
6685
+ }
6686
+ try {
6687
+ const models = await fetchModelsFromApi();
6688
+ writeCache({ fetchedAt: Date.now(), models });
6689
+ return { models, fromCache: false };
6690
+ } catch (error) {
6691
+ logger4.warn("openrouter_models_fetch_failed", {
6692
+ error: getErrorMessage(error)
6693
+ });
6694
+ if (cache) {
6695
+ return { models: cache.models, fromCache: true };
6696
+ }
6697
+ throw error;
6698
+ }
6699
+ }
6700
+ async function getOpenRouterModelCapabilities(modelId) {
6701
+ try {
6702
+ const { models } = await listOpenRouterModels();
6703
+ const model = models.find((entry) => entry.id === modelId);
6704
+ if (!model) {
6705
+ return;
6706
+ }
6707
+ return {
6708
+ supportsNativeTools: model.supportsTools,
6709
+ contextLength: model.contextLength ?? undefined,
6710
+ maxCompletionTokens: model.maxCompletionTokens
6711
+ };
6712
+ } catch {
6713
+ return;
6714
+ }
6715
+ }
6716
+ var logger4, openRouterModelsUrl = "https://openrouter.ai/api/v1/models", cacheTtlMs, fetchTimeoutMs = 15000, openRouterModelSummarySchema, modelsCacheSchema;
6717
+ var init_openrouter_models = __esm(() => {
6718
+ init_src();
6719
+ logger4 = createLogger("openrouter-models");
6720
+ cacheTtlMs = 24 * 60 * 60 * 1000;
6721
+ openRouterModelSummarySchema = z34.object({
6722
+ id: z34.string(),
6723
+ name: z34.string(),
6724
+ contextLength: z34.number().int().positive().nullable(),
6725
+ maxCompletionTokens: z34.number().int().positive().nullable().default(null),
6726
+ supportsTools: z34.boolean(),
6727
+ supportsReasoning: z34.boolean(),
6728
+ pricing: z34.object({
6729
+ prompt: z34.string().nullable(),
6730
+ completion: z34.string().nullable()
6731
+ }).nullable()
6732
+ });
6733
+ modelsCacheSchema = z34.object({
6734
+ fetchedAt: z34.number(),
6735
+ models: z34.array(openRouterModelSummarySchema)
6736
+ });
6202
6737
  });
6203
6738
 
6204
6739
  // apps/server/src/lib/provider-registry.ts
@@ -6233,6 +6768,16 @@ function getAnthropicProviderOptions(modelId) {
6233
6768
  }
6234
6769
  };
6235
6770
  }
6771
+ function resolveHeadroomRouting(config5, realBaseUrl) {
6772
+ const headroom = config5.headroom;
6773
+ if (!headroom?.enabled) {
6774
+ return { effectiveBaseUrl: realBaseUrl, headroomRouted: false };
6775
+ }
6776
+ if (headroom.providers && !headroom.providers.includes(config5.provider)) {
6777
+ return { effectiveBaseUrl: realBaseUrl, headroomRouted: false };
6778
+ }
6779
+ return { effectiveBaseUrl: headroom.proxyUrl, headroomRouted: true };
6780
+ }
6236
6781
  function resolveAnthropicModel({
6237
6782
  config: config5,
6238
6783
  env,
@@ -6243,19 +6788,22 @@ function resolveAnthropicModel({
6243
6788
  const apiKey = getEnvValue(env, "ANTHROPIC_API_KEY") ?? storedCredentials.anthropicApiKey;
6244
6789
  const authToken = getEnvValue(env, "ANTHROPIC_AUTH_TOKEN");
6245
6790
  const baseUrl = config5.baseUrl ?? getEnvValue(env, "ANTHROPIC_BASE_URL") ?? null;
6791
+ const { effectiveBaseUrl, headroomRouted } = resolveHeadroomRouting(config5, baseUrl);
6246
6792
  const missingCredentialHints = apiKey || authToken ? [] : [
6247
6793
  "Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN to call Anthropic models."
6248
6794
  ];
6249
6795
  const provider = createAnthropic({
6250
6796
  apiKey,
6251
6797
  authToken,
6252
- baseURL: baseUrl ?? undefined
6798
+ baseURL: effectiveBaseUrl ?? undefined
6253
6799
  });
6254
6800
  return {
6255
6801
  provider: "anthropic",
6256
6802
  configuredModel,
6257
6803
  resolvedModelId: modelId,
6258
6804
  baseUrl,
6805
+ effectiveBaseUrl,
6806
+ headroomRouted,
6259
6807
  model: provider(modelId),
6260
6808
  providerOptions: getAnthropicProviderOptions(modelId),
6261
6809
  contextWindow: getModelContextWindow("anthropic", modelId),
@@ -6265,7 +6813,8 @@ function resolveAnthropicModel({
6265
6813
  function resolveOpenAITransportModel({
6266
6814
  config: config5,
6267
6815
  env,
6268
- storedCredentials
6816
+ storedCredentials,
6817
+ capabilities
6269
6818
  }) {
6270
6819
  const isOpenCodeZen = config5.provider === "opencode-zen";
6271
6820
  const isOpenRouter = config5.provider === "openrouter";
@@ -6277,6 +6826,7 @@ function resolveOpenAITransportModel({
6277
6826
  if (!baseUrl) {
6278
6827
  throw new Error('OpenAI-compatible provider requires "baseUrl" in config or LIGHTCODE_OPENAI_COMPATIBLE_BASE_URL.');
6279
6828
  }
6829
+ const { effectiveBaseUrl, headroomRouted } = resolveHeadroomRouting(config5, baseUrl);
6280
6830
  const apiKey = isOpenCodeZen ? getEnvValue(env, "OPENCODE_API_KEY", "LIGHTCODE_OPENAI_COMPATIBLE_API_KEY", "OPENAI_API_KEY") ?? storedCredentials.opencodeApiKey : isOpenRouter ? getEnvValue(env, "OPENROUTER_API_KEY", "LIGHTCODE_OPENAI_COMPATIBLE_API_KEY", "OPENAI_API_KEY") ?? storedCredentials.openrouterApiKey : getEnvValue(env, "LIGHTCODE_OPENAI_COMPATIBLE_API_KEY", "OPENAI_API_KEY") ?? storedCredentials.openaiCompatibleApiKey;
6281
6831
  const headers = {};
6282
6832
  if (isOpenRouter) {
@@ -6291,7 +6841,7 @@ function resolveOpenAITransportModel({
6291
6841
  }
6292
6842
  const provider = createOpenAICompatible({
6293
6843
  name: config5.provider,
6294
- baseURL: baseUrl,
6844
+ baseURL: effectiveBaseUrl ?? baseUrl,
6295
6845
  apiKey,
6296
6846
  headers
6297
6847
  });
@@ -6300,21 +6850,30 @@ function resolveOpenAITransportModel({
6300
6850
  configuredModel,
6301
6851
  resolvedModelId: configuredModel,
6302
6852
  baseUrl,
6853
+ effectiveBaseUrl,
6854
+ headroomRouted,
6303
6855
  model: withXmlToolCallSupport(provider(configuredModel)),
6304
6856
  needsToolCallDiscipline: true,
6305
- contextWindow: getModelContextWindow(config5.provider, configuredModel),
6857
+ contextWindow: capabilities?.contextLength ?? getModelContextWindow(config5.provider, configuredModel),
6306
6858
  missingCredentialHints: apiKey ? [] : isOpenCodeZen ? ["Set OPENCODE_API_KEY to call OpenCode Zen models."] : isOpenRouter ? ["Set OPENROUTER_API_KEY to call OpenRouter models."] : [
6307
6859
  "Set LIGHTCODE_OPENAI_COMPATIBLE_API_KEY or OPENAI_API_KEY if your OpenAI-compatible endpoint requires authentication."
6308
- ]
6860
+ ],
6861
+ maxCompletionTokens: capabilities?.maxCompletionTokens ?? null
6309
6862
  };
6310
6863
  }
6311
6864
  function resolveConfiguredProviderModel({
6312
6865
  config: config5,
6313
- env = Bun.env
6866
+ env = Bun.env,
6867
+ capabilities
6314
6868
  }) {
6315
6869
  const storedCredentials = readStoredCredentials(env);
6316
6870
  if (config5.provider === "openai-compatible" || config5.provider === "opencode-zen" || config5.provider === "openrouter") {
6317
- return resolveOpenAITransportModel({ config: config5, env, storedCredentials });
6871
+ return resolveOpenAITransportModel({
6872
+ config: config5,
6873
+ env,
6874
+ storedCredentials,
6875
+ capabilities
6876
+ });
6318
6877
  }
6319
6878
  return resolveAnthropicModel({ config: config5, env, storedCredentials });
6320
6879
  }
@@ -6331,13 +6890,18 @@ function createConfigStatus({
6331
6890
  baseUrl: resolvedProviderModel.baseUrl,
6332
6891
  defaultMode: config5.defaultMode,
6333
6892
  permissionMode: config5.permissionMode ?? null,
6334
- maxOutputTokens: config5.maxOutputTokens,
6893
+ maxOutputTokens: resolveMaxOutputTokens(config5.maxOutputTokens, lightcodeConfigDefaults.maxOutputTokens, resolvedProviderModel.maxCompletionTokens),
6335
6894
  maxSteps: config5.maxSteps,
6336
6895
  autoContinue: config5.autoContinue,
6337
6896
  contextWindow: resolveContextWindowTokens({
6338
6897
  config: config5.context,
6339
6898
  modelContextWindow: resolvedProviderModel.contextWindow
6340
6899
  }),
6900
+ headroom: {
6901
+ enabled: config5.headroom?.enabled ?? false,
6902
+ proxyUrl: config5.headroom?.proxyUrl ?? null,
6903
+ routed: resolvedProviderModel.headroomRouted
6904
+ },
6341
6905
  missingCredentialHints: resolvedProviderModel.missingCredentialHints
6342
6906
  });
6343
6907
  }
@@ -6352,11 +6916,130 @@ var init_provider_registry = __esm(() => {
6352
6916
  });
6353
6917
 
6354
6918
  // apps/server/src/lib/runtime-config.ts
6355
- var lightcodeConfigResult, resolvedProviderModel, configStatus, chatModelId, codingAgentPromptOverride, codingAgent;
6919
+ var exports_runtime_config = {};
6920
+ __export(exports_runtime_config, {
6921
+ resolvedProviderModel: () => resolvedProviderModel,
6922
+ lightcodeConfigResult: () => lightcodeConfigResult,
6923
+ initializeModelCapabilities: () => initializeModelCapabilities,
6924
+ disableHeadroomRouting: () => disableHeadroomRouting,
6925
+ configStatus: () => configStatus,
6926
+ codingAgent: () => codingAgent,
6927
+ chatModelId: () => chatModelId,
6928
+ applyModelSelection: () => applyModelSelection
6929
+ });
6930
+ function buildCodingAgent(config5, model) {
6931
+ return createCodingAgent({
6932
+ model: model.model,
6933
+ promptOverride: codingAgentPromptOverride,
6934
+ providerOptions: model.providerOptions,
6935
+ maxSteps: config5.maxSteps,
6936
+ maxRetries: config5.maxRetries,
6937
+ maxOutputTokens: resolveMaxOutputTokens(config5.maxOutputTokens, lightcodeConfigDefaults.maxOutputTokens, model.maxCompletionTokens),
6938
+ includeToolDiscipline: model.needsToolCallDiscipline ?? false
6939
+ });
6940
+ }
6941
+ function applyModelSelection({
6942
+ provider,
6943
+ model,
6944
+ capabilities,
6945
+ persist = true
6946
+ }) {
6947
+ const nextConfig = {
6948
+ ...lightcodeConfigResult.config,
6949
+ provider,
6950
+ model
6951
+ };
6952
+ const nextResolved = resolveConfiguredProviderModel({
6953
+ config: nextConfig,
6954
+ env: Bun.env,
6955
+ capabilities
6956
+ });
6957
+ const nextAgent = buildCodingAgent(nextConfig, nextResolved);
6958
+ if (persist) {
6959
+ const { path: path11 } = updateUserSettings({ provider, model }, Bun.env);
6960
+ logger5.info("model_selection_persisted", { path: path11, provider, model });
6961
+ }
6962
+ lightcodeConfigResult = {
6963
+ ...lightcodeConfigResult,
6964
+ config: nextConfig
6965
+ };
6966
+ resolvedProviderModel = nextResolved;
6967
+ chatModelId = nextResolved.resolvedModelId;
6968
+ codingAgent = nextAgent;
6969
+ configStatus = createConfigStatus({
6970
+ config: nextConfig,
6971
+ loadedFiles: lightcodeConfigResult.loadedFiles,
6972
+ resolvedProviderModel: nextResolved
6973
+ });
6974
+ logger5.info("model_selection_applied", {
6975
+ provider,
6976
+ model,
6977
+ contextWindow: nextResolved.contextWindow,
6978
+ supportsNativeTools: capabilities?.supportsNativeTools ?? null
6979
+ });
6980
+ return configStatus;
6981
+ }
6982
+ function disableHeadroomRouting() {
6983
+ if (!lightcodeConfigResult.config.headroom?.enabled) {
6984
+ return;
6985
+ }
6986
+ const nextConfig = {
6987
+ ...lightcodeConfigResult.config,
6988
+ headroom: {
6989
+ ...lightcodeConfigResult.config.headroom ?? lightcodeConfigDefaults.headroom,
6990
+ enabled: false
6991
+ }
6992
+ };
6993
+ const nextResolved = resolveConfiguredProviderModel({
6994
+ config: nextConfig,
6995
+ env: Bun.env
6996
+ });
6997
+ const nextAgent = buildCodingAgent(nextConfig, nextResolved);
6998
+ lightcodeConfigResult = {
6999
+ ...lightcodeConfigResult,
7000
+ config: nextConfig
7001
+ };
7002
+ resolvedProviderModel = nextResolved;
7003
+ chatModelId = nextResolved.resolvedModelId;
7004
+ codingAgent = nextAgent;
7005
+ configStatus = createConfigStatus({
7006
+ config: nextConfig,
7007
+ loadedFiles: lightcodeConfigResult.loadedFiles,
7008
+ resolvedProviderModel: nextResolved
7009
+ });
7010
+ logger5.info("headroom_routing_disabled", {
7011
+ provider: nextResolved.provider,
7012
+ model: nextResolved.resolvedModelId
7013
+ });
7014
+ }
7015
+ async function initializeModelCapabilities() {
7016
+ if (lightcodeConfigResult.config.provider !== "openrouter") {
7017
+ return;
7018
+ }
7019
+ const modelId = lightcodeConfigResult.config.model;
7020
+ if (!modelId) {
7021
+ return;
7022
+ }
7023
+ const capabilities = await getOpenRouterModelCapabilities(modelId);
7024
+ if (!capabilities || chatModelId !== modelId) {
7025
+ return;
7026
+ }
7027
+ applyModelSelection({
7028
+ provider: "openrouter",
7029
+ model: modelId,
7030
+ capabilities,
7031
+ persist: false
7032
+ });
7033
+ }
7034
+ var logger5, codingAgentPromptOverride, lightcodeConfigResult, resolvedProviderModel, configStatus, chatModelId, codingAgent;
6356
7035
  var init_runtime_config = __esm(() => {
6357
7036
  init_src2();
7037
+ init_src();
7038
+ init_openrouter_models();
6358
7039
  init_provider_registry();
7040
+ logger5 = createLogger("runtime-config");
6359
7041
  assertProviderToolSchemaBudget();
7042
+ codingAgentPromptOverride = Bun.env.LIGHTCODE_CODING_AGENT_SYSTEM_PROMPT;
6360
7043
  lightcodeConfigResult = loadLightcodeConfig({
6361
7044
  cwd: process.cwd(),
6362
7045
  env: Bun.env
@@ -6371,15 +7054,7 @@ var init_runtime_config = __esm(() => {
6371
7054
  resolvedProviderModel
6372
7055
  });
6373
7056
  chatModelId = resolvedProviderModel.resolvedModelId;
6374
- codingAgentPromptOverride = Bun.env.LIGHTCODE_CODING_AGENT_SYSTEM_PROMPT;
6375
- codingAgent = createCodingAgent({
6376
- model: resolvedProviderModel.model,
6377
- promptOverride: codingAgentPromptOverride,
6378
- providerOptions: resolvedProviderModel.providerOptions,
6379
- maxSteps: lightcodeConfigResult.config.maxSteps,
6380
- maxOutputTokens: lightcodeConfigResult.config.maxOutputTokens,
6381
- includeToolDiscipline: resolvedProviderModel.needsToolCallDiscipline ?? false
6382
- });
7057
+ codingAgent = buildCodingAgent(lightcodeConfigResult.config, resolvedProviderModel);
6383
7058
  });
6384
7059
 
6385
7060
  // apps/server/src/lib/session-auto-title.ts
@@ -6415,7 +7090,7 @@ Response: ${firstAssistantText.slice(0, AUTO_TITLE_INPUT_PREVIEW_CHARS)}`,
6415
7090
  });
6416
7091
  const title = sanitizeGeneratedTitle(result.text);
6417
7092
  if (title && await applyGeneratedSessionTitle(sessionId, title)) {
6418
- logger4.info("session_auto_titled", { sessionId, title, model: chatModelId });
7093
+ logger6.info("session_auto_titled", { sessionId, title, model: chatModelId });
6419
7094
  }
6420
7095
  }
6421
7096
  function maybeScheduleSessionAutoTitle({
@@ -6423,20 +7098,20 @@ function maybeScheduleSessionAutoTitle({
6423
7098
  messages
6424
7099
  }) {
6425
7100
  generateSessionTitle({ sessionId, messages }).catch((error) => {
6426
- logger4.warn("session_auto_title_failed", {
7101
+ logger6.warn("session_auto_title_failed", {
6427
7102
  sessionId,
6428
7103
  error: getErrorMessage(error)
6429
7104
  });
6430
7105
  });
6431
7106
  }
6432
- var logger4, AUTO_TITLE_TIMEOUT_MS = 15000, AUTO_TITLE_MAX_OUTPUT_TOKENS = 24, AUTO_TITLE_INPUT_PREVIEW_CHARS = 600;
7107
+ var logger6, AUTO_TITLE_TIMEOUT_MS = 15000, AUTO_TITLE_MAX_OUTPUT_TOKENS = 24, AUTO_TITLE_INPUT_PREVIEW_CHARS = 600;
6433
7108
  var init_session_auto_title = __esm(() => {
6434
7109
  init_src2();
6435
7110
  init_src();
6436
7111
  init_chat_store();
6437
7112
  init_prisma_client();
6438
7113
  init_runtime_config();
6439
- logger4 = createLogger("session-auto-title");
7114
+ logger6 = createLogger("session-auto-title");
6440
7115
  });
6441
7116
 
6442
7117
  // apps/server/src/lib/chat-stream.ts
@@ -6485,7 +7160,7 @@ async function countPendingChatInteractions(sessionId) {
6485
7160
  });
6486
7161
  return pendingInteractions.interactions.length;
6487
7162
  } catch (error) {
6488
- logger5.warn("context_compaction_pending_interactions_unavailable", {
7163
+ logger7.warn("context_compaction_pending_interactions_unavailable", {
6489
7164
  sessionId,
6490
7165
  error: getErrorMessage(error)
6491
7166
  });
@@ -6496,7 +7171,7 @@ async function loadSessionContextStateSafe(sessionId) {
6496
7171
  try {
6497
7172
  return await getSessionContextState(sessionId);
6498
7173
  } catch (error) {
6499
- logger5.warn("context_state_unavailable", {
7174
+ logger7.warn("context_state_unavailable", {
6500
7175
  sessionId,
6501
7176
  error: getErrorMessage(error)
6502
7177
  });
@@ -6545,7 +7220,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6545
7220
  staleSkip: false
6546
7221
  });
6547
7222
  } catch (error) {
6548
- logger5.error("chat_pre_stream_persist_failed", {
7223
+ logger7.error("chat_pre_stream_persist_failed", {
6549
7224
  sessionId,
6550
7225
  error: getErrorMessage(error)
6551
7226
  });
@@ -6585,7 +7260,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6585
7260
  modelContextWindow: resolvedProviderModel.contextWindow,
6586
7261
  pendingInteractionCount
6587
7262
  });
6588
- logger5.info("context_auto_compacted", {
7263
+ logger7.info("context_auto_compacted", {
6589
7264
  sessionId,
6590
7265
  tier: compaction.state.tier,
6591
7266
  usedFallback: compaction.usedFallback,
@@ -6594,13 +7269,70 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6594
7269
  estimatedTokensAfter: view.estimate.tokens
6595
7270
  });
6596
7271
  } catch (error) {
6597
- logger5.error("context_compaction_failed", {
7272
+ logger7.error("context_compaction_failed", {
6598
7273
  sessionId,
6599
7274
  error: getErrorMessage(error)
6600
7275
  });
6601
7276
  }
6602
7277
  }
6603
- const providerMessages = view.providerMessages;
7278
+ const overflowPreserve = getOverflowPreserveRecentMessages(sessionId);
7279
+ if (overflowPreserve !== null) {
7280
+ const overflowConfig = {
7281
+ ...contextConfig,
7282
+ preserveRecentMessages: Math.min(overflowPreserve, contextConfig.preserveRecentMessages)
7283
+ };
7284
+ try {
7285
+ const overflowView = buildProviderView({
7286
+ messages: validatedMessages,
7287
+ contextState,
7288
+ config: overflowConfig,
7289
+ modelContextWindow: resolvedProviderModel.contextWindow,
7290
+ pendingInteractionCount: 0
7291
+ });
7292
+ if (overflowView.coveredMessages.length > 0) {
7293
+ const compaction = await compactSessionContext({
7294
+ sessionId,
7295
+ coveredMessages: overflowView.coveredMessages,
7296
+ previousState: contextState,
7297
+ model: resolvedProviderModel.model,
7298
+ modelId: chatModelId,
7299
+ cwd,
7300
+ config: overflowConfig,
7301
+ estimatedTokens: overflowView.estimate.tokens
7302
+ });
7303
+ contextState = compaction.state;
7304
+ }
7305
+ view = buildProviderView({
7306
+ messages: validatedMessages,
7307
+ contextState,
7308
+ config: overflowConfig,
7309
+ modelContextWindow: resolvedProviderModel.contextWindow,
7310
+ pendingInteractionCount: 0
7311
+ });
7312
+ logger7.info("context_overflow_recovery", {
7313
+ sessionId,
7314
+ preserveRecentMessages: overflowConfig.preserveRecentMessages,
7315
+ estimatedTokens: view.estimate.tokens,
7316
+ contextWindow: view.contextWindow,
7317
+ withinWindow: view.estimate.tokens < view.contextWindow,
7318
+ compactedMessages: view.coveredMessages.length
7319
+ });
7320
+ } catch (error) {
7321
+ logger7.error("context_overflow_recovery_failed", {
7322
+ sessionId,
7323
+ error: getErrorMessage(error)
7324
+ });
7325
+ }
7326
+ }
7327
+ const providerMessages = normalizeProviderMessages(view.providerMessages);
7328
+ let overflowNotedThisRequest = false;
7329
+ const noteOverflowForSession = () => {
7330
+ if (overflowNotedThisRequest) {
7331
+ return;
7332
+ }
7333
+ overflowNotedThisRequest = true;
7334
+ noteContextOverflow(sessionId);
7335
+ };
6604
7336
  const persistFinishedMessages = async ({
6605
7337
  isAborted,
6606
7338
  isContinuation,
@@ -6610,6 +7342,9 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6610
7342
  if (isAborted) {
6611
7343
  return;
6612
7344
  }
7345
+ if (!overflowNotedThisRequest) {
7346
+ clearContextOverflowRounds(sessionId);
7347
+ }
6613
7348
  const fullHistory = mergeFinishedMessagesIntoFullHistory({
6614
7349
  fullMessages: validatedMessages,
6615
7350
  providerMessageCount: providerMessages.length,
@@ -6619,14 +7354,14 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6619
7354
  });
6620
7355
  const normalizedMessages = removeTrailingEmptyAssistantMessages(fullHistory);
6621
7356
  if (normalizedMessages.length !== fullHistory.length) {
6622
- logger5.warn("chat_finish_empty_assistant_skipped", { sessionId });
7357
+ logger7.warn("chat_finish_empty_assistant_skipped", { sessionId });
6623
7358
  return;
6624
7359
  }
6625
7360
  const validatedFinishResult = await safeValidateUIMessages2({
6626
7361
  messages: normalizedMessages
6627
7362
  });
6628
7363
  if (!validatedFinishResult.success) {
6629
- logger5.warn("chat_invalid_finish_payload", {
7364
+ logger7.warn("chat_invalid_finish_payload", {
6630
7365
  sessionId,
6631
7366
  message: validatedFinishResult.error.message
6632
7367
  });
@@ -6660,7 +7395,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6660
7395
  messages: validatedFinishResult.data
6661
7396
  });
6662
7397
  } catch (error) {
6663
- logger5.error("chat_finish_persist_failed", {
7398
+ logger7.error("chat_finish_persist_failed", {
6664
7399
  sessionId,
6665
7400
  error: getErrorMessage(error)
6666
7401
  });
@@ -6692,7 +7427,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6692
7427
  mode,
6693
7428
  allowedTools
6694
7429
  })) {
6695
- logger5.info("chat_fast_path", {
7430
+ logger7.info("chat_fast_path", {
6696
7431
  sessionId,
6697
7432
  model: chatModelId
6698
7433
  });
@@ -6701,6 +7436,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6701
7436
  system: fastChatSystemPrompt,
6702
7437
  messages: buildFastChatModelMessages(providerMessages),
6703
7438
  maxOutputTokens: Math.min(lightcodeConfigResult.config.maxOutputTokens, fastChatMaxOutputTokens),
7439
+ maxRetries: lightcodeConfigResult.config.maxRetries,
6704
7440
  providerOptions: resolvedProviderModel.providerOptions,
6705
7441
  abortSignal: c.req.raw.signal
6706
7442
  });
@@ -6719,50 +7455,46 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6719
7455
  }
6720
7456
  }
6721
7457
  });
6722
- return result.toUIMessageStreamResponse({
7458
+ return withSseHeartbeat(result.toUIMessageStreamResponse({
6723
7459
  originalMessages: providerMessages,
6724
7460
  generateMessageId: generateId3,
6725
7461
  sendReasoning: false,
6726
7462
  messageMetadata: buildUsageMessageMetadata,
6727
7463
  onError: (error) => {
6728
- if (isProviderSchemaRejectionError(error)) {
6729
- incrementChatFailureCounter("provider_schema_rejection", {
6730
- sessionId
6731
- });
7464
+ const classified = classifyChatError(error);
7465
+ if (classified.kind === "context_overflow") {
7466
+ noteOverflowForSession();
6732
7467
  }
6733
- return genericRecoverableMessage;
7468
+ incrementChatFailureCounter(chatFailureClassForErrorKind(classified.kind), { sessionId, statusCode: classified.statusCode });
7469
+ logChatStreamErrorEvent({
7470
+ sessionId,
7471
+ phase: "stream",
7472
+ classified,
7473
+ error
7474
+ });
7475
+ return formatChatStreamError(classified);
6734
7476
  },
6735
7477
  onFinish: persistFinishedMessages
6736
- });
7478
+ }));
6737
7479
  }
6738
7480
  const mapStreamError = (error) => {
6739
- if (isProviderSchemaRejectionError(error)) {
6740
- incrementChatFailureCounter("provider_schema_rejection", {
6741
- sessionId
6742
- });
6743
- return genericRecoverableMessage;
7481
+ const classified = classifyChatError(error);
7482
+ if (classified.kind === "context_overflow") {
7483
+ noteOverflowForSession();
6744
7484
  }
6745
- if (isProviderBillingOrQuotaError(error)) {
6746
- incrementChatFailureCounter("provider_billing_quota", {
6747
- sessionId
6748
- });
6749
- return providerBillingOrQuotaMessage;
6750
- }
6751
- if (isDisconnectOrTimeoutError(error)) {
6752
- incrementChatFailureCounter("timeout_disconnect", {
6753
- sessionId,
6754
- phase: "stream"
6755
- });
6756
- logChatDisconnectEvent({
6757
- sessionId,
6758
- phase: "stream",
6759
- error
6760
- });
6761
- return recoverableDisconnectMessage;
6762
- }
6763
- return genericRecoverableMessage;
7485
+ incrementChatFailureCounter(chatFailureClassForErrorKind(classified.kind), {
7486
+ sessionId,
7487
+ statusCode: classified.statusCode
7488
+ });
7489
+ logChatStreamErrorEvent({
7490
+ sessionId,
7491
+ phase: "stream",
7492
+ classified,
7493
+ error
7494
+ });
7495
+ return formatChatStreamError(classified);
6764
7496
  };
6765
- return await createAgentUIStreamResponse({
7497
+ return withSseHeartbeat(await createAgentUIStreamResponse({
6766
7498
  agent: codingAgent,
6767
7499
  uiMessages: providerMessages,
6768
7500
  options: {
@@ -6797,24 +7529,27 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6797
7529
  },
6798
7530
  onError: mapStreamError,
6799
7531
  onFinish: persistFinishedMessages
6800
- });
7532
+ }));
6801
7533
  } catch (error) {
6802
- if (isProviderSchemaRejectionError(error)) {
6803
- incrementChatFailureCounter("provider_schema_rejection", {
6804
- sessionId
6805
- });
7534
+ const classified = classifyChatError(error);
7535
+ if (classified.kind === "context_overflow") {
7536
+ noteContextOverflow(sessionId);
6806
7537
  }
6807
- if (isProviderBillingOrQuotaError(error)) {
6808
- incrementChatFailureCounter("provider_billing_quota", {
6809
- sessionId
6810
- });
7538
+ incrementChatFailureCounter(chatFailureClassForErrorKind(classified.kind), {
7539
+ sessionId,
7540
+ phase: "pre-stream",
7541
+ statusCode: classified.statusCode
7542
+ });
7543
+ logChatStreamErrorEvent({
7544
+ sessionId,
7545
+ phase: "pre-stream",
7546
+ classified,
7547
+ error
7548
+ });
7549
+ if (classified.kind === "billing") {
6811
7550
  return c.json({ error: providerBillingOrQuotaMessage }, 402);
6812
7551
  }
6813
- if (isDisconnectOrTimeoutError(error)) {
6814
- incrementChatFailureCounter("timeout_disconnect", {
6815
- sessionId,
6816
- phase: "pre-stream"
6817
- });
7552
+ if (classified.kind === "network" || classified.kind === "aborted") {
6818
7553
  logChatDisconnectEvent({
6819
7554
  sessionId,
6820
7555
  phase: "pre-stream",
@@ -6823,12 +7558,12 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6823
7558
  return c.json({ error: recoverableDisconnectMessage }, 503);
6824
7559
  }
6825
7560
  return c.json({
6826
- error: "Unable to start chat stream.",
7561
+ error: formatChatStreamError(classified),
6827
7562
  details: Bun.env.NODE_ENV === "production" ? undefined : getErrorMessage(error)
6828
7563
  }, 500);
6829
7564
  }
6830
7565
  }
6831
- var logger5, recoverableDisconnectMessage = "Connection interrupted. Please retry or regenerate your last message.", genericRecoverableMessage = "The response stream was interrupted. Please retry.", providerBillingOrQuotaMessage, fastChatSystemPrompt, fastChatMaxOutputTokens = 512, fastChatRecentMessageCount = 6;
7566
+ var logger7, recoverableDisconnectMessage = "Connection interrupted. Please retry or regenerate your last message.", providerBillingOrQuotaMessage, fastChatSystemPrompt, fastChatMaxOutputTokens = 512, fastChatRecentMessageCount = 6;
6832
7567
  var init_chat_stream = __esm(() => {
6833
7568
  init_src2();
6834
7569
  init_src();
@@ -6836,10 +7571,12 @@ var init_chat_stream = __esm(() => {
6836
7571
  init_chat_interaction_store();
6837
7572
  init_chat_store();
6838
7573
  init_context_compaction();
7574
+ init_overflow_recovery();
7575
+ init_sse_heartbeat();
6839
7576
  init_context_state_store();
6840
7577
  init_session_auto_title();
6841
7578
  init_runtime_config();
6842
- logger5 = createLogger("chat-stream");
7579
+ logger7 = createLogger("chat-stream");
6843
7580
  providerBillingOrQuotaMessage = "The configured model provider rejected this request due to billing or quota limits. " + "Update provider credits/quota and retry.";
6844
7581
  fastChatSystemPrompt = "You are Lightcode's friendly coding assistant. For casual conversation, reply briefly and naturally. " + "If the user asks for coding work, say you can help and ask them what they want to change.";
6845
7582
  });
@@ -6850,16 +7587,16 @@ function internalErrorResponse(c, {
6850
7587
  message,
6851
7588
  error
6852
7589
  }) {
6853
- logger6.error(event, { error: getErrorMessage(error) });
7590
+ logger8.error(event, { error: getErrorMessage(error) });
6854
7591
  return c.json({
6855
7592
  error: message,
6856
7593
  details: Bun.env.NODE_ENV === "production" ? undefined : getErrorMessage(error)
6857
7594
  }, 500);
6858
7595
  }
6859
- var logger6;
7596
+ var logger8;
6860
7597
  var init_route_helpers = __esm(() => {
6861
7598
  init_src();
6862
- logger6 = createLogger("routes");
7599
+ logger8 = createLogger("routes");
6863
7600
  });
6864
7601
 
6865
7602
  // apps/server/src/routes/chat-routes.ts
@@ -7141,6 +7878,59 @@ var init_chat_routes = __esm(() => {
7141
7878
  });
7142
7879
  });
7143
7880
 
7881
+ // apps/server/src/routes/config-routes.ts
7882
+ import { Hono as Hono2 } from "hono";
7883
+ import { z as z35 } from "zod";
7884
+ var selectModelRequestSchema, configRoutes;
7885
+ var init_config_routes = __esm(() => {
7886
+ init_src2();
7887
+ init_src();
7888
+ init_openrouter_models();
7889
+ init_runtime_config();
7890
+ selectModelRequestSchema = z35.object({
7891
+ provider: lightcodeProviderSchema,
7892
+ model: z35.string().min(1).max(240)
7893
+ });
7894
+ configRoutes = new Hono2().get("/status", (c) => c.json(configStatus)).get("/models", async (c) => {
7895
+ const provider = c.req.query("provider") ?? "openrouter";
7896
+ if (provider !== "openrouter") {
7897
+ return c.json({ error: `Model listing is not supported for provider "${provider}".` }, 400);
7898
+ }
7899
+ try {
7900
+ const { models, fromCache } = await listOpenRouterModels({
7901
+ forceRefresh: c.req.query("refresh") === "1"
7902
+ });
7903
+ return c.json({
7904
+ generatedAt: new Date().toISOString(),
7905
+ provider,
7906
+ fromCache,
7907
+ models
7908
+ });
7909
+ } catch (error) {
7910
+ return c.json({
7911
+ error: `Unable to load OpenRouter models: ${getErrorMessage(error)}`
7912
+ }, 502);
7913
+ }
7914
+ }).put("/model", async (c) => {
7915
+ const parsed = selectModelRequestSchema.safeParse(await c.req.json().catch(() => null));
7916
+ if (!parsed.success) {
7917
+ return c.json({ error: "Invalid model selection payload." }, 400);
7918
+ }
7919
+ const { provider, model } = parsed.data;
7920
+ try {
7921
+ const capabilities = provider === "openrouter" ? await getOpenRouterModelCapabilities(model) : undefined;
7922
+ const status = applyModelSelection({ provider, model, capabilities });
7923
+ return c.json({
7924
+ ok: true,
7925
+ configStatus: status,
7926
+ capabilities: capabilities ?? null
7927
+ });
7928
+ } catch (error) {
7929
+ return c.json({ error: `Unable to switch model: ${getErrorMessage(error)}` }, 400);
7930
+ }
7931
+ });
7932
+ });
7933
+
7144
7934
  // apps/server/src/lib/extension-runtime.ts
7145
7935
  function getExtensionRuntimeStatus() {
7146
7936
  const skills = listSkills({ cwd: process.cwd() });
@@ -7171,8 +7961,9 @@ var init_extension_runtime = __esm(() => {
7171
7961
  });
7172
7962
 
7173
7963
  // apps/server/src/routes/diagnostics-routes.ts
7174
- import { existsSync as existsSync7 } from "fs";
7175
- import { Hono as Hono2 } from "hono";
7964
+ import { existsSync as existsSync9, readdirSync as readdirSync4 } from "fs";
7965
+ import path11 from "path";
7966
+ import { Hono as Hono3 } from "hono";
7176
7967
  function combineStatuses(statuses) {
7177
7968
  if (statuses.includes("error")) {
7178
7969
  return "error";
@@ -7305,8 +8096,8 @@ function checkWorkspace() {
7305
8096
  return {
7306
8097
  id: "workspace",
7307
8098
  label: "Workspace",
7308
- status: existsSync7(cwd) ? "ok" : "error",
7309
- summary: existsSync7(cwd) ? `Workspace is accessible: ${cwd}` : `Workspace is missing: ${cwd}`,
8099
+ status: existsSync9(cwd) ? "ok" : "error",
8100
+ summary: existsSync9(cwd) ? `Workspace is accessible: ${cwd}` : `Workspace is missing: ${cwd}`,
7310
8101
  details: []
7311
8102
  };
7312
8103
  }
@@ -7427,7 +8218,83 @@ async function buildPermissionsPayload() {
7427
8218
  notes
7428
8219
  });
7429
8220
  }
7430
- var diagnosticsRoutes;
8221
+ async function buildConnectivityPayload() {
8222
+ const baseUrl = resolvedProviderModel.baseUrl ?? (resolvedProviderModel.provider === "anthropic" ? "https://api.anthropic.com/v1" : null);
8223
+ const proxyEnvironment = {
8224
+ httpsProxy: Boolean(Bun.env.HTTPS_PROXY ?? Bun.env.https_proxy),
8225
+ httpProxy: Boolean(Bun.env.HTTP_PROXY ?? Bun.env.http_proxy),
8226
+ noProxy: Boolean(Bun.env.NO_PROXY ?? Bun.env.no_proxy),
8227
+ extraCaCerts: Boolean(Bun.env.NODE_EXTRA_CA_CERTS)
8228
+ };
8229
+ if (!baseUrl) {
8230
+ return {
8231
+ generatedAt: new Date().toISOString(),
8232
+ provider: resolvedProviderModel.provider,
8233
+ target: null,
8234
+ reachable: false,
8235
+ statusCode: null,
8236
+ latencyMs: null,
8237
+ error: "No provider base URL is configured.",
8238
+ proxyEnvironment
8239
+ };
8240
+ }
8241
+ const target = new URL("models", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
8242
+ const startedAt = performance.now();
8243
+ try {
8244
+ const response = await fetch(target, {
8245
+ signal: AbortSignal.timeout(connectivityProbeTimeoutMs)
8246
+ });
8247
+ return {
8248
+ generatedAt: new Date().toISOString(),
8249
+ provider: resolvedProviderModel.provider,
8250
+ target: target.toString(),
8251
+ reachable: true,
8252
+ statusCode: response.status,
8253
+ latencyMs: Math.round(performance.now() - startedAt),
8254
+ error: null,
8255
+ proxyEnvironment
8256
+ };
8257
+ } catch (error) {
8258
+ return {
8259
+ generatedAt: new Date().toISOString(),
8260
+ provider: resolvedProviderModel.provider,
8261
+ target: target.toString(),
8262
+ reachable: false,
8263
+ statusCode: null,
8264
+ latencyMs: Math.round(performance.now() - startedAt),
8265
+ error: getErrorMessage(error),
8266
+ proxyEnvironment
8267
+ };
8268
+ }
8269
+ }
8270
+ async function buildLogsTailPayload(requestedLines) {
8271
+ const directory = getLogDirectory();
8272
+ const lineCount = Math.min(Math.max(Number.isFinite(requestedLines) ? requestedLines : 200, 1), maxLogTailLines);
8273
+ let latestFile = null;
8274
+ try {
8275
+ const logFiles = readdirSync4(directory).filter((name) => /^server-\d{4}-\d{2}-\d{2}\.log$/.test(name)).sort();
8276
+ latestFile = logFiles.at(-1) ?? null;
8277
+ } catch {
8278
+ latestFile = null;
8279
+ }
8280
+ if (!latestFile) {
8281
+ return {
8282
+ generatedAt: new Date().toISOString(),
8283
+ file: null,
8284
+ lines: []
8285
+ };
8286
+ }
8287
+ const filePath = path11.join(directory, latestFile);
8288
+ const content = await Bun.file(filePath).text();
8289
+ const lines = content.split(`
8290
+ `).filter((line) => line.length > 0);
8291
+ return {
8292
+ generatedAt: new Date().toISOString(),
8293
+ file: filePath,
8294
+ lines: lines.slice(-lineCount)
8295
+ };
8296
+ }
8297
+ var connectivityProbeTimeoutMs = 1e4, maxLogTailLines = 1000, diagnosticsRoutes;
7431
8298
  var init_diagnostics_routes = __esm(() => {
7432
8299
  init_src2();
7433
8300
  init_src();
@@ -7436,7 +8303,7 @@ var init_diagnostics_routes = __esm(() => {
7436
8303
  init_prisma_client();
7437
8304
  init_runtime_config();
7438
8305
  init_extension_runtime();
7439
- diagnosticsRoutes = new Hono2().get("/status", async (c) => c.json(await buildStatusPayload())).get("/doctor", async (c) => c.json(await buildDoctorPayload())).get("/tools", (c) => {
8306
+ diagnosticsRoutes = new Hono3().get("/status", async (c) => c.json(await buildStatusPayload())).get("/logs/tail", async (c) => c.json(await buildLogsTailPayload(Number(c.req.query("lines") ?? 200)))).get("/connectivity", async (c) => c.json(await buildConnectivityPayload())).get("/doctor", async (c) => c.json(await buildDoctorPayload())).get("/tools", (c) => {
7440
8307
  const toolDiagnostics = getToolDiagnostics();
7441
8308
  return c.json(diagnosticsToolsResponseSchema.parse({
7442
8309
  generatedAt: new Date().toISOString(),
@@ -7448,19 +8315,19 @@ var init_diagnostics_routes = __esm(() => {
7448
8315
 
7449
8316
  // apps/server/src/routes/extension-routes.ts
7450
8317
  import { zValidator as zValidator2 } from "@hono/zod-validator";
7451
- import { Hono as Hono3 } from "hono";
7452
- import { z as z34 } from "zod";
8318
+ import { Hono as Hono4 } from "hono";
8319
+ import { z as z36 } from "zod";
7453
8320
  var skillParamsSchema, mcpServerParamsSchema, extensionRoutes;
7454
8321
  var init_extension_routes = __esm(() => {
7455
8322
  init_src2();
7456
8323
  init_extension_runtime();
7457
- skillParamsSchema = z34.object({
7458
- name: z34.string().min(1).max(160)
8324
+ skillParamsSchema = z36.object({
8325
+ name: z36.string().min(1).max(160)
7459
8326
  });
7460
- mcpServerParamsSchema = z34.object({
7461
- name: z34.string().min(1).max(120)
8327
+ mcpServerParamsSchema = z36.object({
8328
+ name: z36.string().min(1).max(120)
7462
8329
  });
7463
- extensionRoutes = new Hono3().get("/skills", (c) => {
8330
+ extensionRoutes = new Hono4().get("/skills", (c) => {
7464
8331
  return c.json(skillListOutputSchema.parse({
7465
8332
  skills: listSkills({ cwd: process.cwd() })
7466
8333
  }));
@@ -7481,18 +8348,15 @@ var init_extension_routes = __esm(() => {
7481
8348
  });
7482
8349
 
7483
8350
  // apps/server/src/routes/root-routes.ts
7484
- import { Hono as Hono4 } from "hono";
8351
+ import { Hono as Hono5 } from "hono";
7485
8352
  var rootRoutes;
7486
8353
  var init_root_routes = __esm(() => {
7487
8354
  init_src();
7488
- init_runtime_config();
7489
- rootRoutes = new Hono4().get("/", (c) => {
8355
+ rootRoutes = new Hono5().get("/", (c) => {
7490
8356
  return c.json({
7491
8357
  name: productName,
7492
8358
  message: createGreeting("API client")
7493
8359
  });
7494
- }).get("/config/status", (c) => {
7495
- return c.json(configStatus);
7496
8360
  });
7497
8361
  });
7498
8362
 
@@ -7501,24 +8365,78 @@ var exports_app = {};
7501
8365
  __export(exports_app, {
7502
8366
  app: () => app
7503
8367
  });
7504
- import { Hono as Hono5 } from "hono";
8368
+ import { Hono as Hono6 } from "hono";
7505
8369
  import { bodyLimit } from "hono/body-limit";
7506
8370
  var maxRequestBodyBytes, app;
7507
8371
  var init_app = __esm(() => {
7508
8372
  init_chat_routes();
8373
+ init_config_routes();
7509
8374
  init_diagnostics_routes();
7510
8375
  init_extension_routes();
7511
8376
  init_root_routes();
7512
8377
  maxRequestBodyBytes = 32 * 1024 * 1024;
7513
- app = new Hono5().use(bodyLimit({
8378
+ app = new Hono6().use(bodyLimit({
7514
8379
  maxSize: maxRequestBodyBytes,
7515
8380
  onError: (c) => c.json({ error: "Request body too large." }, 413)
7516
- })).route("/", rootRoutes).route("/diagnostics", diagnosticsRoutes).route("/extensions", extensionRoutes).route("/sessions", sessionRoutes);
8381
+ })).route("/", rootRoutes).route("/config", configRoutes).route("/diagnostics", diagnosticsRoutes).route("/extensions", extensionRoutes).route("/sessions", sessionRoutes);
8382
+ });
8383
+
8384
+ // apps/server/src/lib/headroom-proxy.ts
8385
+ var exports_headroom_proxy = {};
8386
+ __export(exports_headroom_proxy, {
8387
+ ensureHeadroomRouting: () => ensureHeadroomRouting,
8388
+ checkHeadroomHealth: () => checkHeadroomHealth
8389
+ });
8390
+ async function checkHeadroomHealth(proxyUrl) {
8391
+ try {
8392
+ await fetch(new URL("/health", proxyUrl), {
8393
+ signal: AbortSignal.timeout(healthCheckTimeoutMs)
8394
+ });
8395
+ return "healthy";
8396
+ } catch {
8397
+ return "down";
8398
+ }
8399
+ }
8400
+ async function ensureHeadroomRouting() {
8401
+ const headroom = lightcodeConfigResult.config.headroom;
8402
+ if (!headroom?.enabled) {
8403
+ return;
8404
+ }
8405
+ let health;
8406
+ try {
8407
+ health = await checkHeadroomHealth(headroom.proxyUrl);
8408
+ } catch (error) {
8409
+ health = "down";
8410
+ logger9.warn("headroom_health_probe_failed", {
8411
+ proxyUrl: headroom.proxyUrl,
8412
+ error: getErrorMessage(error)
8413
+ });
8414
+ }
8415
+ if (health === "healthy") {
8416
+ logger9.info("headroom_routing_active", { proxyUrl: headroom.proxyUrl });
8417
+ return;
8418
+ }
8419
+ if (headroom.failOpen) {
8420
+ logger9.warn("headroom_unreachable_failing_open", {
8421
+ proxyUrl: headroom.proxyUrl
8422
+ });
8423
+ disableHeadroomRouting();
8424
+ return;
8425
+ }
8426
+ logger9.error("headroom_unreachable_fail_closed", {
8427
+ proxyUrl: headroom.proxyUrl
8428
+ });
8429
+ }
8430
+ var logger9, healthCheckTimeoutMs = 1500;
8431
+ var init_headroom_proxy = __esm(() => {
8432
+ init_src();
8433
+ init_runtime_config();
8434
+ logger9 = createLogger("headroom-proxy");
7517
8435
  });
7518
8436
 
7519
8437
  // apps/server/src/index.ts
7520
8438
  init_src();
7521
- var logger7 = createLogger("server");
8439
+ var logger10 = createLogger("server");
7522
8440
  var httpIdleTimeoutSeconds = 30;
7523
8441
  var shutdownForceExitMs = 5000;
7524
8442
  function isChatStreamingRoute(request) {
@@ -7529,7 +8447,11 @@ function isChatStreamingRoute(request) {
7529
8447
  return /^\/sessions\/[^/]+\/chat$/.test(pathname);
7530
8448
  }
7531
8449
  async function startServer() {
7532
- const port = Number(Bun.env.PORT ?? 3000);
8450
+ const logDirectory = enableFileLogSink(Bun.env);
8451
+ if (logDirectory) {
8452
+ logger10.info("file_log_sink_enabled", { directory: logDirectory });
8453
+ }
8454
+ const port = Number(Bun.env.PORT ?? 4983);
7533
8455
  const hostname = Bun.env.LIGHTCODE_HOST ?? "127.0.0.1";
7534
8456
  let app2;
7535
8457
  try {
@@ -7547,24 +8469,49 @@ async function startServer() {
7547
8469
  `));
7548
8470
  process.exit(1);
7549
8471
  }
7550
- const server = Bun.serve({
7551
- idleTimeout: httpIdleTimeoutSeconds,
7552
- port,
7553
- hostname,
7554
- async fetch(request, bunServer) {
7555
- if (isChatStreamingRoute(request)) {
7556
- bunServer.timeout(request, 0);
7557
- }
7558
- return app2.fetch(request);
7559
- }
8472
+ Promise.resolve().then(() => (init_runtime_config(), exports_runtime_config)).then((runtime19) => runtime19.initializeModelCapabilities()).catch((error) => {
8473
+ logger10.warn("model_capability_init_failed", {
8474
+ error: getErrorMessage(error)
8475
+ });
8476
+ });
8477
+ Promise.resolve().then(() => (init_headroom_proxy(), exports_headroom_proxy)).then((headroom) => headroom.ensureHeadroomRouting()).catch((error) => {
8478
+ logger10.warn("headroom_init_failed", {
8479
+ error: getErrorMessage(error)
8480
+ });
7560
8481
  });
8482
+ let server;
8483
+ try {
8484
+ server = Bun.serve({
8485
+ idleTimeout: httpIdleTimeoutSeconds,
8486
+ port,
8487
+ hostname,
8488
+ async fetch(request, bunServer) {
8489
+ if (isChatStreamingRoute(request)) {
8490
+ bunServer.timeout(request, 0);
8491
+ }
8492
+ return app2.fetch(request);
8493
+ }
8494
+ });
8495
+ } catch (error) {
8496
+ process.stderr.write([
8497
+ "",
8498
+ `Lightcode server could not bind ${hostname}:${port}:`,
8499
+ ` ${getErrorMessage(error)}`,
8500
+ "",
8501
+ "Another process is likely using this port. Set PORT (or",
8502
+ "LIGHTCODE_API_URL for the CLI) to a free port and restart.",
8503
+ ""
8504
+ ].join(`
8505
+ `));
8506
+ process.exit(1);
8507
+ }
7561
8508
  let shuttingDown = false;
7562
8509
  const shutdown = async (signal) => {
7563
8510
  if (shuttingDown) {
7564
8511
  return;
7565
8512
  }
7566
8513
  shuttingDown = true;
7567
- logger7.info("server_shutdown", { signal });
8514
+ logger10.info("server_shutdown", { signal });
7568
8515
  const forceExitTimer = setTimeout(() => {
7569
8516
  server.stop(true);
7570
8517
  process.exit(0);