@kmugalkhod/lightcode 0.4.0 → 0.6.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/README.md +20 -1
  2. package/cli.js +1575 -935
  3. package/lightcode.cjs +63 -18
  4. package/package.json +2 -1
  5. package/server.js +695 -238
package/server.js CHANGED
@@ -747,9 +747,10 @@ var init_schema = __esm(() => {
747
747
 
748
748
  // packages/ai/src/edit-file/schema.ts
749
749
  import { z as z6 } from "zod";
750
- var editFileDescription = "Edit a file via literal search and replace. Use this for focused, small edits.", editFileInputSchema, editFileProviderInputSchema, editFileOutputSchema;
750
+ var editFileDescription, editFileInputSchema, editFileProviderInputSchema, editFileOutputSchema;
751
751
  var init_schema2 = __esm(() => {
752
752
  init_base_schemas();
753
+ editFileDescription = "Edit a file via search and replace. Use this for focused, small edits. " + "Line endings (CRLF/LF) and surrounding whitespace are matched tolerantly, so " + "copy the lines as shown by read_file. The search text must match exactly one " + "place \u2014 include enough surrounding context to make it unique, or set " + "replaceAll: true to change every occurrence.";
753
754
  editFileInputSchema = z6.object({
754
755
  path: boundedPathSchema,
755
756
  search: z6.string().min(1),
@@ -1549,7 +1550,8 @@ var init_agent_tools = __esm(() => {
1549
1550
  permissionMode: permissionModeSchema.optional(),
1550
1551
  allowedTools: z21.array(codingAgentToolNameSchema).optional(),
1551
1552
  permissionRules: permissionRulesSchema.optional(),
1552
- sandbox: sandboxConfigSchema.optional()
1553
+ sandbox: sandboxConfigSchema.optional(),
1554
+ environmentContext: z21.string().optional()
1553
1555
  });
1554
1556
  });
1555
1557
 
@@ -1883,7 +1885,8 @@ function buildCodingAgentSystemPrompt({
1883
1885
  cwd,
1884
1886
  override,
1885
1887
  mode = defaultCodingAgentMode,
1886
- includeToolDiscipline = false
1888
+ includeToolDiscipline = false,
1889
+ environmentContext
1887
1890
  }) {
1888
1891
  const promptTemplate = override?.trim() ? override : defaultCodingAgentSystemPrompt;
1889
1892
  const normalizedPrompt = normalizePromptTemplate(promptTemplate, cwd);
@@ -1894,12 +1897,14 @@ function buildCodingAgentSystemPrompt({
1894
1897
  ` + `Mode purpose: ${modeDefinition.purpose}
1895
1898
  ` + `${modeDefinition.instructions}` + (includeToolDiscipline ? `
1896
1899
 
1897
- ${toolCallingDisciplineAddendum}` : "");
1900
+ ${toolCallingDisciplineAddendum}` : "") + (environmentContext?.trim() ? `
1901
+
1902
+ ${environmentContext.trim()}` : "");
1898
1903
  }
1899
1904
  var cwdPromptPlaceholder = "{cwd}", defaultCodingAgentSystemPrompt, toolCallingDisciplineAddendum;
1900
1905
  var init_coding_agent_prompt = __esm(() => {
1901
1906
  init_coding_agent_modes();
1902
- defaultCodingAgentSystemPrompt = "You are a coding agent. Use tools for filesystem and codebase tasks instead of guessing, and only interact with files under this working directory: {cwd}. " + "Prefer dedicated tools \u2014 glob_search for paths, grep for text search, structured git tools for repository inspection \u2014 and bash only when those are not enough. " + `Use todo_write to keep multi-step work visible and current, inspect context before risky operations, and emit a brief, natural progress note before major tool actions and after important findings.
1907
+ defaultCodingAgentSystemPrompt = "You are a coding agent working inside the user's project at this working directory: {cwd}. " + 'You can already see the project \u2014 proactively read it. When the user asks you to review, explain, or work on "my code"/"this", inspect the working directory yourself with list_files, glob_search, grep, and read_file; NEVER ask the user to paste or share code that lives in this directory \u2014 open it. ' + "Use tools for filesystem and codebase tasks instead of guessing, and only interact with files under the working directory. " + "Prefer dedicated tools \u2014 glob_search for paths, grep for text search, structured git tools for repository inspection \u2014 and bash only when those are not enough. " + `Use todo_write to keep multi-step work visible and current, inspect context before risky operations, and emit a brief, natural progress note before major tool actions and after important findings.
1903
1908
 
1904
1909
  ` + `You are an agent \u2014 keep working until the request is fully resolved. Never stop with a partial answer; if you say you will do something, do it in the same turn by calling a tool. Do not finish while any todo is pending or in_progress \u2014 end your turn only when the task is complete or you are blocked on input that only the user can provide.
1905
1910
 
@@ -2221,78 +2226,7 @@ function selectCodingAgentIntentTools({
2221
2226
  if (!normalizedText || casualPromptPattern.test(normalizedText)) {
2222
2227
  return [];
2223
2228
  }
2224
- const selectedTools = [];
2225
- const hasCodeIntent = includesAny(normalizedText, [
2226
- "file",
2227
- "folder",
2228
- "code",
2229
- "repo",
2230
- "project",
2231
- "implement",
2232
- "fix",
2233
- "add",
2234
- "build",
2235
- "refactor",
2236
- "change",
2237
- "create",
2238
- "update",
2239
- "edit",
2240
- "delete",
2241
- "run",
2242
- "test",
2243
- "typecheck",
2244
- "debug",
2245
- "error",
2246
- "issue",
2247
- "problem",
2248
- "fails",
2249
- "failed",
2250
- "failing",
2251
- "bug",
2252
- "crash",
2253
- "slow",
2254
- "slowness",
2255
- "not working",
2256
- "epic",
2257
- "ticket"
2258
- ]);
2259
- const hasReadIntent = includesAny(normalizedText, [
2260
- "read",
2261
- "list",
2262
- "find",
2263
- "search",
2264
- "grep",
2265
- "glob",
2266
- "inspect",
2267
- "analyse",
2268
- "analyze",
2269
- "check"
2270
- ]);
2271
- const hasWriteIntent = mode === "build" && includesAny(normalizedText, [
2272
- "write",
2273
- "edit",
2274
- "change",
2275
- "create",
2276
- "implement",
2277
- "fix",
2278
- "update",
2279
- "delete",
2280
- "remove",
2281
- "rename"
2282
- ]);
2283
- const hasShellIntent = mode === "build" && includesAny(normalizedText, [
2284
- "run",
2285
- "command",
2286
- "terminal",
2287
- "shell",
2288
- "bash",
2289
- "powershell",
2290
- "bun",
2291
- "tests",
2292
- "run test",
2293
- "run tests",
2294
- "typecheck"
2295
- ]);
2229
+ const situational = [];
2296
2230
  const hasGitIntent = includesAny(normalizedText, [
2297
2231
  "git",
2298
2232
  "diff",
@@ -2303,95 +2237,64 @@ function selectCodingAgentIntentTools({
2303
2237
  "revision",
2304
2238
  "changes"
2305
2239
  ]);
2306
- const hasTodoIntent = includesAny(normalizedText, [
2307
- "todo",
2308
- "tasks",
2309
- "checklist",
2310
- "steps",
2311
- "epic",
2312
- "ticket"
2313
- ]);
2314
- const hasWebIntent = includesAny(normalizedText, [
2315
- "http://",
2316
- "https://",
2317
- "url",
2318
- "fetch",
2319
- "website",
2320
- "web",
2321
- "online",
2322
- "search internet",
2323
- "latest"
2324
- ]);
2325
- const explicitToolSearch = includesAny(normalizedText, [
2326
- "tool_search",
2327
- "tool search",
2328
- "find tool",
2329
- "available tool"
2330
- ]);
2331
- const hasSkillIntent = includesAny(normalizedText, [
2332
- "skill",
2333
- "skills",
2334
- "load skill"
2335
- ]);
2336
- const hasMcpIntent = includesAny(normalizedText, [
2337
- "mcp",
2338
- "resource",
2339
- "resources",
2340
- "mcp tool",
2341
- "mcp server"
2342
- ]);
2343
- if (hasCodeIntent || hasReadIntent && !hasGitIntent && !hasWebIntent && !explicitToolSearch) {
2344
- for (const toolName of baseReadTools) {
2345
- pushUniqueTool(selectedTools, toolName);
2346
- }
2347
- }
2348
2240
  if (hasGitIntent) {
2349
2241
  if (includesAny(normalizedText, ["status", "state", "dirty", "working tree"]) || !includesAny(normalizedText, ["diff", "log", "show", "history", "commit"])) {
2350
- pushUniqueTool(selectedTools, "git_status");
2242
+ pushUniqueTool(situational, "git_status");
2351
2243
  }
2352
2244
  if (includesAny(normalizedText, ["diff", "changes", "patch"])) {
2353
- pushUniqueTool(selectedTools, "git_diff");
2245
+ pushUniqueTool(situational, "git_diff");
2354
2246
  }
2355
2247
  if (includesAny(normalizedText, ["log", "history", "commits"])) {
2356
- pushUniqueTool(selectedTools, "git_log");
2248
+ pushUniqueTool(situational, "git_log");
2357
2249
  }
2358
2250
  if (includesAny(normalizedText, ["show", "revision", "commit"])) {
2359
- pushUniqueTool(selectedTools, "git_show");
2251
+ pushUniqueTool(situational, "git_show");
2360
2252
  }
2361
2253
  }
2362
- if (hasTodoIntent && mode === "build") {
2363
- pushUniqueTool(selectedTools, "todo_write");
2254
+ if (mode === "build" && includesAny(normalizedText, ["todo", "tasks", "checklist", "steps", "epic", "ticket"])) {
2255
+ pushUniqueTool(situational, "todo_write");
2364
2256
  }
2365
- if (explicitToolSearch) {
2366
- pushUniqueTool(selectedTools, "tool_search");
2257
+ if (includesAny(normalizedText, ["tool_search", "tool search", "find tool", "available tool"])) {
2258
+ pushUniqueTool(situational, "tool_search");
2367
2259
  }
2368
- if (hasSkillIntent) {
2369
- pushUniqueTool(selectedTools, "skill");
2260
+ if (includesAny(normalizedText, ["skill", "skills", "load skill"])) {
2261
+ pushUniqueTool(situational, "skill");
2370
2262
  }
2371
- if (hasMcpIntent) {
2372
- pushUniqueTool(selectedTools, "list_mcp_resources");
2263
+ if (includesAny(normalizedText, ["mcp", "resource", "resources", "mcp tool", "mcp server"])) {
2264
+ pushUniqueTool(situational, "list_mcp_resources");
2373
2265
  if (includesAny(normalizedText, ["read", "resource", "resources"])) {
2374
- pushUniqueTool(selectedTools, "read_mcp_resource");
2266
+ pushUniqueTool(situational, "read_mcp_resource");
2375
2267
  }
2376
2268
  if (mode === "build" && includesAny(normalizedText, ["call", "execute", "run"])) {
2377
- pushUniqueTool(selectedTools, "call_mcp_tool");
2269
+ pushUniqueTool(situational, "call_mcp_tool");
2378
2270
  }
2379
2271
  }
2380
- if (hasWriteIntent) {
2381
- pushUniqueTool(selectedTools, "write_file");
2382
- pushUniqueTool(selectedTools, "edit_file");
2272
+ if (includesAny(normalizedText, [
2273
+ "http://",
2274
+ "https://",
2275
+ "url",
2276
+ "fetch",
2277
+ "website",
2278
+ "web",
2279
+ "online",
2280
+ "search internet",
2281
+ "latest"
2282
+ ])) {
2283
+ pushUniqueTool(situational, "web_fetch");
2284
+ pushUniqueTool(situational, "web_search");
2383
2285
  }
2384
- if (hasShellIntent) {
2385
- pushUniqueTool(selectedTools, "bash");
2286
+ if (mode === "plan" && includesAny(normalizedText, ["question", "ask", "clarify"])) {
2287
+ pushUniqueTool(situational, "request_user_input");
2386
2288
  }
2387
- if (hasWebIntent) {
2388
- pushUniqueTool(selectedTools, "web_fetch");
2389
- pushUniqueTool(selectedTools, "web_search");
2289
+ const ordered = [...baseReadTools, ...situational];
2290
+ if (mode === "build") {
2291
+ ordered.push("write_file", "edit_file", "bash");
2390
2292
  }
2391
- if (mode === "plan" && includesAny(normalizedText, ["question", "ask", "clarify"])) {
2392
- pushUniqueTool(selectedTools, "request_user_input");
2293
+ const selected = [];
2294
+ for (const toolName of ordered) {
2295
+ pushUniqueTool(selected, toolName);
2393
2296
  }
2394
- return selectedTools;
2297
+ return selected.slice(0, maxProviderActiveTools);
2395
2298
  }
2396
2299
  function limitProviderActiveTools(tools, maxTools = maxProviderActiveTools) {
2397
2300
  if (tools.length <= maxTools) {
@@ -2594,7 +2497,8 @@ function createCodingAgent({
2594
2497
  cwd: options.cwd,
2595
2498
  override: promptOverride,
2596
2499
  mode,
2597
- includeToolDiscipline
2500
+ includeToolDiscipline,
2501
+ environmentContext: options.environmentContext
2598
2502
  })
2599
2503
  };
2600
2504
  }
@@ -2661,17 +2565,33 @@ function normalizeContextOptimizerConfig(config) {
2661
2565
  }
2662
2566
  function resolveContextWindowTokens({
2663
2567
  config,
2664
- modelContextWindow
2568
+ modelContextWindow,
2569
+ endpointLimitTokens
2570
+ }) {
2571
+ const resolved = config.contextWindowOverride ?? modelContextWindow ?? DEFAULT_CONTEXT_WINDOW_TOKENS;
2572
+ if (typeof endpointLimitTokens === "number" && Number.isFinite(endpointLimitTokens) && endpointLimitTokens > 0) {
2573
+ return Math.min(resolved, endpointLimitTokens);
2574
+ }
2575
+ return resolved;
2576
+ }
2577
+ function resolveInputBudgetTokens({
2578
+ contextWindow,
2579
+ reservedOutputTokens,
2580
+ safetyFraction = DEFAULT_INPUT_BUDGET_SAFETY_FRACTION
2665
2581
  }) {
2666
- return config.contextWindowOverride ?? modelContextWindow ?? DEFAULT_CONTEXT_WINDOW_TOKENS;
2582
+ const reserved = typeof reservedOutputTokens === "number" && Number.isFinite(reservedOutputTokens) && reservedOutputTokens > 0 ? reservedOutputTokens : 0;
2583
+ const clampedFraction = Math.min(1, Math.max(0.1, safetyFraction));
2584
+ const usable = Math.max(0, contextWindow - reserved);
2585
+ return Math.max(0, Math.floor(usable * clampedFraction));
2667
2586
  }
2668
- var DEFAULT_CONTEXT_WINDOW_TOKENS = 128000, contextOptimizerConfigSchema, resolvedContextOptimizerConfigSchema, defaultContextOptimizerConfig;
2587
+ var DEFAULT_CONTEXT_WINDOW_TOKENS = 128000, contextOptimizerConfigSchema, resolvedContextOptimizerConfigSchema, defaultContextOptimizerConfig, DEFAULT_INPUT_BUDGET_SAFETY_FRACTION = 0.92;
2669
2588
  var init_config3 = __esm(() => {
2670
2589
  contextOptimizerConfigSchema = z26.object({
2671
2590
  autoCompact: z26.boolean().optional(),
2672
2591
  compactAtFraction: z26.number().min(0.3).max(0.95).optional(),
2673
2592
  pruneAtFraction: z26.number().min(0.2).max(0.9).optional(),
2674
2593
  preserveRecentMessages: z26.number().int().min(2).max(100).optional(),
2594
+ maxCoverageTokensPerCompaction: z26.number().int().min(2000).max(200000).optional(),
2675
2595
  contextWindowOverride: z26.number().int().min(8000).max(1e7).optional(),
2676
2596
  summaryMaxChars: z26.number().int().min(200).max(50000).optional(),
2677
2597
  aggressivePruneWhenUncached: z26.boolean().optional(),
@@ -2685,6 +2605,7 @@ var init_config3 = __esm(() => {
2685
2605
  compactAtFraction: z26.number().min(0.3).max(0.95),
2686
2606
  pruneAtFraction: z26.number().min(0.2).max(0.9),
2687
2607
  preserveRecentMessages: z26.number().int().min(2).max(100),
2608
+ maxCoverageTokensPerCompaction: z26.number().int().min(2000).max(200000),
2688
2609
  contextWindowOverride: z26.number().int().min(8000).max(1e7).nullable(),
2689
2610
  summaryMaxChars: z26.number().int().min(200).max(50000),
2690
2611
  aggressivePruneWhenUncached: z26.boolean(),
@@ -2697,6 +2618,7 @@ var init_config3 = __esm(() => {
2697
2618
  compactAtFraction: 0.7,
2698
2619
  pruneAtFraction: 0.6,
2699
2620
  preserveRecentMessages: 6,
2621
+ maxCoverageTokensPerCompaction: 12000,
2700
2622
  contextWindowOverride: null,
2701
2623
  summaryMaxChars: 4000,
2702
2624
  aggressivePruneWhenUncached: true,
@@ -2776,10 +2698,44 @@ var init_estimate = __esm(() => {
2776
2698
  totalTokens: z27.number().nonnegative().optional()
2777
2699
  }),
2778
2700
  modelId: z27.string().optional(),
2779
- finishReason: languageModelFinishReasonSchema.optional()
2701
+ finishReason: languageModelFinishReasonSchema.optional(),
2702
+ context: z27.object({
2703
+ inputTokens: z27.number().nonnegative(),
2704
+ contextWindow: z27.number().positive(),
2705
+ compactedMessages: z27.number().nonnegative().optional()
2706
+ }).optional()
2780
2707
  }).loose();
2781
2708
  });
2782
2709
 
2710
+ // packages/ai/src/common/output-utils.ts
2711
+ function omissionMarker(omitted) {
2712
+ return `
2713
+
2714
+ ... [${omitted} characters omitted; re-read a line range with read_file or raise maxChars] ...
2715
+
2716
+ `;
2717
+ }
2718
+ function truncateText(text, maxChars = DEFAULT_TOOL_TEXT_OUTPUT_CHARS) {
2719
+ if (text.length <= maxChars) {
2720
+ return {
2721
+ text,
2722
+ truncated: false
2723
+ };
2724
+ }
2725
+ const markerReserve = omissionMarker(text.length).length;
2726
+ const usable = Math.max(0, maxChars - markerReserve);
2727
+ const headChars = Math.floor(usable * 0.7);
2728
+ const tailChars = usable - headChars;
2729
+ const head = text.slice(0, headChars);
2730
+ const tail = tailChars > 0 ? text.slice(text.length - tailChars) : "";
2731
+ const omitted = text.length - head.length - tail.length;
2732
+ return {
2733
+ text: `${head}${omissionMarker(omitted)}${tail}`,
2734
+ truncated: true
2735
+ };
2736
+ }
2737
+ var init_output_utils = () => {};
2738
+
2783
2739
  // packages/ai/src/context/heuristic-summary.ts
2784
2740
  function truncateInline(text, maxChars) {
2785
2741
  const normalized = text.replace(/\s+/g, " ").trim();
@@ -3129,6 +3085,127 @@ var init_tier1_prune = __esm(() => {
3129
3085
  init_message_parts();
3130
3086
  });
3131
3087
 
3088
+ // packages/ai/src/context/fit-to-budget.ts
3089
+ function estimateStructuralTokens(messages) {
3090
+ return messages.reduce((total, message) => total + estimateMessageTokens(message), 0);
3091
+ }
3092
+ function getPartState2(part) {
3093
+ const state = isRecord3(part) ? Reflect.get(part, "state") : undefined;
3094
+ return typeof state === "string" ? state : null;
3095
+ }
3096
+ function elideOutput(part, originalChars) {
3097
+ const stub = {
3098
+ lightcodeElided: true,
3099
+ note: "Tool output elided to fit the model's context budget. Re-run the tool if this output is needed again.",
3100
+ originalChars
3101
+ };
3102
+ return { ...part, output: stub };
3103
+ }
3104
+ function elideToolOutputsInMessage(message) {
3105
+ let elided = 0;
3106
+ const parts = message.parts.map((part) => {
3107
+ if (!getToolNameFromPart(part) || getPartState2(part) !== "output-available") {
3108
+ return part;
3109
+ }
3110
+ const output = isRecord3(part) ? Reflect.get(part, "output") : undefined;
3111
+ if (output === undefined || isElidedToolOutput(output)) {
3112
+ return part;
3113
+ }
3114
+ elided += 1;
3115
+ return elideOutput(part, safeStringify(output).length);
3116
+ });
3117
+ return elided > 0 ? { message: { ...message, parts }, elided } : { message, elided: 0 };
3118
+ }
3119
+ function truncateTextPartsInMessage(message, capChars) {
3120
+ let truncated = 0;
3121
+ const parts = message.parts.map((part) => {
3122
+ if (!isRecord3(part)) {
3123
+ return part;
3124
+ }
3125
+ const text = Reflect.get(part, "text");
3126
+ if (typeof text !== "string" || text.length <= capChars) {
3127
+ return part;
3128
+ }
3129
+ const result = truncateText(text, capChars);
3130
+ if (!result.truncated) {
3131
+ return part;
3132
+ }
3133
+ truncated += 1;
3134
+ return { ...part, text: result.text };
3135
+ });
3136
+ return truncated > 0 ? { message: { ...message, parts }, truncated } : { message, truncated: 0 };
3137
+ }
3138
+ function fitMessagesToBudget(messages, options) {
3139
+ const budget = Math.max(0, Math.floor(options.inputBudgetTokens));
3140
+ const preserveRecentMessages = Math.max(0, options.preserveRecentMessages);
3141
+ let working = [...messages];
3142
+ let elidedToolOutputs = 0;
3143
+ let droppedMessages = 0;
3144
+ let truncatedTextParts = 0;
3145
+ const fits = () => estimateStructuralTokens(working) <= budget;
3146
+ if (fits()) {
3147
+ return {
3148
+ messages: working,
3149
+ elidedToolOutputs: 0,
3150
+ droppedMessages: 0,
3151
+ truncatedTextParts: 0,
3152
+ estimatedTokens: estimateStructuralTokens(working),
3153
+ fitted: false,
3154
+ withinBudget: true
3155
+ };
3156
+ }
3157
+ const preserveFrom = Math.max(0, working.length - preserveRecentMessages);
3158
+ for (let index = 0;index < preserveFrom && !fits(); index += 1) {
3159
+ const { message, elided } = elideToolOutputsInMessage(working[index]);
3160
+ if (elided > 0) {
3161
+ working[index] = message;
3162
+ elidedToolOutputs += elided;
3163
+ }
3164
+ }
3165
+ const firstIsSummary = isSummaryMessage(working[0]);
3166
+ let dropFrom = firstIsSummary ? 1 : 0;
3167
+ while (!fits() && working.length - dropFrom > preserveRecentMessages && dropFrom < working.length) {
3168
+ working.splice(dropFrom, 1);
3169
+ droppedMessages += 1;
3170
+ }
3171
+ if (!fits()) {
3172
+ working = working.map((message) => {
3173
+ const { message: elidedMessage, elided } = elideToolOutputsInMessage(message);
3174
+ elidedToolOutputs += elided;
3175
+ return elidedMessage;
3176
+ });
3177
+ let cap = MAX_TOOL_TEXT_OUTPUT_CHARS;
3178
+ while (!fits() && cap >= MIN_TRUNCATION_CAP_CHARS) {
3179
+ working = working.map((message) => {
3180
+ const { message: truncatedMessage, truncated } = truncateTextPartsInMessage(message, cap);
3181
+ truncatedTextParts += truncated;
3182
+ return truncatedMessage;
3183
+ });
3184
+ cap = Math.floor(cap / 2);
3185
+ }
3186
+ }
3187
+ return {
3188
+ messages: working,
3189
+ elidedToolOutputs,
3190
+ droppedMessages,
3191
+ truncatedTextParts,
3192
+ estimatedTokens: estimateStructuralTokens(working),
3193
+ fitted: elidedToolOutputs > 0 || droppedMessages > 0 || truncatedTextParts > 0,
3194
+ withinBudget: fits()
3195
+ };
3196
+ }
3197
+ function isSummaryMessage(message) {
3198
+ return message?.id === contextSummaryMessageId;
3199
+ }
3200
+ var MIN_TRUNCATION_CAP_CHARS = 256;
3201
+ var init_fit_to_budget = __esm(() => {
3202
+ init_output_utils();
3203
+ init_estimate();
3204
+ init_heuristic_summary();
3205
+ init_message_parts();
3206
+ init_tier1_prune();
3207
+ });
3208
+
3132
3209
  // packages/ai/src/context/build-provider-view.ts
3133
3210
  function computeCompactionCutoffIndex(messages, preserveRecentMessages, afterAnchorIndex) {
3134
3211
  const candidate = messages.length - preserveRecentMessages;
@@ -3142,11 +3219,32 @@ function computeCompactionCutoffIndex(messages, preserveRecentMessages, afterAnc
3142
3219
  }
3143
3220
  return afterAnchorIndex;
3144
3221
  }
3222
+ function capCoverageToTokenBudget(coveredMessages, maxTokens) {
3223
+ if (coveredMessages.length <= 1) {
3224
+ return [...coveredMessages];
3225
+ }
3226
+ let total = 0;
3227
+ const capped = [];
3228
+ for (const message of coveredMessages) {
3229
+ const messageTokens = estimateStructuralTokens([message]);
3230
+ if (capped.length > 0 && total + messageTokens > maxTokens) {
3231
+ break;
3232
+ }
3233
+ capped.push(message);
3234
+ total += messageTokens;
3235
+ }
3236
+ return capped;
3237
+ }
3145
3238
  function buildProviderView(options) {
3146
3239
  const { messages, contextState, config } = options;
3147
3240
  const contextWindow = resolveContextWindowTokens({
3148
3241
  config,
3149
- modelContextWindow: options.modelContextWindow
3242
+ modelContextWindow: options.modelContextWindow,
3243
+ endpointLimitTokens: options.endpointLimitTokens
3244
+ });
3245
+ const inputBudgetTokens = resolveInputBudgetTokens({
3246
+ contextWindow,
3247
+ reservedOutputTokens: options.reservedOutputTokens
3150
3248
  });
3151
3249
  let anchorResolved = false;
3152
3250
  let afterAnchorIndex = 0;
@@ -3170,7 +3268,8 @@ function buildProviderView(options) {
3170
3268
  let tier1 = null;
3171
3269
  const uncachedActive = options.cacheActive === false && config.aggressivePruneWhenUncached;
3172
3270
  const pruneFraction = uncachedActive ? config.uncachedPruneAtFraction : config.pruneAtFraction;
3173
- if (estimate.tokens > pruneFraction * contextWindow) {
3271
+ const triggerTokens = (currentEstimateTokens) => Math.max(currentEstimateTokens, estimateStructuralTokens(providerMessages), typeof options.measuredInputTokens === "number" ? options.measuredInputTokens : 0);
3272
+ if (triggerTokens(estimate.tokens) > pruneFraction * inputBudgetTokens) {
3174
3273
  tier1 = pruneToolOutputs(providerMessages, {
3175
3274
  preserveRecentMessages: config.preserveRecentMessages,
3176
3275
  minOutputChars: uncachedActive ? config.uncachedPruneMinOutputChars : undefined,
@@ -3185,15 +3284,15 @@ function buildProviderView(options) {
3185
3284
  }
3186
3285
  }
3187
3286
  const compactionCutoffIndex = computeCompactionCutoffIndex(messages, config.preserveRecentMessages, afterAnchorIndex);
3188
- const coveredMessages = messages.slice(afterAnchorIndex, compactionCutoffIndex);
3189
- const overCompactThreshold = estimate.tokens > config.compactAtFraction * contextWindow;
3287
+ const coveredMessages = capCoverageToTokenBudget(messages.slice(afterAnchorIndex, compactionCutoffIndex), config.maxCoverageTokensPerCompaction);
3288
+ const overCompactThreshold = triggerTokens(estimate.tokens) > config.compactAtFraction * inputBudgetTokens;
3190
3289
  let compactionBlockedReason = null;
3191
3290
  if (overCompactThreshold) {
3192
3291
  if (coveredMessages.length === 0) {
3193
3292
  compactionBlockedReason = "not_enough_messages";
3194
3293
  } else if ((options.pendingInteractionCount ?? 0) > 0) {
3195
3294
  compactionBlockedReason = "pending_interactions";
3196
- } else if (hasUnresolvedToolWork(messages)) {
3295
+ } else if (hasUnresolvedToolWork(coveredMessages)) {
3197
3296
  compactionBlockedReason = "unresolved_tool_work";
3198
3297
  }
3199
3298
  }
@@ -3201,6 +3300,7 @@ function buildProviderView(options) {
3201
3300
  providerMessages,
3202
3301
  estimate,
3203
3302
  contextWindow,
3303
+ inputBudgetTokens,
3204
3304
  needsCompaction: overCompactThreshold && compactionBlockedReason === null,
3205
3305
  compactionBlockedReason,
3206
3306
  anchorResolved,
@@ -3212,6 +3312,7 @@ function buildProviderView(options) {
3212
3312
  var init_build_provider_view = __esm(() => {
3213
3313
  init_config3();
3214
3314
  init_estimate();
3315
+ init_fit_to_budget();
3215
3316
  init_heuristic_summary();
3216
3317
  init_message_parts();
3217
3318
  init_tier1_prune();
@@ -3257,6 +3358,7 @@ var init_context = __esm(() => {
3257
3358
  init_config3();
3258
3359
  init_context_state();
3259
3360
  init_estimate();
3361
+ init_fit_to_budget();
3260
3362
  init_heuristic_summary();
3261
3363
  init_message_parts();
3262
3364
  init_normalize_provider_messages();
@@ -3486,8 +3588,8 @@ var init_lightcode_config = __esm(() => {
3486
3588
  defaultAutoContinueConfig = {
3487
3589
  enabled: true,
3488
3590
  maxAutoContinues: 50,
3489
- maxErrorRetries: 5,
3490
- stallTimeoutSeconds: 180
3591
+ maxErrorRetries: 8,
3592
+ stallTimeoutSeconds: 300
3491
3593
  };
3492
3594
  headroomConfigSchema = z28.object({
3493
3595
  enabled: z28.boolean().optional(),
@@ -3530,7 +3632,7 @@ var init_lightcode_config = __esm(() => {
3530
3632
  context: defaultContextOptimizerConfig,
3531
3633
  maxOutputTokens: 32768,
3532
3634
  maxSteps: 30,
3533
- maxRetries: 5,
3635
+ maxRetries: 3,
3534
3636
  autoContinue: defaultAutoContinueConfig,
3535
3637
  headroom: defaultHeadroomConfig
3536
3638
  };
@@ -4044,6 +4146,10 @@ async function extractToolCalls(text, knownToolNames) {
4044
4146
  while ((match = DEEPSEEK_WRAPPER_RE.exec(text)) !== null) {
4045
4147
  segments.push({ start: match.index, end: match.index + match[0].length, call: null });
4046
4148
  }
4149
+ FUNCTION_CALLS_WRAPPER_RE.lastIndex = 0;
4150
+ while ((match = FUNCTION_CALLS_WRAPPER_RE.exec(text)) !== null) {
4151
+ segments.push({ start: match.index, end: match.index + match[0].length, call: null });
4152
+ }
4047
4153
  const covered = (idx) => segments.some((s) => idx >= s.start && idx < s.end);
4048
4154
  const orphanIdx = findOrphanMarker(text, covered);
4049
4155
  if (orphanIdx !== -1) {
@@ -4131,7 +4237,7 @@ async function parseBareTrailingJson(text, knownToolNames) {
4131
4237
  return null;
4132
4238
  return { start: idx, payload };
4133
4239
  }
4134
- var TOOL_CALL_OPEN_MARKERS, MAX_MARKER_LENGTH, MARKER_CONFIRMATION_WINDOW = 256, BLOCK_CLOSE_MARKERS, CLOSED_BLOCK_RES, DEEPSEEK_WRAPPER_RE, PARAMETER_TAG_RE, PARAMETER_EQ_RE;
4240
+ var TOOL_CALL_OPEN_MARKERS, MAX_MARKER_LENGTH, MARKER_CONFIRMATION_WINDOW = 256, BLOCK_CLOSE_MARKERS, CLOSED_BLOCK_RES, DEEPSEEK_WRAPPER_RE, FUNCTION_CALLS_WRAPPER_RE, PARAMETER_TAG_RE, PARAMETER_EQ_RE;
4135
4241
  var init_xml_tool_call_parser = __esm(() => {
4136
4242
  init_tool_call_json_repair();
4137
4243
  TOOL_CALL_OPEN_MARKERS = [
@@ -4158,6 +4264,7 @@ var init_xml_tool_call_parser = __esm(() => {
4158
4264
  deepseek: /<\uFF5Ctool\u2581call\u2581begin\uFF5C>\s*(?:function)?\s*(?:<\uFF5Ctool\u2581sep\uFF5C>)?\s*([\w.-]+)\s*\n?([\s\S]*?)<\uFF5Ctool\u2581call\u2581end\uFF5C>/g
4159
4265
  };
4160
4266
  DEEPSEEK_WRAPPER_RE = /<\uFF5Ctool\u2581calls\u2581(?:begin|end)\uFF5C>/g;
4267
+ FUNCTION_CALLS_WRAPPER_RE = /<\/?function_calls\s*>/gi;
4161
4268
  PARAMETER_TAG_RE = /<parameter\s+name=["']?([\w.-]+)["']?\s*>([\s\S]*?)(?:<\/parameter>|(?=<parameter\s)|$)/gi;
4162
4269
  PARAMETER_EQ_RE = /<parameter=([\w.-]+)>([\s\S]*?)(?=<parameter=|<\/function>|$)/gi;
4163
4270
  });
@@ -5046,6 +5153,14 @@ function isProviderBillingOrQuotaError(error) {
5046
5153
  function isContextOverflowMessage(message) {
5047
5154
  return contextOverflowPattern.test(message);
5048
5155
  }
5156
+ function extractContextLimitTokens(message) {
5157
+ const match = contextLimitPattern.exec(message);
5158
+ if (!match) {
5159
+ return;
5160
+ }
5161
+ const value = Number.parseInt(match[1], 10);
5162
+ return Number.isFinite(value) && value > 0 ? value : undefined;
5163
+ }
5049
5164
  function isAbortError(error) {
5050
5165
  return error instanceof Error && error.name === "AbortError" || normalizeErrorMessage(getErrorMessage(error)).includes("aborted");
5051
5166
  }
@@ -5086,7 +5201,8 @@ function classifyChatError(error) {
5086
5201
  kind,
5087
5202
  statusCode: error.statusCode,
5088
5203
  retryable,
5089
- message: message2
5204
+ message: message2,
5205
+ contextLimitTokens: kind === "context_overflow" ? extractContextLimitTokens(message2) : undefined
5090
5206
  };
5091
5207
  }
5092
5208
  return {
@@ -5100,7 +5216,12 @@ function classifyChatError(error) {
5100
5216
  return { kind: "aborted", retryable: false, message };
5101
5217
  }
5102
5218
  if (isContextOverflowMessage(message)) {
5103
- return { kind: "context_overflow", retryable: true, message };
5219
+ return {
5220
+ kind: "context_overflow",
5221
+ retryable: true,
5222
+ message,
5223
+ contextLimitTokens: extractContextLimitTokens(message)
5224
+ };
5104
5225
  }
5105
5226
  if (isProviderBillingOrQuotaError(error)) {
5106
5227
  return { kind: "billing", retryable: false, message };
@@ -5166,7 +5287,7 @@ function logChatDisconnectEvent({
5166
5287
  message: getErrorMessage(error)
5167
5288
  });
5168
5289
  }
5169
- var chatFailureCounters, logger2, retryableStatusCodes, contextOverflowPattern;
5290
+ var chatFailureCounters, logger2, retryableStatusCodes, contextOverflowPattern, contextLimitPattern;
5170
5291
  var init_chat_observability = __esm(() => {
5171
5292
  init_src();
5172
5293
  chatFailureCounters = {
@@ -5185,6 +5306,7 @@ var init_chat_observability = __esm(() => {
5185
5306
  logger2 = createLogger("chat");
5186
5307
  retryableStatusCodes = new Set([408, 409, 429, 500, 502, 503, 504]);
5187
5308
  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;
5309
+ contextLimitPattern = /(?:maximum context (?:length|window)|max(?:imum)? input (?:length|tokens)|context (?:length|window) of)\D{0,40}?(\d{4,})/i;
5188
5310
  });
5189
5311
 
5190
5312
  // packages/db/generated/prisma/internal/class.ts
@@ -6080,9 +6202,6 @@ var init_resolve_within_workspace = __esm(() => {
6080
6202
  WORKSPACE = defaultWorkspaceContext.root;
6081
6203
  });
6082
6204
 
6083
- // packages/ai/src/common/output-utils.ts
6084
- var init_output_utils = () => {};
6085
-
6086
6205
  // packages/ai/src/bash/runtime.ts
6087
6206
  import { exec } from "child_process";
6088
6207
  import { promisify } from "util";
@@ -6118,11 +6237,15 @@ var init_runtime5 = __esm(() => {
6118
6237
  import { createTwoFilesPatch } from "diff";
6119
6238
  var init_unified_diff = () => {};
6120
6239
 
6240
+ // packages/ai/src/edit-file/match.ts
6241
+ var init_match = () => {};
6242
+
6121
6243
  // packages/ai/src/edit-file/runtime.ts
6122
6244
  var init_runtime6 = __esm(() => {
6123
6245
  init_runtime5();
6124
6246
  init_resolve_within_workspace();
6125
6247
  init_unified_diff();
6248
+ init_match();
6126
6249
  init_schema2();
6127
6250
  });
6128
6251
 
@@ -6169,6 +6292,112 @@ var init_runtime8 = __esm(() => {
6169
6292
  // packages/ai/src/git/runtime.ts
6170
6293
  import { execFile as execFile2 } from "child_process";
6171
6294
  import { promisify as promisify3 } from "util";
6295
+ function isRecord7(value) {
6296
+ return typeof value === "object" && value !== null;
6297
+ }
6298
+ function getStringProperty(record, key) {
6299
+ const value = record[key];
6300
+ return typeof value === "string" ? value : "";
6301
+ }
6302
+ function getExitCode(error) {
6303
+ const code = error.code;
6304
+ return typeof code === "number" ? code : null;
6305
+ }
6306
+ async function runGit(args, workspaceContext, maxBuffer = 10 * 1024 * 1024) {
6307
+ try {
6308
+ const result = await execFileAsync2("git", args, {
6309
+ cwd: workspaceContext.root,
6310
+ maxBuffer,
6311
+ windowsHide: true
6312
+ });
6313
+ return {
6314
+ ok: true,
6315
+ stdout: result.stdout,
6316
+ stderr: result.stderr,
6317
+ exitCode: 0
6318
+ };
6319
+ } catch (error) {
6320
+ const record = isRecord7(error) ? error : {};
6321
+ const code = record.code;
6322
+ const codeText = typeof code === "string" ? code : "git_failed";
6323
+ const message = getStringProperty(record, "message") || "Git command failed.";
6324
+ return {
6325
+ ok: false,
6326
+ error: {
6327
+ code: codeText,
6328
+ message,
6329
+ exitCode: getExitCode(record),
6330
+ stdout: getStringProperty(record, "stdout"),
6331
+ stderr: getStringProperty(record, "stderr")
6332
+ }
6333
+ };
6334
+ }
6335
+ }
6336
+ function parseBranchLine(line) {
6337
+ const branchLine = line.replace(/^##\s*/, "");
6338
+ const [branchSegment, trackingSegment] = branchLine.split("...");
6339
+ const branch = branchSegment && branchSegment !== "No commits yet on " ? branchSegment.replace(/^No commits yet on\s+/, "") : null;
6340
+ const trackingMatch = trackingSegment?.match(/^([^\s]+)(?:\s+\[(.*)\])?/);
6341
+ const upstream = trackingMatch?.[1] ?? null;
6342
+ const trackingDetails = trackingMatch?.[2] ?? "";
6343
+ const aheadMatch = trackingDetails.match(/ahead\s+(\d+)/);
6344
+ const behindMatch = trackingDetails.match(/behind\s+(\d+)/);
6345
+ return {
6346
+ branch,
6347
+ upstream,
6348
+ ahead: aheadMatch ? Number(aheadMatch[1]) : 0,
6349
+ behind: behindMatch ? Number(behindMatch[1]) : 0
6350
+ };
6351
+ }
6352
+ function parseStatusEntries(stdout, maxEntries) {
6353
+ const lines = stdout.split(/\r?\n/).filter((line) => line.length > 0);
6354
+ const branchInfo = lines[0]?.startsWith("##") ? parseBranchLine(lines[0]) : { branch: null, upstream: null, ahead: 0, behind: 0 };
6355
+ const statusLines = lines.filter((line) => !line.startsWith("##"));
6356
+ const entries = statusLines.slice(0, maxEntries).map((line) => {
6357
+ const indexStatus = line[0] === " " ? "" : line[0] ?? "";
6358
+ const workingTreeStatus = line[1] === " " ? "" : line[1] ?? "";
6359
+ const rawPath = line.slice(3);
6360
+ const renameParts = rawPath.split(" -> ");
6361
+ const originalPath = renameParts.length > 1 ? renameParts[0] : null;
6362
+ const filePath = renameParts.length > 1 ? renameParts[1] : rawPath;
6363
+ return {
6364
+ path: filePath,
6365
+ indexStatus,
6366
+ workingTreeStatus,
6367
+ originalPath
6368
+ };
6369
+ });
6370
+ return {
6371
+ ...branchInfo,
6372
+ entries,
6373
+ totalEntries: statusLines.length,
6374
+ truncated: statusLines.length > entries.length
6375
+ };
6376
+ }
6377
+ async function executeGitStatus(input, workspaceContext = getDefaultWorkspaceContext()) {
6378
+ const parsedInput = gitStatusInputSchema.parse(input);
6379
+ const result = await runGit(["status", "--porcelain=v1", "-b", "--untracked-files=all", "--", "."], workspaceContext);
6380
+ if (!result.ok) {
6381
+ return gitStatusOutputSchema.parse({
6382
+ ok: false,
6383
+ cwd: workspaceContext.root,
6384
+ branch: null,
6385
+ upstream: null,
6386
+ ahead: 0,
6387
+ behind: 0,
6388
+ entries: [],
6389
+ totalEntries: 0,
6390
+ truncated: false,
6391
+ error: result.error
6392
+ });
6393
+ }
6394
+ return gitStatusOutputSchema.parse({
6395
+ ok: true,
6396
+ cwd: workspaceContext.root,
6397
+ ...parseStatusEntries(result.stdout, parsedInput.maxEntries),
6398
+ error: null
6399
+ });
6400
+ }
6172
6401
  var execFileAsync2;
6173
6402
  var init_runtime9 = __esm(() => {
6174
6403
  init_resolve_within_workspace();
@@ -6563,6 +6792,24 @@ var init_overflow_recovery = __esm(() => {
6563
6792
  overflowRecoveryRounds = new Map;
6564
6793
  });
6565
6794
 
6795
+ // apps/server/src/lib/learned-context-limit.ts
6796
+ function noteLearnedContextLimit(modelId, limitTokens) {
6797
+ if (!modelId || typeof limitTokens !== "number" || !Number.isFinite(limitTokens) || limitTokens <= 0) {
6798
+ return;
6799
+ }
6800
+ const existing = learnedLimits.get(modelId);
6801
+ if (existing === undefined || limitTokens < existing) {
6802
+ learnedLimits.set(modelId, Math.floor(limitTokens));
6803
+ }
6804
+ }
6805
+ function getLearnedContextLimit(modelId) {
6806
+ return learnedLimits.get(modelId) ?? null;
6807
+ }
6808
+ var learnedLimits;
6809
+ var init_learned_context_limit = __esm(() => {
6810
+ learnedLimits = new Map;
6811
+ });
6812
+
6566
6813
  // apps/server/src/lib/sse-heartbeat.ts
6567
6814
  function endsWithEventBoundary(chunk) {
6568
6815
  const length = chunk.length;
@@ -6788,6 +7035,16 @@ function getAnthropicProviderOptions(modelId) {
6788
7035
  }
6789
7036
  };
6790
7037
  }
7038
+ function getOpenRouterProviderOptions() {
7039
+ return {
7040
+ openrouter: {
7041
+ provider: {
7042
+ allow_fallbacks: true,
7043
+ require_parameters: true
7044
+ }
7045
+ }
7046
+ };
7047
+ }
6791
7048
  function resolveHeadroomRouting(config5, realBaseUrl) {
6792
7049
  const headroom = config5.headroom;
6793
7050
  if (!headroom?.enabled) {
@@ -6852,12 +7109,8 @@ function resolveOpenAITransportModel({
6852
7109
  if (isOpenRouter) {
6853
7110
  const referer = getEnvValue(env, "OPENROUTER_HTTP_REFERER");
6854
7111
  const title = getEnvValue(env, "OPENROUTER_APP_TITLE", "LIGHTCODE_APP_TITLE");
6855
- if (referer) {
6856
- headers["HTTP-Referer"] = referer;
6857
- }
6858
- if (title) {
6859
- headers["X-OpenRouter-Title"] = title;
6860
- }
7112
+ headers["HTTP-Referer"] = referer ?? "https://github.com/kmugalkhod/lightcode";
7113
+ headers["X-Title"] = title ?? "Lightcode";
6861
7114
  }
6862
7115
  const provider = createOpenAICompatible({
6863
7116
  name: config5.provider,
@@ -6874,6 +7127,7 @@ function resolveOpenAITransportModel({
6874
7127
  headroomRouted,
6875
7128
  model: withXmlToolCallSupport(provider(configuredModel)),
6876
7129
  needsToolCallDiscipline: true,
7130
+ providerOptions: isOpenRouter ? getOpenRouterProviderOptions() : undefined,
6877
7131
  contextWindow: capabilities?.contextLength ?? getModelContextWindow(config5.provider, configuredModel),
6878
7132
  missingCredentialHints: apiKey ? [] : isOpenCodeZen ? ["Set OPENCODE_API_KEY to call OpenCode Zen models."] : isOpenRouter ? ["Set OPENROUTER_API_KEY to call OpenRouter models."] : [
6879
7133
  "Set LIGHTCODE_OPENAI_COMPATIBLE_API_KEY or OPENAI_API_KEY if your OpenAI-compatible endpoint requires authentication."
@@ -6939,6 +7193,7 @@ var init_provider_registry = __esm(() => {
6939
7193
  var exports_runtime_config = {};
6940
7194
  __export(exports_runtime_config, {
6941
7195
  resolvedProviderModel: () => resolvedProviderModel,
7196
+ reloadRuntimeConfig: () => reloadRuntimeConfig,
6942
7197
  lightcodeConfigResult: () => lightcodeConfigResult,
6943
7198
  initializeModelCapabilities: () => initializeModelCapabilities,
6944
7199
  disableHeadroomRouting: () => disableHeadroomRouting,
@@ -6999,6 +7254,31 @@ function applyModelSelection({
6999
7254
  });
7000
7255
  return configStatus;
7001
7256
  }
7257
+ function reloadRuntimeConfig() {
7258
+ const nextConfigResult = loadLightcodeConfig({
7259
+ cwd: process.cwd(),
7260
+ env: Bun.env
7261
+ });
7262
+ const nextResolved = resolveConfiguredProviderModel({
7263
+ config: nextConfigResult.config,
7264
+ env: Bun.env
7265
+ });
7266
+ const nextAgent = buildCodingAgent(nextConfigResult.config, nextResolved);
7267
+ lightcodeConfigResult = nextConfigResult;
7268
+ resolvedProviderModel = nextResolved;
7269
+ chatModelId = nextResolved.resolvedModelId;
7270
+ codingAgent = nextAgent;
7271
+ configStatus = createConfigStatus({
7272
+ config: nextConfigResult.config,
7273
+ loadedFiles: nextConfigResult.loadedFiles,
7274
+ resolvedProviderModel: nextResolved
7275
+ });
7276
+ logger5.info("runtime_config_reloaded", {
7277
+ provider: nextConfigResult.config.provider,
7278
+ model: nextConfigResult.config.model
7279
+ });
7280
+ return configStatus;
7281
+ }
7002
7282
  function disableHeadroomRouting() {
7003
7283
  if (!lightcodeConfigResult.config.headroom?.enabled) {
7004
7284
  return;
@@ -7134,11 +7414,122 @@ var init_session_auto_title = __esm(() => {
7134
7414
  logger6 = createLogger("session-auto-title");
7135
7415
  });
7136
7416
 
7417
+ // apps/server/src/lib/workspace-context.ts
7418
+ import { readFile as readFile2, readdir } from "fs/promises";
7419
+ import { platform as platform3 } from "os";
7420
+ import path11 from "path";
7421
+ function describePlatform() {
7422
+ const map = {
7423
+ darwin: "macOS",
7424
+ win32: "Windows",
7425
+ linux: "Linux"
7426
+ };
7427
+ return map[platform3()] ?? platform3();
7428
+ }
7429
+ async function buildDirectoryListing(cwd) {
7430
+ try {
7431
+ const dirents = await readdir(cwd, { withFileTypes: true });
7432
+ const names = dirents.filter((entry) => entry.name !== ".git").map((entry) => entry.isDirectory() ? `${entry.name}/` : entry.name).sort((a, b) => a.localeCompare(b));
7433
+ const shown = names.slice(0, maxListedEntries);
7434
+ const suffix = names.length > shown.length ? `
7435
+ \u2026 and ${names.length - shown.length} more` : "";
7436
+ return shown.length > 0 ? shown.join(`
7437
+ `) + suffix : "(empty directory)";
7438
+ } catch (error) {
7439
+ logger7.warn("workspace_listing_failed", { cwd, error: getErrorMessage(error) });
7440
+ return "(unable to list directory)";
7441
+ }
7442
+ }
7443
+ async function buildGitSummary(cwd) {
7444
+ try {
7445
+ const context2 = createWorkspaceContext(cwd);
7446
+ const status = await executeGitStatus({}, context2);
7447
+ if (!status.ok) {
7448
+ return "Not a git repository.";
7449
+ }
7450
+ const header = [
7451
+ `branch: ${status.branch ?? "(detached)"}`,
7452
+ status.ahead || status.behind ? `ahead ${status.ahead}, behind ${status.behind}` : null
7453
+ ].filter(Boolean).join(" | ");
7454
+ if (status.totalEntries === 0) {
7455
+ return `${header}
7456
+ working tree clean`;
7457
+ }
7458
+ const lines = status.entries.slice(0, maxListedEntries).map((entry) => `${entry.indexStatus}${entry.workingTreeStatus} ${entry.path}`.trim());
7459
+ const more = status.totalEntries > lines.length ? `
7460
+ \u2026 and ${status.totalEntries - lines.length} more` : "";
7461
+ return `${header}
7462
+ changes:
7463
+ ${lines.join(`
7464
+ `)}${more}`;
7465
+ } catch (error) {
7466
+ logger7.warn("workspace_git_summary_failed", { cwd, error: getErrorMessage(error) });
7467
+ return "Not a git repository.";
7468
+ }
7469
+ }
7470
+ async function buildProjectDocs(cwd) {
7471
+ const sections = [];
7472
+ for (const name of projectDocNames) {
7473
+ try {
7474
+ const content = (await readFile2(path11.join(cwd, name), "utf8")).trim();
7475
+ if (!content) {
7476
+ continue;
7477
+ }
7478
+ const truncated = content.length > maxDocChars ? `${content.slice(0, maxDocChars)}
7479
+ \u2026 (truncated)` : content;
7480
+ sections.push(`### ${name}
7481
+ ${truncated}`);
7482
+ } catch {}
7483
+ }
7484
+ return sections.join(`
7485
+
7486
+ `);
7487
+ }
7488
+ async function buildWorkspaceContext({
7489
+ cwd
7490
+ }) {
7491
+ const [listing, git, docs] = await Promise.all([
7492
+ buildDirectoryListing(cwd),
7493
+ buildGitSummary(cwd),
7494
+ buildProjectDocs(cwd)
7495
+ ]);
7496
+ const today = new Date().toISOString().slice(0, 10);
7497
+ const parts = [
7498
+ "<environment>",
7499
+ `Working directory: ${cwd}`,
7500
+ `Platform: ${describePlatform()}`,
7501
+ `Date: ${today}`,
7502
+ "",
7503
+ "Git:",
7504
+ git,
7505
+ "",
7506
+ "Top-level entries:",
7507
+ listing,
7508
+ docs ? `
7509
+ Project instructions:
7510
+ ${docs}` : "",
7511
+ "</environment>"
7512
+ ];
7513
+ const block = parts.join(`
7514
+ `);
7515
+ return block.length > maxBlockChars ? `${block.slice(0, maxBlockChars)}
7516
+ \u2026 (environment truncated)
7517
+ </environment>` : block;
7518
+ }
7519
+ var logger7, projectDocNames, maxListedEntries = 40, maxDocChars = 1500, maxBlockChars = 8000;
7520
+ var init_workspace_context = __esm(() => {
7521
+ init_runtime17();
7522
+ init_src();
7523
+ logger7 = createLogger("workspace-context");
7524
+ projectDocNames = ["AGENTS.md", "CLAUDE.md", "README.md"];
7525
+ });
7526
+
7137
7527
  // apps/server/src/lib/chat-stream.ts
7138
7528
  import {
7139
7529
  createAgentUIStreamResponse,
7140
7530
  consumeStream,
7141
7531
  generateId as generateId3,
7532
+ isToolUIPart as isToolUIPart2,
7142
7533
  safeValidateUIMessages as safeValidateUIMessages2,
7143
7534
  streamText
7144
7535
  } from "ai";
@@ -7158,6 +7549,9 @@ function buildFastChatModelMessages(messages) {
7158
7549
  content: collectMessageText(message)
7159
7550
  })).filter((message) => message.content.length > 0).slice(-fastChatRecentMessageCount);
7160
7551
  }
7552
+ function messagesHaveToolActivity(messages) {
7553
+ return messages.some((message) => message.parts?.some((part) => isToolUIPart2(part)));
7554
+ }
7161
7555
  function shouldUseFastChatPath({
7162
7556
  messages,
7163
7557
  mode,
@@ -7166,6 +7560,9 @@ function shouldUseFastChatPath({
7166
7560
  if (allowedTools && allowedTools.length > 0) {
7167
7561
  return false;
7168
7562
  }
7563
+ if (messagesHaveToolActivity(messages)) {
7564
+ return false;
7565
+ }
7169
7566
  return selectCodingAgentIntentTools({
7170
7567
  mode,
7171
7568
  prompt: "",
@@ -7180,7 +7577,7 @@ async function countPendingChatInteractions(sessionId) {
7180
7577
  });
7181
7578
  return pendingInteractions.interactions.length;
7182
7579
  } catch (error) {
7183
- logger7.warn("context_compaction_pending_interactions_unavailable", {
7580
+ logger8.warn("context_compaction_pending_interactions_unavailable", {
7184
7581
  sessionId,
7185
7582
  error: getErrorMessage(error)
7186
7583
  });
@@ -7191,7 +7588,7 @@ async function loadSessionContextStateSafe(sessionId) {
7191
7588
  try {
7192
7589
  return await getSessionContextState(sessionId);
7193
7590
  } catch (error) {
7194
- logger7.warn("context_state_unavailable", {
7591
+ logger8.warn("context_state_unavailable", {
7195
7592
  sessionId,
7196
7593
  error: getErrorMessage(error)
7197
7594
  });
@@ -7240,7 +7637,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7240
7637
  staleSkip: false
7241
7638
  });
7242
7639
  } catch (error) {
7243
- logger7.error("chat_pre_stream_persist_failed", {
7640
+ logger8.error("chat_pre_stream_persist_failed", {
7244
7641
  sessionId,
7245
7642
  error: getErrorMessage(error)
7246
7643
  });
@@ -7251,6 +7648,8 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7251
7648
  }
7252
7649
  const contextConfig = lightcodeConfigResult.config.context;
7253
7650
  const cacheActive = resolvedProviderModel.provider === "anthropic";
7651
+ const reservedOutputTokens = resolveMaxOutputTokens(lightcodeConfigResult.config.maxOutputTokens, lightcodeConfigDefaults.maxOutputTokens, resolvedProviderModel.maxCompletionTokens);
7652
+ const endpointLimitTokens = getLearnedContextLimit(chatModelId);
7254
7653
  const pendingInteractionCount = await countPendingChatInteractions(sessionId);
7255
7654
  let contextState = await loadSessionContextStateSafe(sessionId);
7256
7655
  let view = buildProviderView({
@@ -7258,44 +7657,56 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7258
7657
  contextState,
7259
7658
  config: contextConfig,
7260
7659
  modelContextWindow: resolvedProviderModel.contextWindow,
7660
+ reservedOutputTokens,
7661
+ endpointLimitTokens,
7261
7662
  pendingInteractionCount,
7262
7663
  cacheActive
7263
7664
  });
7264
- if (view.needsCompaction && contextConfig.autoCompact) {
7265
- try {
7266
- const estimatedTokensBefore = view.estimate.tokens;
7267
- const compaction = await compactSessionContext({
7268
- sessionId,
7269
- coveredMessages: view.coveredMessages,
7270
- previousState: contextState,
7271
- model: resolvedProviderModel.model,
7272
- modelId: chatModelId,
7273
- cwd,
7274
- config: contextConfig,
7275
- estimatedTokens: estimatedTokensBefore
7276
- });
7277
- contextState = compaction.state;
7278
- view = buildProviderView({
7279
- messages: validatedMessages,
7280
- contextState,
7281
- config: contextConfig,
7282
- modelContextWindow: resolvedProviderModel.contextWindow,
7283
- pendingInteractionCount,
7284
- cacheActive
7285
- });
7286
- logger7.info("context_auto_compacted", {
7287
- sessionId,
7288
- tier: compaction.state.tier,
7289
- usedFallback: compaction.usedFallback,
7290
- coveredMessageCount: compaction.state.coveredMessageCount,
7291
- estimatedTokensBefore,
7292
- estimatedTokensAfter: view.estimate.tokens
7293
- });
7294
- } catch (error) {
7295
- logger7.error("context_compaction_failed", {
7296
- sessionId,
7297
- error: getErrorMessage(error)
7298
- });
7665
+ if (contextConfig.autoCompact) {
7666
+ let compactionRound = 0;
7667
+ while (view.needsCompaction && compactionRound < MAX_COMPACTION_ROUNDS) {
7668
+ compactionRound += 1;
7669
+ try {
7670
+ const estimatedTokensBefore = view.estimate.tokens;
7671
+ const compaction = await compactSessionContext({
7672
+ sessionId,
7673
+ coveredMessages: view.coveredMessages,
7674
+ previousState: contextState,
7675
+ model: resolvedProviderModel.model,
7676
+ modelId: chatModelId,
7677
+ cwd,
7678
+ config: contextConfig,
7679
+ estimatedTokens: estimatedTokensBefore
7680
+ });
7681
+ contextState = compaction.state;
7682
+ view = buildProviderView({
7683
+ messages: validatedMessages,
7684
+ contextState,
7685
+ config: contextConfig,
7686
+ modelContextWindow: resolvedProviderModel.contextWindow,
7687
+ reservedOutputTokens,
7688
+ endpointLimitTokens,
7689
+ pendingInteractionCount,
7690
+ cacheActive
7691
+ });
7692
+ logger8.info("context_auto_compacted", {
7693
+ sessionId,
7694
+ round: compactionRound,
7695
+ tier: compaction.state.tier,
7696
+ usedFallback: compaction.usedFallback,
7697
+ coveredMessageCount: compaction.state.coveredMessageCount,
7698
+ estimatedTokensBefore,
7699
+ estimatedTokensAfter: view.estimate.tokens,
7700
+ stillNeedsCompaction: view.needsCompaction
7701
+ });
7702
+ } catch (error) {
7703
+ logger8.error("context_compaction_failed", {
7704
+ sessionId,
7705
+ round: compactionRound,
7706
+ error: getErrorMessage(error)
7707
+ });
7708
+ break;
7709
+ }
7299
7710
  }
7300
7711
  }
7301
7712
  const overflowPreserve = getOverflowPreserveRecentMessages(sessionId);
@@ -7310,6 +7721,8 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7310
7721
  contextState,
7311
7722
  config: overflowConfig,
7312
7723
  modelContextWindow: resolvedProviderModel.contextWindow,
7724
+ reservedOutputTokens,
7725
+ endpointLimitTokens,
7313
7726
  pendingInteractionCount: 0,
7314
7727
  cacheActive
7315
7728
  });
@@ -7331,10 +7744,12 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7331
7744
  contextState,
7332
7745
  config: overflowConfig,
7333
7746
  modelContextWindow: resolvedProviderModel.contextWindow,
7747
+ reservedOutputTokens,
7748
+ endpointLimitTokens,
7334
7749
  pendingInteractionCount: 0,
7335
7750
  cacheActive
7336
7751
  });
7337
- logger7.info("context_overflow_recovery", {
7752
+ logger8.info("context_overflow_recovery", {
7338
7753
  sessionId,
7339
7754
  preserveRecentMessages: overflowConfig.preserveRecentMessages,
7340
7755
  estimatedTokens: view.estimate.tokens,
@@ -7343,13 +7758,30 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7343
7758
  compactedMessages: view.coveredMessages.length
7344
7759
  });
7345
7760
  } catch (error) {
7346
- logger7.error("context_overflow_recovery_failed", {
7761
+ logger8.error("context_overflow_recovery_failed", {
7347
7762
  sessionId,
7348
7763
  error: getErrorMessage(error)
7349
7764
  });
7350
7765
  }
7351
7766
  }
7352
- const providerMessages = normalizeProviderMessages(view.providerMessages);
7767
+ const fitted = fitMessagesToBudget(view.providerMessages, {
7768
+ inputBudgetTokens: view.inputBudgetTokens,
7769
+ preserveRecentMessages: contextConfig.preserveRecentMessages
7770
+ });
7771
+ if (fitted.fitted) {
7772
+ logger8.warn("context_fit_to_budget", {
7773
+ sessionId,
7774
+ inputBudgetTokens: view.inputBudgetTokens,
7775
+ contextWindow: view.contextWindow,
7776
+ estimatedTokensBefore: estimateStructuralTokens(view.providerMessages),
7777
+ estimatedTokensAfter: fitted.estimatedTokens,
7778
+ elidedToolOutputs: fitted.elidedToolOutputs,
7779
+ droppedMessages: fitted.droppedMessages,
7780
+ truncatedTextParts: fitted.truncatedTextParts,
7781
+ withinBudget: fitted.withinBudget
7782
+ });
7783
+ }
7784
+ const providerMessages = normalizeProviderMessages(fitted.messages);
7353
7785
  let overflowNotedThisRequest = false;
7354
7786
  const noteOverflowForSession = () => {
7355
7787
  if (overflowNotedThisRequest) {
@@ -7379,14 +7811,14 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7379
7811
  });
7380
7812
  const normalizedMessages = removeTrailingEmptyAssistantMessages(fullHistory);
7381
7813
  if (normalizedMessages.length !== fullHistory.length) {
7382
- logger7.warn("chat_finish_empty_assistant_skipped", { sessionId });
7814
+ logger8.warn("chat_finish_empty_assistant_skipped", { sessionId });
7383
7815
  return;
7384
7816
  }
7385
7817
  const validatedFinishResult = await safeValidateUIMessages2({
7386
7818
  messages: normalizedMessages
7387
7819
  });
7388
7820
  if (!validatedFinishResult.success) {
7389
- logger7.warn("chat_invalid_finish_payload", {
7821
+ logger8.warn("chat_invalid_finish_payload", {
7390
7822
  sessionId,
7391
7823
  message: validatedFinishResult.error.message
7392
7824
  });
@@ -7420,7 +7852,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7420
7852
  messages: validatedFinishResult.data
7421
7853
  });
7422
7854
  } catch (error) {
7423
- logger7.error("chat_finish_persist_failed", {
7855
+ logger8.error("chat_finish_persist_failed", {
7424
7856
  sessionId,
7425
7857
  error: getErrorMessage(error)
7426
7858
  });
@@ -7443,22 +7875,30 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7443
7875
  totalTokens: part.totalUsage.totalTokens
7444
7876
  },
7445
7877
  modelId: chatModelId,
7446
- finishReason: part.finishReason
7878
+ finishReason: part.finishReason,
7879
+ context: {
7880
+ inputTokens: view.estimate.tokens,
7881
+ contextWindow: view.contextWindow,
7882
+ compactedMessages: contextState?.coveredMessageCount ?? 0
7883
+ }
7447
7884
  };
7448
7885
  };
7886
+ const environmentContext = await buildWorkspaceContext({ cwd });
7449
7887
  try {
7450
7888
  if (shouldUseFastChatPath({
7451
7889
  messages: providerMessages,
7452
7890
  mode,
7453
7891
  allowedTools
7454
7892
  })) {
7455
- logger7.info("chat_fast_path", {
7893
+ logger8.info("chat_fast_path", {
7456
7894
  sessionId,
7457
7895
  model: chatModelId
7458
7896
  });
7459
7897
  const result = streamText({
7460
7898
  model: resolvedProviderModel.model,
7461
- system: fastChatSystemPrompt,
7899
+ system: `${fastChatSystemPrompt}
7900
+
7901
+ ${environmentContext}`,
7462
7902
  messages: buildFastChatModelMessages(providerMessages),
7463
7903
  maxOutputTokens: Math.min(lightcodeConfigResult.config.maxOutputTokens, fastChatMaxOutputTokens),
7464
7904
  maxRetries: lightcodeConfigResult.config.maxRetries,
@@ -7488,6 +7928,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7488
7928
  onError: (error) => {
7489
7929
  const classified = classifyChatError(error);
7490
7930
  if (classified.kind === "context_overflow") {
7931
+ noteLearnedContextLimit(chatModelId, classified.contextLimitTokens);
7491
7932
  noteOverflowForSession();
7492
7933
  }
7493
7934
  incrementChatFailureCounter(chatFailureClassForErrorKind(classified.kind), { sessionId, statusCode: classified.statusCode });
@@ -7505,6 +7946,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7505
7946
  const mapStreamError = (error) => {
7506
7947
  const classified = classifyChatError(error);
7507
7948
  if (classified.kind === "context_overflow") {
7949
+ noteLearnedContextLimit(chatModelId, classified.contextLimitTokens);
7508
7950
  noteOverflowForSession();
7509
7951
  }
7510
7952
  incrementChatFailureCounter(chatFailureClassForErrorKind(classified.kind), {
@@ -7528,7 +7970,8 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7528
7970
  permissionMode,
7529
7971
  allowedTools,
7530
7972
  permissionRules,
7531
- sandbox
7973
+ sandbox,
7974
+ environmentContext
7532
7975
  },
7533
7976
  abortSignal: c.req.raw.signal,
7534
7977
  generateMessageId: generateId3,
@@ -7558,6 +8001,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7558
8001
  } catch (error) {
7559
8002
  const classified = classifyChatError(error);
7560
8003
  if (classified.kind === "context_overflow") {
8004
+ noteLearnedContextLimit(chatModelId, classified.contextLimitTokens);
7561
8005
  noteContextOverflow(sessionId);
7562
8006
  }
7563
8007
  incrementChatFailureCounter(chatFailureClassForErrorKind(classified.kind), {
@@ -7588,7 +8032,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7588
8032
  }, 500);
7589
8033
  }
7590
8034
  }
7591
- var logger7, recoverableDisconnectMessage = "Connection interrupted. Please retry or regenerate your last message.", providerBillingOrQuotaMessage, fastChatSystemPrompt, fastChatMaxOutputTokens = 512, fastChatRecentMessageCount = 6;
8035
+ var logger8, recoverableDisconnectMessage = "Connection interrupted. Please retry or regenerate your last message.", providerBillingOrQuotaMessage, fastChatSystemPrompt, fastChatMaxOutputTokens = 512, fastChatRecentMessageCount = 6, MAX_COMPACTION_ROUNDS = 3;
7592
8036
  var init_chat_stream = __esm(() => {
7593
8037
  init_src2();
7594
8038
  init_src();
@@ -7597,13 +8041,15 @@ var init_chat_stream = __esm(() => {
7597
8041
  init_chat_store();
7598
8042
  init_context_compaction();
7599
8043
  init_overflow_recovery();
8044
+ init_learned_context_limit();
7600
8045
  init_sse_heartbeat();
7601
8046
  init_context_state_store();
7602
8047
  init_session_auto_title();
8048
+ init_workspace_context();
7603
8049
  init_runtime_config();
7604
- logger7 = createLogger("chat-stream");
8050
+ logger8 = createLogger("chat-stream");
7605
8051
  providerBillingOrQuotaMessage = "The configured model provider rejected this request due to billing or quota limits. " + "Update provider credits/quota and retry.";
7606
- 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.";
8052
+ fastChatSystemPrompt = "You are Lightcode's coding assistant, running inside the user's project. " + "For casual conversation, reply briefly and naturally. " + "If the user asks about or wants work on their code, do not ask them to paste it \u2014 you can read the project files directly; offer to look and proceed.";
7607
8053
  });
7608
8054
 
7609
8055
  // apps/server/src/routes/route-helpers.ts
@@ -7612,16 +8058,16 @@ function internalErrorResponse(c, {
7612
8058
  message,
7613
8059
  error
7614
8060
  }) {
7615
- logger8.error(event, { error: getErrorMessage(error) });
8061
+ logger9.error(event, { error: getErrorMessage(error) });
7616
8062
  return c.json({
7617
8063
  error: message,
7618
8064
  details: Bun.env.NODE_ENV === "production" ? undefined : getErrorMessage(error)
7619
8065
  }, 500);
7620
8066
  }
7621
- var logger8;
8067
+ var logger9;
7622
8068
  var init_route_helpers = __esm(() => {
7623
8069
  init_src();
7624
- logger8 = createLogger("routes");
8070
+ logger9 = createLogger("routes");
7625
8071
  });
7626
8072
 
7627
8073
  // apps/server/src/routes/chat-routes.ts
@@ -7916,7 +8362,7 @@ var init_config_routes = __esm(() => {
7916
8362
  provider: lightcodeProviderSchema,
7917
8363
  model: z35.string().min(1).max(240)
7918
8364
  });
7919
- configRoutes = new Hono2().get("/status", (c) => c.json(configStatus)).get("/models", async (c) => {
8365
+ configRoutes = new Hono2().get("/status", (c) => c.json(configStatus)).post("/reload", (c) => c.json({ ok: true, configStatus: reloadRuntimeConfig() })).get("/models", async (c) => {
7920
8366
  const provider = c.req.query("provider") ?? "openrouter";
7921
8367
  if (provider !== "openrouter") {
7922
8368
  return c.json({ error: `Model listing is not supported for provider "${provider}".` }, 400);
@@ -7987,7 +8433,7 @@ var init_extension_runtime = __esm(() => {
7987
8433
 
7988
8434
  // apps/server/src/routes/diagnostics-routes.ts
7989
8435
  import { existsSync as existsSync9, readdirSync as readdirSync4 } from "fs";
7990
- import path11 from "path";
8436
+ import path12 from "path";
7991
8437
  import { Hono as Hono3 } from "hono";
7992
8438
  function combineStatuses(statuses) {
7993
8439
  if (statuses.includes("error")) {
@@ -8309,7 +8755,7 @@ async function buildLogsTailPayload(requestedLines) {
8309
8755
  lines: []
8310
8756
  };
8311
8757
  }
8312
- const filePath = path11.join(directory, latestFile);
8758
+ const filePath = path12.join(directory, latestFile);
8313
8759
  const content = await Bun.file(filePath).text();
8314
8760
  const lines = content.split(`
8315
8761
  `).filter((line) => line.length > 0);
@@ -8432,36 +8878,36 @@ async function ensureHeadroomRouting() {
8432
8878
  health = await checkHeadroomHealth(headroom.proxyUrl);
8433
8879
  } catch (error) {
8434
8880
  health = "down";
8435
- logger9.warn("headroom_health_probe_failed", {
8881
+ logger10.warn("headroom_health_probe_failed", {
8436
8882
  proxyUrl: headroom.proxyUrl,
8437
8883
  error: getErrorMessage(error)
8438
8884
  });
8439
8885
  }
8440
8886
  if (health === "healthy") {
8441
- logger9.info("headroom_routing_active", { proxyUrl: headroom.proxyUrl });
8887
+ logger10.info("headroom_routing_active", { proxyUrl: headroom.proxyUrl });
8442
8888
  return;
8443
8889
  }
8444
8890
  if (headroom.failOpen) {
8445
- logger9.warn("headroom_unreachable_failing_open", {
8891
+ logger10.warn("headroom_unreachable_failing_open", {
8446
8892
  proxyUrl: headroom.proxyUrl
8447
8893
  });
8448
8894
  disableHeadroomRouting();
8449
8895
  return;
8450
8896
  }
8451
- logger9.error("headroom_unreachable_fail_closed", {
8897
+ logger10.error("headroom_unreachable_fail_closed", {
8452
8898
  proxyUrl: headroom.proxyUrl
8453
8899
  });
8454
8900
  }
8455
- var logger9, healthCheckTimeoutMs = 1500;
8901
+ var logger10, healthCheckTimeoutMs = 1500;
8456
8902
  var init_headroom_proxy = __esm(() => {
8457
8903
  init_src();
8458
8904
  init_runtime_config();
8459
- logger9 = createLogger("headroom-proxy");
8905
+ logger10 = createLogger("headroom-proxy");
8460
8906
  });
8461
8907
 
8462
8908
  // apps/server/src/index.ts
8463
8909
  init_src();
8464
- var logger10 = createLogger("server");
8910
+ var logger11 = createLogger("server");
8465
8911
  var httpIdleTimeoutSeconds = 30;
8466
8912
  var shutdownForceExitMs = 5000;
8467
8913
  function isChatStreamingRoute(request) {
@@ -8474,7 +8920,7 @@ function isChatStreamingRoute(request) {
8474
8920
  async function startServer() {
8475
8921
  const logDirectory = enableFileLogSink(Bun.env);
8476
8922
  if (logDirectory) {
8477
- logger10.info("file_log_sink_enabled", { directory: logDirectory });
8923
+ logger11.info("file_log_sink_enabled", { directory: logDirectory });
8478
8924
  }
8479
8925
  const port = Number(Bun.env.PORT ?? 4983);
8480
8926
  const hostname = Bun.env.LIGHTCODE_HOST ?? "127.0.0.1";
@@ -8495,12 +8941,12 @@ async function startServer() {
8495
8941
  process.exit(1);
8496
8942
  }
8497
8943
  Promise.resolve().then(() => (init_runtime_config(), exports_runtime_config)).then((runtime19) => runtime19.initializeModelCapabilities()).catch((error) => {
8498
- logger10.warn("model_capability_init_failed", {
8944
+ logger11.warn("model_capability_init_failed", {
8499
8945
  error: getErrorMessage(error)
8500
8946
  });
8501
8947
  });
8502
8948
  Promise.resolve().then(() => (init_headroom_proxy(), exports_headroom_proxy)).then((headroom) => headroom.ensureHeadroomRouting()).catch((error) => {
8503
- logger10.warn("headroom_init_failed", {
8949
+ logger11.warn("headroom_init_failed", {
8504
8950
  error: getErrorMessage(error)
8505
8951
  });
8506
8952
  });
@@ -8536,7 +8982,7 @@ async function startServer() {
8536
8982
  return;
8537
8983
  }
8538
8984
  shuttingDown = true;
8539
- logger10.info("server_shutdown", { signal });
8985
+ logger11.info("server_shutdown", { signal });
8540
8986
  const forceExitTimer = setTimeout(() => {
8541
8987
  server.stop(true);
8542
8988
  process.exit(0);
@@ -8551,6 +8997,17 @@ async function startServer() {
8551
8997
  };
8552
8998
  process.on("SIGINT", () => void shutdown("SIGINT"));
8553
8999
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
9000
+ const parentPid = Number(Bun.env.LIGHTCODE_PARENT_PID);
9001
+ if (Number.isInteger(parentPid) && parentPid > 0) {
9002
+ const parentWatch = setInterval(() => {
9003
+ try {
9004
+ process.kill(parentPid, 0);
9005
+ } catch {
9006
+ shutdown("parent-exit");
9007
+ }
9008
+ }, 2000);
9009
+ parentWatch.unref();
9010
+ }
8554
9011
  console.log(`Server listening on http://${hostname}:${port}`);
8555
9012
  }
8556
9013
  if (import.meta.main) {