claude-code-infinite 1.0.35 → 1.0.37

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 (117) hide show
  1. package/README.md +114 -10
  2. package/dist/body-shape.d.ts +49 -0
  3. package/dist/body-shape.d.ts.map +1 -0
  4. package/dist/body-shape.js +232 -0
  5. package/dist/body-shape.js.map +1 -0
  6. package/dist/cc-request.d.ts +15 -0
  7. package/dist/cc-request.d.ts.map +1 -1
  8. package/dist/cc-request.js +30 -0
  9. package/dist/cc-request.js.map +1 -1
  10. package/dist/claude-env.d.ts +24 -1
  11. package/dist/claude-env.d.ts.map +1 -1
  12. package/dist/claude-env.js +48 -7
  13. package/dist/claude-env.js.map +1 -1
  14. package/dist/cli-args.d.ts +1 -1
  15. package/dist/cli-args.d.ts.map +1 -1
  16. package/dist/cli-args.js +1 -1
  17. package/dist/cli-args.js.map +1 -1
  18. package/dist/cli-main.js +113 -11
  19. package/dist/cli-main.js.map +1 -1
  20. package/dist/config.d.ts +5 -0
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js.map +1 -1
  23. package/dist/count-tokens.d.ts +15 -2
  24. package/dist/count-tokens.d.ts.map +1 -1
  25. package/dist/count-tokens.js +13 -4
  26. package/dist/count-tokens.js.map +1 -1
  27. package/dist/desktop-command.d.ts +38 -0
  28. package/dist/desktop-command.d.ts.map +1 -0
  29. package/dist/desktop-command.js +324 -0
  30. package/dist/desktop-command.js.map +1 -0
  31. package/dist/desktop-service.d.ts +41 -0
  32. package/dist/desktop-service.d.ts.map +1 -0
  33. package/dist/desktop-service.js +449 -0
  34. package/dist/desktop-service.js.map +1 -0
  35. package/dist/desktop-setup.d.ts +94 -0
  36. package/dist/desktop-setup.d.ts.map +1 -0
  37. package/dist/desktop-setup.js +288 -0
  38. package/dist/desktop-setup.js.map +1 -0
  39. package/dist/financial-redaction.d.ts +12 -0
  40. package/dist/financial-redaction.d.ts.map +1 -0
  41. package/dist/financial-redaction.js +509 -0
  42. package/dist/financial-redaction.js.map +1 -0
  43. package/dist/hooks.d.ts +2 -0
  44. package/dist/hooks.d.ts.map +1 -1
  45. package/dist/hooks.js +4 -0
  46. package/dist/hooks.js.map +1 -1
  47. package/dist/https-proxy-eligibility.d.ts +31 -0
  48. package/dist/https-proxy-eligibility.d.ts.map +1 -0
  49. package/dist/https-proxy-eligibility.js +204 -0
  50. package/dist/https-proxy-eligibility.js.map +1 -0
  51. package/dist/https-proxy-mode.d.ts +59 -0
  52. package/dist/https-proxy-mode.d.ts.map +1 -0
  53. package/dist/https-proxy-mode.js +255 -0
  54. package/dist/https-proxy-mode.js.map +1 -0
  55. package/dist/https-proxy-session.d.ts +64 -0
  56. package/dist/https-proxy-session.d.ts.map +1 -0
  57. package/dist/https-proxy-session.js +165 -0
  58. package/dist/https-proxy-session.js.map +1 -0
  59. package/dist/image-tokens.d.ts +6 -1
  60. package/dist/image-tokens.d.ts.map +1 -1
  61. package/dist/image-tokens.js +75 -14
  62. package/dist/image-tokens.js.map +1 -1
  63. package/dist/lane-activity.d.ts +42 -0
  64. package/dist/lane-activity.d.ts.map +1 -0
  65. package/dist/lane-activity.js +79 -0
  66. package/dist/lane-activity.js.map +1 -0
  67. package/dist/memtree-mcp.d.ts +32 -1
  68. package/dist/memtree-mcp.d.ts.map +1 -1
  69. package/dist/memtree-mcp.js +73 -7
  70. package/dist/memtree-mcp.js.map +1 -1
  71. package/dist/memtree.d.ts +10 -0
  72. package/dist/memtree.d.ts.map +1 -1
  73. package/dist/memtree.js +59 -12
  74. package/dist/memtree.js.map +1 -1
  75. package/dist/message-refs.d.ts +81 -0
  76. package/dist/message-refs.d.ts.map +1 -0
  77. package/dist/message-refs.js +378 -0
  78. package/dist/message-refs.js.map +1 -0
  79. package/dist/mitm-ca.d.ts +31 -0
  80. package/dist/mitm-ca.d.ts.map +1 -0
  81. package/dist/mitm-ca.js +236 -0
  82. package/dist/mitm-ca.js.map +1 -0
  83. package/dist/paused-session.d.ts.map +1 -1
  84. package/dist/paused-session.js +9 -2
  85. package/dist/paused-session.js.map +1 -1
  86. package/dist/proxy.d.ts +25 -0
  87. package/dist/proxy.d.ts.map +1 -1
  88. package/dist/proxy.js +168 -22
  89. package/dist/proxy.js.map +1 -1
  90. package/dist/refusal-reason.d.ts +3 -2
  91. package/dist/refusal-reason.d.ts.map +1 -1
  92. package/dist/refusal-reason.js +4 -3
  93. package/dist/refusal-reason.js.map +1 -1
  94. package/dist/reqlog.d.ts +13 -3
  95. package/dist/reqlog.d.ts.map +1 -1
  96. package/dist/reqlog.js +1 -1
  97. package/dist/reqlog.js.map +1 -1
  98. package/dist/request-sizing.d.ts +38 -2
  99. package/dist/request-sizing.d.ts.map +1 -1
  100. package/dist/request-sizing.js +72 -6
  101. package/dist/request-sizing.js.map +1 -1
  102. package/dist/secret-redaction.d.ts +25 -0
  103. package/dist/secret-redaction.d.ts.map +1 -0
  104. package/dist/secret-redaction.js +492 -0
  105. package/dist/secret-redaction.js.map +1 -0
  106. package/dist/statusline-auto.d.ts +15 -0
  107. package/dist/statusline-auto.d.ts.map +1 -0
  108. package/dist/statusline-auto.js +125 -0
  109. package/dist/statusline-auto.js.map +1 -0
  110. package/dist/statusline.d.ts.map +1 -1
  111. package/dist/statusline.js +3 -2
  112. package/dist/statusline.js.map +1 -1
  113. package/dist/utf8-bytes.d.ts +10 -0
  114. package/dist/utf8-bytes.d.ts.map +1 -0
  115. package/dist/utf8-bytes.js +107 -0
  116. package/dist/utf8-bytes.js.map +1 -0
  117. package/package.json +1 -1
package/dist/proxy.js CHANGED
@@ -40,9 +40,11 @@
40
40
  */
41
41
  import { UNKNOWN_INDEX_WAIT_SECONDS } from "./index-wait-413.js";
42
42
  import { capCacheBreakpoints } from "./route-cache.js";
43
+ import { attachHttpsProxyMode, } from "./https-proxy-mode.js";
43
44
  import { PromptAccounting } from "./prompt-accounting.js";
44
45
  import { INDEX_WAIT_RETRY_AFTER_SECONDS, RefusalStreaks, RouteFallbackFailures, pausedRefusal, unreachableRefusal } from "./route-fallback.js";
45
46
  import { refusalCause, refusalMessage } from "./refusal-reason.js";
47
+ import { AGENT_RESULT_WAITING_LINE, LaneActivity, heavyRequestNoticeText } from "./lane-activity.js";
46
48
  import { SlowUploads } from "./slow-upload.js";
47
49
  import { freshFailure, networkFailureOf, unreachableMessage, } from "./network-failure.js";
48
50
  import { createCountTokensSession, CountTokensCooldowns, plausibleSample, } from "./count-tokens.js";
@@ -67,12 +69,13 @@ import { keyInvalidNoticeText, keyInvalidRefusalMessage } from "./key-status.js"
67
69
  import { KEY_INVALID_KIND, ModelContextNotices, PAUSED_KIND, keyInvalidModelContext, lastToldKindInFile, pausedModelContext } from "./model-context.js";
68
70
  import { PaymentNoticeThrottle, pausedCompactionText, pausedRefusalMessage, paymentNoticeText, subscribeUrlFromDetail } from "./payment-notice.js";
69
71
  import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
70
- import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
72
+ import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, notSessionConversation, sessionTag, } from "./cc-request.js";
71
73
  import { isResumableSessionId } from "./resume-hint.js";
72
74
  import { isSubagentCall, postToolUseOutput, SUBAGENT_RESULT_HINT, subagentMemoryHintText, subagentStartOutput, } from "./subagent-hint.js";
73
75
  import { PROJECT_TREES_HOOK_SUFFIX, RESUME_GUIDANCE_HOOK_SUFFIX, SUBAGENT_RESULT_HOOK_SUFFIX, } from "./hooks.js";
74
76
  import { conservativeBodyTokens } from "./token-estimate.js";
75
77
  import { bodyImages } from "./image-tokens.js";
78
+ import { bodyShape, prefixDigest } from "./body-shape.js";
76
79
  import { claudeAuthFromHeaders, upstreamErrorCode } from "./claude-auth.js";
77
80
  import { rateLimitFromResponse } from "./rate-limit.js";
78
81
  import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
@@ -382,6 +385,7 @@ export function startProxy(opts) {
382
385
  fallbackFailures: new RouteFallbackFailures(undefined, opts.indexWaitDeadlineMs, opts.indexWaitMaxMs),
383
386
  refusalStreaks: new RefusalStreaks(),
384
387
  activeSubagents: new Set(),
388
+ laneActivity: new LaneActivity(),
385
389
  deferredAgentIndexes: new Map(),
386
390
  adoptingPrepared: new Map(),
387
391
  memoryRoutes: new Map(),
@@ -424,10 +428,12 @@ export function startProxy(opts) {
424
428
  // A newer tree from the background lookup reaches the status line without a turn ending.
425
429
  opts.newestTrees?.onTree((sessionId) => publishStatusLink(state, sessionId));
426
430
  state.notices.setFirstTreeIntro(opts.firstTreeIntro ?? null);
427
- const server = http.createServer((req, res) => {
431
+ // One handler for the loopback server and, in HTTPS-proxy mode, for the decrypted
432
+ // api.anthropic.com streams (`intercepted`): both are tracked, drained and cancelled alike.
433
+ const onRequest = (intercepted) => (req, res) => {
428
434
  const accepted = { req, res };
429
435
  acceptedRequests.add(accepted);
430
- const task = handleRequest(req, res, opts, upstream, state, hookPath).catch((err) => {
436
+ const task = handleRequest(req, res, opts, upstream, state, hookPath, intercepted).catch((err) => {
431
437
  try {
432
438
  sendAnthropicError(res, `local proxy error: ${err?.message ?? err}`);
433
439
  }
@@ -445,7 +451,20 @@ export function startProxy(opts) {
445
451
  });
446
452
  if (shutdownAbort.signal.aborted)
447
453
  cancelAcceptedRequest(accepted);
448
- });
454
+ };
455
+ const server = http.createServer(opts.innerToken === undefined ? onRequest(false)
456
+ : (req, res) => {
457
+ const inner = acceptInnerRequest(req, opts.innerToken);
458
+ if (inner === undefined) {
459
+ res.writeHead(403, { "content-type": "application/json" });
460
+ res.end(JSON.stringify({ detail: "ccc inner proxy: not for direct use" }));
461
+ return;
462
+ }
463
+ onRequest(inner.intercepted)(req, res);
464
+ });
465
+ const closeTunnels = opts.httpsProxyMode
466
+ ? attachHttpsProxyMode(server, onRequest(true), interceptUpstream(upstream), opts.httpsProxyMode)
467
+ : { blind: () => { }, all: () => { } };
449
468
  // Long-running SSE responses must not be cut by idle timeouts.
450
469
  server.requestTimeout = 0;
451
470
  server.headersTimeout = 60_000;
@@ -467,6 +486,9 @@ export function startProxy(opts) {
467
486
  ? Math.floor(timeoutMs)
468
487
  : 5_000;
469
488
  backgroundAbort.abort();
489
+ // CONNECT tunnels are raw sockets that keep server.close() waiting: the blind ones
490
+ // (and idle decrypted streams) carry no ccc work, so they go now.
491
+ closeTunnels.blind();
470
492
  const quiesced = (async () => {
471
493
  // Once close completes, every accepted request has entered the tracked
472
494
  // set.
@@ -495,6 +517,7 @@ export function startProxy(opts) {
495
517
  shutdownAbort.abort();
496
518
  for (const accepted of acceptedRequests)
497
519
  cancelAcceptedRequest(accepted);
520
+ closeTunnels.all();
498
521
  await quiesced;
499
522
  sendDeferredAgentIndexes(state);
500
523
  return false;
@@ -529,8 +552,10 @@ function cancelAcceptedRequest(accepted) {
529
552
  if (!res.destroyed && !res.writableEnded)
530
553
  res.destroy();
531
554
  }
532
- async function handleRequest(req, res, opts, upstream, state, hookPath) {
555
+ async function handleRequest(req, res, opts, upstream, state, hookPath, intercepted) {
533
556
  const url = new URL(req.url ?? "/", `http://127.0.0.1`);
557
+ if (intercepted)
558
+ return handleInterceptedRequest(req, res, url, opts, upstream, state);
534
559
  if (!isLocalCaller(req)) {
535
560
  res.writeHead(403, { "content-type": "application/json" });
536
561
  res.end(JSON.stringify({ detail: "The ccc proxy only serves programs on this machine" }));
@@ -575,6 +600,32 @@ async function handleRequest(req, res, opts, upstream, state, hookPath) {
575
600
  }
576
601
  return passThroughStreaming(req, res, upstream, state.shutdownSignal);
577
602
  }
603
+ /**
604
+ * Claude Code's own api.anthropic.com traffic, decrypted (HTTPS-proxy mode): model calls
605
+ * get MemTree exactly as through the base URL; everything else (Remote Control's bridge,
606
+ * sessions, files, OAuth, telemetry) goes verbatim to api.anthropic.com. ccc's
607
+ * loopback-only routes (hooks, /memtree/*) are never answered here: such a path is
608
+ * Anthropic's.
609
+ */
610
+ function handleInterceptedRequest(req, res, url, opts, upstream, state) {
611
+ if (req.method === "POST" && url.pathname === "/v1/messages") {
612
+ try {
613
+ opts.httpsProxyMode?.onModelCall?.();
614
+ }
615
+ catch {
616
+ // A notice hook must never fail a model call.
617
+ }
618
+ return handleMessages(req, res, opts, upstream, state);
619
+ }
620
+ if (req.method === "POST" && url.pathname === "/v1/messages/count_tokens") {
621
+ return handleCountTokens(req, res, opts, upstream, state);
622
+ }
623
+ return passThroughStreaming(req, res, upstream, state.shutdownSignal);
624
+ }
625
+ /** Where a WebSocket upgrade on a decrypted stream is replayed: the same upstream. */
626
+ function interceptUpstream(upstream) {
627
+ return { host: upstream.host, port: upstream.port, secure: upstream.module === https };
628
+ }
578
629
  const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "localhost", "[::1]"]);
579
630
  const BROWSER_SAME_SITE = new Set(["same-origin", "none"]);
580
631
  /**
@@ -1256,6 +1307,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1256
1307
  finally {
1257
1308
  if (rec.totalMs === undefined)
1258
1309
  rec.totalMs = Date.now() - received;
1310
+ noteForwardedSize(state, rec, requestSessionId(req));
1259
1311
  opts.reqlog?.log(rec);
1260
1312
  }
1261
1313
  };
@@ -1379,6 +1431,12 @@ async function handleMessages(req, res, opts, upstream, state) {
1379
1431
  const { lane: requestRouteLane, key: requestRouteKey } = routeIdentity(req, isAwaySummary);
1380
1432
  const isSubagentRequest = requestRouteLane === "agent";
1381
1433
  const isMainRequest = requestRouteLane === "main";
1434
+ // A side request or a thread continuation carries a fragment, not the conversation:
1435
+ // it is forwarded as sent, with no compress, route, recovery or MemTree upload.
1436
+ // The away summary keeps its own lane and handling.
1437
+ const notConversation = isAwaySummary ? undefined : notSessionConversation(body, clientInfo);
1438
+ if (notConversation)
1439
+ rec.notConversation = notConversation;
1382
1440
  const requestSizeKey = laneSizeKey(req, requestRouteKey, body.model);
1383
1441
  const hold = indexHolds.get(rec);
1384
1442
  if (hold) {
@@ -1431,6 +1489,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1431
1489
  }
1432
1490
  }
1433
1491
  rec.routeLane = requestRouteLane;
1492
+ noteLaneActivity(state, rec, requestSessionId(req));
1434
1493
  // Stored by the server on the usage row (client_meta): which Claude Code,
1435
1494
  // lane, agent and model produced this request.
1436
1495
  const clientMeta = memtreeClientMeta({
@@ -1444,7 +1503,9 @@ async function handleMessages(req, res, opts, upstream, state) {
1444
1503
  // API identity and validated history alone govern route eligibility.
1445
1504
  const rideableCandidate = getMemoryRoute(state, requestRouteKey);
1446
1505
  const existingRide = rideableCandidate && memoryRoutedToolBody(body, messages, rideableCandidate, state.mainRouteEpoch, requestSessionId(req));
1447
- const isFollowupUserTurn = isUserTurn && hasEarlierNonToolUserMessage(messages);
1506
+ // A fragment is never a follow-up: it must not reserve a route decision (it would never
1507
+ // release it, and an in-flight main compression could not install its route).
1508
+ const isFollowupUserTurn = !notConversation && isUserTurn && hasEarlierNonToolUserMessage(messages);
1448
1509
  const routeEpoch = state.mainRouteEpoch;
1449
1510
  let routeDecisionGeneration = state.mainRouteDecisionGeneration;
1450
1511
  let routeDecisionSettled = false;
@@ -1490,7 +1551,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1490
1551
  // ORIGINAL conversation (the one MemTree indexed), never the ride itself.
1491
1552
  recoverLate: async (_ride, rideTokens, target) => {
1492
1553
  // A compression already made may be redone only to a smaller target.
1493
- if ((rec.compress && target === undefined) ||
1554
+ if (notConversation || (rec.compress && target === undefined) ||
1494
1555
  compactionMode(opts, state, requestSessionKey).mode === "off" ||
1495
1556
  !hasEarlierNonToolUserMessage(messages) ||
1496
1557
  (rec.turnType.startsWith("tool") && opts.toolRouteRecovery === false))
@@ -1513,9 +1574,14 @@ async function handleMessages(req, res, opts, upstream, state) {
1513
1574
  // The main lane's latest conversation: what its final index sends, plus the
1514
1575
  // reply the session ends with (scheduleFinalIndex, on Stop).
1515
1576
  const mainSessionId = isMainRequest ? requestSessionId(req) : undefined;
1516
- if (mainSessionId !== undefined) {
1517
- if (isResumableSessionId(mainSessionId))
1518
- state.resumableMainSession = mainSessionId;
1577
+ if (mainSessionId !== undefined && isResumableSessionId(mainSessionId)) {
1578
+ state.resumableMainSession = mainSessionId;
1579
+ }
1580
+ // A continuation is the session at work (a side request, e.g. a status summary, is not).
1581
+ if (mainSessionId !== undefined && notConversation === "thread-continue") {
1582
+ opts.memtree.noteMainActivity(mainSessionId);
1583
+ }
1584
+ if (mainSessionId !== undefined && !notConversation) {
1519
1585
  opts.memtree.noteMainConversation(mainSessionId, {
1520
1586
  messages: msgsForMemtree,
1521
1587
  modelContextLimit,
@@ -1535,7 +1601,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1535
1601
  // Observation only: which requests carry Claude Code's turn origin.
1536
1602
  if (clientInfo)
1537
1603
  rec.client = clientInfo;
1538
- if (!isFollowupUserTurn) {
1604
+ if (!isFollowupUserTurn || notConversation) {
1539
1605
  // Tool turn or FIRST user turn: keep the index fed off the response path
1540
1606
  // and forward as-is (on the lane's memory route or the session's stable
1541
1607
  // prefix when one applies) — except a tool turn whose estimated size
@@ -1551,7 +1617,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1551
1617
  let routedTool = false;
1552
1618
  let routedCandidate;
1553
1619
  let routeMiss;
1554
- if (isToolResultTurn || isContinuationTurn || isUserTurn) {
1620
+ if (!notConversation && (isToolResultTurn || isContinuationTurn || isUserTurn)) {
1555
1621
  // Reuse only this API session/lane's independently validated prefix.
1556
1622
  const activeRoute = rideableCandidate;
1557
1623
  if (activeRoute) {
@@ -1612,7 +1678,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1612
1678
  rec.continuation = true;
1613
1679
  // Cheap shape check: without an earlier real user message there is
1614
1680
  // nothing MemTree could compress, so no attempt is worth making.
1615
- const canCompress = hasEarlierNonToolUserMessage(messages);
1681
+ const canCompress = !notConversation && hasEarlierNonToolUserMessage(messages);
1616
1682
  let toolRide = routedTool
1617
1683
  ? {
1618
1684
  raw: routedBody,
@@ -1621,7 +1687,10 @@ async function handleMessages(req, res, opts, upstream, state) {
1621
1687
  }
1622
1688
  : undefined;
1623
1689
  let toolNeedsOriginal = false;
1624
- if (opts.toolRouteRecovery === false) {
1690
+ if (notConversation) {
1691
+ // A fragment: forwarded as sent (no route, no stable prefix, no compaction).
1692
+ }
1693
+ else if (opts.toolRouteRecovery === false) {
1625
1694
  // Kill switch: no size check and no compress call on any tool turn.
1626
1695
  // A route still rides; a miss forwards whole and records "disabled".
1627
1696
  if ((routeMiss === "missing" || routeMiss === "rejected") && canCompress) {
@@ -1798,8 +1867,10 @@ async function handleMessages(req, res, opts, upstream, state) {
1798
1867
  requestSendPolicies.get(req)?.allowed.add(toolRide.raw);
1799
1868
  const sendBody = toolRide?.raw ?? (toolNeedsOriginal ? rawBody : forwardBody);
1800
1869
  recordTurn(rec, toolRide ? toolRide.turnType : isUserTurn ? "first-user" : "tool", sendBody);
1801
- indexTurn({ opts, state, req, rec, hash, msgsForMemtree, modelContextLimit,
1802
- sessionId: toolSessionId, clientMeta });
1870
+ if (!notConversation) {
1871
+ indexTurn({ opts, state, req, rec, hash, msgsForMemtree, modelContextLimit,
1872
+ sessionId: toolSessionId, clientMeta });
1873
+ }
1803
1874
  capture(opts, toolRide ? "anthropic-request-memory-tool" : "anthropic-request", sendBody);
1804
1875
  // The size Anthropic reports is the next budget check's anchor: a ride
1805
1876
  // reports to its prefix (or route), a whole request to the session's (or
@@ -3326,8 +3397,16 @@ function reportedSample(rec, sent) {
3326
3397
  }
3327
3398
  /** A size sample of `sent` carrying its inline images (estimateRequestTokens). */
3328
3399
  function sizeSampleOf(sent, tokens, model) {
3329
- const { imageBytes, imageTokens } = bodyImages(sent, typeof model === "string" ? model : undefined);
3330
- return { tokens, forwardedBytes: sent.length, imageBytes, imageTokens };
3400
+ const id = typeof model === "string" ? model : undefined;
3401
+ const { imageBytes, imageTokens } = bodyImages(sent, id);
3402
+ return { tokens, forwardedBytes: sent.length, imageBytes, imageTokens, ...sampleLineage(sent, id) };
3403
+ }
3404
+ /** The sampled body's lineage (body-shape.ts) for anchoredBound; none for a small body. */
3405
+ function sampleLineage(sent, model) {
3406
+ const shape = bodyShape(sent);
3407
+ const digest = shape && prefixDigest(shape, shape.messages.length);
3408
+ return shape && digest && model
3409
+ ? { model, messageCount: shape.messages.length, prefixDigest: digest } : {};
3331
3410
  }
3332
3411
  /**
3333
3412
  * Anthropic's input tokens for the body this request forwarded, when its usage
@@ -4112,6 +4191,8 @@ const exactSizedRecords = new WeakSet();
4112
4191
  * it is over the window for planning and takes the normal gated compression.
4113
4192
  */
4114
4193
  function noteUncountedBound(record, sizing, candidate, body, modelContextLimit) {
4194
+ if (candidate.anchored)
4195
+ record.anchoredBound = true;
4115
4196
  if (sizing.isExact(candidate)) {
4116
4197
  exactSizedRecords.add(record);
4117
4198
  return;
@@ -5227,7 +5308,23 @@ async function recoverToolRouteMiss(args) {
5227
5308
  function recordTurn(rec, turnType, forwardBody) {
5228
5309
  rec.turnType = turnType;
5229
5310
  rec.forwardedBytes = forwardBody.length;
5230
- rec.approxInputTokens = approxTokensFromBytes(forwardBody.length);
5311
+ rec.approxInputTokens = approxBodyTokens(forwardBody, rec.model);
5312
+ forwardedImageBytes.set(rec, bodyImages(forwardBody, typeof rec.model === "string" ? rec.model : undefined).imageBytes);
5313
+ }
5314
+ /** The forwarded body's inline image bytes (recordTurn), for the heavy-request notice. */
5315
+ const forwardedImageBytes = new WeakMap();
5316
+ /** What this request uploaded to Anthropic, for the session's heavy-request notice. */
5317
+ function noteForwardedSize(state, rec, sessionId) {
5318
+ const bytes = rec.forwardedBytes ?? 0;
5319
+ if (bytes > 0) {
5320
+ state.laneActivity.sent(sessionId, { bytes, imageBytes: forwardedImageBytes.get(rec) ?? 0 });
5321
+ }
5322
+ }
5323
+ /** A body's text at bytes/4 plus its inline images at their visual tokens (log only):
5324
+ * bytes/4 alone logged a 22 MB contact-sheet lane at 5.5M tokens (Anthropic: 491k). */
5325
+ function approxBodyTokens(body, model) {
5326
+ const images = bodyImages(body, typeof model === "string" ? model : undefined);
5327
+ return approxTokensFromBytes(body.length - images.imageBytes) + images.imageTokens;
5231
5328
  }
5232
5329
  function headerText(headers, name) {
5233
5330
  const value = headers[name];
@@ -5707,7 +5804,7 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
5707
5804
  : offline ? unreachableRefusal(offline.message)
5708
5805
  : args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true,
5709
5806
  indexSecondsLeft: activeIndexWaits.get(args.rec),
5710
- refusalMessage: refusalMessage(cause, currentSessionLink(args.state, requestSessionId(args.req))) });
5807
+ refusalMessage: refusalMessage(cause, currentSessionLink(args.state, requestSessionId(args.req)), agentResultNote(args)) });
5711
5808
  args.rec.forwardedBytes = 0;
5712
5809
  args.rec.approxInputTokens = 0;
5713
5810
  // Nothing went out uncompressed: Claude Code retries it (2026-10-28).
@@ -5750,6 +5847,17 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
5750
5847
  args.state.keyNotice.markShown();
5751
5848
  return false;
5752
5849
  }
5850
+ /**
5851
+ * A main-lane refusal while an agent lane of this session was recently active: a
5852
+ * background agent's finished result reaches the main session only as part of its
5853
+ * next turn, so say it may be waiting (2026-11-13: a refused main lane held one back).
5854
+ */
5855
+ function agentResultNote(args) {
5856
+ // This session's own agent traffic only: activeSubagents is process-wide (review r1 P3).
5857
+ return args.rec.routeLane === "main" &&
5858
+ args.state.laneActivity.agentRecent(requestSessionId(args.req))
5859
+ ? AGENT_RESULT_WAITING_LINE : undefined;
5860
+ }
5753
5861
  /** Requests held back by a lane backoff whose attempt MemTree answered (unreachableNow). */
5754
5862
  const answeredBackoffs = new WeakSet();
5755
5863
  /**
@@ -5781,6 +5889,25 @@ function unreachableNow(state, rec) {
5781
5889
  return { message: unreachableMessage(memtree, anthropic),
5782
5890
  what: anthropic ? "internet" : "memtree" };
5783
5891
  }
5892
+ /**
5893
+ * Note this request's lane (lane-activity.ts) and, on a main-lane request of the current
5894
+ * prompt, queue the session's one heavy-request warning when an earlier upload passed the
5895
+ * size and no other notice waits. It is the lowest priority (otherNoticePending): any
5896
+ * other notice may replace it, which leaves it due for a later turn.
5897
+ */
5898
+ function noteLaneActivity(state, rec, sessionId) {
5899
+ state.laneActivity.seen(sessionId, rec.routeLane);
5900
+ const heavy = state.laneActivity.heavyDue(sessionId);
5901
+ const prompt = noticeContexts.get(rec);
5902
+ if (!heavy || sessionId === undefined || rec.routeLane !== "main" || prompt === undefined ||
5903
+ prompt.generation !== state.mainPromptGeneration || state.notices.hasPending())
5904
+ return;
5905
+ state.heavyNotice = state.notices.queueSuffix(heavyRequestNoticeText(heavy), () => state.laneActivity.markWarned(sessionId), prompt.promptId);
5906
+ }
5907
+ /** Whether a notice other than the heavy-request one (the lowest priority) waits. */
5908
+ function otherNoticePending(state, promptId) {
5909
+ return state.notices.hasPending(promptId) && !state.notices.pendingIs(state.heavyNotice);
5910
+ }
5784
5911
  /**
5785
5912
  * A repeated retryable refusal on the main thread: say once per streak that
5786
5913
  * MemTree is slow and Claude Code is retrying, unless another notice waits.
@@ -5793,7 +5920,7 @@ function noteSlowRetry(args, failure) {
5793
5920
  const canShow = failure.status === 503 && rec.routeLane === "main" &&
5794
5921
  rec.indexBehind !== true && prompt !== undefined &&
5795
5922
  prompt.generation === state.mainPromptGeneration && !args.res.destroyed &&
5796
- !state.notices.hasPending();
5923
+ !otherNoticePending(state);
5797
5924
  if (!state.refusalStreaks.refused(args.routeKey, Date.now(), canShow))
5798
5925
  return;
5799
5926
  state.notices.queueSuffix(SLOW_RETRY_NOTICE, undefined, prompt.promptId);
@@ -5844,7 +5971,7 @@ function warnPausedNearWindow(state, rec, sessionId) {
5844
5971
  // Never replace another queued notice (or this one again): a later request tries.
5845
5972
  if (detail === null || rec.routeLane !== "main" || sessionId === undefined ||
5846
5973
  pausedNotices.has(rec) || nearWindowWarned(state, sessionId) ||
5847
- state.notices.hasPending(state.mainPromptId))
5974
+ otherNoticePending(state, state.mainPromptId))
5848
5975
  return;
5849
5976
  state.nearWindowNotice = state.notices.queueSuffix(paymentNoticeText(detail, undefined, { overBudget: true }), () => {
5850
5977
  state.paymentNotice.markShown();
@@ -6700,4 +6827,23 @@ function transcriptTimesFor(opts, sessionId, agentId) {
6700
6827
  return (messages) => source.timesFor(sessionId, messages, agentId);
6701
6828
  }
6702
6829
  export const __testHoldingLane = holdingLane;
6830
+ /**
6831
+ * The inner-proxy headers (ProxyOptions.innerToken): checked, then stripped so nothing
6832
+ * forwards them. undefined: not the service.
6833
+ */
6834
+ function acceptInnerRequest(req, token) {
6835
+ if (!sameSecret(req.headers[INNER_TOKEN_HEADER], token))
6836
+ return undefined;
6837
+ const intercepted = req.headers[INNER_INTERCEPTED_HEADER] === "1";
6838
+ delete req.headers[INNER_TOKEN_HEADER];
6839
+ delete req.headers[INNER_INTERCEPTED_HEADER];
6840
+ for (let i = req.rawHeaders.length - 2; i >= 0; i -= 2) {
6841
+ const name = req.rawHeaders[i].toLowerCase();
6842
+ if (name === INNER_TOKEN_HEADER || name === INNER_INTERCEPTED_HEADER)
6843
+ req.rawHeaders.splice(i, 2);
6844
+ }
6845
+ return { intercepted };
6846
+ }
6847
+ export const INNER_TOKEN_HEADER = "x-ccc-inner";
6848
+ export const INNER_INTERCEPTED_HEADER = "x-ccc-intercepted";
6703
6849
  //# sourceMappingURL=proxy.js.map