@kmugalkhod/lightcode 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/cli.js +207 -56
  2. package/package.json +1 -1
  3. package/server.js +230 -19
package/cli.js CHANGED
@@ -124,7 +124,7 @@ var init_logger = __esm(() => {
124
124
  });
125
125
 
126
126
  // packages/shared/src/version.ts
127
- var lightcodeVersion = "0.2.0";
127
+ var lightcodeVersion = "0.3.0";
128
128
 
129
129
  // packages/shared/src/index.ts
130
130
  var productName = "Lightcode";
@@ -4173,6 +4173,43 @@ var init_build_provider_view = __esm(() => {
4173
4173
  init_tier1_prune();
4174
4174
  });
4175
4175
 
4176
+ // packages/ai/src/context/normalize-provider-messages.ts
4177
+ import { isToolUIPart } from "ai";
4178
+ function hasDanglingToolParts(messages) {
4179
+ return messages.some((message) => message.parts.some((part) => isToolUIPart(part) && !terminalToolStates.has(part.state)));
4180
+ }
4181
+ function resolveDanglingToolParts(messages, errorText = interruptedToolErrorText) {
4182
+ return messages.map((message) => {
4183
+ if (message.role !== "assistant") {
4184
+ return message;
4185
+ }
4186
+ let changed = false;
4187
+ const parts = message.parts.map((part) => {
4188
+ if (!isToolUIPart(part) || terminalToolStates.has(part.state)) {
4189
+ return part;
4190
+ }
4191
+ changed = true;
4192
+ return {
4193
+ ...part,
4194
+ state: "output-error",
4195
+ errorText
4196
+ };
4197
+ });
4198
+ return changed ? { ...message, parts } : message;
4199
+ });
4200
+ }
4201
+ function normalizeProviderMessages(messages) {
4202
+ return resolveDanglingToolParts(messages);
4203
+ }
4204
+ var terminalToolStates, interruptedToolErrorText = "Tool call was interrupted by a connection retry. Re-issue the tool call if it is still needed.";
4205
+ var init_normalize_provider_messages = __esm(() => {
4206
+ terminalToolStates = new Set([
4207
+ "output-available",
4208
+ "output-error",
4209
+ "output-denied"
4210
+ ]);
4211
+ });
4212
+
4176
4213
  // packages/ai/src/context/index.ts
4177
4214
  var init_context = __esm(() => {
4178
4215
  init_build_provider_view();
@@ -4181,6 +4218,7 @@ var init_context = __esm(() => {
4181
4218
  init_estimate();
4182
4219
  init_heuristic_summary();
4183
4220
  init_message_parts();
4221
+ init_normalize_provider_messages();
4184
4222
  init_tier1_prune();
4185
4223
  });
4186
4224
 
@@ -4279,6 +4317,12 @@ function readEnvConfig(env) {
4279
4317
  summaryMaxChars: parseNumberEnv(env.LIGHTCODE_CONTEXT_SUMMARY_MAX_CHARS, "LIGHTCODE_CONTEXT_SUMMARY_MAX_CHARS")
4280
4318
  };
4281
4319
  const compactContextConfig = Object.fromEntries(Object.entries(contextConfig).filter(([, value]) => value !== undefined));
4320
+ const headroomConfig = {
4321
+ enabled: parseBooleanEnv(env.LIGHTCODE_HEADROOM_ENABLED, "LIGHTCODE_HEADROOM_ENABLED"),
4322
+ proxyUrl: env.LIGHTCODE_HEADROOM_PROXY_URL?.trim() || undefined,
4323
+ failOpen: parseBooleanEnv(env.LIGHTCODE_HEADROOM_FAIL_OPEN, "LIGHTCODE_HEADROOM_FAIL_OPEN")
4324
+ };
4325
+ const compactHeadroomConfig = Object.fromEntries(Object.entries(headroomConfig).filter(([, value]) => value !== undefined));
4282
4326
  const rawConfig = {
4283
4327
  provider: env.LIGHTCODE_PROVIDER,
4284
4328
  model: env.LIGHTCODE_CHAT_MODEL,
@@ -4288,7 +4332,8 @@ function readEnvConfig(env) {
4288
4332
  maxOutputTokens: parseNumberEnv(env.LIGHTCODE_MAX_OUTPUT_TOKENS, "LIGHTCODE_MAX_OUTPUT_TOKENS"),
4289
4333
  maxSteps: parseNumberEnv(env.LIGHTCODE_MAX_STEPS, "LIGHTCODE_MAX_STEPS"),
4290
4334
  maxRetries: parseNumberEnv(env.LIGHTCODE_MAX_RETRIES, "LIGHTCODE_MAX_RETRIES"),
4291
- context: Object.keys(compactContextConfig).length > 0 ? compactContextConfig : undefined
4335
+ context: Object.keys(compactContextConfig).length > 0 ? compactContextConfig : undefined,
4336
+ headroom: Object.keys(compactHeadroomConfig).length > 0 ? compactHeadroomConfig : undefined
4292
4337
  };
4293
4338
  const compactConfig = Object.fromEntries(Object.entries(rawConfig).filter(([, value]) => value !== undefined));
4294
4339
  const parsedConfig = lightcodeConfigSchema.safeParse(compactConfig);
@@ -4312,6 +4357,10 @@ function mergeConfig(...configs) {
4312
4357
  ...mergedConfig.autoContinue,
4313
4358
  ...config2.autoContinue
4314
4359
  } : mergedConfig.autoContinue,
4360
+ headroom: config2.headroom ? {
4361
+ ...mergedConfig.headroom,
4362
+ ...config2.headroom
4363
+ } : mergedConfig.headroom,
4315
4364
  allowedTools: config2.allowedTools ?? mergedConfig.allowedTools
4316
4365
  }), {});
4317
4366
  }
@@ -4352,6 +4401,10 @@ function loadLightcodeConfig({
4352
4401
  autoContinue: {
4353
4402
  ...defaultAutoContinueConfig,
4354
4403
  ...mergedConfig.autoContinue
4404
+ },
4405
+ headroom: {
4406
+ ...defaultHeadroomConfig,
4407
+ ...mergedConfig.headroom
4355
4408
  }
4356
4409
  };
4357
4410
  return lightcodeConfigLoadResultSchema.parse({
@@ -4359,7 +4412,7 @@ function loadLightcodeConfig({
4359
4412
  loadedFiles: [userConfig.file, projectConfig.file]
4360
4413
  });
4361
4414
  }
4362
- var lightcodeProviderSchema, autoContinueConfigSchema, resolvedAutoContinueConfigSchema, defaultAutoContinueConfig, lightcodeConfigSchema, lightcodeConfigDefaults, loadedConfigFileSchema, lightcodeResolvedConfigSchema, lightcodeConfigLoadResultSchema, lightcodeConfigStatusSchema, LightcodeConfigError;
4415
+ var lightcodeProviderSchema, autoContinueConfigSchema, resolvedAutoContinueConfigSchema, defaultAutoContinueConfig, headroomConfigSchema, resolvedHeadroomConfigSchema, defaultHeadroomConfig, lightcodeConfigSchema, lightcodeConfigDefaults, loadedConfigFileSchema, lightcodeResolvedConfigSchema, lightcodeConfigLoadResultSchema, lightcodeConfigStatusSchema, LightcodeConfigError;
4363
4416
  var init_lightcode_config = __esm(() => {
4364
4417
  init_coding_agent_modes();
4365
4418
  init_coding_agent_modes();
@@ -4391,6 +4444,24 @@ var init_lightcode_config = __esm(() => {
4391
4444
  maxErrorRetries: 5,
4392
4445
  stallTimeoutSeconds: 180
4393
4446
  };
4447
+ headroomConfigSchema = z28.object({
4448
+ enabled: z28.boolean().optional(),
4449
+ proxyUrl: z28.string().min(1).max(4096).optional(),
4450
+ providers: z28.array(lightcodeProviderSchema).optional(),
4451
+ failOpen: z28.boolean().optional()
4452
+ }).strict();
4453
+ resolvedHeadroomConfigSchema = z28.object({
4454
+ enabled: z28.boolean(),
4455
+ proxyUrl: z28.string().min(1).max(4096),
4456
+ providers: z28.array(lightcodeProviderSchema).nullable(),
4457
+ failOpen: z28.boolean()
4458
+ });
4459
+ defaultHeadroomConfig = {
4460
+ enabled: false,
4461
+ proxyUrl: "http://127.0.0.1:8787",
4462
+ providers: null,
4463
+ failOpen: true
4464
+ };
4394
4465
  lightcodeConfigSchema = z28.object({
4395
4466
  provider: lightcodeProviderSchema.optional(),
4396
4467
  model: z28.string().min(1).max(240).optional(),
@@ -4402,10 +4473,11 @@ var init_lightcode_config = __esm(() => {
4402
4473
  sandbox: sandboxConfigSchema.optional(),
4403
4474
  mcp: mcpConfigSchema.optional(),
4404
4475
  context: contextOptimizerConfigSchema.optional(),
4405
- maxOutputTokens: z28.number().int().min(1).max(200000).optional(),
4476
+ maxOutputTokens: z28.number().int().min(1).max(1e6).optional(),
4406
4477
  maxSteps: z28.number().int().min(1).max(100).optional(),
4407
4478
  maxRetries: z28.number().int().min(0).max(10).optional(),
4408
- autoContinue: autoContinueConfigSchema.optional()
4479
+ autoContinue: autoContinueConfigSchema.optional(),
4480
+ headroom: headroomConfigSchema.optional()
4409
4481
  }).strict();
4410
4482
  lightcodeConfigDefaults = {
4411
4483
  provider: "anthropic",
@@ -4414,7 +4486,8 @@ var init_lightcode_config = __esm(() => {
4414
4486
  maxOutputTokens: 32768,
4415
4487
  maxSteps: 30,
4416
4488
  maxRetries: 5,
4417
- autoContinue: defaultAutoContinueConfig
4489
+ autoContinue: defaultAutoContinueConfig,
4490
+ headroom: defaultHeadroomConfig
4418
4491
  };
4419
4492
  loadedConfigFileSchema = z28.object({
4420
4493
  scope: z28.enum(["user", "project"]),
@@ -4426,10 +4499,11 @@ var init_lightcode_config = __esm(() => {
4426
4499
  provider: lightcodeProviderSchema,
4427
4500
  defaultMode: codingAgentModeSchema,
4428
4501
  context: resolvedContextOptimizerConfigSchema,
4429
- maxOutputTokens: z28.number().int().min(1).max(200000),
4502
+ maxOutputTokens: z28.number().int().min(1).max(1e6),
4430
4503
  maxSteps: z28.number().int().min(1).max(100),
4431
4504
  maxRetries: z28.number().int().min(0).max(10),
4432
- autoContinue: resolvedAutoContinueConfigSchema
4505
+ autoContinue: resolvedAutoContinueConfigSchema,
4506
+ headroom: resolvedHeadroomConfigSchema.optional()
4433
4507
  });
4434
4508
  lightcodeConfigLoadResultSchema = z28.object({
4435
4509
  config: lightcodeResolvedConfigSchema,
@@ -4443,10 +4517,15 @@ var init_lightcode_config = __esm(() => {
4443
4517
  baseUrl: z28.string().nullable(),
4444
4518
  defaultMode: codingAgentModeSchema,
4445
4519
  permissionMode: permissionModeSchema.nullable(),
4446
- maxOutputTokens: z28.number().int().min(1).max(200000),
4520
+ maxOutputTokens: z28.number().int().min(1).max(1e6),
4447
4521
  maxSteps: z28.number().int().min(1).max(100),
4448
4522
  autoContinue: resolvedAutoContinueConfigSchema,
4449
4523
  contextWindow: z28.number().int().positive(),
4524
+ headroom: z28.object({
4525
+ enabled: z28.boolean(),
4526
+ proxyUrl: z28.string().nullable(),
4527
+ routed: z28.boolean()
4528
+ }),
4450
4529
  missingCredentialHints: z28.array(z28.string())
4451
4530
  });
4452
4531
  LightcodeConfigError = class LightcodeConfigError extends Error {
@@ -4705,6 +4784,21 @@ var init_runtime = __esm(() => {
4705
4784
  init_schema13();
4706
4785
  });
4707
4786
 
4787
+ // packages/ai/src/providers/clamp-output-tokens.ts
4788
+ function clampMaxOutputTokens(configured, modelMaxCompletionTokens) {
4789
+ if (typeof modelMaxCompletionTokens === "number" && Number.isFinite(modelMaxCompletionTokens) && modelMaxCompletionTokens > 0) {
4790
+ return Math.min(configured, modelMaxCompletionTokens);
4791
+ }
4792
+ return configured;
4793
+ }
4794
+ function resolveMaxOutputTokens(configured, defaultValue, modelMaxCompletionTokens) {
4795
+ const hasModelCap = typeof modelMaxCompletionTokens === "number" && Number.isFinite(modelMaxCompletionTokens) && modelMaxCompletionTokens > 0;
4796
+ if (configured === defaultValue) {
4797
+ return hasModelCap ? modelMaxCompletionTokens : configured;
4798
+ }
4799
+ return clampMaxOutputTokens(configured, modelMaxCompletionTokens);
4800
+ }
4801
+
4708
4802
  // packages/ai/src/providers/model-metadata.ts
4709
4803
  function getModelContextWindow(provider, modelId) {
4710
4804
  if (provider === "anthropic") {
@@ -7515,9 +7609,12 @@ __export(exports_runtime, {
7515
7609
  sandboxConfigSchema: () => sandboxConfigSchema,
7516
7610
  safeStringify: () => safeStringify,
7517
7611
  runPluginHooks: () => runPluginHooks,
7612
+ resolvedHeadroomConfigSchema: () => resolvedHeadroomConfigSchema,
7518
7613
  resolvedContextOptimizerConfigSchema: () => resolvedContextOptimizerConfigSchema,
7519
7614
  resolvedAutoContinueConfigSchema: () => resolvedAutoContinueConfigSchema,
7520
7615
  resolveWithinWorkspace: () => resolveWithinWorkspace,
7616
+ resolveMaxOutputTokens: () => resolveMaxOutputTokens,
7617
+ resolveDanglingToolParts: () => resolveDanglingToolParts,
7521
7618
  resolveContextWindowTokens: () => resolveContextWindowTokens,
7522
7619
  resolveCodingPermissionMode: () => resolveCodingPermissionMode,
7523
7620
  requestUserInputToolProviderInputSchema: () => requestUserInputToolProviderInputSchema,
@@ -7547,6 +7644,7 @@ __export(exports_runtime, {
7547
7644
  parseCodingToolInput: () => parseCodingToolInput,
7548
7645
  parseChatStreamError: () => parseChatStreamError,
7549
7646
  normalizeSandboxConfig: () => normalizeSandboxConfig,
7647
+ normalizeProviderMessages: () => normalizeProviderMessages,
7550
7648
  normalizeContextOptimizerConfig: () => normalizeContextOptimizerConfig,
7551
7649
  messageUsageMetadataSchema: () => messageUsageMetadataSchema,
7552
7650
  mcpStdioServerConfigSchema: () => mcpStdioServerConfigSchema,
@@ -7577,7 +7675,10 @@ __export(exports_runtime, {
7577
7675
  isRecord: () => isRecord3,
7578
7676
  isPermissionDeniedError: () => isPermissionDeniedError,
7579
7677
  isElidedToolOutput: () => isElidedToolOutput,
7678
+ interruptedToolErrorText: () => interruptedToolErrorText,
7679
+ headroomConfigSchema: () => headroomConfigSchema,
7580
7680
  hasUnresolvedToolWork: () => hasUnresolvedToolWork,
7681
+ hasDanglingToolParts: () => hasDanglingToolParts,
7581
7682
  getToolNameFromPart: () => getToolNameFromPart,
7582
7683
  getTextPartContent: () => getTextPartContent,
7583
7684
  getSandboxRuntimeStatus: () => getSandboxRuntimeStatus,
@@ -7628,6 +7729,7 @@ __export(exports_runtime, {
7628
7729
  describeChatStreamError: () => describeChatStreamError,
7629
7730
  defaultSubagentTaskMode: () => defaultSubagentTaskMode,
7630
7731
  defaultPermissionModeForCodingMode: () => defaultPermissionModeForCodingMode,
7732
+ defaultHeadroomConfig: () => defaultHeadroomConfig,
7631
7733
  defaultContextOptimizerConfig: () => defaultContextOptimizerConfig,
7632
7734
  defaultCodingAgentSystemPrompt: () => defaultCodingAgentSystemPrompt,
7633
7735
  defaultCodingAgentMode: () => defaultCodingAgentMode,
@@ -7663,6 +7765,7 @@ __export(exports_runtime, {
7663
7765
  codingAgentModeSchema: () => codingAgentModeSchema,
7664
7766
  codingAgentModeOrder: () => codingAgentModeOrder,
7665
7767
  codingAgentCallOptionsSchema: () => codingAgentCallOptionsSchema,
7768
+ clampMaxOutputTokens: () => clampMaxOutputTokens,
7666
7769
  chatInteractionUserPromptPayloadSchema: () => chatInteractionUserPromptPayloadSchema,
7667
7770
  chatInteractionUpsertRequestSchema: () => chatInteractionUpsertRequestSchema,
7668
7771
  chatInteractionToolCallIdSchema: () => chatInteractionToolCallIdSchema,
@@ -8026,7 +8129,7 @@ var init_auto_continue = __esm(() => {
8026
8129
  import { useChat } from "@ai-sdk/react";
8027
8130
  import {
8028
8131
  DefaultChatTransport,
8029
- isToolUIPart,
8132
+ isToolUIPart as isToolUIPart2,
8030
8133
  lastAssistantMessageIsCompleteWithToolCalls,
8031
8134
  safeValidateUIMessages
8032
8135
  } from "ai";
@@ -8118,30 +8221,38 @@ function queueToolOutput(outputResult) {
8118
8221
  console.warn("Failed to add tool output.", outputError);
8119
8222
  });
8120
8223
  }
8121
- function isIncompleteAssistantMessage(message) {
8122
- if (message.role !== "assistant") {
8123
- return false;
8124
- }
8125
- if (message.parts.length === 0) {
8224
+ function isIncompletePart(part) {
8225
+ if ((part.type === "text" || part.type === "reasoning") && Reflect.get(part, "state") === "streaming") {
8126
8226
  return true;
8127
8227
  }
8128
- return message.parts.some((part) => {
8129
- if ((part.type === "text" || part.type === "reasoning") && Reflect.get(part, "state") === "streaming") {
8130
- return true;
8131
- }
8132
- return isToolUIPart(part) && part.state === "input-streaming";
8133
- });
8228
+ return isToolUIPart2(part) && part.state === "input-streaming";
8134
8229
  }
8135
- function trimIncompleteTrailingAssistantMessages(messages) {
8136
- let endIndex = messages.length;
8137
- while (endIndex > 0 && isIncompleteAssistantMessage(messages[endIndex - 1])) {
8138
- endIndex -= 1;
8230
+ function pruneIncompleteTrailingAssistantParts(messages) {
8231
+ let result = messages;
8232
+ while (result.length > 0) {
8233
+ const lastIndex = result.length - 1;
8234
+ const last = result[lastIndex];
8235
+ if (last.role !== "assistant") {
8236
+ break;
8237
+ }
8238
+ const completeParts = last.parts.filter((part) => !isIncompletePart(part));
8239
+ if (completeParts.length === last.parts.length) {
8240
+ break;
8241
+ }
8242
+ if (completeParts.length === 0) {
8243
+ result = result.slice(0, lastIndex);
8244
+ continue;
8245
+ }
8246
+ const next = result.slice();
8247
+ next[lastIndex] = { ...last, parts: completeParts };
8248
+ result = next;
8249
+ break;
8139
8250
  }
8140
- return endIndex === messages.length ? messages : messages.slice(0, endIndex);
8251
+ return result;
8141
8252
  }
8142
8253
  function hasUnresolvedToolParts(messages) {
8143
8254
  return messages.some((message) => message.parts.some((part) => {
8144
- if (!isToolUIPart(part)) {
8255
+ if (!isToolUIPart2(part)) {
8145
8256
  return false;
8146
8257
  }
8147
8258
  return ![
@@ -8151,26 +8262,21 @@ function hasUnresolvedToolParts(messages) {
8151
8262
  ].includes(part.state);
8152
8263
  }));
8153
8264
  }
8154
- function sanitizeMessagesForRetry(messages) {
8155
- const trimmed = trimIncompleteTrailingAssistantMessages(messages);
8156
- return trimmed.map((message) => {
8157
- if (message.role !== "assistant") {
8158
- return message;
8159
- }
8160
- let changed = false;
8161
- const parts = message.parts.map((part) => {
8162
- if (!isToolUIPart(part) || terminalToolPartStates.has(part.state)) {
8163
- return part;
8265
+ function countCompletedWork(messages) {
8266
+ let total = 0;
8267
+ for (const message of messages) {
8268
+ for (const part of message.parts) {
8269
+ if ((part.type === "text" || part.type === "reasoning") && Reflect.get(part, "state") !== "streaming") {
8270
+ total += 1;
8271
+ } else if (isToolUIPart2(part) && terminalToolPartStates.has(part.state)) {
8272
+ total += 1;
8164
8273
  }
8165
- changed = true;
8166
- return {
8167
- ...part,
8168
- state: "output-error",
8169
- errorText: interruptedToolRetryErrorText
8170
- };
8171
- });
8172
- return changed ? { ...message, parts } : message;
8173
- });
8274
+ }
8275
+ }
8276
+ return total;
8277
+ }
8278
+ function sanitizeMessagesForRetry(messages) {
8279
+ return resolveDanglingToolParts(pruneIncompleteTrailingAssistantParts(messages));
8174
8280
  }
8175
8281
  function hasActiveClientToolWork({
8176
8282
  messages,
@@ -8180,7 +8286,7 @@ function hasActiveClientToolWork({
8180
8286
  const pendingApprovalIds = new Set(pendingApprovals.map((approval) => approval.toolCallId));
8181
8287
  const pendingPromptIds = new Set(pendingUserPrompts.map((prompt) => prompt.toolCallId));
8182
8288
  return messages.some((message) => message.parts.some((part) => {
8183
- if (!isToolUIPart(part)) {
8289
+ if (!isToolUIPart2(part)) {
8184
8290
  return false;
8185
8291
  }
8186
8292
  if (part.state === "input-streaming" || part.state === "approval-responded") {
@@ -8308,6 +8414,9 @@ function useCodingSessionChat({
8308
8414
  autoContinueLimitsRef.current = autoContinueLimits;
8309
8415
  const maxErrorRetriesRef = useRef(maxErrorRetries);
8310
8416
  maxErrorRetriesRef.current = maxErrorRetries;
8417
+ const maxNoProgressRetries = maxErrorRetries + 3;
8418
+ const maxNoProgressRetriesRef = useRef(maxNoProgressRetries);
8419
+ maxNoProgressRetriesRef.current = maxNoProgressRetries;
8311
8420
  const stallTimeoutMsRef = useRef(stallTimeoutMs);
8312
8421
  stallTimeoutMsRef.current = stallTimeoutMs;
8313
8422
  const lastStreamActivityRef = useRef(Date.now());
@@ -8316,11 +8425,18 @@ function useCodingSessionChat({
8316
8425
  const errorRetriesRef = useRef(0);
8317
8426
  const errorRetryTimerRef = useRef(null);
8318
8427
  const lastDecidedAssistantIdRef = useRef(null);
8428
+ const lastProgressMarkerRef = useRef(0);
8429
+ const noProgressRetriesRef = useRef(0);
8430
+ const taskStuckRef = useRef(false);
8431
+ const messagesRef = useRef([]);
8319
8432
  const advanceTurnKey = useCallback(() => {
8320
8433
  turnCounterRef.current += 1;
8321
8434
  turnKeyRef.current = `turn-${Date.now()}-${turnCounterRef.current}`;
8322
8435
  autoContinuesRef.current = 0;
8323
8436
  errorRetriesRef.current = 0;
8437
+ lastProgressMarkerRef.current = 0;
8438
+ noProgressRetriesRef.current = 0;
8439
+ taskStuckRef.current = false;
8324
8440
  lastDecidedAssistantIdRef.current = null;
8325
8441
  if (errorRetryTimerRef.current) {
8326
8442
  clearTimeout(errorRetryTimerRef.current);
@@ -8328,6 +8444,20 @@ function useCodingSessionChat({
8328
8444
  }
8329
8445
  setAutoContinueState(null);
8330
8446
  }, []);
8447
+ const registerRetryProgress = useCallback((currentMessages) => {
8448
+ const marker = countCompletedWork(currentMessages);
8449
+ if (marker > lastProgressMarkerRef.current) {
8450
+ lastProgressMarkerRef.current = marker;
8451
+ noProgressRetriesRef.current = 0;
8452
+ return true;
8453
+ }
8454
+ noProgressRetriesRef.current += 1;
8455
+ if (noProgressRetriesRef.current > maxNoProgressRetriesRef.current) {
8456
+ taskStuckRef.current = true;
8457
+ return false;
8458
+ }
8459
+ return true;
8460
+ }, []);
8331
8461
  modeRef.current = mode;
8332
8462
  cwdRef.current = cwd;
8333
8463
  permissionModeRef.current = permissionMode;
@@ -8707,6 +8837,9 @@ function useCodingSessionChat({
8707
8837
  lastStreamActivityRef.current = Date.now();
8708
8838
  }
8709
8839
  }, [messages, status]);
8840
+ useEffect(() => {
8841
+ messagesRef.current = messages;
8842
+ }, [messages]);
8710
8843
  useEffect(() => {
8711
8844
  if (status !== "streaming" && status !== "submitted") {
8712
8845
  return;
@@ -8719,7 +8852,15 @@ function useCodingSessionChat({
8719
8852
  })) {
8720
8853
  return;
8721
8854
  }
8722
- if (stallRecoveryInFlightRef.current || errorRetriesRef.current >= maxErrorRetriesRef.current) {
8855
+ if (taskStuckRef.current || stallRecoveryInFlightRef.current || errorRetriesRef.current >= maxErrorRetriesRef.current) {
8856
+ return;
8857
+ }
8858
+ if (!registerRetryProgress(messagesRef.current)) {
8859
+ setAutoContinueState({
8860
+ attempt: errorRetriesRef.current,
8861
+ kind: "none",
8862
+ guardTripped: "no-progress"
8863
+ });
8723
8864
  return;
8724
8865
  }
8725
8866
  stallRecoveryInFlightRef.current = true;
@@ -8741,12 +8882,12 @@ function useCodingSessionChat({
8741
8882
  })();
8742
8883
  }, 15000);
8743
8884
  return () => clearInterval(intervalId);
8744
- }, [clearError, sendMessage, setMessages, status, stop]);
8885
+ }, [clearError, registerRetryProgress, sendMessage, setMessages, status, stop]);
8745
8886
  useEffect(() => {
8746
8887
  if (status !== "ready" || error) {
8747
8888
  return;
8748
8889
  }
8749
- if (isHistoryLoading || pendingApprovals.length > 0 || pendingUserPrompts.length > 0 || hasUnresolvedToolParts(messages)) {
8890
+ if (taskStuckRef.current || isHistoryLoading || pendingApprovals.length > 0 || pendingUserPrompts.length > 0 || hasUnresolvedToolParts(messages)) {
8750
8891
  return;
8751
8892
  }
8752
8893
  const lastMessage = messages[messages.length - 1];
@@ -8798,7 +8939,7 @@ function useCodingSessionChat({
8798
8939
  if (!error?.message || !isRecoverableChatErrorMessage(error.message)) {
8799
8940
  return;
8800
8941
  }
8801
- if (pendingApprovals.length > 0 || pendingUserPrompts.length > 0 || errorRetryTimerRef.current || stallRecoveryInFlightRef.current) {
8942
+ if (taskStuckRef.current || pendingApprovals.length > 0 || pendingUserPrompts.length > 0 || errorRetryTimerRef.current || stallRecoveryInFlightRef.current) {
8802
8943
  return;
8803
8944
  }
8804
8945
  if (errorRetriesRef.current >= maxErrorRetriesRef.current) {
@@ -8809,6 +8950,14 @@ function useCodingSessionChat({
8809
8950
  const delay = Math.min(errorRetryBaseDelayMs * 2 ** (attempt - 1), errorRetryMaxDelayMs);
8810
8951
  errorRetryTimerRef.current = setTimeout(() => {
8811
8952
  errorRetryTimerRef.current = null;
8953
+ if (!registerRetryProgress(messagesRef.current)) {
8954
+ setAutoContinueState({
8955
+ attempt: errorRetriesRef.current,
8956
+ kind: "none",
8957
+ guardTripped: "no-progress"
8958
+ });
8959
+ return;
8960
+ }
8812
8961
  errorRetriesRef.current = attempt;
8813
8962
  setAutoContinueState({ attempt, kind: "retry-error" });
8814
8963
  setMessages(retryMessages);
@@ -8828,6 +8977,7 @@ function useCodingSessionChat({
8828
8977
  messages,
8829
8978
  pendingApprovals.length,
8830
8979
  pendingUserPrompts.length,
8980
+ registerRetryProgress,
8831
8981
  sendMessage,
8832
8982
  setMessages
8833
8983
  ]);
@@ -8906,7 +9056,7 @@ function useCodingSessionChat({
8906
9056
  if (cancelled) {
8907
9057
  return;
8908
9058
  }
8909
- const resumableMessages = restoredApprovals.length === 0 && restoredPrompts.length === 0 ? trimIncompleteTrailingAssistantMessages(validatedMessages) : validatedMessages;
9059
+ const resumableMessages = restoredApprovals.length === 0 && restoredPrompts.length === 0 ? pruneIncompleteTrailingAssistantParts(validatedMessages) : validatedMessages;
8910
9060
  setMessagesRef.current(resumableMessages);
8911
9061
  setPendingApprovals(restoredApprovals);
8912
9062
  setPendingUserPrompts(restoredPrompts);
@@ -9027,7 +9177,7 @@ function useCodingSessionChat({
9027
9177
  isSessionIdValid
9028
9178
  };
9029
9179
  }
9030
- var recoverableDisconnectMessage = "Connection interrupted. Please retry or regenerate your last message.", buildModeAutoPromptResponse = "Proceed with implementation in Build mode. Continue without additional plan questions.", chatMessageUpdateThrottleMs = 50, retryCommandPattern, codingToolNameSet, errorRetryBaseDelayMs = 1000, errorRetryMaxDelayMs = 8000, disconnectErrorPattern, transientProviderErrorPattern, interruptedToolRetryErrorText = "Tool call was interrupted by a connection retry. Re-issue the tool call if it is still needed.", terminalToolPartStates;
9180
+ var recoverableDisconnectMessage = "Connection interrupted. Please retry or regenerate your last message.", buildModeAutoPromptResponse = "Proceed with implementation in Build mode. Continue without additional plan questions.", chatMessageUpdateThrottleMs = 50, retryCommandPattern, codingToolNameSet, errorRetryBaseDelayMs = 1000, errorRetryMaxDelayMs = 8000, disconnectErrorPattern, transientProviderErrorPattern, terminalToolPartStates;
9031
9181
  var init_use_coding_session_chat = __esm(() => {
9032
9182
  init_agent_tools();
9033
9183
  init_chat_error();
@@ -9040,6 +9190,7 @@ var init_use_coding_session_chat = __esm(() => {
9040
9190
  init_schema9();
9041
9191
  init_runtime12();
9042
9192
  init_auto_continue();
9193
+ init_normalize_provider_messages();
9043
9194
  retryCommandPattern = /^\/?(retry|regenerate)$/i;
9044
9195
  codingToolNameSet = new Set(Object.keys(codingToolInputSchemas));
9045
9196
  disconnectErrorPattern = /(failed to fetch|networkerror|socket|connection|aborted|terminated|timed?\s?out|timeout|econnreset)/i;
@@ -10156,7 +10307,7 @@ var init_chat_message_tool_invocation_part = __esm(() => {
10156
10307
  });
10157
10308
 
10158
10309
  // apps/cli/src/components/chat/chat-message.tsx
10159
- import { isToolUIPart as isToolUIPart2 } from "ai";
10310
+ import { isToolUIPart as isToolUIPart3 } from "ai";
10160
10311
  import { TextAttributes as TextAttributes9 } from "@opentui/core";
10161
10312
  import { jsxDEV as jsxDEV13 } from "@opentui/react/jsx-dev-runtime";
10162
10313
  function ChatMessage({ message, pendingApprovalIds }) {
@@ -10173,7 +10324,7 @@ function ChatMessage({ message, pendingApprovalIds }) {
10173
10324
  part
10174
10325
  }, key, false, undefined, this);
10175
10326
  }
10176
- if (isToolUIPart2(part)) {
10327
+ if (isToolUIPart3(part)) {
10177
10328
  return /* @__PURE__ */ jsxDEV13(ChatMessageToolInvocationPart, {
10178
10329
  part,
10179
10330
  pendingApprovalIds
@@ -11904,7 +12055,7 @@ function ChatScreen() {
11904
12055
  paddingX: 1,
11905
12056
  children: /* @__PURE__ */ jsxDEV21("text", {
11906
12057
  fg: cliTheme.semantic.warning,
11907
- children: autoContinueState.guardTripped === "doom-loop" ? "Agent appears stuck repeating itself \u2014 automatic continuation stopped. Send a message to resume." : `Reached the automatic continuation limit (${autoContinueState.attempt}). Send a message to resume.`
12058
+ children: autoContinueState.guardTripped === "doom-loop" ? "Agent appears stuck repeating itself \u2014 automatic continuation stopped. Send a message to resume." : autoContinueState.guardTripped === "no-progress" ? "The connection keeps dropping without making progress \u2014 automatic retry stopped. Send a message to resume." : `Reached the automatic continuation limit (${autoContinueState.attempt}). Send a message to resume.`
11908
12059
  }, undefined, false, undefined, this)
11909
12060
  }, undefined, false, undefined, this) : autoContinueState ? /* @__PURE__ */ jsxDEV21("box", {
11910
12061
  paddingX: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kmugalkhod/lightcode",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Lightcode — a personal AI coding agent with a terminal UI and local session storage.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/server.js CHANGED
@@ -3201,6 +3201,40 @@ var init_build_provider_view = __esm(() => {
3201
3201
  init_tier1_prune();
3202
3202
  });
3203
3203
 
3204
+ // packages/ai/src/context/normalize-provider-messages.ts
3205
+ import { isToolUIPart } from "ai";
3206
+ function resolveDanglingToolParts(messages, errorText = interruptedToolErrorText) {
3207
+ return messages.map((message) => {
3208
+ if (message.role !== "assistant") {
3209
+ return message;
3210
+ }
3211
+ let changed = false;
3212
+ const parts = message.parts.map((part) => {
3213
+ if (!isToolUIPart(part) || terminalToolStates.has(part.state)) {
3214
+ return part;
3215
+ }
3216
+ changed = true;
3217
+ return {
3218
+ ...part,
3219
+ state: "output-error",
3220
+ errorText
3221
+ };
3222
+ });
3223
+ return changed ? { ...message, parts } : message;
3224
+ });
3225
+ }
3226
+ function normalizeProviderMessages(messages) {
3227
+ return resolveDanglingToolParts(messages);
3228
+ }
3229
+ var terminalToolStates, interruptedToolErrorText = "Tool call was interrupted by a connection retry. Re-issue the tool call if it is still needed.";
3230
+ var init_normalize_provider_messages = __esm(() => {
3231
+ terminalToolStates = new Set([
3232
+ "output-available",
3233
+ "output-error",
3234
+ "output-denied"
3235
+ ]);
3236
+ });
3237
+
3204
3238
  // packages/ai/src/context/index.ts
3205
3239
  var init_context = __esm(() => {
3206
3240
  init_build_provider_view();
@@ -3209,6 +3243,7 @@ var init_context = __esm(() => {
3209
3243
  init_estimate();
3210
3244
  init_heuristic_summary();
3211
3245
  init_message_parts();
3246
+ init_normalize_provider_messages();
3212
3247
  init_tier1_prune();
3213
3248
  });
3214
3249
 
@@ -3307,6 +3342,12 @@ function readEnvConfig(env) {
3307
3342
  summaryMaxChars: parseNumberEnv(env.LIGHTCODE_CONTEXT_SUMMARY_MAX_CHARS, "LIGHTCODE_CONTEXT_SUMMARY_MAX_CHARS")
3308
3343
  };
3309
3344
  const compactContextConfig = Object.fromEntries(Object.entries(contextConfig).filter(([, value]) => value !== undefined));
3345
+ const headroomConfig = {
3346
+ enabled: parseBooleanEnv(env.LIGHTCODE_HEADROOM_ENABLED, "LIGHTCODE_HEADROOM_ENABLED"),
3347
+ proxyUrl: env.LIGHTCODE_HEADROOM_PROXY_URL?.trim() || undefined,
3348
+ failOpen: parseBooleanEnv(env.LIGHTCODE_HEADROOM_FAIL_OPEN, "LIGHTCODE_HEADROOM_FAIL_OPEN")
3349
+ };
3350
+ const compactHeadroomConfig = Object.fromEntries(Object.entries(headroomConfig).filter(([, value]) => value !== undefined));
3310
3351
  const rawConfig = {
3311
3352
  provider: env.LIGHTCODE_PROVIDER,
3312
3353
  model: env.LIGHTCODE_CHAT_MODEL,
@@ -3316,7 +3357,8 @@ function readEnvConfig(env) {
3316
3357
  maxOutputTokens: parseNumberEnv(env.LIGHTCODE_MAX_OUTPUT_TOKENS, "LIGHTCODE_MAX_OUTPUT_TOKENS"),
3317
3358
  maxSteps: parseNumberEnv(env.LIGHTCODE_MAX_STEPS, "LIGHTCODE_MAX_STEPS"),
3318
3359
  maxRetries: parseNumberEnv(env.LIGHTCODE_MAX_RETRIES, "LIGHTCODE_MAX_RETRIES"),
3319
- context: Object.keys(compactContextConfig).length > 0 ? compactContextConfig : undefined
3360
+ context: Object.keys(compactContextConfig).length > 0 ? compactContextConfig : undefined,
3361
+ headroom: Object.keys(compactHeadroomConfig).length > 0 ? compactHeadroomConfig : undefined
3320
3362
  };
3321
3363
  const compactConfig = Object.fromEntries(Object.entries(rawConfig).filter(([, value]) => value !== undefined));
3322
3364
  const parsedConfig = lightcodeConfigSchema.safeParse(compactConfig);
@@ -3340,6 +3382,10 @@ function mergeConfig(...configs) {
3340
3382
  ...mergedConfig.autoContinue,
3341
3383
  ...config2.autoContinue
3342
3384
  } : mergedConfig.autoContinue,
3385
+ headroom: config2.headroom ? {
3386
+ ...mergedConfig.headroom,
3387
+ ...config2.headroom
3388
+ } : mergedConfig.headroom,
3343
3389
  allowedTools: config2.allowedTools ?? mergedConfig.allowedTools
3344
3390
  }), {});
3345
3391
  }
@@ -3380,6 +3426,10 @@ function loadLightcodeConfig({
3380
3426
  autoContinue: {
3381
3427
  ...defaultAutoContinueConfig,
3382
3428
  ...mergedConfig.autoContinue
3429
+ },
3430
+ headroom: {
3431
+ ...defaultHeadroomConfig,
3432
+ ...mergedConfig.headroom
3383
3433
  }
3384
3434
  };
3385
3435
  return lightcodeConfigLoadResultSchema.parse({
@@ -3387,7 +3437,7 @@ function loadLightcodeConfig({
3387
3437
  loadedFiles: [userConfig.file, projectConfig.file]
3388
3438
  });
3389
3439
  }
3390
- var lightcodeProviderSchema, autoContinueConfigSchema, resolvedAutoContinueConfigSchema, defaultAutoContinueConfig, lightcodeConfigSchema, lightcodeConfigDefaults, loadedConfigFileSchema, lightcodeResolvedConfigSchema, lightcodeConfigLoadResultSchema, lightcodeConfigStatusSchema, LightcodeConfigError;
3440
+ var lightcodeProviderSchema, autoContinueConfigSchema, resolvedAutoContinueConfigSchema, defaultAutoContinueConfig, headroomConfigSchema, resolvedHeadroomConfigSchema, defaultHeadroomConfig, lightcodeConfigSchema, lightcodeConfigDefaults, loadedConfigFileSchema, lightcodeResolvedConfigSchema, lightcodeConfigLoadResultSchema, lightcodeConfigStatusSchema, LightcodeConfigError;
3391
3441
  var init_lightcode_config = __esm(() => {
3392
3442
  init_coding_agent_modes();
3393
3443
  init_coding_agent_modes();
@@ -3419,6 +3469,24 @@ var init_lightcode_config = __esm(() => {
3419
3469
  maxErrorRetries: 5,
3420
3470
  stallTimeoutSeconds: 180
3421
3471
  };
3472
+ headroomConfigSchema = z28.object({
3473
+ enabled: z28.boolean().optional(),
3474
+ proxyUrl: z28.string().min(1).max(4096).optional(),
3475
+ providers: z28.array(lightcodeProviderSchema).optional(),
3476
+ failOpen: z28.boolean().optional()
3477
+ }).strict();
3478
+ resolvedHeadroomConfigSchema = z28.object({
3479
+ enabled: z28.boolean(),
3480
+ proxyUrl: z28.string().min(1).max(4096),
3481
+ providers: z28.array(lightcodeProviderSchema).nullable(),
3482
+ failOpen: z28.boolean()
3483
+ });
3484
+ defaultHeadroomConfig = {
3485
+ enabled: false,
3486
+ proxyUrl: "http://127.0.0.1:8787",
3487
+ providers: null,
3488
+ failOpen: true
3489
+ };
3422
3490
  lightcodeConfigSchema = z28.object({
3423
3491
  provider: lightcodeProviderSchema.optional(),
3424
3492
  model: z28.string().min(1).max(240).optional(),
@@ -3430,10 +3498,11 @@ var init_lightcode_config = __esm(() => {
3430
3498
  sandbox: sandboxConfigSchema.optional(),
3431
3499
  mcp: mcpConfigSchema.optional(),
3432
3500
  context: contextOptimizerConfigSchema.optional(),
3433
- maxOutputTokens: z28.number().int().min(1).max(200000).optional(),
3501
+ maxOutputTokens: z28.number().int().min(1).max(1e6).optional(),
3434
3502
  maxSteps: z28.number().int().min(1).max(100).optional(),
3435
3503
  maxRetries: z28.number().int().min(0).max(10).optional(),
3436
- autoContinue: autoContinueConfigSchema.optional()
3504
+ autoContinue: autoContinueConfigSchema.optional(),
3505
+ headroom: headroomConfigSchema.optional()
3437
3506
  }).strict();
3438
3507
  lightcodeConfigDefaults = {
3439
3508
  provider: "anthropic",
@@ -3442,7 +3511,8 @@ var init_lightcode_config = __esm(() => {
3442
3511
  maxOutputTokens: 32768,
3443
3512
  maxSteps: 30,
3444
3513
  maxRetries: 5,
3445
- autoContinue: defaultAutoContinueConfig
3514
+ autoContinue: defaultAutoContinueConfig,
3515
+ headroom: defaultHeadroomConfig
3446
3516
  };
3447
3517
  loadedConfigFileSchema = z28.object({
3448
3518
  scope: z28.enum(["user", "project"]),
@@ -3454,10 +3524,11 @@ var init_lightcode_config = __esm(() => {
3454
3524
  provider: lightcodeProviderSchema,
3455
3525
  defaultMode: codingAgentModeSchema,
3456
3526
  context: resolvedContextOptimizerConfigSchema,
3457
- maxOutputTokens: z28.number().int().min(1).max(200000),
3527
+ maxOutputTokens: z28.number().int().min(1).max(1e6),
3458
3528
  maxSteps: z28.number().int().min(1).max(100),
3459
3529
  maxRetries: z28.number().int().min(0).max(10),
3460
- autoContinue: resolvedAutoContinueConfigSchema
3530
+ autoContinue: resolvedAutoContinueConfigSchema,
3531
+ headroom: resolvedHeadroomConfigSchema.optional()
3461
3532
  });
3462
3533
  lightcodeConfigLoadResultSchema = z28.object({
3463
3534
  config: lightcodeResolvedConfigSchema,
@@ -3471,10 +3542,15 @@ var init_lightcode_config = __esm(() => {
3471
3542
  baseUrl: z28.string().nullable(),
3472
3543
  defaultMode: codingAgentModeSchema,
3473
3544
  permissionMode: permissionModeSchema.nullable(),
3474
- maxOutputTokens: z28.number().int().min(1).max(200000),
3545
+ maxOutputTokens: z28.number().int().min(1).max(1e6),
3475
3546
  maxSteps: z28.number().int().min(1).max(100),
3476
3547
  autoContinue: resolvedAutoContinueConfigSchema,
3477
3548
  contextWindow: z28.number().int().positive(),
3549
+ headroom: z28.object({
3550
+ enabled: z28.boolean(),
3551
+ proxyUrl: z28.string().nullable(),
3552
+ routed: z28.boolean()
3553
+ }),
3478
3554
  missingCredentialHints: z28.array(z28.string())
3479
3555
  });
3480
3556
  LightcodeConfigError = class LightcodeConfigError extends Error {
@@ -3714,6 +3790,21 @@ var init_runtime = __esm(() => {
3714
3790
  init_schema13();
3715
3791
  });
3716
3792
 
3793
+ // packages/ai/src/providers/clamp-output-tokens.ts
3794
+ function clampMaxOutputTokens(configured, modelMaxCompletionTokens) {
3795
+ if (typeof modelMaxCompletionTokens === "number" && Number.isFinite(modelMaxCompletionTokens) && modelMaxCompletionTokens > 0) {
3796
+ return Math.min(configured, modelMaxCompletionTokens);
3797
+ }
3798
+ return configured;
3799
+ }
3800
+ function resolveMaxOutputTokens(configured, defaultValue, modelMaxCompletionTokens) {
3801
+ const hasModelCap = typeof modelMaxCompletionTokens === "number" && Number.isFinite(modelMaxCompletionTokens) && modelMaxCompletionTokens > 0;
3802
+ if (configured === defaultValue) {
3803
+ return hasModelCap ? modelMaxCompletionTokens : configured;
3804
+ }
3805
+ return clampMaxOutputTokens(configured, modelMaxCompletionTokens);
3806
+ }
3807
+
3717
3808
  // packages/ai/src/providers/model-metadata.ts
3718
3809
  function getModelContextWindow(provider, modelId) {
3719
3810
  if (provider === "anthropic") {
@@ -6551,6 +6642,9 @@ function summarizeRawModel(raw2) {
6551
6642
  }
6552
6643
  const name = Reflect.get(raw2, "name");
6553
6644
  const contextLength = Reflect.get(raw2, "context_length");
6645
+ const topProvider = Reflect.get(raw2, "top_provider");
6646
+ const topProviderMaxCompletion = typeof topProvider === "object" && topProvider !== null ? Reflect.get(topProvider, "max_completion_tokens") : undefined;
6647
+ const rawMaxCompletion = typeof topProviderMaxCompletion === "number" ? topProviderMaxCompletion : Reflect.get(raw2, "max_completion_tokens");
6554
6648
  const supportedParameters = Reflect.get(raw2, "supported_parameters");
6555
6649
  const parameterList = Array.isArray(supportedParameters) ? supportedParameters.filter((entry) => typeof entry === "string") : [];
6556
6650
  const pricingRaw = Reflect.get(raw2, "pricing");
@@ -6562,6 +6656,7 @@ function summarizeRawModel(raw2) {
6562
6656
  id,
6563
6657
  name: typeof name === "string" && name.length > 0 ? name : id,
6564
6658
  contextLength: typeof contextLength === "number" && contextLength > 0 ? Math.round(contextLength) : null,
6659
+ maxCompletionTokens: typeof rawMaxCompletion === "number" && rawMaxCompletion > 0 ? Math.round(rawMaxCompletion) : null,
6565
6660
  supportsTools: parameterList.includes("tools"),
6566
6661
  supportsReasoning: parameterList.includes("reasoning"),
6567
6662
  pricing
@@ -6611,7 +6706,8 @@ async function getOpenRouterModelCapabilities(modelId) {
6611
6706
  }
6612
6707
  return {
6613
6708
  supportsNativeTools: model.supportsTools,
6614
- contextLength: model.contextLength ?? undefined
6709
+ contextLength: model.contextLength ?? undefined,
6710
+ maxCompletionTokens: model.maxCompletionTokens
6615
6711
  };
6616
6712
  } catch {
6617
6713
  return;
@@ -6626,6 +6722,7 @@ var init_openrouter_models = __esm(() => {
6626
6722
  id: z34.string(),
6627
6723
  name: z34.string(),
6628
6724
  contextLength: z34.number().int().positive().nullable(),
6725
+ maxCompletionTokens: z34.number().int().positive().nullable().default(null),
6629
6726
  supportsTools: z34.boolean(),
6630
6727
  supportsReasoning: z34.boolean(),
6631
6728
  pricing: z34.object({
@@ -6671,6 +6768,16 @@ function getAnthropicProviderOptions(modelId) {
6671
6768
  }
6672
6769
  };
6673
6770
  }
6771
+ function resolveHeadroomRouting(config5, realBaseUrl) {
6772
+ const headroom = config5.headroom;
6773
+ if (!headroom?.enabled) {
6774
+ return { effectiveBaseUrl: realBaseUrl, headroomRouted: false };
6775
+ }
6776
+ if (headroom.providers && !headroom.providers.includes(config5.provider)) {
6777
+ return { effectiveBaseUrl: realBaseUrl, headroomRouted: false };
6778
+ }
6779
+ return { effectiveBaseUrl: headroom.proxyUrl, headroomRouted: true };
6780
+ }
6674
6781
  function resolveAnthropicModel({
6675
6782
  config: config5,
6676
6783
  env,
@@ -6681,19 +6788,22 @@ function resolveAnthropicModel({
6681
6788
  const apiKey = getEnvValue(env, "ANTHROPIC_API_KEY") ?? storedCredentials.anthropicApiKey;
6682
6789
  const authToken = getEnvValue(env, "ANTHROPIC_AUTH_TOKEN");
6683
6790
  const baseUrl = config5.baseUrl ?? getEnvValue(env, "ANTHROPIC_BASE_URL") ?? null;
6791
+ const { effectiveBaseUrl, headroomRouted } = resolveHeadroomRouting(config5, baseUrl);
6684
6792
  const missingCredentialHints = apiKey || authToken ? [] : [
6685
6793
  "Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN to call Anthropic models."
6686
6794
  ];
6687
6795
  const provider = createAnthropic({
6688
6796
  apiKey,
6689
6797
  authToken,
6690
- baseURL: baseUrl ?? undefined
6798
+ baseURL: effectiveBaseUrl ?? undefined
6691
6799
  });
6692
6800
  return {
6693
6801
  provider: "anthropic",
6694
6802
  configuredModel,
6695
6803
  resolvedModelId: modelId,
6696
6804
  baseUrl,
6805
+ effectiveBaseUrl,
6806
+ headroomRouted,
6697
6807
  model: provider(modelId),
6698
6808
  providerOptions: getAnthropicProviderOptions(modelId),
6699
6809
  contextWindow: getModelContextWindow("anthropic", modelId),
@@ -6716,6 +6826,7 @@ function resolveOpenAITransportModel({
6716
6826
  if (!baseUrl) {
6717
6827
  throw new Error('OpenAI-compatible provider requires "baseUrl" in config or LIGHTCODE_OPENAI_COMPATIBLE_BASE_URL.');
6718
6828
  }
6829
+ const { effectiveBaseUrl, headroomRouted } = resolveHeadroomRouting(config5, baseUrl);
6719
6830
  const apiKey = isOpenCodeZen ? getEnvValue(env, "OPENCODE_API_KEY", "LIGHTCODE_OPENAI_COMPATIBLE_API_KEY", "OPENAI_API_KEY") ?? storedCredentials.opencodeApiKey : isOpenRouter ? getEnvValue(env, "OPENROUTER_API_KEY", "LIGHTCODE_OPENAI_COMPATIBLE_API_KEY", "OPENAI_API_KEY") ?? storedCredentials.openrouterApiKey : getEnvValue(env, "LIGHTCODE_OPENAI_COMPATIBLE_API_KEY", "OPENAI_API_KEY") ?? storedCredentials.openaiCompatibleApiKey;
6720
6831
  const headers = {};
6721
6832
  if (isOpenRouter) {
@@ -6730,7 +6841,7 @@ function resolveOpenAITransportModel({
6730
6841
  }
6731
6842
  const provider = createOpenAICompatible({
6732
6843
  name: config5.provider,
6733
- baseURL: baseUrl,
6844
+ baseURL: effectiveBaseUrl ?? baseUrl,
6734
6845
  apiKey,
6735
6846
  headers
6736
6847
  });
@@ -6739,12 +6850,15 @@ function resolveOpenAITransportModel({
6739
6850
  configuredModel,
6740
6851
  resolvedModelId: configuredModel,
6741
6852
  baseUrl,
6853
+ effectiveBaseUrl,
6854
+ headroomRouted,
6742
6855
  model: withXmlToolCallSupport(provider(configuredModel)),
6743
6856
  needsToolCallDiscipline: true,
6744
6857
  contextWindow: capabilities?.contextLength ?? getModelContextWindow(config5.provider, configuredModel),
6745
6858
  missingCredentialHints: apiKey ? [] : isOpenCodeZen ? ["Set OPENCODE_API_KEY to call OpenCode Zen models."] : isOpenRouter ? ["Set OPENROUTER_API_KEY to call OpenRouter models."] : [
6746
6859
  "Set LIGHTCODE_OPENAI_COMPATIBLE_API_KEY or OPENAI_API_KEY if your OpenAI-compatible endpoint requires authentication."
6747
- ]
6860
+ ],
6861
+ maxCompletionTokens: capabilities?.maxCompletionTokens ?? null
6748
6862
  };
6749
6863
  }
6750
6864
  function resolveConfiguredProviderModel({
@@ -6776,13 +6890,18 @@ function createConfigStatus({
6776
6890
  baseUrl: resolvedProviderModel.baseUrl,
6777
6891
  defaultMode: config5.defaultMode,
6778
6892
  permissionMode: config5.permissionMode ?? null,
6779
- maxOutputTokens: config5.maxOutputTokens,
6893
+ maxOutputTokens: resolveMaxOutputTokens(config5.maxOutputTokens, lightcodeConfigDefaults.maxOutputTokens, resolvedProviderModel.maxCompletionTokens),
6780
6894
  maxSteps: config5.maxSteps,
6781
6895
  autoContinue: config5.autoContinue,
6782
6896
  contextWindow: resolveContextWindowTokens({
6783
6897
  config: config5.context,
6784
6898
  modelContextWindow: resolvedProviderModel.contextWindow
6785
6899
  }),
6900
+ headroom: {
6901
+ enabled: config5.headroom?.enabled ?? false,
6902
+ proxyUrl: config5.headroom?.proxyUrl ?? null,
6903
+ routed: resolvedProviderModel.headroomRouted
6904
+ },
6786
6905
  missingCredentialHints: resolvedProviderModel.missingCredentialHints
6787
6906
  });
6788
6907
  }
@@ -6802,6 +6921,7 @@ __export(exports_runtime_config, {
6802
6921
  resolvedProviderModel: () => resolvedProviderModel,
6803
6922
  lightcodeConfigResult: () => lightcodeConfigResult,
6804
6923
  initializeModelCapabilities: () => initializeModelCapabilities,
6924
+ disableHeadroomRouting: () => disableHeadroomRouting,
6805
6925
  configStatus: () => configStatus,
6806
6926
  codingAgent: () => codingAgent,
6807
6927
  chatModelId: () => chatModelId,
@@ -6814,7 +6934,7 @@ function buildCodingAgent(config5, model) {
6814
6934
  providerOptions: model.providerOptions,
6815
6935
  maxSteps: config5.maxSteps,
6816
6936
  maxRetries: config5.maxRetries,
6817
- maxOutputTokens: config5.maxOutputTokens,
6937
+ maxOutputTokens: resolveMaxOutputTokens(config5.maxOutputTokens, lightcodeConfigDefaults.maxOutputTokens, model.maxCompletionTokens),
6818
6938
  includeToolDiscipline: model.needsToolCallDiscipline ?? false
6819
6939
  });
6820
6940
  }
@@ -6859,6 +6979,39 @@ function applyModelSelection({
6859
6979
  });
6860
6980
  return configStatus;
6861
6981
  }
6982
+ function disableHeadroomRouting() {
6983
+ if (!lightcodeConfigResult.config.headroom?.enabled) {
6984
+ return;
6985
+ }
6986
+ const nextConfig = {
6987
+ ...lightcodeConfigResult.config,
6988
+ headroom: {
6989
+ ...lightcodeConfigResult.config.headroom ?? lightcodeConfigDefaults.headroom,
6990
+ enabled: false
6991
+ }
6992
+ };
6993
+ const nextResolved = resolveConfiguredProviderModel({
6994
+ config: nextConfig,
6995
+ env: Bun.env
6996
+ });
6997
+ const nextAgent = buildCodingAgent(nextConfig, nextResolved);
6998
+ lightcodeConfigResult = {
6999
+ ...lightcodeConfigResult,
7000
+ config: nextConfig
7001
+ };
7002
+ resolvedProviderModel = nextResolved;
7003
+ chatModelId = nextResolved.resolvedModelId;
7004
+ codingAgent = nextAgent;
7005
+ configStatus = createConfigStatus({
7006
+ config: nextConfig,
7007
+ loadedFiles: lightcodeConfigResult.loadedFiles,
7008
+ resolvedProviderModel: nextResolved
7009
+ });
7010
+ logger5.info("headroom_routing_disabled", {
7011
+ provider: nextResolved.provider,
7012
+ model: nextResolved.resolvedModelId
7013
+ });
7014
+ }
6862
7015
  async function initializeModelCapabilities() {
6863
7016
  if (lightcodeConfigResult.config.provider !== "openrouter") {
6864
7017
  return;
@@ -7171,7 +7324,7 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7171
7324
  });
7172
7325
  }
7173
7326
  }
7174
- const providerMessages = view.providerMessages;
7327
+ const providerMessages = normalizeProviderMessages(view.providerMessages);
7175
7328
  let overflowNotedThisRequest = false;
7176
7329
  const noteOverflowForSession = () => {
7177
7330
  if (overflowNotedThisRequest) {
@@ -8228,9 +8381,62 @@ var init_app = __esm(() => {
8228
8381
  })).route("/", rootRoutes).route("/config", configRoutes).route("/diagnostics", diagnosticsRoutes).route("/extensions", extensionRoutes).route("/sessions", sessionRoutes);
8229
8382
  });
8230
8383
 
8384
+ // apps/server/src/lib/headroom-proxy.ts
8385
+ var exports_headroom_proxy = {};
8386
+ __export(exports_headroom_proxy, {
8387
+ ensureHeadroomRouting: () => ensureHeadroomRouting,
8388
+ checkHeadroomHealth: () => checkHeadroomHealth
8389
+ });
8390
+ async function checkHeadroomHealth(proxyUrl) {
8391
+ try {
8392
+ await fetch(new URL("/health", proxyUrl), {
8393
+ signal: AbortSignal.timeout(healthCheckTimeoutMs)
8394
+ });
8395
+ return "healthy";
8396
+ } catch {
8397
+ return "down";
8398
+ }
8399
+ }
8400
+ async function ensureHeadroomRouting() {
8401
+ const headroom = lightcodeConfigResult.config.headroom;
8402
+ if (!headroom?.enabled) {
8403
+ return;
8404
+ }
8405
+ let health;
8406
+ try {
8407
+ health = await checkHeadroomHealth(headroom.proxyUrl);
8408
+ } catch (error) {
8409
+ health = "down";
8410
+ logger9.warn("headroom_health_probe_failed", {
8411
+ proxyUrl: headroom.proxyUrl,
8412
+ error: getErrorMessage(error)
8413
+ });
8414
+ }
8415
+ if (health === "healthy") {
8416
+ logger9.info("headroom_routing_active", { proxyUrl: headroom.proxyUrl });
8417
+ return;
8418
+ }
8419
+ if (headroom.failOpen) {
8420
+ logger9.warn("headroom_unreachable_failing_open", {
8421
+ proxyUrl: headroom.proxyUrl
8422
+ });
8423
+ disableHeadroomRouting();
8424
+ return;
8425
+ }
8426
+ logger9.error("headroom_unreachable_fail_closed", {
8427
+ proxyUrl: headroom.proxyUrl
8428
+ });
8429
+ }
8430
+ var logger9, healthCheckTimeoutMs = 1500;
8431
+ var init_headroom_proxy = __esm(() => {
8432
+ init_src();
8433
+ init_runtime_config();
8434
+ logger9 = createLogger("headroom-proxy");
8435
+ });
8436
+
8231
8437
  // apps/server/src/index.ts
8232
8438
  init_src();
8233
- var logger9 = createLogger("server");
8439
+ var logger10 = createLogger("server");
8234
8440
  var httpIdleTimeoutSeconds = 30;
8235
8441
  var shutdownForceExitMs = 5000;
8236
8442
  function isChatStreamingRoute(request) {
@@ -8243,7 +8449,7 @@ function isChatStreamingRoute(request) {
8243
8449
  async function startServer() {
8244
8450
  const logDirectory = enableFileLogSink(Bun.env);
8245
8451
  if (logDirectory) {
8246
- logger9.info("file_log_sink_enabled", { directory: logDirectory });
8452
+ logger10.info("file_log_sink_enabled", { directory: logDirectory });
8247
8453
  }
8248
8454
  const port = Number(Bun.env.PORT ?? 4983);
8249
8455
  const hostname = Bun.env.LIGHTCODE_HOST ?? "127.0.0.1";
@@ -8264,7 +8470,12 @@ async function startServer() {
8264
8470
  process.exit(1);
8265
8471
  }
8266
8472
  Promise.resolve().then(() => (init_runtime_config(), exports_runtime_config)).then((runtime19) => runtime19.initializeModelCapabilities()).catch((error) => {
8267
- logger9.warn("model_capability_init_failed", {
8473
+ logger10.warn("model_capability_init_failed", {
8474
+ error: getErrorMessage(error)
8475
+ });
8476
+ });
8477
+ Promise.resolve().then(() => (init_headroom_proxy(), exports_headroom_proxy)).then((headroom) => headroom.ensureHeadroomRouting()).catch((error) => {
8478
+ logger10.warn("headroom_init_failed", {
8268
8479
  error: getErrorMessage(error)
8269
8480
  });
8270
8481
  });
@@ -8300,7 +8511,7 @@ async function startServer() {
8300
8511
  return;
8301
8512
  }
8302
8513
  shuttingDown = true;
8303
- logger9.info("server_shutdown", { signal });
8514
+ logger10.info("server_shutdown", { signal });
8304
8515
  const forceExitTimer = setTimeout(() => {
8305
8516
  server.stop(true);
8306
8517
  process.exit(0);