@kmugalkhod/lightcode 0.1.3 → 0.2.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 +1350 -503
  4. package/package.json +4 -2
  5. package/server.js +917 -181
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;
@@ -3119,18 +3213,18 @@ var init_context = __esm(() => {
3119
3213
  });
3120
3214
 
3121
3215
  // packages/ai/src/config/lightcode-config.ts
3122
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
3216
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
3123
3217
  import os2 from "os";
3124
- import path2 from "path";
3218
+ import path4 from "path";
3125
3219
  import { z as z28 } from "zod";
3126
3220
  function getDefaultUserConfigPath(env) {
3127
3221
  if (env.LIGHTCODE_USER_CONFIG) {
3128
3222
  return env.LIGHTCODE_USER_CONFIG;
3129
3223
  }
3130
- return path2.join(os2.homedir(), ".lightcode", "settings.json");
3224
+ return path4.join(os2.homedir(), ".lightcode", "settings.json");
3131
3225
  }
3132
3226
  function readConfigFile(scope, configPath) {
3133
- if (!existsSync2(configPath)) {
3227
+ if (!existsSync3(configPath)) {
3134
3228
  return {
3135
3229
  config: {},
3136
3230
  file: {
@@ -3221,6 +3315,7 @@ function readEnvConfig(env) {
3221
3315
  permissionMode: env.LIGHTCODE_PERMISSION_MODE,
3222
3316
  maxOutputTokens: parseNumberEnv(env.LIGHTCODE_MAX_OUTPUT_TOKENS, "LIGHTCODE_MAX_OUTPUT_TOKENS"),
3223
3317
  maxSteps: parseNumberEnv(env.LIGHTCODE_MAX_STEPS, "LIGHTCODE_MAX_STEPS"),
3318
+ maxRetries: parseNumberEnv(env.LIGHTCODE_MAX_RETRIES, "LIGHTCODE_MAX_RETRIES"),
3224
3319
  context: Object.keys(compactContextConfig).length > 0 ? compactContextConfig : undefined
3225
3320
  };
3226
3321
  const compactConfig = Object.fromEntries(Object.entries(rawConfig).filter(([, value]) => value !== undefined));
@@ -3248,10 +3343,30 @@ function mergeConfig(...configs) {
3248
3343
  allowedTools: config2.allowedTools ?? mergedConfig.allowedTools
3249
3344
  }), {});
3250
3345
  }
3346
+ function updateUserSettings(update, env = process.env) {
3347
+ const configPath = getDefaultUserConfigPath(env);
3348
+ const existing = readConfigFile("user", configPath).config;
3349
+ const merged = { ...existing };
3350
+ for (const [key, value] of Object.entries(update)) {
3351
+ if (value === undefined || value === null) {
3352
+ delete merged[key];
3353
+ } else {
3354
+ Reflect.set(merged, key, value);
3355
+ }
3356
+ }
3357
+ const parsed = lightcodeConfigSchema.safeParse(merged);
3358
+ if (!parsed.success) {
3359
+ throw new LightcodeConfigError(`Invalid settings update: ${parsed.error.message}`);
3360
+ }
3361
+ mkdirSync3(path4.dirname(configPath), { recursive: true });
3362
+ writeFileSync2(configPath, `${JSON.stringify(parsed.data, null, 2)}
3363
+ `, "utf8");
3364
+ return { path: configPath, config: parsed.data };
3365
+ }
3251
3366
  function loadLightcodeConfig({
3252
3367
  cwd = process.cwd(),
3253
3368
  env = process.env,
3254
- projectConfigPath = path2.join(cwd, ".lightcode", "settings.json"),
3369
+ projectConfigPath = path4.join(cwd, ".lightcode", "settings.json"),
3255
3370
  userConfigPath = getDefaultUserConfigPath(env)
3256
3371
  } = {}) {
3257
3372
  const userConfig = readConfigFile("user", userConfigPath);
@@ -3302,7 +3417,7 @@ var init_lightcode_config = __esm(() => {
3302
3417
  enabled: true,
3303
3418
  maxAutoContinues: 50,
3304
3419
  maxErrorRetries: 5,
3305
- stallTimeoutSeconds: 120
3420
+ stallTimeoutSeconds: 180
3306
3421
  };
3307
3422
  lightcodeConfigSchema = z28.object({
3308
3423
  provider: lightcodeProviderSchema.optional(),
@@ -3317,6 +3432,7 @@ var init_lightcode_config = __esm(() => {
3317
3432
  context: contextOptimizerConfigSchema.optional(),
3318
3433
  maxOutputTokens: z28.number().int().min(1).max(200000).optional(),
3319
3434
  maxSteps: z28.number().int().min(1).max(100).optional(),
3435
+ maxRetries: z28.number().int().min(0).max(10).optional(),
3320
3436
  autoContinue: autoContinueConfigSchema.optional()
3321
3437
  }).strict();
3322
3438
  lightcodeConfigDefaults = {
@@ -3325,6 +3441,7 @@ var init_lightcode_config = __esm(() => {
3325
3441
  context: defaultContextOptimizerConfig,
3326
3442
  maxOutputTokens: 32768,
3327
3443
  maxSteps: 30,
3444
+ maxRetries: 5,
3328
3445
  autoContinue: defaultAutoContinueConfig
3329
3446
  };
3330
3447
  loadedConfigFileSchema = z28.object({
@@ -3339,6 +3456,7 @@ var init_lightcode_config = __esm(() => {
3339
3456
  context: resolvedContextOptimizerConfigSchema,
3340
3457
  maxOutputTokens: z28.number().int().min(1).max(200000),
3341
3458
  maxSteps: z28.number().int().min(1).max(100),
3459
+ maxRetries: z28.number().int().min(0).max(10),
3342
3460
  autoContinue: resolvedAutoContinueConfigSchema
3343
3461
  });
3344
3462
  lightcodeConfigLoadResultSchema = z28.object({
@@ -4227,27 +4345,27 @@ var init_schema16 = __esm(() => {
4227
4345
  });
4228
4346
 
4229
4347
  // packages/ai/src/plugins/runtime.ts
4230
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
4348
+ import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
4231
4349
  import os3 from "os";
4232
- import path3 from "path";
4350
+ import path5 from "path";
4233
4351
  function pluginRoots(cwd) {
4234
4352
  return [
4235
- path3.join(cwd, ".lightcode", "plugins"),
4236
- path3.join(os3.homedir(), ".lightcode", "plugins")
4353
+ path5.join(cwd, ".lightcode", "plugins"),
4354
+ path5.join(os3.homedir(), ".lightcode", "plugins")
4237
4355
  ];
4238
4356
  }
4239
4357
  function listPlugins({ cwd = process.cwd() } = {}) {
4240
4358
  const plugins = [];
4241
4359
  for (const root of pluginRoots(cwd)) {
4242
- if (!existsSync3(root)) {
4360
+ if (!existsSync4(root)) {
4243
4361
  continue;
4244
4362
  }
4245
- for (const entry of readdirSync(root, { withFileTypes: true })) {
4363
+ for (const entry of readdirSync2(root, { withFileTypes: true })) {
4246
4364
  if (!entry.isDirectory()) {
4247
4365
  continue;
4248
4366
  }
4249
- const manifestPath = path3.join(root, entry.name, "plugin.json");
4250
- if (!existsSync3(manifestPath)) {
4367
+ const manifestPath = path5.join(root, entry.name, "plugin.json");
4368
+ if (!existsSync4(manifestPath)) {
4251
4369
  continue;
4252
4370
  }
4253
4371
  const manifest = pluginManifestSchema.parse(JSON.parse(readFileSync3(manifestPath, "utf8")));
@@ -4269,14 +4387,14 @@ var init_runtime2 = __esm(() => {
4269
4387
  });
4270
4388
 
4271
4389
  // packages/ai/src/skills/runtime.ts
4272
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync } from "fs";
4390
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync } from "fs";
4273
4391
  import os4 from "os";
4274
- import path4 from "path";
4392
+ import path6 from "path";
4275
4393
  function defaultSkillRoots(cwd) {
4276
4394
  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") }
4395
+ { source: "project", path: path6.join(cwd, ".lightcode", "skills") },
4396
+ { source: "project", path: path6.join(cwd, ".agents", "skills") },
4397
+ { source: "user", path: path6.join(os4.homedir(), ".lightcode", "skills") }
4280
4398
  ];
4281
4399
  }
4282
4400
  function normalizeSkillName(value) {
@@ -4298,7 +4416,7 @@ function parseBasicFrontmatter(content) {
4298
4416
  ]));
4299
4417
  }
4300
4418
  function findSkillFiles(root) {
4301
- if (!existsSync4(root.path)) {
4419
+ if (!existsSync5(root.path)) {
4302
4420
  return [];
4303
4421
  }
4304
4422
  const files = [];
@@ -4306,8 +4424,8 @@ function findSkillFiles(root) {
4306
4424
  if (depth > 4) {
4307
4425
  return;
4308
4426
  }
4309
- for (const entry of readdirSync2(directory)) {
4310
- const entryPath = path4.join(directory, entry);
4427
+ for (const entry of readdirSync3(directory)) {
4428
+ const entryPath = path6.join(directory, entry);
4311
4429
  const stat = statSync(entryPath);
4312
4430
  if (stat.isDirectory()) {
4313
4431
  visit(entryPath, depth + 1);
@@ -4328,7 +4446,7 @@ function listSkills({
4328
4446
  for (const skillPath of findSkillFiles(root)) {
4329
4447
  const content = readFileSync4(skillPath, "utf8");
4330
4448
  const frontmatter = parseBasicFrontmatter(content);
4331
- const fallbackName = path4.basename(path4.dirname(skillPath));
4449
+ const fallbackName = path6.basename(path6.dirname(skillPath));
4332
4450
  const name = normalizeSkillName(frontmatter.name ?? fallbackName);
4333
4451
  if (!name || seen.has(name)) {
4334
4452
  continue;
@@ -4403,6 +4521,7 @@ var init_subagent_schemas = __esm(() => {
4403
4521
  // packages/ai/src/index.ts
4404
4522
  var init_src2 = __esm(() => {
4405
4523
  init_agent_tools();
4524
+ init_chat_error();
4406
4525
  init_chat_interaction_schemas();
4407
4526
  init_plan_block();
4408
4527
  init_chat_schemas();
@@ -4433,20 +4552,20 @@ var init_src2 = __esm(() => {
4433
4552
 
4434
4553
  // packages/db/src/local-database.ts
4435
4554
  import { Database } from "bun:sqlite";
4436
- import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
4555
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
4437
4556
  import { dirname, isAbsolute, join, resolve } from "path";
4438
4557
  import { fileURLToPath, pathToFileURL } from "url";
4439
- import { homedir, platform } from "os";
4558
+ import { homedir as homedir2, platform as platform2 } from "os";
4440
4559
  function getDefaultLightcodeDataDir() {
4441
4560
  const configuredHome = process.env.LIGHTCODE_HOME?.trim();
4442
4561
  if (configuredHome) {
4443
4562
  return resolve(configuredHome);
4444
4563
  }
4445
- if (platform() === "win32") {
4564
+ if (platform2() === "win32") {
4446
4565
  const appData = process.env.APPDATA || process.env.LOCALAPPDATA;
4447
- return appData ? join(appData, appDirectoryName) : join(homedir(), "AppData", "Roaming", appDirectoryName);
4566
+ return appData ? join(appData, appDirectoryName) : join(homedir2(), "AppData", "Roaming", appDirectoryName);
4448
4567
  }
4449
- return join(homedir(), `.${appDirectoryName}`);
4568
+ return join(homedir2(), `.${appDirectoryName}`);
4450
4569
  }
4451
4570
  function getDefaultDatabasePath() {
4452
4571
  const configuredPath = process.env.LIGHTCODE_DATABASE_PATH?.trim();
@@ -4456,11 +4575,9 @@ function getDefaultDatabaseUrl() {
4456
4575
  return pathToFileURL(getDefaultDatabasePath()).href;
4457
4576
  }
4458
4577
  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;
4578
+ const configuredUrl = process.env.LIGHTCODE_DATABASE_URL?.trim();
4579
+ const supportedConfiguredUrl = configuredUrl && (configuredUrl.startsWith("file:") || configuredUrl.startsWith("libsql:") || configuredUrl.startsWith("http://") || configuredUrl.startsWith("https://")) ? configuredUrl : null;
4580
+ return supportedConfiguredUrl ?? getDefaultDatabaseUrl();
4464
4581
  }
4465
4582
  function filePathFromDatabaseUrl(databaseUrl) {
4466
4583
  if (!databaseUrl.startsWith("file:")) {
@@ -4474,8 +4591,8 @@ function filePathFromDatabaseUrl(databaseUrl) {
4474
4591
  }
4475
4592
  function ensureParentDirectory(filePath) {
4476
4593
  const parentDirectory = dirname(filePath);
4477
- if (!existsSync5(parentDirectory)) {
4478
- mkdirSync2(parentDirectory, { recursive: true });
4594
+ if (!existsSync6(parentDirectory)) {
4595
+ mkdirSync4(parentDirectory, { recursive: true });
4479
4596
  }
4480
4597
  }
4481
4598
  function initializeLocalDatabase(databaseUrl) {
@@ -4611,14 +4728,10 @@ var init_local_database = () => {};
4611
4728
  // apps/server/src/lib/chat-interaction-store.ts
4612
4729
  import { Database as Database2 } from "bun:sqlite";
4613
4730
  function getDatabase() {
4614
- if (!_initialized) {
4731
+ if (!_db) {
4615
4732
  const dbUrl = resolveDatabaseUrl();
4616
4733
  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);
4734
+ const dbPath = filePathFromDatabaseUrl(dbUrl) ?? getDefaultDatabasePath();
4622
4735
  _db = new Database2(dbPath);
4623
4736
  }
4624
4737
  return _db;
@@ -4769,7 +4882,7 @@ async function summarizePendingChatInteractions({
4769
4882
  recoverable: total
4770
4883
  };
4771
4884
  }
4772
- var ChatInteractionNotFoundError, _db = null, _initialized = false;
4885
+ var ChatInteractionNotFoundError, _db = null;
4773
4886
  var init_chat_interaction_store = __esm(() => {
4774
4887
  init_src2();
4775
4888
  init_local_database();
@@ -4786,6 +4899,28 @@ var init_chat_interaction_store = __esm(() => {
4786
4899
  });
4787
4900
 
4788
4901
  // apps/server/src/lib/chat-observability.ts
4902
+ import { APICallError, RetryError } from "ai";
4903
+ function chatFailureClassForErrorKind(kind) {
4904
+ switch (kind) {
4905
+ case "auth":
4906
+ return "provider_auth";
4907
+ case "billing":
4908
+ return "provider_billing_quota";
4909
+ case "rate_limit":
4910
+ return "provider_rate_limit";
4911
+ case "invalid_request":
4912
+ return "provider_invalid_request";
4913
+ case "context_overflow":
4914
+ return "context_overflow";
4915
+ case "provider_unavailable":
4916
+ return "provider_unavailable";
4917
+ case "network":
4918
+ case "aborted":
4919
+ return "timeout_disconnect";
4920
+ default:
4921
+ return "provider_unknown";
4922
+ }
4923
+ }
4789
4924
  function normalizeErrorMessage(message) {
4790
4925
  return message.toLowerCase();
4791
4926
  }
@@ -4797,6 +4932,76 @@ function isProviderBillingOrQuotaError(error) {
4797
4932
  const message = normalizeErrorMessage(getErrorMessage(error));
4798
4933
  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
4934
  }
4935
+ function isContextOverflowMessage(message) {
4936
+ return contextOverflowPattern.test(message);
4937
+ }
4938
+ function isAbortError(error) {
4939
+ return error instanceof Error && error.name === "AbortError" || normalizeErrorMessage(getErrorMessage(error)).includes("aborted");
4940
+ }
4941
+ function classifyByStatusCode(statusCode, message) {
4942
+ if (statusCode === 401 || statusCode === 403) {
4943
+ return { kind: "auth", retryable: false };
4944
+ }
4945
+ if (statusCode === 402) {
4946
+ return { kind: "billing", retryable: false };
4947
+ }
4948
+ if (statusCode === 429) {
4949
+ return { kind: "rate_limit", retryable: true };
4950
+ }
4951
+ if (statusCode === 400 || statusCode === 422 || statusCode === 413) {
4952
+ if (isContextOverflowMessage(message)) {
4953
+ return { kind: "context_overflow", retryable: true };
4954
+ }
4955
+ if (isProviderBillingOrQuotaError(message)) {
4956
+ return { kind: "billing", retryable: false };
4957
+ }
4958
+ return { kind: "invalid_request", retryable: false };
4959
+ }
4960
+ if (retryableStatusCodes.has(statusCode) || statusCode >= 500) {
4961
+ return { kind: "provider_unavailable", retryable: true };
4962
+ }
4963
+ return { kind: "unknown", retryable: false };
4964
+ }
4965
+ function classifyChatError(error) {
4966
+ if (RetryError.isInstance(error) && error.lastError) {
4967
+ return classifyChatError(error.lastError);
4968
+ }
4969
+ if (APICallError.isInstance(error)) {
4970
+ const responseBody = error.responseBody ?? "";
4971
+ const message2 = responseBody.length > 0 ? responseBody : error.message;
4972
+ if (typeof error.statusCode === "number") {
4973
+ const { kind, retryable } = classifyByStatusCode(error.statusCode, message2);
4974
+ return {
4975
+ kind,
4976
+ statusCode: error.statusCode,
4977
+ retryable,
4978
+ message: message2
4979
+ };
4980
+ }
4981
+ return {
4982
+ kind: error.isRetryable ? "network" : "unknown",
4983
+ retryable: error.isRetryable,
4984
+ message: message2
4985
+ };
4986
+ }
4987
+ const message = getErrorMessage(error);
4988
+ if (isAbortError(error)) {
4989
+ return { kind: "aborted", retryable: false, message };
4990
+ }
4991
+ if (isContextOverflowMessage(message)) {
4992
+ return { kind: "context_overflow", retryable: true, message };
4993
+ }
4994
+ if (isProviderBillingOrQuotaError(error)) {
4995
+ return { kind: "billing", retryable: false, message };
4996
+ }
4997
+ if (isProviderSchemaRejectionError(error)) {
4998
+ return { kind: "invalid_request", retryable: false, message };
4999
+ }
5000
+ if (isDisconnectOrTimeoutError(error)) {
5001
+ return { kind: "network", retryable: true, message };
5002
+ }
5003
+ return { kind: "unknown", retryable: false, message };
5004
+ }
4800
5005
  function isDisconnectOrTimeoutError(error) {
4801
5006
  const message = normalizeErrorMessage(getErrorMessage(error));
4802
5007
  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 +5027,22 @@ function incrementChatFailureCounter(failureClass, context2 = {}) {
4822
5027
  ...context2
4823
5028
  });
4824
5029
  }
5030
+ function logChatStreamErrorEvent({
5031
+ sessionId,
5032
+ phase,
5033
+ classified,
5034
+ error
5035
+ }) {
5036
+ logger2.error("chat_stream_error", {
5037
+ sessionId,
5038
+ phase,
5039
+ kind: classified.kind,
5040
+ statusCode: classified.statusCode,
5041
+ retryable: classified.retryable,
5042
+ message: classified.message,
5043
+ rawMessage: getErrorMessage(error)
5044
+ });
5045
+ }
4825
5046
  function logChatDisconnectEvent({
4826
5047
  sessionId,
4827
5048
  phase,
@@ -4834,17 +5055,25 @@ function logChatDisconnectEvent({
4834
5055
  message: getErrorMessage(error)
4835
5056
  });
4836
5057
  }
4837
- var chatFailureCounters, logger2;
5058
+ var chatFailureCounters, logger2, retryableStatusCodes, contextOverflowPattern;
4838
5059
  var init_chat_observability = __esm(() => {
4839
5060
  init_src();
4840
5061
  chatFailureCounters = {
4841
5062
  provider_schema_rejection: 0,
4842
5063
  provider_billing_quota: 0,
5064
+ provider_auth: 0,
5065
+ provider_rate_limit: 0,
5066
+ provider_invalid_request: 0,
5067
+ provider_unavailable: 0,
5068
+ context_overflow: 0,
5069
+ provider_unknown: 0,
4843
5070
  timeout_disconnect: 0,
4844
5071
  stale_finish_skip: 0,
4845
5072
  db_unique_conflict: 0
4846
5073
  };
4847
5074
  logger2 = createLogger("chat");
5075
+ retryableStatusCodes = new Set([408, 409, 429, 500, 502, 503, 504]);
5076
+ 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
5077
  });
4849
5078
 
4850
5079
  // packages/db/generated/prisma/internal/class.ts
@@ -5057,7 +5286,7 @@ var init_prismaNamespace = __esm(() => {
5057
5286
  var init_enums = () => {};
5058
5287
 
5059
5288
  // packages/db/generated/prisma/client.ts
5060
- import * as path5 from "path";
5289
+ import * as path7 from "path";
5061
5290
  import { fileURLToPath as fileURLToPath2 } from "url";
5062
5291
  var PrismaClient;
5063
5292
  var init_client = __esm(() => {
@@ -5065,7 +5294,7 @@ var init_client = __esm(() => {
5065
5294
  init_prismaNamespace();
5066
5295
  init_enums();
5067
5296
  init_enums();
5068
- globalThis["__dirname"] = path5.dirname(fileURLToPath2(import.meta.url));
5297
+ globalThis["__dirname"] = path7.dirname(fileURLToPath2(import.meta.url));
5069
5298
  PrismaClient = getPrismaClientClass();
5070
5299
  });
5071
5300
 
@@ -5643,28 +5872,28 @@ function mergeFinishedMessagesIntoFullHistory({
5643
5872
  }
5644
5873
 
5645
5874
  // packages/ai/src/common/resolve-within-workspace.ts
5646
- import path6 from "path";
5875
+ import path8 from "path";
5647
5876
  import {
5648
- existsSync as existsSync6,
5877
+ existsSync as existsSync7,
5649
5878
  lstatSync,
5650
5879
  realpathSync,
5651
5880
  statSync as statSync2
5652
5881
  } from "fs";
5653
5882
  function stripTrailingSeparator(value) {
5654
- const parsed = path6.parse(value);
5883
+ const parsed = path8.parse(value);
5655
5884
  if (value === parsed.root) {
5656
5885
  return value;
5657
5886
  }
5658
5887
  return value.replace(/[\\/]+$/, "");
5659
5888
  }
5660
5889
  function normalizeForComparison(value) {
5661
- const normalized = stripTrailingSeparator(path6.resolve(value));
5890
+ const normalized = stripTrailingSeparator(path8.resolve(value));
5662
5891
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
5663
5892
  }
5664
5893
  function isWithinWorkspace(absolutePath, context2) {
5665
5894
  const workspace = normalizeForComparison(context2.root);
5666
5895
  const target = normalizeForComparison(absolutePath);
5667
- return target === workspace || target.startsWith(`${workspace}${path6.sep}`);
5896
+ return target === workspace || target.startsWith(`${workspace}${path8.sep}`);
5668
5897
  }
5669
5898
  function assertWithinWorkspace(absolutePath, context2, input) {
5670
5899
  if (!isWithinWorkspace(absolutePath, context2)) {
@@ -5679,17 +5908,17 @@ function getExistingRealPath(absolutePath) {
5679
5908
  }
5680
5909
  }
5681
5910
  function findNearestExistingParent(absolutePath, context2, input) {
5682
- let currentPath = path6.dirname(absolutePath);
5911
+ let currentPath = path8.dirname(absolutePath);
5683
5912
  while (true) {
5684
5913
  assertWithinWorkspace(currentPath, context2, input);
5685
- if (existsSync6(currentPath)) {
5914
+ if (existsSync7(currentPath)) {
5686
5915
  const realParentPath = getExistingRealPath(currentPath);
5687
5916
  if (!realParentPath) {
5688
5917
  throw new Error(`Unable to resolve existing parent for path: ${input}`);
5689
5918
  }
5690
5919
  return realParentPath;
5691
5920
  }
5692
- const parentPath = path6.dirname(currentPath);
5921
+ const parentPath = path8.dirname(currentPath);
5693
5922
  if (parentPath === currentPath) {
5694
5923
  throw new Error(`Unable to find an existing workspace parent for path: ${input}`);
5695
5924
  }
@@ -5709,7 +5938,7 @@ function getDefaultWorkspaceContext() {
5709
5938
  }
5710
5939
  function resolveWithinWorkspace(input, options = {}) {
5711
5940
  const context2 = options.context ?? defaultWorkspaceContext;
5712
- const resolved = path6.resolve(context2.root, input);
5941
+ const resolved = path8.resolve(context2.root, input);
5713
5942
  assertWithinWorkspace(resolved, context2, input);
5714
5943
  const realPath = getExistingRealPath(resolved);
5715
5944
  if (realPath) {
@@ -5852,7 +6081,7 @@ var init_runtime11 = __esm(() => {
5852
6081
 
5853
6082
  // packages/ai/src/todo-write/runtime.ts
5854
6083
  import { mkdir, readFile, writeFile } from "fs/promises";
5855
- import path7 from "path";
6084
+ import path9 from "path";
5856
6085
  import { z as z33 } from "zod";
5857
6086
  function safeTodoSessionId(sessionId) {
5858
6087
  const trimmed = sessionId?.trim();
@@ -5862,7 +6091,7 @@ function safeTodoSessionId(sessionId) {
5862
6091
  return trimmed.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 120);
5863
6092
  }
5864
6093
  function getTodoFilePath(sessionId, workspaceContext) {
5865
- return resolveWithinWorkspace(path7.join(".lightcode", "todos", `${safeTodoSessionId(sessionId)}.json`), {
6094
+ return resolveWithinWorkspace(path9.join(".lightcode", "todos", `${safeTodoSessionId(sessionId)}.json`), {
5866
6095
  context: workspaceContext,
5867
6096
  allowMissing: true
5868
6097
  });
@@ -6066,6 +6295,7 @@ ${transcript}`
6066
6295
  system: COMPACTION_SYSTEM_PROMPT,
6067
6296
  prompt,
6068
6297
  maxOutputTokens: COMPACTION_MAX_OUTPUT_TOKENS,
6298
+ maxRetries: 2,
6069
6299
  abortSignal: AbortSignal.timeout(COMPACTION_TIMEOUT_MS)
6070
6300
  });
6071
6301
  const text = result.text.trim();
@@ -6114,7 +6344,7 @@ function composeSummary({
6114
6344
  `).trim();
6115
6345
  const bodyBudget = Math.max(MIN_SUMMARY_BODY_CHARS, summaryMaxChars - (pinnedText ? pinnedText.length + 2 : 0));
6116
6346
  const compressedBody = compressSummary(body, bodyBudget);
6117
- return [compressedBody, pinnedText].filter(Boolean).join(`
6347
+ return [compressedBody, pinnedText, compactionDirectResumeInstruction].filter(Boolean).join(`
6118
6348
 
6119
6349
  `);
6120
6350
  }
@@ -6192,13 +6422,221 @@ Unfinished work, known follow-ups, and the immediate next step.
6192
6422
  ## Tools and commands that mattered
6193
6423
  Important commands run and their relevant outcomes (test results, errors, etc.).
6194
6424
 
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.`;
6425
+ 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
6426
  var init_context_compaction = __esm(() => {
6197
6427
  init_src2();
6198
6428
  init_runtime17();
6199
6429
  init_src();
6200
6430
  init_context_state_store();
6201
6431
  logger3 = createLogger("context-compaction");
6432
+ 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.";
6433
+ });
6434
+
6435
+ // apps/server/src/lib/overflow-recovery.ts
6436
+ function noteContextOverflow(sessionId) {
6437
+ overflowRecoveryRounds.set(sessionId, (overflowRecoveryRounds.get(sessionId) ?? 0) + 1);
6438
+ }
6439
+ function clearContextOverflowRounds(sessionId) {
6440
+ overflowRecoveryRounds.delete(sessionId);
6441
+ }
6442
+ function getOverflowPreserveRecentMessages(sessionId) {
6443
+ const rounds = overflowRecoveryRounds.get(sessionId) ?? 0;
6444
+ if (rounds === 0) {
6445
+ return null;
6446
+ }
6447
+ return overflowPreserveSchedule[Math.min(rounds, overflowPreserveSchedule.length) - 1];
6448
+ }
6449
+ var overflowPreserveSchedule, overflowRecoveryRounds;
6450
+ var init_overflow_recovery = __esm(() => {
6451
+ overflowPreserveSchedule = [4, 2, 1, 0];
6452
+ overflowRecoveryRounds = new Map;
6453
+ });
6454
+
6455
+ // apps/server/src/lib/sse-heartbeat.ts
6456
+ function endsWithEventBoundary(chunk) {
6457
+ const length = chunk.length;
6458
+ if (length >= 2 && chunk[length - 1] === 10 && chunk[length - 2] === 10) {
6459
+ return true;
6460
+ }
6461
+ return length >= 4 && chunk[length - 1] === 10 && chunk[length - 2] === 13 && chunk[length - 3] === 10 && chunk[length - 4] === 13;
6462
+ }
6463
+ function withSseHeartbeat(response, intervalMs = defaultHeartbeatIntervalMs) {
6464
+ const body = response.body;
6465
+ if (!body) {
6466
+ return response;
6467
+ }
6468
+ let atEventBoundary = true;
6469
+ let heartbeatTimer = null;
6470
+ let lastChunkAt = Date.now();
6471
+ const stopHeartbeat = () => {
6472
+ if (heartbeatTimer) {
6473
+ clearInterval(heartbeatTimer);
6474
+ heartbeatTimer = null;
6475
+ }
6476
+ };
6477
+ const transform = new TransformStream({
6478
+ start(controller) {
6479
+ heartbeatTimer = setInterval(() => {
6480
+ if (!atEventBoundary || Date.now() - lastChunkAt < intervalMs) {
6481
+ return;
6482
+ }
6483
+ try {
6484
+ controller.enqueue(heartbeatFrame);
6485
+ lastChunkAt = Date.now();
6486
+ } catch {
6487
+ stopHeartbeat();
6488
+ }
6489
+ }, Math.min(intervalMs, 5000));
6490
+ },
6491
+ transform(chunk, controller) {
6492
+ atEventBoundary = endsWithEventBoundary(chunk);
6493
+ lastChunkAt = Date.now();
6494
+ controller.enqueue(chunk);
6495
+ },
6496
+ flush() {
6497
+ stopHeartbeat();
6498
+ }
6499
+ });
6500
+ const heartbeatBody = body.pipeThrough(transform);
6501
+ return new Response(heartbeatBody, {
6502
+ status: response.status,
6503
+ statusText: response.statusText,
6504
+ headers: response.headers
6505
+ });
6506
+ }
6507
+ var encoder, heartbeatFrame, defaultHeartbeatIntervalMs = 15000;
6508
+ var init_sse_heartbeat = __esm(() => {
6509
+ encoder = new TextEncoder;
6510
+ heartbeatFrame = encoder.encode(`: heartbeat
6511
+
6512
+ `);
6513
+ });
6514
+
6515
+ // apps/server/src/lib/openrouter-models.ts
6516
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
6517
+ import path10 from "path";
6518
+ import { z as z34 } from "zod";
6519
+ function getCacheFilePath() {
6520
+ return path10.join(getLightcodeDataDir(), "cache", "openrouter-models.json");
6521
+ }
6522
+ function readCache() {
6523
+ try {
6524
+ const cachePath = getCacheFilePath();
6525
+ if (!existsSync8(cachePath)) {
6526
+ return null;
6527
+ }
6528
+ return modelsCacheSchema.parse(JSON.parse(readFileSync5(cachePath, "utf8")));
6529
+ } catch {
6530
+ return null;
6531
+ }
6532
+ }
6533
+ function writeCache(cache) {
6534
+ try {
6535
+ const cachePath = getCacheFilePath();
6536
+ mkdirSync5(path10.dirname(cachePath), { recursive: true });
6537
+ writeFileSync3(cachePath, JSON.stringify(cache), "utf8");
6538
+ } catch (error) {
6539
+ logger4.warn("openrouter_models_cache_write_failed", {
6540
+ error: getErrorMessage(error)
6541
+ });
6542
+ }
6543
+ }
6544
+ function summarizeRawModel(raw2) {
6545
+ if (typeof raw2 !== "object" || raw2 === null) {
6546
+ return null;
6547
+ }
6548
+ const id = Reflect.get(raw2, "id");
6549
+ if (typeof id !== "string" || id.length === 0) {
6550
+ return null;
6551
+ }
6552
+ const name = Reflect.get(raw2, "name");
6553
+ const contextLength = Reflect.get(raw2, "context_length");
6554
+ const supportedParameters = Reflect.get(raw2, "supported_parameters");
6555
+ const parameterList = Array.isArray(supportedParameters) ? supportedParameters.filter((entry) => typeof entry === "string") : [];
6556
+ const pricingRaw = Reflect.get(raw2, "pricing");
6557
+ const pricing = typeof pricingRaw === "object" && pricingRaw !== null ? {
6558
+ prompt: typeof Reflect.get(pricingRaw, "prompt") === "string" ? Reflect.get(pricingRaw, "prompt") : null,
6559
+ completion: typeof Reflect.get(pricingRaw, "completion") === "string" ? Reflect.get(pricingRaw, "completion") : null
6560
+ } : null;
6561
+ return {
6562
+ id,
6563
+ name: typeof name === "string" && name.length > 0 ? name : id,
6564
+ contextLength: typeof contextLength === "number" && contextLength > 0 ? Math.round(contextLength) : null,
6565
+ supportsTools: parameterList.includes("tools"),
6566
+ supportsReasoning: parameterList.includes("reasoning"),
6567
+ pricing
6568
+ };
6569
+ }
6570
+ async function fetchModelsFromApi() {
6571
+ const response = await fetch(openRouterModelsUrl, {
6572
+ signal: AbortSignal.timeout(fetchTimeoutMs)
6573
+ });
6574
+ if (!response.ok) {
6575
+ throw new Error(`OpenRouter models API returned HTTP ${response.status}.`);
6576
+ }
6577
+ const payload = await response.json();
6578
+ const models = (payload.data ?? []).map(summarizeRawModel).filter((model) => model !== null);
6579
+ if (models.length === 0) {
6580
+ throw new Error("OpenRouter models API returned no models.");
6581
+ }
6582
+ return models;
6583
+ }
6584
+ async function listOpenRouterModels({
6585
+ forceRefresh = false
6586
+ } = {}) {
6587
+ const cache = readCache();
6588
+ if (!forceRefresh && cache && Date.now() - cache.fetchedAt < cacheTtlMs) {
6589
+ return { models: cache.models, fromCache: true };
6590
+ }
6591
+ try {
6592
+ const models = await fetchModelsFromApi();
6593
+ writeCache({ fetchedAt: Date.now(), models });
6594
+ return { models, fromCache: false };
6595
+ } catch (error) {
6596
+ logger4.warn("openrouter_models_fetch_failed", {
6597
+ error: getErrorMessage(error)
6598
+ });
6599
+ if (cache) {
6600
+ return { models: cache.models, fromCache: true };
6601
+ }
6602
+ throw error;
6603
+ }
6604
+ }
6605
+ async function getOpenRouterModelCapabilities(modelId) {
6606
+ try {
6607
+ const { models } = await listOpenRouterModels();
6608
+ const model = models.find((entry) => entry.id === modelId);
6609
+ if (!model) {
6610
+ return;
6611
+ }
6612
+ return {
6613
+ supportsNativeTools: model.supportsTools,
6614
+ contextLength: model.contextLength ?? undefined
6615
+ };
6616
+ } catch {
6617
+ return;
6618
+ }
6619
+ }
6620
+ var logger4, openRouterModelsUrl = "https://openrouter.ai/api/v1/models", cacheTtlMs, fetchTimeoutMs = 15000, openRouterModelSummarySchema, modelsCacheSchema;
6621
+ var init_openrouter_models = __esm(() => {
6622
+ init_src();
6623
+ logger4 = createLogger("openrouter-models");
6624
+ cacheTtlMs = 24 * 60 * 60 * 1000;
6625
+ openRouterModelSummarySchema = z34.object({
6626
+ id: z34.string(),
6627
+ name: z34.string(),
6628
+ contextLength: z34.number().int().positive().nullable(),
6629
+ supportsTools: z34.boolean(),
6630
+ supportsReasoning: z34.boolean(),
6631
+ pricing: z34.object({
6632
+ prompt: z34.string().nullable(),
6633
+ completion: z34.string().nullable()
6634
+ }).nullable()
6635
+ });
6636
+ modelsCacheSchema = z34.object({
6637
+ fetchedAt: z34.number(),
6638
+ models: z34.array(openRouterModelSummarySchema)
6639
+ });
6202
6640
  });
6203
6641
 
6204
6642
  // apps/server/src/lib/provider-registry.ts
@@ -6265,7 +6703,8 @@ function resolveAnthropicModel({
6265
6703
  function resolveOpenAITransportModel({
6266
6704
  config: config5,
6267
6705
  env,
6268
- storedCredentials
6706
+ storedCredentials,
6707
+ capabilities
6269
6708
  }) {
6270
6709
  const isOpenCodeZen = config5.provider === "opencode-zen";
6271
6710
  const isOpenRouter = config5.provider === "openrouter";
@@ -6302,7 +6741,7 @@ function resolveOpenAITransportModel({
6302
6741
  baseUrl,
6303
6742
  model: withXmlToolCallSupport(provider(configuredModel)),
6304
6743
  needsToolCallDiscipline: true,
6305
- contextWindow: getModelContextWindow(config5.provider, configuredModel),
6744
+ contextWindow: capabilities?.contextLength ?? getModelContextWindow(config5.provider, configuredModel),
6306
6745
  missingCredentialHints: apiKey ? [] : isOpenCodeZen ? ["Set OPENCODE_API_KEY to call OpenCode Zen models."] : isOpenRouter ? ["Set OPENROUTER_API_KEY to call OpenRouter models."] : [
6307
6746
  "Set LIGHTCODE_OPENAI_COMPATIBLE_API_KEY or OPENAI_API_KEY if your OpenAI-compatible endpoint requires authentication."
6308
6747
  ]
@@ -6310,11 +6749,17 @@ function resolveOpenAITransportModel({
6310
6749
  }
6311
6750
  function resolveConfiguredProviderModel({
6312
6751
  config: config5,
6313
- env = Bun.env
6752
+ env = Bun.env,
6753
+ capabilities
6314
6754
  }) {
6315
6755
  const storedCredentials = readStoredCredentials(env);
6316
6756
  if (config5.provider === "openai-compatible" || config5.provider === "opencode-zen" || config5.provider === "openrouter") {
6317
- return resolveOpenAITransportModel({ config: config5, env, storedCredentials });
6757
+ return resolveOpenAITransportModel({
6758
+ config: config5,
6759
+ env,
6760
+ storedCredentials,
6761
+ capabilities
6762
+ });
6318
6763
  }
6319
6764
  return resolveAnthropicModel({ config: config5, env, storedCredentials });
6320
6765
  }
@@ -6352,11 +6797,96 @@ var init_provider_registry = __esm(() => {
6352
6797
  });
6353
6798
 
6354
6799
  // apps/server/src/lib/runtime-config.ts
6355
- var lightcodeConfigResult, resolvedProviderModel, configStatus, chatModelId, codingAgentPromptOverride, codingAgent;
6800
+ var exports_runtime_config = {};
6801
+ __export(exports_runtime_config, {
6802
+ resolvedProviderModel: () => resolvedProviderModel,
6803
+ lightcodeConfigResult: () => lightcodeConfigResult,
6804
+ initializeModelCapabilities: () => initializeModelCapabilities,
6805
+ configStatus: () => configStatus,
6806
+ codingAgent: () => codingAgent,
6807
+ chatModelId: () => chatModelId,
6808
+ applyModelSelection: () => applyModelSelection
6809
+ });
6810
+ function buildCodingAgent(config5, model) {
6811
+ return createCodingAgent({
6812
+ model: model.model,
6813
+ promptOverride: codingAgentPromptOverride,
6814
+ providerOptions: model.providerOptions,
6815
+ maxSteps: config5.maxSteps,
6816
+ maxRetries: config5.maxRetries,
6817
+ maxOutputTokens: config5.maxOutputTokens,
6818
+ includeToolDiscipline: model.needsToolCallDiscipline ?? false
6819
+ });
6820
+ }
6821
+ function applyModelSelection({
6822
+ provider,
6823
+ model,
6824
+ capabilities,
6825
+ persist = true
6826
+ }) {
6827
+ const nextConfig = {
6828
+ ...lightcodeConfigResult.config,
6829
+ provider,
6830
+ model
6831
+ };
6832
+ const nextResolved = resolveConfiguredProviderModel({
6833
+ config: nextConfig,
6834
+ env: Bun.env,
6835
+ capabilities
6836
+ });
6837
+ const nextAgent = buildCodingAgent(nextConfig, nextResolved);
6838
+ if (persist) {
6839
+ const { path: path11 } = updateUserSettings({ provider, model }, Bun.env);
6840
+ logger5.info("model_selection_persisted", { path: path11, provider, model });
6841
+ }
6842
+ lightcodeConfigResult = {
6843
+ ...lightcodeConfigResult,
6844
+ config: nextConfig
6845
+ };
6846
+ resolvedProviderModel = nextResolved;
6847
+ chatModelId = nextResolved.resolvedModelId;
6848
+ codingAgent = nextAgent;
6849
+ configStatus = createConfigStatus({
6850
+ config: nextConfig,
6851
+ loadedFiles: lightcodeConfigResult.loadedFiles,
6852
+ resolvedProviderModel: nextResolved
6853
+ });
6854
+ logger5.info("model_selection_applied", {
6855
+ provider,
6856
+ model,
6857
+ contextWindow: nextResolved.contextWindow,
6858
+ supportsNativeTools: capabilities?.supportsNativeTools ?? null
6859
+ });
6860
+ return configStatus;
6861
+ }
6862
+ async function initializeModelCapabilities() {
6863
+ if (lightcodeConfigResult.config.provider !== "openrouter") {
6864
+ return;
6865
+ }
6866
+ const modelId = lightcodeConfigResult.config.model;
6867
+ if (!modelId) {
6868
+ return;
6869
+ }
6870
+ const capabilities = await getOpenRouterModelCapabilities(modelId);
6871
+ if (!capabilities || chatModelId !== modelId) {
6872
+ return;
6873
+ }
6874
+ applyModelSelection({
6875
+ provider: "openrouter",
6876
+ model: modelId,
6877
+ capabilities,
6878
+ persist: false
6879
+ });
6880
+ }
6881
+ var logger5, codingAgentPromptOverride, lightcodeConfigResult, resolvedProviderModel, configStatus, chatModelId, codingAgent;
6356
6882
  var init_runtime_config = __esm(() => {
6357
6883
  init_src2();
6884
+ init_src();
6885
+ init_openrouter_models();
6358
6886
  init_provider_registry();
6887
+ logger5 = createLogger("runtime-config");
6359
6888
  assertProviderToolSchemaBudget();
6889
+ codingAgentPromptOverride = Bun.env.LIGHTCODE_CODING_AGENT_SYSTEM_PROMPT;
6360
6890
  lightcodeConfigResult = loadLightcodeConfig({
6361
6891
  cwd: process.cwd(),
6362
6892
  env: Bun.env
@@ -6371,15 +6901,7 @@ var init_runtime_config = __esm(() => {
6371
6901
  resolvedProviderModel
6372
6902
  });
6373
6903
  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
- });
6904
+ codingAgent = buildCodingAgent(lightcodeConfigResult.config, resolvedProviderModel);
6383
6905
  });
6384
6906
 
6385
6907
  // apps/server/src/lib/session-auto-title.ts
@@ -6415,7 +6937,7 @@ Response: ${firstAssistantText.slice(0, AUTO_TITLE_INPUT_PREVIEW_CHARS)}`,
6415
6937
  });
6416
6938
  const title = sanitizeGeneratedTitle(result.text);
6417
6939
  if (title && await applyGeneratedSessionTitle(sessionId, title)) {
6418
- logger4.info("session_auto_titled", { sessionId, title, model: chatModelId });
6940
+ logger6.info("session_auto_titled", { sessionId, title, model: chatModelId });
6419
6941
  }
6420
6942
  }
6421
6943
  function maybeScheduleSessionAutoTitle({
@@ -6423,20 +6945,20 @@ function maybeScheduleSessionAutoTitle({
6423
6945
  messages
6424
6946
  }) {
6425
6947
  generateSessionTitle({ sessionId, messages }).catch((error) => {
6426
- logger4.warn("session_auto_title_failed", {
6948
+ logger6.warn("session_auto_title_failed", {
6427
6949
  sessionId,
6428
6950
  error: getErrorMessage(error)
6429
6951
  });
6430
6952
  });
6431
6953
  }
6432
- var logger4, AUTO_TITLE_TIMEOUT_MS = 15000, AUTO_TITLE_MAX_OUTPUT_TOKENS = 24, AUTO_TITLE_INPUT_PREVIEW_CHARS = 600;
6954
+ var logger6, AUTO_TITLE_TIMEOUT_MS = 15000, AUTO_TITLE_MAX_OUTPUT_TOKENS = 24, AUTO_TITLE_INPUT_PREVIEW_CHARS = 600;
6433
6955
  var init_session_auto_title = __esm(() => {
6434
6956
  init_src2();
6435
6957
  init_src();
6436
6958
  init_chat_store();
6437
6959
  init_prisma_client();
6438
6960
  init_runtime_config();
6439
- logger4 = createLogger("session-auto-title");
6961
+ logger6 = createLogger("session-auto-title");
6440
6962
  });
6441
6963
 
6442
6964
  // apps/server/src/lib/chat-stream.ts
@@ -6485,7 +7007,7 @@ async function countPendingChatInteractions(sessionId) {
6485
7007
  });
6486
7008
  return pendingInteractions.interactions.length;
6487
7009
  } catch (error) {
6488
- logger5.warn("context_compaction_pending_interactions_unavailable", {
7010
+ logger7.warn("context_compaction_pending_interactions_unavailable", {
6489
7011
  sessionId,
6490
7012
  error: getErrorMessage(error)
6491
7013
  });
@@ -6496,7 +7018,7 @@ async function loadSessionContextStateSafe(sessionId) {
6496
7018
  try {
6497
7019
  return await getSessionContextState(sessionId);
6498
7020
  } catch (error) {
6499
- logger5.warn("context_state_unavailable", {
7021
+ logger7.warn("context_state_unavailable", {
6500
7022
  sessionId,
6501
7023
  error: getErrorMessage(error)
6502
7024
  });
@@ -6545,7 +7067,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6545
7067
  staleSkip: false
6546
7068
  });
6547
7069
  } catch (error) {
6548
- logger5.error("chat_pre_stream_persist_failed", {
7070
+ logger7.error("chat_pre_stream_persist_failed", {
6549
7071
  sessionId,
6550
7072
  error: getErrorMessage(error)
6551
7073
  });
@@ -6585,7 +7107,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6585
7107
  modelContextWindow: resolvedProviderModel.contextWindow,
6586
7108
  pendingInteractionCount
6587
7109
  });
6588
- logger5.info("context_auto_compacted", {
7110
+ logger7.info("context_auto_compacted", {
6589
7111
  sessionId,
6590
7112
  tier: compaction.state.tier,
6591
7113
  usedFallback: compaction.usedFallback,
@@ -6594,13 +7116,70 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6594
7116
  estimatedTokensAfter: view.estimate.tokens
6595
7117
  });
6596
7118
  } catch (error) {
6597
- logger5.error("context_compaction_failed", {
7119
+ logger7.error("context_compaction_failed", {
7120
+ sessionId,
7121
+ error: getErrorMessage(error)
7122
+ });
7123
+ }
7124
+ }
7125
+ const overflowPreserve = getOverflowPreserveRecentMessages(sessionId);
7126
+ if (overflowPreserve !== null) {
7127
+ const overflowConfig = {
7128
+ ...contextConfig,
7129
+ preserveRecentMessages: Math.min(overflowPreserve, contextConfig.preserveRecentMessages)
7130
+ };
7131
+ try {
7132
+ const overflowView = buildProviderView({
7133
+ messages: validatedMessages,
7134
+ contextState,
7135
+ config: overflowConfig,
7136
+ modelContextWindow: resolvedProviderModel.contextWindow,
7137
+ pendingInteractionCount: 0
7138
+ });
7139
+ if (overflowView.coveredMessages.length > 0) {
7140
+ const compaction = await compactSessionContext({
7141
+ sessionId,
7142
+ coveredMessages: overflowView.coveredMessages,
7143
+ previousState: contextState,
7144
+ model: resolvedProviderModel.model,
7145
+ modelId: chatModelId,
7146
+ cwd,
7147
+ config: overflowConfig,
7148
+ estimatedTokens: overflowView.estimate.tokens
7149
+ });
7150
+ contextState = compaction.state;
7151
+ }
7152
+ view = buildProviderView({
7153
+ messages: validatedMessages,
7154
+ contextState,
7155
+ config: overflowConfig,
7156
+ modelContextWindow: resolvedProviderModel.contextWindow,
7157
+ pendingInteractionCount: 0
7158
+ });
7159
+ logger7.info("context_overflow_recovery", {
7160
+ sessionId,
7161
+ preserveRecentMessages: overflowConfig.preserveRecentMessages,
7162
+ estimatedTokens: view.estimate.tokens,
7163
+ contextWindow: view.contextWindow,
7164
+ withinWindow: view.estimate.tokens < view.contextWindow,
7165
+ compactedMessages: view.coveredMessages.length
7166
+ });
7167
+ } catch (error) {
7168
+ logger7.error("context_overflow_recovery_failed", {
6598
7169
  sessionId,
6599
7170
  error: getErrorMessage(error)
6600
7171
  });
6601
7172
  }
6602
7173
  }
6603
7174
  const providerMessages = view.providerMessages;
7175
+ let overflowNotedThisRequest = false;
7176
+ const noteOverflowForSession = () => {
7177
+ if (overflowNotedThisRequest) {
7178
+ return;
7179
+ }
7180
+ overflowNotedThisRequest = true;
7181
+ noteContextOverflow(sessionId);
7182
+ };
6604
7183
  const persistFinishedMessages = async ({
6605
7184
  isAborted,
6606
7185
  isContinuation,
@@ -6610,6 +7189,9 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6610
7189
  if (isAborted) {
6611
7190
  return;
6612
7191
  }
7192
+ if (!overflowNotedThisRequest) {
7193
+ clearContextOverflowRounds(sessionId);
7194
+ }
6613
7195
  const fullHistory = mergeFinishedMessagesIntoFullHistory({
6614
7196
  fullMessages: validatedMessages,
6615
7197
  providerMessageCount: providerMessages.length,
@@ -6619,14 +7201,14 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6619
7201
  });
6620
7202
  const normalizedMessages = removeTrailingEmptyAssistantMessages(fullHistory);
6621
7203
  if (normalizedMessages.length !== fullHistory.length) {
6622
- logger5.warn("chat_finish_empty_assistant_skipped", { sessionId });
7204
+ logger7.warn("chat_finish_empty_assistant_skipped", { sessionId });
6623
7205
  return;
6624
7206
  }
6625
7207
  const validatedFinishResult = await safeValidateUIMessages2({
6626
7208
  messages: normalizedMessages
6627
7209
  });
6628
7210
  if (!validatedFinishResult.success) {
6629
- logger5.warn("chat_invalid_finish_payload", {
7211
+ logger7.warn("chat_invalid_finish_payload", {
6630
7212
  sessionId,
6631
7213
  message: validatedFinishResult.error.message
6632
7214
  });
@@ -6660,7 +7242,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6660
7242
  messages: validatedFinishResult.data
6661
7243
  });
6662
7244
  } catch (error) {
6663
- logger5.error("chat_finish_persist_failed", {
7245
+ logger7.error("chat_finish_persist_failed", {
6664
7246
  sessionId,
6665
7247
  error: getErrorMessage(error)
6666
7248
  });
@@ -6692,7 +7274,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6692
7274
  mode,
6693
7275
  allowedTools
6694
7276
  })) {
6695
- logger5.info("chat_fast_path", {
7277
+ logger7.info("chat_fast_path", {
6696
7278
  sessionId,
6697
7279
  model: chatModelId
6698
7280
  });
@@ -6701,6 +7283,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6701
7283
  system: fastChatSystemPrompt,
6702
7284
  messages: buildFastChatModelMessages(providerMessages),
6703
7285
  maxOutputTokens: Math.min(lightcodeConfigResult.config.maxOutputTokens, fastChatMaxOutputTokens),
7286
+ maxRetries: lightcodeConfigResult.config.maxRetries,
6704
7287
  providerOptions: resolvedProviderModel.providerOptions,
6705
7288
  abortSignal: c.req.raw.signal
6706
7289
  });
@@ -6719,50 +7302,46 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6719
7302
  }
6720
7303
  }
6721
7304
  });
6722
- return result.toUIMessageStreamResponse({
7305
+ return withSseHeartbeat(result.toUIMessageStreamResponse({
6723
7306
  originalMessages: providerMessages,
6724
7307
  generateMessageId: generateId3,
6725
7308
  sendReasoning: false,
6726
7309
  messageMetadata: buildUsageMessageMetadata,
6727
7310
  onError: (error) => {
6728
- if (isProviderSchemaRejectionError(error)) {
6729
- incrementChatFailureCounter("provider_schema_rejection", {
6730
- sessionId
6731
- });
7311
+ const classified = classifyChatError(error);
7312
+ if (classified.kind === "context_overflow") {
7313
+ noteOverflowForSession();
6732
7314
  }
6733
- return genericRecoverableMessage;
7315
+ incrementChatFailureCounter(chatFailureClassForErrorKind(classified.kind), { sessionId, statusCode: classified.statusCode });
7316
+ logChatStreamErrorEvent({
7317
+ sessionId,
7318
+ phase: "stream",
7319
+ classified,
7320
+ error
7321
+ });
7322
+ return formatChatStreamError(classified);
6734
7323
  },
6735
7324
  onFinish: persistFinishedMessages
6736
- });
7325
+ }));
6737
7326
  }
6738
7327
  const mapStreamError = (error) => {
6739
- if (isProviderSchemaRejectionError(error)) {
6740
- incrementChatFailureCounter("provider_schema_rejection", {
6741
- sessionId
6742
- });
6743
- return genericRecoverableMessage;
6744
- }
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;
7328
+ const classified = classifyChatError(error);
7329
+ if (classified.kind === "context_overflow") {
7330
+ noteOverflowForSession();
6762
7331
  }
6763
- return genericRecoverableMessage;
7332
+ incrementChatFailureCounter(chatFailureClassForErrorKind(classified.kind), {
7333
+ sessionId,
7334
+ statusCode: classified.statusCode
7335
+ });
7336
+ logChatStreamErrorEvent({
7337
+ sessionId,
7338
+ phase: "stream",
7339
+ classified,
7340
+ error
7341
+ });
7342
+ return formatChatStreamError(classified);
6764
7343
  };
6765
- return await createAgentUIStreamResponse({
7344
+ return withSseHeartbeat(await createAgentUIStreamResponse({
6766
7345
  agent: codingAgent,
6767
7346
  uiMessages: providerMessages,
6768
7347
  options: {
@@ -6797,24 +7376,27 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6797
7376
  },
6798
7377
  onError: mapStreamError,
6799
7378
  onFinish: persistFinishedMessages
6800
- });
7379
+ }));
6801
7380
  } catch (error) {
6802
- if (isProviderSchemaRejectionError(error)) {
6803
- incrementChatFailureCounter("provider_schema_rejection", {
6804
- sessionId
6805
- });
7381
+ const classified = classifyChatError(error);
7382
+ if (classified.kind === "context_overflow") {
7383
+ noteContextOverflow(sessionId);
6806
7384
  }
6807
- if (isProviderBillingOrQuotaError(error)) {
6808
- incrementChatFailureCounter("provider_billing_quota", {
6809
- sessionId
6810
- });
7385
+ incrementChatFailureCounter(chatFailureClassForErrorKind(classified.kind), {
7386
+ sessionId,
7387
+ phase: "pre-stream",
7388
+ statusCode: classified.statusCode
7389
+ });
7390
+ logChatStreamErrorEvent({
7391
+ sessionId,
7392
+ phase: "pre-stream",
7393
+ classified,
7394
+ error
7395
+ });
7396
+ if (classified.kind === "billing") {
6811
7397
  return c.json({ error: providerBillingOrQuotaMessage }, 402);
6812
7398
  }
6813
- if (isDisconnectOrTimeoutError(error)) {
6814
- incrementChatFailureCounter("timeout_disconnect", {
6815
- sessionId,
6816
- phase: "pre-stream"
6817
- });
7399
+ if (classified.kind === "network" || classified.kind === "aborted") {
6818
7400
  logChatDisconnectEvent({
6819
7401
  sessionId,
6820
7402
  phase: "pre-stream",
@@ -6823,12 +7405,12 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
6823
7405
  return c.json({ error: recoverableDisconnectMessage }, 503);
6824
7406
  }
6825
7407
  return c.json({
6826
- error: "Unable to start chat stream.",
7408
+ error: formatChatStreamError(classified),
6827
7409
  details: Bun.env.NODE_ENV === "production" ? undefined : getErrorMessage(error)
6828
7410
  }, 500);
6829
7411
  }
6830
7412
  }
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;
7413
+ var logger7, recoverableDisconnectMessage = "Connection interrupted. Please retry or regenerate your last message.", providerBillingOrQuotaMessage, fastChatSystemPrompt, fastChatMaxOutputTokens = 512, fastChatRecentMessageCount = 6;
6832
7414
  var init_chat_stream = __esm(() => {
6833
7415
  init_src2();
6834
7416
  init_src();
@@ -6836,10 +7418,12 @@ var init_chat_stream = __esm(() => {
6836
7418
  init_chat_interaction_store();
6837
7419
  init_chat_store();
6838
7420
  init_context_compaction();
7421
+ init_overflow_recovery();
7422
+ init_sse_heartbeat();
6839
7423
  init_context_state_store();
6840
7424
  init_session_auto_title();
6841
7425
  init_runtime_config();
6842
- logger5 = createLogger("chat-stream");
7426
+ logger7 = createLogger("chat-stream");
6843
7427
  providerBillingOrQuotaMessage = "The configured model provider rejected this request due to billing or quota limits. " + "Update provider credits/quota and retry.";
6844
7428
  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
7429
  });
@@ -6850,16 +7434,16 @@ function internalErrorResponse(c, {
6850
7434
  message,
6851
7435
  error
6852
7436
  }) {
6853
- logger6.error(event, { error: getErrorMessage(error) });
7437
+ logger8.error(event, { error: getErrorMessage(error) });
6854
7438
  return c.json({
6855
7439
  error: message,
6856
7440
  details: Bun.env.NODE_ENV === "production" ? undefined : getErrorMessage(error)
6857
7441
  }, 500);
6858
7442
  }
6859
- var logger6;
7443
+ var logger8;
6860
7444
  var init_route_helpers = __esm(() => {
6861
7445
  init_src();
6862
- logger6 = createLogger("routes");
7446
+ logger8 = createLogger("routes");
6863
7447
  });
6864
7448
 
6865
7449
  // apps/server/src/routes/chat-routes.ts
@@ -7141,6 +7725,59 @@ var init_chat_routes = __esm(() => {
7141
7725
  });
7142
7726
  });
7143
7727
 
7728
+ // apps/server/src/routes/config-routes.ts
7729
+ import { Hono as Hono2 } from "hono";
7730
+ import { z as z35 } from "zod";
7731
+ var selectModelRequestSchema, configRoutes;
7732
+ var init_config_routes = __esm(() => {
7733
+ init_src2();
7734
+ init_src();
7735
+ init_openrouter_models();
7736
+ init_runtime_config();
7737
+ selectModelRequestSchema = z35.object({
7738
+ provider: lightcodeProviderSchema,
7739
+ model: z35.string().min(1).max(240)
7740
+ });
7741
+ configRoutes = new Hono2().get("/status", (c) => c.json(configStatus)).get("/models", async (c) => {
7742
+ const provider = c.req.query("provider") ?? "openrouter";
7743
+ if (provider !== "openrouter") {
7744
+ return c.json({ error: `Model listing is not supported for provider "${provider}".` }, 400);
7745
+ }
7746
+ try {
7747
+ const { models, fromCache } = await listOpenRouterModels({
7748
+ forceRefresh: c.req.query("refresh") === "1"
7749
+ });
7750
+ return c.json({
7751
+ generatedAt: new Date().toISOString(),
7752
+ provider,
7753
+ fromCache,
7754
+ models
7755
+ });
7756
+ } catch (error) {
7757
+ return c.json({
7758
+ error: `Unable to load OpenRouter models: ${getErrorMessage(error)}`
7759
+ }, 502);
7760
+ }
7761
+ }).put("/model", async (c) => {
7762
+ const parsed = selectModelRequestSchema.safeParse(await c.req.json().catch(() => null));
7763
+ if (!parsed.success) {
7764
+ return c.json({ error: "Invalid model selection payload." }, 400);
7765
+ }
7766
+ const { provider, model } = parsed.data;
7767
+ try {
7768
+ const capabilities = provider === "openrouter" ? await getOpenRouterModelCapabilities(model) : undefined;
7769
+ const status = applyModelSelection({ provider, model, capabilities });
7770
+ return c.json({
7771
+ ok: true,
7772
+ configStatus: status,
7773
+ capabilities: capabilities ?? null
7774
+ });
7775
+ } catch (error) {
7776
+ return c.json({ error: `Unable to switch model: ${getErrorMessage(error)}` }, 400);
7777
+ }
7778
+ });
7779
+ });
7780
+
7144
7781
  // apps/server/src/lib/extension-runtime.ts
7145
7782
  function getExtensionRuntimeStatus() {
7146
7783
  const skills = listSkills({ cwd: process.cwd() });
@@ -7171,8 +7808,9 @@ var init_extension_runtime = __esm(() => {
7171
7808
  });
7172
7809
 
7173
7810
  // apps/server/src/routes/diagnostics-routes.ts
7174
- import { existsSync as existsSync7 } from "fs";
7175
- import { Hono as Hono2 } from "hono";
7811
+ import { existsSync as existsSync9, readdirSync as readdirSync4 } from "fs";
7812
+ import path11 from "path";
7813
+ import { Hono as Hono3 } from "hono";
7176
7814
  function combineStatuses(statuses) {
7177
7815
  if (statuses.includes("error")) {
7178
7816
  return "error";
@@ -7305,8 +7943,8 @@ function checkWorkspace() {
7305
7943
  return {
7306
7944
  id: "workspace",
7307
7945
  label: "Workspace",
7308
- status: existsSync7(cwd) ? "ok" : "error",
7309
- summary: existsSync7(cwd) ? `Workspace is accessible: ${cwd}` : `Workspace is missing: ${cwd}`,
7946
+ status: existsSync9(cwd) ? "ok" : "error",
7947
+ summary: existsSync9(cwd) ? `Workspace is accessible: ${cwd}` : `Workspace is missing: ${cwd}`,
7310
7948
  details: []
7311
7949
  };
7312
7950
  }
@@ -7427,7 +8065,83 @@ async function buildPermissionsPayload() {
7427
8065
  notes
7428
8066
  });
7429
8067
  }
7430
- var diagnosticsRoutes;
8068
+ async function buildConnectivityPayload() {
8069
+ const baseUrl = resolvedProviderModel.baseUrl ?? (resolvedProviderModel.provider === "anthropic" ? "https://api.anthropic.com/v1" : null);
8070
+ const proxyEnvironment = {
8071
+ httpsProxy: Boolean(Bun.env.HTTPS_PROXY ?? Bun.env.https_proxy),
8072
+ httpProxy: Boolean(Bun.env.HTTP_PROXY ?? Bun.env.http_proxy),
8073
+ noProxy: Boolean(Bun.env.NO_PROXY ?? Bun.env.no_proxy),
8074
+ extraCaCerts: Boolean(Bun.env.NODE_EXTRA_CA_CERTS)
8075
+ };
8076
+ if (!baseUrl) {
8077
+ return {
8078
+ generatedAt: new Date().toISOString(),
8079
+ provider: resolvedProviderModel.provider,
8080
+ target: null,
8081
+ reachable: false,
8082
+ statusCode: null,
8083
+ latencyMs: null,
8084
+ error: "No provider base URL is configured.",
8085
+ proxyEnvironment
8086
+ };
8087
+ }
8088
+ const target = new URL("models", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
8089
+ const startedAt = performance.now();
8090
+ try {
8091
+ const response = await fetch(target, {
8092
+ signal: AbortSignal.timeout(connectivityProbeTimeoutMs)
8093
+ });
8094
+ return {
8095
+ generatedAt: new Date().toISOString(),
8096
+ provider: resolvedProviderModel.provider,
8097
+ target: target.toString(),
8098
+ reachable: true,
8099
+ statusCode: response.status,
8100
+ latencyMs: Math.round(performance.now() - startedAt),
8101
+ error: null,
8102
+ proxyEnvironment
8103
+ };
8104
+ } catch (error) {
8105
+ return {
8106
+ generatedAt: new Date().toISOString(),
8107
+ provider: resolvedProviderModel.provider,
8108
+ target: target.toString(),
8109
+ reachable: false,
8110
+ statusCode: null,
8111
+ latencyMs: Math.round(performance.now() - startedAt),
8112
+ error: getErrorMessage(error),
8113
+ proxyEnvironment
8114
+ };
8115
+ }
8116
+ }
8117
+ async function buildLogsTailPayload(requestedLines) {
8118
+ const directory = getLogDirectory();
8119
+ const lineCount = Math.min(Math.max(Number.isFinite(requestedLines) ? requestedLines : 200, 1), maxLogTailLines);
8120
+ let latestFile = null;
8121
+ try {
8122
+ const logFiles = readdirSync4(directory).filter((name) => /^server-\d{4}-\d{2}-\d{2}\.log$/.test(name)).sort();
8123
+ latestFile = logFiles.at(-1) ?? null;
8124
+ } catch {
8125
+ latestFile = null;
8126
+ }
8127
+ if (!latestFile) {
8128
+ return {
8129
+ generatedAt: new Date().toISOString(),
8130
+ file: null,
8131
+ lines: []
8132
+ };
8133
+ }
8134
+ const filePath = path11.join(directory, latestFile);
8135
+ const content = await Bun.file(filePath).text();
8136
+ const lines = content.split(`
8137
+ `).filter((line) => line.length > 0);
8138
+ return {
8139
+ generatedAt: new Date().toISOString(),
8140
+ file: filePath,
8141
+ lines: lines.slice(-lineCount)
8142
+ };
8143
+ }
8144
+ var connectivityProbeTimeoutMs = 1e4, maxLogTailLines = 1000, diagnosticsRoutes;
7431
8145
  var init_diagnostics_routes = __esm(() => {
7432
8146
  init_src2();
7433
8147
  init_src();
@@ -7436,7 +8150,7 @@ var init_diagnostics_routes = __esm(() => {
7436
8150
  init_prisma_client();
7437
8151
  init_runtime_config();
7438
8152
  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) => {
8153
+ 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
8154
  const toolDiagnostics = getToolDiagnostics();
7441
8155
  return c.json(diagnosticsToolsResponseSchema.parse({
7442
8156
  generatedAt: new Date().toISOString(),
@@ -7448,19 +8162,19 @@ var init_diagnostics_routes = __esm(() => {
7448
8162
 
7449
8163
  // apps/server/src/routes/extension-routes.ts
7450
8164
  import { zValidator as zValidator2 } from "@hono/zod-validator";
7451
- import { Hono as Hono3 } from "hono";
7452
- import { z as z34 } from "zod";
8165
+ import { Hono as Hono4 } from "hono";
8166
+ import { z as z36 } from "zod";
7453
8167
  var skillParamsSchema, mcpServerParamsSchema, extensionRoutes;
7454
8168
  var init_extension_routes = __esm(() => {
7455
8169
  init_src2();
7456
8170
  init_extension_runtime();
7457
- skillParamsSchema = z34.object({
7458
- name: z34.string().min(1).max(160)
8171
+ skillParamsSchema = z36.object({
8172
+ name: z36.string().min(1).max(160)
7459
8173
  });
7460
- mcpServerParamsSchema = z34.object({
7461
- name: z34.string().min(1).max(120)
8174
+ mcpServerParamsSchema = z36.object({
8175
+ name: z36.string().min(1).max(120)
7462
8176
  });
7463
- extensionRoutes = new Hono3().get("/skills", (c) => {
8177
+ extensionRoutes = new Hono4().get("/skills", (c) => {
7464
8178
  return c.json(skillListOutputSchema.parse({
7465
8179
  skills: listSkills({ cwd: process.cwd() })
7466
8180
  }));
@@ -7481,18 +8195,15 @@ var init_extension_routes = __esm(() => {
7481
8195
  });
7482
8196
 
7483
8197
  // apps/server/src/routes/root-routes.ts
7484
- import { Hono as Hono4 } from "hono";
8198
+ import { Hono as Hono5 } from "hono";
7485
8199
  var rootRoutes;
7486
8200
  var init_root_routes = __esm(() => {
7487
8201
  init_src();
7488
- init_runtime_config();
7489
- rootRoutes = new Hono4().get("/", (c) => {
8202
+ rootRoutes = new Hono5().get("/", (c) => {
7490
8203
  return c.json({
7491
8204
  name: productName,
7492
8205
  message: createGreeting("API client")
7493
8206
  });
7494
- }).get("/config/status", (c) => {
7495
- return c.json(configStatus);
7496
8207
  });
7497
8208
  });
7498
8209
 
@@ -7501,24 +8212,25 @@ var exports_app = {};
7501
8212
  __export(exports_app, {
7502
8213
  app: () => app
7503
8214
  });
7504
- import { Hono as Hono5 } from "hono";
8215
+ import { Hono as Hono6 } from "hono";
7505
8216
  import { bodyLimit } from "hono/body-limit";
7506
8217
  var maxRequestBodyBytes, app;
7507
8218
  var init_app = __esm(() => {
7508
8219
  init_chat_routes();
8220
+ init_config_routes();
7509
8221
  init_diagnostics_routes();
7510
8222
  init_extension_routes();
7511
8223
  init_root_routes();
7512
8224
  maxRequestBodyBytes = 32 * 1024 * 1024;
7513
- app = new Hono5().use(bodyLimit({
8225
+ app = new Hono6().use(bodyLimit({
7514
8226
  maxSize: maxRequestBodyBytes,
7515
8227
  onError: (c) => c.json({ error: "Request body too large." }, 413)
7516
- })).route("/", rootRoutes).route("/diagnostics", diagnosticsRoutes).route("/extensions", extensionRoutes).route("/sessions", sessionRoutes);
8228
+ })).route("/", rootRoutes).route("/config", configRoutes).route("/diagnostics", diagnosticsRoutes).route("/extensions", extensionRoutes).route("/sessions", sessionRoutes);
7517
8229
  });
7518
8230
 
7519
8231
  // apps/server/src/index.ts
7520
8232
  init_src();
7521
- var logger7 = createLogger("server");
8233
+ var logger9 = createLogger("server");
7522
8234
  var httpIdleTimeoutSeconds = 30;
7523
8235
  var shutdownForceExitMs = 5000;
7524
8236
  function isChatStreamingRoute(request) {
@@ -7529,7 +8241,11 @@ function isChatStreamingRoute(request) {
7529
8241
  return /^\/sessions\/[^/]+\/chat$/.test(pathname);
7530
8242
  }
7531
8243
  async function startServer() {
7532
- const port = Number(Bun.env.PORT ?? 3000);
8244
+ const logDirectory = enableFileLogSink(Bun.env);
8245
+ if (logDirectory) {
8246
+ logger9.info("file_log_sink_enabled", { directory: logDirectory });
8247
+ }
8248
+ const port = Number(Bun.env.PORT ?? 4983);
7533
8249
  const hostname = Bun.env.LIGHTCODE_HOST ?? "127.0.0.1";
7534
8250
  let app2;
7535
8251
  try {
@@ -7547,24 +8263,44 @@ async function startServer() {
7547
8263
  `));
7548
8264
  process.exit(1);
7549
8265
  }
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
- }
8266
+ Promise.resolve().then(() => (init_runtime_config(), exports_runtime_config)).then((runtime19) => runtime19.initializeModelCapabilities()).catch((error) => {
8267
+ logger9.warn("model_capability_init_failed", {
8268
+ error: getErrorMessage(error)
8269
+ });
7560
8270
  });
8271
+ let server;
8272
+ try {
8273
+ server = Bun.serve({
8274
+ idleTimeout: httpIdleTimeoutSeconds,
8275
+ port,
8276
+ hostname,
8277
+ async fetch(request, bunServer) {
8278
+ if (isChatStreamingRoute(request)) {
8279
+ bunServer.timeout(request, 0);
8280
+ }
8281
+ return app2.fetch(request);
8282
+ }
8283
+ });
8284
+ } catch (error) {
8285
+ process.stderr.write([
8286
+ "",
8287
+ `Lightcode server could not bind ${hostname}:${port}:`,
8288
+ ` ${getErrorMessage(error)}`,
8289
+ "",
8290
+ "Another process is likely using this port. Set PORT (or",
8291
+ "LIGHTCODE_API_URL for the CLI) to a free port and restart.",
8292
+ ""
8293
+ ].join(`
8294
+ `));
8295
+ process.exit(1);
8296
+ }
7561
8297
  let shuttingDown = false;
7562
8298
  const shutdown = async (signal) => {
7563
8299
  if (shuttingDown) {
7564
8300
  return;
7565
8301
  }
7566
8302
  shuttingDown = true;
7567
- logger7.info("server_shutdown", { signal });
8303
+ logger9.info("server_shutdown", { signal });
7568
8304
  const forceExitTimer = setTimeout(() => {
7569
8305
  server.stop(true);
7570
8306
  process.exit(0);